feat: add issue kanban/attachments/deadlines, dashboard widget DnD, and checklisten system

This commit is contained in:
Matthias Hochmeister
2026-03-28 15:19:41 +01:00
parent a1cda5be51
commit 0c2ea829aa
42 changed files with 4804 additions and 201 deletions

View File

@@ -0,0 +1,383 @@
import { Request, Response } from 'express';
import checklistService from '../services/checklist.service';
import logger from '../utils/logger';
const param = (req: Request, key: string): string => req.params[key] as string;
class ChecklistController {
// --- Vorlagen (Templates) ---
async getVorlagen(req: Request, res: Response): Promise<void> {
try {
const filter: { fahrzeug_typ_id?: number; aktiv?: boolean } = {};
if (req.query.fahrzeug_typ_id) {
filter.fahrzeug_typ_id = parseInt(req.query.fahrzeug_typ_id as string, 10);
}
if (req.query.aktiv !== undefined) {
filter.aktiv = req.query.aktiv === 'true';
}
const vorlagen = await checklistService.getVorlagen(filter);
res.status(200).json({ success: true, data: vorlagen });
} catch (error) {
logger.error('ChecklistController.getVorlagen error', { error });
res.status(500).json({ success: false, message: 'Vorlagen konnten nicht geladen werden' });
}
}
async getVorlageById(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 vorlage = await checklistService.getVorlageById(id);
if (!vorlage) {
res.status(404).json({ success: false, message: 'Vorlage nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: vorlage });
} catch (error) {
logger.error('ChecklistController.getVorlageById error', { error });
res.status(500).json({ success: false, message: 'Vorlage konnte nicht geladen werden' });
}
}
async createVorlage(req: Request, res: Response): Promise<void> {
const { name } = req.body;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
return;
}
try {
const vorlage = await checklistService.createVorlage(req.body);
res.status(201).json({ success: true, data: vorlage });
} catch (error) {
logger.error('ChecklistController.createVorlage error', { error });
res.status(500).json({ success: false, message: 'Vorlage konnte nicht erstellt werden' });
}
}
async updateVorlage(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 vorlage = await checklistService.updateVorlage(id, req.body);
if (!vorlage) {
res.status(404).json({ success: false, message: 'Vorlage nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: vorlage });
} catch (error) {
logger.error('ChecklistController.updateVorlage error', { error });
res.status(500).json({ success: false, message: 'Vorlage konnte nicht aktualisiert werden' });
}
}
async deleteVorlage(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 vorlage = await checklistService.deleteVorlage(id);
if (!vorlage) {
res.status(404).json({ success: false, message: 'Vorlage nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Vorlage gelöscht' });
} catch (error) {
logger.error('ChecklistController.deleteVorlage error', { error });
res.status(500).json({ success: false, message: 'Vorlage konnte nicht gelöscht werden' });
}
}
// --- Vorlage Items ---
async getVorlageItems(req: Request, res: Response): Promise<void> {
const vorlageId = parseInt(param(req, 'id'), 10);
if (isNaN(vorlageId)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const items = await checklistService.getVorlageItems(vorlageId);
res.status(200).json({ success: true, data: items });
} catch (error) {
logger.error('ChecklistController.getVorlageItems error', { error });
res.status(500).json({ success: false, message: 'Items konnten nicht geladen werden' });
}
}
async addVorlageItem(req: Request, res: Response): Promise<void> {
const vorlageId = parseInt(param(req, 'id'), 10);
if (isNaN(vorlageId)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
const { bezeichnung } = req.body;
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
res.status(400).json({ success: false, message: 'Bezeichnung ist erforderlich' });
return;
}
try {
const item = await checklistService.addVorlageItem(vorlageId, req.body);
res.status(201).json({ success: true, data: item });
} catch (error) {
logger.error('ChecklistController.addVorlageItem error', { error });
res.status(500).json({ success: false, message: 'Item konnte nicht erstellt werden' });
}
}
async updateVorlageItem(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 item = await checklistService.updateVorlageItem(id, req.body);
if (!item) {
res.status(404).json({ success: false, message: 'Item nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: item });
} catch (error) {
logger.error('ChecklistController.updateVorlageItem error', { error });
res.status(500).json({ success: false, message: 'Item konnte nicht aktualisiert werden' });
}
}
async deleteVorlageItem(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 item = await checklistService.deleteVorlageItem(id);
if (!item) {
res.status(404).json({ success: false, message: 'Item nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Item gelöscht' });
} catch (error) {
logger.error('ChecklistController.deleteVorlageItem error', { error });
res.status(500).json({ success: false, message: 'Item konnte nicht gelöscht werden' });
}
}
// --- Vehicle-specific items ---
async getVehicleItems(req: Request, res: Response): Promise<void> {
const fahrzeugId = param(req, 'fahrzeugId');
if (!fahrzeugId) {
res.status(400).json({ success: false, message: 'Fahrzeug-ID erforderlich' });
return;
}
try {
const items = await checklistService.getVehicleItems(fahrzeugId);
res.status(200).json({ success: true, data: items });
} catch (error) {
logger.error('ChecklistController.getVehicleItems error', { error });
res.status(500).json({ success: false, message: 'Items konnten nicht geladen werden' });
}
}
async addVehicleItem(req: Request, res: Response): Promise<void> {
const fahrzeugId = param(req, 'fahrzeugId');
if (!fahrzeugId) {
res.status(400).json({ success: false, message: 'Fahrzeug-ID erforderlich' });
return;
}
const { bezeichnung } = req.body;
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
res.status(400).json({ success: false, message: 'Bezeichnung ist erforderlich' });
return;
}
try {
const item = await checklistService.addVehicleItem(fahrzeugId, req.body);
res.status(201).json({ success: true, data: item });
} catch (error) {
logger.error('ChecklistController.addVehicleItem error', { error });
res.status(500).json({ success: false, message: 'Item konnte nicht erstellt werden' });
}
}
async updateVehicleItem(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 item = await checklistService.updateVehicleItem(id, req.body);
if (!item) {
res.status(404).json({ success: false, message: 'Item nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: item });
} catch (error) {
logger.error('ChecklistController.updateVehicleItem error', { error });
res.status(500).json({ success: false, message: 'Item konnte nicht aktualisiert werden' });
}
}
async deleteVehicleItem(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 item = await checklistService.deleteVehicleItem(id);
if (!item) {
res.status(404).json({ success: false, message: 'Item nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Item deaktiviert' });
} catch (error) {
logger.error('ChecklistController.deleteVehicleItem error', { error });
res.status(500).json({ success: false, message: 'Item konnte nicht deaktiviert werden' });
}
}
// --- Templates for vehicle ---
async getTemplatesForVehicle(req: Request, res: Response): Promise<void> {
const fahrzeugId = param(req, 'fahrzeugId');
if (!fahrzeugId) {
res.status(400).json({ success: false, message: 'Fahrzeug-ID erforderlich' });
return;
}
try {
const templates = await checklistService.getTemplatesForVehicle(fahrzeugId);
res.status(200).json({ success: true, data: templates });
} catch (error) {
logger.error('ChecklistController.getTemplatesForVehicle error', { error });
res.status(500).json({ success: false, message: 'Checklisten konnten nicht geladen werden' });
}
}
// --- Ausführungen (Executions) ---
async startExecution(req: Request, res: Response): Promise<void> {
const { fahrzeugId, vorlageId } = req.body;
if (!fahrzeugId || !vorlageId) {
res.status(400).json({ success: false, message: 'fahrzeugId und vorlageId sind erforderlich' });
return;
}
try {
const execution = await checklistService.startExecution(fahrzeugId, vorlageId, req.user!.id);
res.status(201).json({ success: true, data: execution });
} catch (error) {
logger.error('ChecklistController.startExecution error', { error });
res.status(500).json({ success: false, message: 'Ausführung konnte nicht gestartet werden' });
}
}
async submitExecution(req: Request, res: Response): Promise<void> {
const id = param(req, 'id');
if (!id) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
const { items, notizen } = req.body;
if (!Array.isArray(items)) {
res.status(400).json({ success: false, message: 'items muss ein Array sein' });
return;
}
try {
const execution = await checklistService.submitExecution(id, items, notizen ?? null, req.user!.id);
res.status(200).json({ success: true, data: execution });
} catch (error) {
logger.error('ChecklistController.submitExecution error', { error });
res.status(500).json({ success: false, message: 'Ausführung konnte nicht abgeschlossen werden' });
}
}
async approveExecution(req: Request, res: Response): Promise<void> {
const id = param(req, 'id');
if (!id) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const execution = await checklistService.approveExecution(id, req.user!.id);
if (!execution) {
res.status(404).json({ success: false, message: 'Ausführung nicht gefunden oder Status ungültig' });
return;
}
res.status(200).json({ success: true, data: execution });
} catch (error) {
logger.error('ChecklistController.approveExecution error', { error });
res.status(500).json({ success: false, message: 'Freigabe konnte nicht erteilt werden' });
}
}
async getExecutions(req: Request, res: Response): Promise<void> {
try {
const filter: { fahrzeugId?: string; vorlageId?: number; status?: string } = {};
if (req.query.fahrzeugId) filter.fahrzeugId = req.query.fahrzeugId as string;
if (req.query.vorlageId) filter.vorlageId = parseInt(req.query.vorlageId as string, 10);
if (req.query.status) filter.status = req.query.status as string;
const executions = await checklistService.getExecutions(filter);
res.status(200).json({ success: true, data: executions });
} catch (error) {
logger.error('ChecklistController.getExecutions error', { error });
res.status(500).json({ success: false, message: 'Ausführungen konnten nicht geladen werden' });
}
}
async getExecutionById(req: Request, res: Response): Promise<void> {
const id = param(req, 'id');
if (!id) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const execution = await checklistService.getExecutionById(id);
if (!execution) {
res.status(404).json({ success: false, message: 'Ausführung nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: execution });
} catch (error) {
logger.error('ChecklistController.getExecutionById error', { error });
res.status(500).json({ success: false, message: 'Ausführung konnte nicht geladen werden' });
}
}
// --- Fälligkeiten ---
async getOverdueChecklists(_req: Request, res: Response): Promise<void> {
try {
const overdue = await checklistService.getOverdueChecklists();
res.status(200).json({ success: true, data: overdue });
} catch (error) {
logger.error('ChecklistController.getOverdueChecklists error', { error });
res.status(500).json({ success: false, message: 'Überfällige Checklisten konnten nicht geladen werden' });
}
}
async getDueChecklists(req: Request, res: Response): Promise<void> {
const fahrzeugId = param(req, 'fahrzeugId');
if (!fahrzeugId) {
res.status(400).json({ success: false, message: 'Fahrzeug-ID erforderlich' });
return;
}
try {
const due = await checklistService.getDueChecklists(fahrzeugId);
res.status(200).json({ success: true, data: due });
} catch (error) {
logger.error('ChecklistController.getDueChecklists error', { error });
res.status(500).json({ success: false, message: 'Fälligkeiten konnten nicht geladen werden' });
}
}
}
export default new ChecklistController();

View File

@@ -0,0 +1,126 @@
import { Request, Response } from 'express';
import fahrzeugTypService from '../services/fahrzeugTyp.service';
import logger from '../utils/logger';
const param = (req: Request, key: string): string => req.params[key] as string;
class FahrzeugTypController {
async getAll(_req: Request, res: Response): Promise<void> {
try {
const types = await fahrzeugTypService.getAll();
res.status(200).json({ success: true, data: types });
} catch (error) {
logger.error('FahrzeugTypController.getAll error', { error });
res.status(500).json({ success: false, message: 'Fahrzeug-Typen konnten nicht geladen werden' });
}
}
async getById(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 type = await fahrzeugTypService.getById(id);
if (!type) {
res.status(404).json({ success: false, message: 'Fahrzeug-Typ nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: type });
} catch (error) {
logger.error('FahrzeugTypController.getById error', { error });
res.status(500).json({ success: false, message: 'Fahrzeug-Typ konnte nicht geladen werden' });
}
}
async create(req: Request, res: Response): Promise<void> {
const { name } = req.body;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
return;
}
try {
const type = await fahrzeugTypService.create(req.body);
res.status(201).json({ success: true, data: type });
} catch (error) {
logger.error('FahrzeugTypController.create error', { error });
res.status(500).json({ success: false, message: 'Fahrzeug-Typ konnte nicht erstellt werden' });
}
}
async update(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 type = await fahrzeugTypService.update(id, req.body);
if (!type) {
res.status(404).json({ success: false, message: 'Fahrzeug-Typ nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: type });
} catch (error) {
logger.error('FahrzeugTypController.update error', { error });
res.status(500).json({ success: false, message: 'Fahrzeug-Typ konnte nicht aktualisiert werden' });
}
}
async delete(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 type = await fahrzeugTypService.delete(id);
if (!type) {
res.status(404).json({ success: false, message: 'Fahrzeug-Typ nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Fahrzeug-Typ gelöscht' });
} catch (error) {
logger.error('FahrzeugTypController.delete error', { error });
res.status(500).json({ success: false, message: 'Fahrzeug-Typ konnte nicht gelöscht werden' });
}
}
async getTypesForVehicle(req: Request, res: Response): Promise<void> {
const fahrzeugId = param(req, 'fahrzeugId');
if (!fahrzeugId) {
res.status(400).json({ success: false, message: 'Fahrzeug-ID erforderlich' });
return;
}
try {
const types = await fahrzeugTypService.getTypesForVehicle(fahrzeugId);
res.status(200).json({ success: true, data: types });
} catch (error) {
logger.error('FahrzeugTypController.getTypesForVehicle error', { error });
res.status(500).json({ success: false, message: 'Fahrzeug-Typen konnten nicht geladen werden' });
}
}
async setTypesForVehicle(req: Request, res: Response): Promise<void> {
const fahrzeugId = param(req, 'fahrzeugId');
if (!fahrzeugId) {
res.status(400).json({ success: false, message: 'Fahrzeug-ID erforderlich' });
return;
}
const { typIds } = req.body;
if (!Array.isArray(typIds)) {
res.status(400).json({ success: false, message: 'typIds muss ein Array sein' });
return;
}
try {
const types = await fahrzeugTypService.setTypesForVehicle(fahrzeugId, typIds);
res.status(200).json({ success: true, data: types });
} catch (error) {
logger.error('FahrzeugTypController.setTypesForVehicle error', { error });
res.status(500).json({ success: false, message: 'Fahrzeug-Typen konnten nicht gesetzt werden' });
}
}
}
export default new FahrzeugTypController();

View File

@@ -175,6 +175,7 @@ class IssueController {
titel: 'Titel geändert',
beschreibung: 'Beschreibung geändert',
typ_id: 'Typ geändert',
faellig_am: 'Fälligkeitsdatum geändert',
};
for (const [field, label] of Object.entries(fieldLabels)) {
if (field in updateData && updateData[field] !== existing[field]) {
@@ -503,6 +504,67 @@ class IssueController {
res.status(500).json({ success: false, message: 'Priorität konnte nicht deaktiviert werden' });
}
}
// --- File management ---
async uploadFile(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 Issue-ID' });
return;
}
const file = req.file as Express.Multer.File | undefined;
if (!file) {
res.status(400).json({ success: false, message: 'Keine Datei hochgeladen' });
return;
}
try {
const fileRecord = await issueService.addFile(issueId, {
dateiname: file.originalname,
dateipfad: file.path,
dateityp: file.mimetype,
dateigroesse: file.size,
}, req.user!.id);
res.status(201).json({ success: true, data: fileRecord });
} catch (error) {
logger.error('IssueController.uploadFile error', { error });
res.status(500).json({ success: false, message: 'Datei konnte nicht hochgeladen werden' });
}
}
async getFiles(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 files = await issueService.getFiles(issueId);
res.status(200).json({ success: true, data: files });
} catch (error) {
logger.error('IssueController.getFiles error', { error });
res.status(500).json({ success: false, message: 'Dateien konnten nicht geladen werden' });
}
}
async deleteFile(req: Request, res: Response): Promise<void> {
const fileId = param(req, 'fileId');
if (!fileId) {
res.status(400).json({ success: false, message: 'Ungültige Datei-ID' });
return;
}
try {
const result = await issueService.deleteFile(fileId, req.user!.id);
if (!result) {
res.status(404).json({ success: false, message: 'Datei nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Datei gelöscht' });
} catch (error) {
logger.error('IssueController.deleteFile error', { error });
res.status(500).json({ success: false, message: 'Datei konnte nicht gelöscht werden' });
}
}
}
export default new IssueController();