feat: add issue kanban/attachments/deadlines, dashboard widget DnD, and checklisten system
This commit is contained in:
@@ -104,6 +104,8 @@ import permissionRoutes from './routes/permission.routes';
|
||||
import ausruestungsanfrageRoutes from './routes/ausruestungsanfrage.routes';
|
||||
import issueRoutes from './routes/issue.routes';
|
||||
import buchungskategorieRoutes from './routes/buchungskategorie.routes';
|
||||
import checklistRoutes from './routes/checklist.routes';
|
||||
import fahrzeugTypRoutes from './routes/fahrzeugTyp.routes';
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/user', userRoutes);
|
||||
@@ -130,6 +132,8 @@ app.use('/api/permissions', permissionRoutes);
|
||||
app.use('/api/ausruestungsanfragen', ausruestungsanfrageRoutes);
|
||||
app.use('/api/issues', issueRoutes);
|
||||
app.use('/api/buchungskategorien', buchungskategorieRoutes);
|
||||
app.use('/api/checklisten', checklistRoutes);
|
||||
app.use('/api/fahrzeug-typen', fahrzeugTypRoutes);
|
||||
|
||||
// Static file serving for uploads (authenticated)
|
||||
const uploadsDir = process.env.NODE_ENV === 'production' ? '/app/uploads' : path.resolve(__dirname, '../../uploads');
|
||||
|
||||
383
backend/src/controllers/checklist.controller.ts
Normal file
383
backend/src/controllers/checklist.controller.ts
Normal 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();
|
||||
126
backend/src/controllers/fahrzeugTyp.controller.ts
Normal file
126
backend/src/controllers/fahrzeugTyp.controller.ts
Normal 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();
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Migration 066: Issue due dates + file attachments
|
||||
-- Adds faellig_am column to issues and creates issue_dateien table.
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 1. Add due date column to issues
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE issues ADD COLUMN IF NOT EXISTS faellig_am TIMESTAMPTZ;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_issues_faellig_am ON issues(faellig_am) WHERE faellig_am IS NOT NULL;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 2. Issue file attachments
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS issue_dateien (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
issue_id INT NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
|
||||
dateiname VARCHAR(500) NOT NULL,
|
||||
dateipfad VARCHAR(1000) NOT NULL,
|
||||
dateityp VARCHAR(100),
|
||||
dateigroesse BIGINT,
|
||||
hochgeladen_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
hochgeladen_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_dateien_issue_id ON issue_dateien(issue_id);
|
||||
46
backend/src/database/migrations/067_fahrzeug_typen.sql
Normal file
46
backend/src/database/migrations/067_fahrzeug_typen.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- Migration 067: Fahrzeug-Typen (Vehicle Types)
|
||||
-- Dynamic vehicle type table with many-to-many junction to fahrzeuge.
|
||||
-- Seeds initial types from existing fahrzeuge.typ_schluessel values.
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 1. Fahrzeug-Typen
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fahrzeug_typen (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
beschreibung TEXT,
|
||||
icon VARCHAR(50),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 2. Junction table (many-to-many)
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fahrzeug_fahrzeug_typen (
|
||||
fahrzeug_id UUID NOT NULL REFERENCES fahrzeuge(id) ON DELETE CASCADE,
|
||||
fahrzeug_typ_id INT NOT NULL REFERENCES fahrzeug_typen(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (fahrzeug_id, fahrzeug_typ_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fft_fahrzeug_id ON fahrzeug_fahrzeug_typen(fahrzeug_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_fft_typ_id ON fahrzeug_fahrzeug_typen(fahrzeug_typ_id);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 3. Seed types from existing typ_schluessel values
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
INSERT INTO fahrzeug_typen (name)
|
||||
SELECT DISTINCT typ_schluessel
|
||||
FROM fahrzeuge
|
||||
WHERE typ_schluessel IS NOT NULL AND typ_schluessel != ''
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Populate junction table from existing assignments
|
||||
INSERT INTO fahrzeug_fahrzeug_typen (fahrzeug_id, fahrzeug_typ_id)
|
||||
SELECT f.id, ft.id
|
||||
FROM fahrzeuge f
|
||||
JOIN fahrzeug_typen ft ON ft.name = f.typ_schluessel
|
||||
WHERE f.typ_schluessel IS NOT NULL AND f.typ_schluessel != ''
|
||||
ON CONFLICT DO NOTHING;
|
||||
113
backend/src/database/migrations/068_checklisten.sql
Normal file
113
backend/src/database/migrations/068_checklisten.sql
Normal file
@@ -0,0 +1,113 @@
|
||||
-- Migration 068: Checklisten (Checklist system)
|
||||
-- Templates, vehicle-specific items, execution records, and due date tracking.
|
||||
-- Depends on: 067_fahrzeug_typen.sql (fahrzeug_typen table)
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 1. Checklist-Vorlagen (Templates)
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checklist_vorlagen (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
fahrzeug_typ_id INT REFERENCES fahrzeug_typen(id) ON DELETE SET NULL,
|
||||
intervall VARCHAR(20) CHECK (intervall IN ('weekly','monthly','yearly','custom')),
|
||||
intervall_tage INT,
|
||||
beschreibung TEXT,
|
||||
aktiv BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 2. Vorlage Items (Template line items)
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checklist_vorlage_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vorlage_id INT NOT NULL REFERENCES checklist_vorlagen(id) ON DELETE CASCADE,
|
||||
bezeichnung VARCHAR(500) NOT NULL,
|
||||
beschreibung TEXT,
|
||||
pflicht BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cvi_vorlage_id ON checklist_vorlage_items(vorlage_id);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 3. Fahrzeug-spezifische Checklist Items
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fahrzeug_checklist_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
fahrzeug_id UUID NOT NULL REFERENCES fahrzeuge(id) ON DELETE CASCADE,
|
||||
bezeichnung VARCHAR(500) NOT NULL,
|
||||
beschreibung TEXT,
|
||||
pflicht BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
aktiv BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fci_fahrzeug_id ON fahrzeug_checklist_items(fahrzeug_id);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 4. Checklist-Ausführungen (Execution records)
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checklist_ausfuehrungen (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
fahrzeug_id UUID NOT NULL REFERENCES fahrzeuge(id) ON DELETE CASCADE,
|
||||
vorlage_id INT REFERENCES checklist_vorlagen(id) ON DELETE SET NULL,
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'offen'
|
||||
CHECK (status IN ('offen','abgeschlossen','unvollstaendig','freigegeben')),
|
||||
ausgefuehrt_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
ausgefuehrt_am TIMESTAMPTZ,
|
||||
freigegeben_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
freigegeben_am TIMESTAMPTZ,
|
||||
notizen TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ca_fahrzeug_id ON checklist_ausfuehrungen(fahrzeug_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ca_vorlage_id ON checklist_ausfuehrungen(vorlage_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ca_status ON checklist_ausfuehrungen(status);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 5. Ausführung Items (Execution line items / answers)
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checklist_ausfuehrung_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
ausfuehrung_id UUID NOT NULL REFERENCES checklist_ausfuehrungen(id) ON DELETE CASCADE,
|
||||
vorlage_item_id INT REFERENCES checklist_vorlage_items(id) ON DELETE SET NULL,
|
||||
fahrzeug_item_id INT REFERENCES fahrzeug_checklist_items(id) ON DELETE SET NULL,
|
||||
bezeichnung VARCHAR(500),
|
||||
ergebnis VARCHAR(20) CHECK (ergebnis IN ('ok','nok','na')),
|
||||
kommentar TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cai_ausfuehrung_id ON checklist_ausfuehrung_items(ausfuehrung_id);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 6. Fälligkeiten (Due date tracking per vehicle+template)
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checklist_faelligkeit (
|
||||
fahrzeug_id UUID NOT NULL REFERENCES fahrzeuge(id) ON DELETE CASCADE,
|
||||
vorlage_id INT NOT NULL REFERENCES checklist_vorlagen(id) ON DELETE CASCADE,
|
||||
naechste_faellig_am DATE NOT NULL,
|
||||
letzte_ausfuehrung_id UUID REFERENCES checklist_ausfuehrungen(id) ON DELETE SET NULL,
|
||||
PRIMARY KEY (fahrzeug_id, vorlage_id)
|
||||
);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 7. Auto-update updated_at trigger for checklist_vorlagen
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_checklist_vorlagen_updated') THEN
|
||||
CREATE TRIGGER trg_checklist_vorlagen_updated BEFORE UPDATE ON checklist_vorlagen
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,78 @@
|
||||
-- Migration 069: Checklisten permissions
|
||||
-- Adds checklisten feature group and permissions, seeds group_permissions.
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 1. Feature group
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
INSERT INTO feature_groups (id, label, sort_order) VALUES
|
||||
('checklisten', 'Checklisten', 15)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 2. Permissions
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||
('checklisten:view', 'checklisten', 'Ansehen', 'Checklisten und Ausführungen einsehen', 1),
|
||||
('checklisten:execute', 'checklisten', 'Ausfüllen', 'Checklisten ausfüllen und abschließen', 2),
|
||||
('checklisten:approve', 'checklisten', 'Freigeben', 'Checklisten nach Prüfung freigeben', 3),
|
||||
('checklisten:manage_templates', 'checklisten', 'Vorlagen verwalten', 'Vorlagen und Fahrzeugtypen erstellen und bearbeiten', 4),
|
||||
('checklisten:widget', 'checklisten', 'Widget', 'Checklisten-Widget im Dashboard anzeigen', 5)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 3. Seed group permissions
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- dashboard_admin has hardwired full access (not seeded).
|
||||
|
||||
-- Kommando: all permissions
|
||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||
('dashboard_kommando', 'checklisten:view'),
|
||||
('dashboard_kommando', 'checklisten:execute'),
|
||||
('dashboard_kommando', 'checklisten:approve'),
|
||||
('dashboard_kommando', 'checklisten:manage_templates'),
|
||||
('dashboard_kommando', 'checklisten:widget')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Fahrmeister: view, execute, approve, widget (vehicle specialist)
|
||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||
('dashboard_fahrmeister', 'checklisten:view'),
|
||||
('dashboard_fahrmeister', 'checklisten:execute'),
|
||||
('dashboard_fahrmeister', 'checklisten:approve'),
|
||||
('dashboard_fahrmeister', 'checklisten:widget')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Zeugmeister: view, execute, approve, widget (equipment specialist)
|
||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||
('dashboard_zeugmeister', 'checklisten:view'),
|
||||
('dashboard_zeugmeister', 'checklisten:execute'),
|
||||
('dashboard_zeugmeister', 'checklisten:approve'),
|
||||
('dashboard_zeugmeister', 'checklisten:widget')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Chargen: view, execute, widget
|
||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||
('dashboard_chargen', 'checklisten:view'),
|
||||
('dashboard_chargen', 'checklisten:execute'),
|
||||
('dashboard_chargen', 'checklisten:widget')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Moderator: view, widget
|
||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||
('dashboard_moderator', 'checklisten:view'),
|
||||
('dashboard_moderator', 'checklisten:widget')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Atemschutz: view, execute, widget
|
||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||
('dashboard_atemschutz', 'checklisten:view'),
|
||||
('dashboard_atemschutz', 'checklisten:execute'),
|
||||
('dashboard_atemschutz', 'checklisten:widget')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Mitglied: view, widget
|
||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||
('dashboard_mitglied', 'checklisten:view'),
|
||||
('dashboard_mitglied', 'checklisten:widget')
|
||||
ON CONFLICT DO NOTHING;
|
||||
85
backend/src/jobs/checklist-reminder.job.ts
Normal file
85
backend/src/jobs/checklist-reminder.job.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import pool from '../config/database';
|
||||
import notificationService from '../services/notification.service';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
||||
let jobInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let isRunning = false;
|
||||
|
||||
async function runChecklistReminderCheck(): Promise<void> {
|
||||
if (isRunning) {
|
||||
logger.warn('ChecklistReminderJob: previous run still in progress — skipping');
|
||||
return;
|
||||
}
|
||||
isRunning = true;
|
||||
try {
|
||||
// Find overdue checklists
|
||||
const result = await pool.query(`
|
||||
SELECT cf.fahrzeug_id, cf.vorlage_id, cf.naechste_faellig_am,
|
||||
f.bezeichnung AS fahrzeug_name,
|
||||
v.name AS vorlage_name
|
||||
FROM checklist_faelligkeit cf
|
||||
JOIN fahrzeuge f ON f.id = cf.fahrzeug_id AND f.deleted_at IS NULL
|
||||
JOIN checklist_vorlagen v ON v.id = cf.vorlage_id AND v.aktiv = true
|
||||
WHERE cf.naechste_faellig_am <= CURRENT_DATE
|
||||
`);
|
||||
|
||||
if (result.rows.length === 0) return;
|
||||
|
||||
// Find users who can execute checklists (Zeugmeister, Fahrmeister, Kommandant groups)
|
||||
const usersResult = await pool.query(`
|
||||
SELECT id FROM users
|
||||
WHERE authentik_groups && ARRAY['dashboard_zeugmeister', 'dashboard_fahrmeister', 'dashboard_kommando', 'dashboard_admin']::text[]
|
||||
`);
|
||||
|
||||
const targetUserIds = usersResult.rows.map((r: any) => r.id);
|
||||
if (targetUserIds.length === 0) return;
|
||||
|
||||
for (const row of result.rows) {
|
||||
const faelligDatum = new Date(row.naechste_faellig_am).toLocaleDateString('de-AT', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
});
|
||||
|
||||
// Notify first responsible user (avoid spam by using quell_id dedup)
|
||||
for (const userId of targetUserIds) {
|
||||
await notificationService.createNotification({
|
||||
user_id: userId,
|
||||
typ: 'checklist_faellig',
|
||||
titel: `Checkliste überfällig: ${row.vorlage_name}`,
|
||||
nachricht: `Die Checkliste "${row.vorlage_name}" für ${row.fahrzeug_name} war fällig am ${faelligDatum}`,
|
||||
schwere: 'warnung',
|
||||
link: `/checklisten`,
|
||||
quell_id: `${row.fahrzeug_id}_${row.vorlage_id}`,
|
||||
quell_typ: 'checklist_faellig',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`ChecklistReminderJob: processed ${result.rows.length} overdue checklists`);
|
||||
} catch (error) {
|
||||
logger.error('ChecklistReminderJob: unexpected error', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function startChecklistReminderJob(): void {
|
||||
if (jobInterval !== null) {
|
||||
logger.warn('Checklist reminder job already running — skipping duplicate start');
|
||||
return;
|
||||
}
|
||||
// Run once after short delay, then repeat
|
||||
setTimeout(() => runChecklistReminderCheck(), 75 * 1000);
|
||||
jobInterval = setInterval(() => runChecklistReminderCheck(), INTERVAL_MS);
|
||||
logger.info('Checklist reminder job scheduled (every 15 minutes)');
|
||||
}
|
||||
|
||||
export function stopChecklistReminderJob(): void {
|
||||
if (jobInterval !== null) {
|
||||
clearInterval(jobInterval);
|
||||
jobInterval = null;
|
||||
}
|
||||
logger.info('Checklist reminder job stopped');
|
||||
}
|
||||
71
backend/src/jobs/issue-reminder.job.ts
Normal file
71
backend/src/jobs/issue-reminder.job.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import pool from '../config/database';
|
||||
import notificationService from '../services/notification.service';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
||||
let jobInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let isRunning = false;
|
||||
|
||||
async function runIssueReminderCheck(): Promise<void> {
|
||||
if (isRunning) {
|
||||
logger.warn('IssueReminderJob: previous run still in progress — skipping');
|
||||
return;
|
||||
}
|
||||
isRunning = true;
|
||||
try {
|
||||
// Find overdue issues that are not in a terminal status
|
||||
const result = await pool.query(`
|
||||
SELECT i.id, i.titel, i.faellig_am, i.erstellt_von, i.zugewiesen_an
|
||||
FROM issues i
|
||||
LEFT JOIN issue_statuses s ON s.schluessel = i.status
|
||||
WHERE i.faellig_am < NOW()
|
||||
AND (s.ist_abschluss = false OR s.ist_abschluss IS NULL)
|
||||
`);
|
||||
|
||||
for (const row of result.rows) {
|
||||
// Notify the assignee, or the creator if no assignee
|
||||
const targetUserId = row.zugewiesen_an || row.erstellt_von;
|
||||
if (!targetUserId) continue;
|
||||
|
||||
await notificationService.createNotification({
|
||||
user_id: targetUserId,
|
||||
typ: 'issue_ueberfaellig',
|
||||
titel: 'Issue überfällig',
|
||||
nachricht: `Issue "${row.titel}" ist überfällig`,
|
||||
schwere: 'warnung',
|
||||
link: `/issues/${row.id}`,
|
||||
quell_id: `issue-ueberfaellig-${row.id}`,
|
||||
quell_typ: 'issue_ueberfaellig',
|
||||
});
|
||||
}
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
logger.info(`IssueReminderJob: processed ${result.rows.length} overdue issues`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('IssueReminderJob: unexpected error', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function startIssueReminderJob(): void {
|
||||
if (jobInterval !== null) {
|
||||
logger.warn('Issue reminder job already running — skipping duplicate start');
|
||||
return;
|
||||
}
|
||||
// Run once after short delay, then repeat
|
||||
setTimeout(() => runIssueReminderCheck(), 60 * 1000);
|
||||
jobInterval = setInterval(() => runIssueReminderCheck(), INTERVAL_MS);
|
||||
logger.info('Issue reminder job scheduled (every 15 minutes)');
|
||||
}
|
||||
|
||||
export function stopIssueReminderJob(): void {
|
||||
if (jobInterval !== null) {
|
||||
clearInterval(jobInterval);
|
||||
jobInterval = null;
|
||||
}
|
||||
logger.info('Issue reminder job stopped');
|
||||
}
|
||||
@@ -94,4 +94,42 @@ const wartungOptions: any = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const uploadWartung: any = multer(wartungOptions);
|
||||
|
||||
export { UPLOAD_DIR, THUMBNAIL_DIR, WARTUNG_DIR };
|
||||
// ── Issue uploads ────────────────────────────────────────────────────────────
|
||||
|
||||
const ISSUE_DIR = path.join(APP_ROOT, 'uploads', 'issues');
|
||||
try {
|
||||
if (!fs.existsSync(ISSUE_DIR)) {
|
||||
fs.mkdirSync(ISSUE_DIR, { recursive: true });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(`Could not create issue upload directory`, { err });
|
||||
}
|
||||
|
||||
const issueStorage = multer.diskStorage({
|
||||
destination(_req: any, _file: any, cb: any) {
|
||||
cb(null, ISSUE_DIR);
|
||||
},
|
||||
filename(_req: any, file: any, cb: any) {
|
||||
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `${uniqueSuffix}${ext}`);
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const issueOptions: any = {
|
||||
storage: issueStorage,
|
||||
fileFilter(_req: any, file: any, cb: any) {
|
||||
if (ALLOWED_TYPES.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error(`Dateityp ${file.mimetype} ist nicht erlaubt.`));
|
||||
}
|
||||
},
|
||||
limits: { fileSize: 20 * 1024 * 1024 },
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const uploadIssue: any = multer(issueOptions);
|
||||
|
||||
export { UPLOAD_DIR, THUMBNAIL_DIR, WARTUNG_DIR, ISSUE_DIR };
|
||||
|
||||
163
backend/src/routes/checklist.routes.ts
Normal file
163
backend/src/routes/checklist.routes.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Router } from 'express';
|
||||
import checklistController from '../controllers/checklist.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// --- Fälligkeiten (before /:id routes) ---
|
||||
router.get(
|
||||
'/faellig',
|
||||
authenticate,
|
||||
requirePermission('checklisten:view'),
|
||||
checklistController.getOverdueChecklists.bind(checklistController)
|
||||
);
|
||||
|
||||
// --- Vorlagen (Templates) ---
|
||||
router.get(
|
||||
'/vorlagen',
|
||||
authenticate,
|
||||
requirePermission('checklisten:view'),
|
||||
checklistController.getVorlagen.bind(checklistController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/vorlagen',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
checklistController.createVorlage.bind(checklistController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/vorlagen/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:view'),
|
||||
checklistController.getVorlageById.bind(checklistController)
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/vorlagen/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
checklistController.updateVorlage.bind(checklistController)
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/vorlagen/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
checklistController.deleteVorlage.bind(checklistController)
|
||||
);
|
||||
|
||||
// --- Vorlage Items ---
|
||||
router.get(
|
||||
'/vorlagen/:id/items',
|
||||
authenticate,
|
||||
requirePermission('checklisten:view'),
|
||||
checklistController.getVorlageItems.bind(checklistController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/vorlagen/:id/items',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
checklistController.addVorlageItem.bind(checklistController)
|
||||
);
|
||||
|
||||
// Item-level routes (not nested under vorlage)
|
||||
router.put(
|
||||
'/items/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
checklistController.updateVorlageItem.bind(checklistController)
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/items/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
checklistController.deleteVorlageItem.bind(checklistController)
|
||||
);
|
||||
|
||||
// --- Vehicle-specific items ---
|
||||
router.get(
|
||||
'/fahrzeug/:fahrzeugId/items',
|
||||
authenticate,
|
||||
requirePermission('checklisten:view'),
|
||||
checklistController.getVehicleItems.bind(checklistController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/fahrzeug/:fahrzeugId/items',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
checklistController.addVehicleItem.bind(checklistController)
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/fahrzeug-items/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
checklistController.updateVehicleItem.bind(checklistController)
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/fahrzeug-items/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
checklistController.deleteVehicleItem.bind(checklistController)
|
||||
);
|
||||
|
||||
// --- Applicable checklists for a vehicle ---
|
||||
router.get(
|
||||
'/fahrzeug/:fahrzeugId/checklisten',
|
||||
authenticate,
|
||||
requirePermission('checklisten:view'),
|
||||
checklistController.getTemplatesForVehicle.bind(checklistController)
|
||||
);
|
||||
|
||||
// --- Vehicle due dates ---
|
||||
router.get(
|
||||
'/fahrzeug/:fahrzeugId/faellig',
|
||||
authenticate,
|
||||
requirePermission('checklisten:view'),
|
||||
checklistController.getDueChecklists.bind(checklistController)
|
||||
);
|
||||
|
||||
// --- Ausführungen (Executions) ---
|
||||
router.get(
|
||||
'/ausfuehrungen',
|
||||
authenticate,
|
||||
requirePermission('checklisten:view'),
|
||||
checklistController.getExecutions.bind(checklistController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/ausfuehrungen',
|
||||
authenticate,
|
||||
requirePermission('checklisten:execute'),
|
||||
checklistController.startExecution.bind(checklistController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/ausfuehrungen/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:view'),
|
||||
checklistController.getExecutionById.bind(checklistController)
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/ausfuehrungen/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:execute'),
|
||||
checklistController.submitExecution.bind(checklistController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/ausfuehrungen/:id/freigabe',
|
||||
authenticate,
|
||||
requirePermission('checklisten:approve'),
|
||||
checklistController.approveExecution.bind(checklistController)
|
||||
);
|
||||
|
||||
export default router;
|
||||
58
backend/src/routes/fahrzeugTyp.routes.ts
Normal file
58
backend/src/routes/fahrzeugTyp.routes.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Router } from 'express';
|
||||
import fahrzeugTypController from '../controllers/fahrzeugTyp.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// List all vehicle types
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
fahrzeugTypController.getAll.bind(fahrzeugTypController)
|
||||
);
|
||||
|
||||
// Get single vehicle type
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
fahrzeugTypController.getById.bind(fahrzeugTypController)
|
||||
);
|
||||
|
||||
// CRUD — admin-only
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
fahrzeugTypController.create.bind(fahrzeugTypController)
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
fahrzeugTypController.update.bind(fahrzeugTypController)
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
fahrzeugTypController.delete.bind(fahrzeugTypController)
|
||||
);
|
||||
|
||||
// Vehicle-specific type management
|
||||
router.get(
|
||||
'/vehicle/:fahrzeugId',
|
||||
authenticate,
|
||||
fahrzeugTypController.getTypesForVehicle.bind(fahrzeugTypController)
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/vehicle/:fahrzeugId',
|
||||
authenticate,
|
||||
requirePermission('checklisten:manage_templates'),
|
||||
fahrzeugTypController.setTypesForVehicle.bind(fahrzeugTypController)
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -2,6 +2,7 @@ import { Router } from 'express';
|
||||
import issueController from '../controllers/issue.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
import { uploadIssue } from '../middleware/upload';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -60,6 +61,13 @@ router.get(
|
||||
issueController.getMembers.bind(issueController)
|
||||
);
|
||||
|
||||
// --- File management (BEFORE /:id to avoid conflict) ---
|
||||
router.delete(
|
||||
'/files/:fileId',
|
||||
authenticate,
|
||||
issueController.deleteFile.bind(issueController)
|
||||
);
|
||||
|
||||
// --- Issue CRUD ---
|
||||
router.get(
|
||||
'/',
|
||||
@@ -92,6 +100,19 @@ router.post(
|
||||
issueController.addComment.bind(issueController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:id/files',
|
||||
authenticate,
|
||||
uploadIssue.single('file'),
|
||||
issueController.uploadFile.bind(issueController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id/files',
|
||||
authenticate,
|
||||
issueController.getFiles.bind(issueController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
|
||||
@@ -5,6 +5,8 @@ import { testConnection, closePool, runMigrations } from './config/database';
|
||||
import { startAuditCleanupJob, stopAuditCleanupJob } from './jobs/audit-cleanup.job';
|
||||
import { startNotificationJob, stopNotificationJob } from './jobs/notification-generation.job';
|
||||
import { startReminderJob, stopReminderJob } from './jobs/reminder.job';
|
||||
import { startIssueReminderJob, stopIssueReminderJob } from './jobs/issue-reminder.job';
|
||||
import { startChecklistReminderJob, stopChecklistReminderJob } from './jobs/checklist-reminder.job';
|
||||
import { permissionService } from './services/permission.service';
|
||||
|
||||
const startServer = async (): Promise<void> => {
|
||||
@@ -32,6 +34,12 @@ const startServer = async (): Promise<void> => {
|
||||
// Start the order reminder job
|
||||
startReminderJob();
|
||||
|
||||
// Start the issue reminder job
|
||||
startIssueReminderJob();
|
||||
|
||||
// Start the checklist reminder job
|
||||
startChecklistReminderJob();
|
||||
|
||||
// Start the server
|
||||
const server = app.listen(environment.port, () => {
|
||||
logger.info('Server started successfully', {
|
||||
@@ -56,6 +64,8 @@ const startServer = async (): Promise<void> => {
|
||||
stopAuditCleanupJob();
|
||||
stopNotificationJob();
|
||||
stopReminderJob();
|
||||
stopIssueReminderJob();
|
||||
stopChecklistReminderJob();
|
||||
|
||||
server.close(async () => {
|
||||
logger.info('HTTP server closed');
|
||||
|
||||
650
backend/src/services/checklist.service.ts
Normal file
650
backend/src/services/checklist.service.ts
Normal file
@@ -0,0 +1,650 @@
|
||||
import pool from '../config/database';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function calculateNextDueDate(intervall: string | null, intervall_tage: number | null): Date | null {
|
||||
const now = new Date();
|
||||
if (intervall_tage && intervall_tage > 0) {
|
||||
now.setDate(now.getDate() + intervall_tage);
|
||||
return now;
|
||||
}
|
||||
switch (intervall) {
|
||||
case 'weekly':
|
||||
now.setDate(now.getDate() + 7);
|
||||
return now;
|
||||
case 'monthly':
|
||||
now.setMonth(now.getMonth() + 1);
|
||||
return now;
|
||||
case 'yearly':
|
||||
now.setFullYear(now.getFullYear() + 1);
|
||||
return now;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vorlagen (Templates)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getVorlagen(filter?: { fahrzeug_typ_id?: number; aktiv?: boolean }) {
|
||||
try {
|
||||
const conditions: string[] = [];
|
||||
const values: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (filter?.fahrzeug_typ_id !== undefined) {
|
||||
conditions.push(`v.fahrzeug_typ_id = $${idx}`);
|
||||
values.push(filter.fahrzeug_typ_id);
|
||||
idx++;
|
||||
}
|
||||
if (filter?.aktiv !== undefined) {
|
||||
conditions.push(`v.aktiv = $${idx}`);
|
||||
values.push(filter.aktiv);
|
||||
idx++;
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const result = await pool.query(
|
||||
`SELECT v.*, ft.name AS fahrzeug_typ_name
|
||||
FROM checklist_vorlagen v
|
||||
LEFT JOIN fahrzeug_typen ft ON ft.id = v.fahrzeug_typ_id
|
||||
${where}
|
||||
ORDER BY v.name ASC`,
|
||||
values
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.getVorlagen failed', { error });
|
||||
throw new Error('Vorlagen konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function getVorlageById(id: number) {
|
||||
try {
|
||||
const vorlageResult = await pool.query(
|
||||
`SELECT v.*, ft.name AS fahrzeug_typ_name
|
||||
FROM checklist_vorlagen v
|
||||
LEFT JOIN fahrzeug_typen ft ON ft.id = v.fahrzeug_typ_id
|
||||
WHERE v.id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (vorlageResult.rows.length === 0) return null;
|
||||
|
||||
const vorlage = vorlageResult.rows[0];
|
||||
const itemsResult = await pool.query(
|
||||
`SELECT * FROM checklist_vorlage_items WHERE vorlage_id = $1 ORDER BY sort_order ASC, id ASC`,
|
||||
[id]
|
||||
);
|
||||
vorlage.items = itemsResult.rows;
|
||||
return vorlage;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.getVorlageById failed', { error, id });
|
||||
throw new Error('Vorlage konnte nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function createVorlage(data: {
|
||||
name: string;
|
||||
fahrzeug_typ_id?: number | null;
|
||||
intervall?: string | null;
|
||||
intervall_tage?: number | null;
|
||||
beschreibung?: string | null;
|
||||
}) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO checklist_vorlagen (name, fahrzeug_typ_id, intervall, intervall_tage, beschreibung)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[
|
||||
data.name,
|
||||
data.fahrzeug_typ_id ?? null,
|
||||
data.intervall ?? null,
|
||||
data.intervall_tage ?? null,
|
||||
data.beschreibung ?? null,
|
||||
]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.createVorlage failed', { error });
|
||||
throw new Error('Vorlage konnte nicht erstellt werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateVorlage(id: number, data: {
|
||||
name?: string;
|
||||
fahrzeug_typ_id?: number | null;
|
||||
intervall?: string | null;
|
||||
intervall_tage?: number | null;
|
||||
beschreibung?: string | null;
|
||||
aktiv?: boolean;
|
||||
}) {
|
||||
try {
|
||||
const setClauses: string[] = [];
|
||||
const values: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (data.name !== undefined) { setClauses.push(`name = $${idx}`); values.push(data.name); idx++; }
|
||||
if ('fahrzeug_typ_id' in data) { setClauses.push(`fahrzeug_typ_id = $${idx}`); values.push(data.fahrzeug_typ_id); idx++; }
|
||||
if ('intervall' in data) { setClauses.push(`intervall = $${idx}`); values.push(data.intervall); idx++; }
|
||||
if ('intervall_tage' in data) { setClauses.push(`intervall_tage = $${idx}`); values.push(data.intervall_tage); idx++; }
|
||||
if ('beschreibung' in data) { setClauses.push(`beschreibung = $${idx}`); values.push(data.beschreibung); idx++; }
|
||||
if (data.aktiv !== undefined) { setClauses.push(`aktiv = $${idx}`); values.push(data.aktiv); idx++; }
|
||||
|
||||
if (setClauses.length === 0) return getVorlageById(id);
|
||||
|
||||
values.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE checklist_vorlagen SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`,
|
||||
values
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.updateVorlage failed', { error, id });
|
||||
throw new Error('Vorlage konnte nicht aktualisiert werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVorlage(id: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`DELETE FROM checklist_vorlagen WHERE id = $1 RETURNING *`,
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.deleteVorlage failed', { error, id });
|
||||
throw new Error('Vorlage konnte nicht gelöscht werden');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vorlage Items (Template line items)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getVorlageItems(vorlageId: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM checklist_vorlage_items WHERE vorlage_id = $1 ORDER BY sort_order ASC, id ASC`,
|
||||
[vorlageId]
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.getVorlageItems failed', { error, vorlageId });
|
||||
throw new Error('Vorlage-Items konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function addVorlageItem(vorlageId: number, data: {
|
||||
bezeichnung: string;
|
||||
beschreibung?: string | null;
|
||||
pflicht?: boolean;
|
||||
sort_order?: number;
|
||||
}) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO checklist_vorlage_items (vorlage_id, bezeichnung, beschreibung, pflicht, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[vorlageId, data.bezeichnung, data.beschreibung ?? null, data.pflicht ?? true, data.sort_order ?? 0]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.addVorlageItem failed', { error, vorlageId });
|
||||
throw new Error('Vorlage-Item konnte nicht erstellt werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateVorlageItem(id: number, data: {
|
||||
bezeichnung?: string;
|
||||
beschreibung?: string | null;
|
||||
pflicht?: boolean;
|
||||
sort_order?: number;
|
||||
}) {
|
||||
try {
|
||||
const setClauses: string[] = [];
|
||||
const values: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (data.bezeichnung !== undefined) { setClauses.push(`bezeichnung = $${idx}`); values.push(data.bezeichnung); idx++; }
|
||||
if ('beschreibung' in data) { setClauses.push(`beschreibung = $${idx}`); values.push(data.beschreibung); idx++; }
|
||||
if (data.pflicht !== undefined) { setClauses.push(`pflicht = $${idx}`); values.push(data.pflicht); idx++; }
|
||||
if (data.sort_order !== undefined) { setClauses.push(`sort_order = $${idx}`); values.push(data.sort_order); idx++; }
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
const r = await pool.query(`SELECT * FROM checklist_vorlage_items WHERE id = $1`, [id]);
|
||||
return r.rows[0] || null;
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE checklist_vorlage_items SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`,
|
||||
values
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.updateVorlageItem failed', { error, id });
|
||||
throw new Error('Vorlage-Item konnte nicht aktualisiert werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVorlageItem(id: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`DELETE FROM checklist_vorlage_items WHERE id = $1 RETURNING *`,
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.deleteVorlageItem failed', { error, id });
|
||||
throw new Error('Vorlage-Item konnte nicht gelöscht werden');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fahrzeug-spezifische Items
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getVehicleItems(fahrzeugId: string) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM fahrzeug_checklist_items WHERE fahrzeug_id = $1 AND aktiv = true ORDER BY sort_order ASC, id ASC`,
|
||||
[fahrzeugId]
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.getVehicleItems failed', { error, fahrzeugId });
|
||||
throw new Error('Fahrzeug-Items konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function addVehicleItem(fahrzeugId: string, data: {
|
||||
bezeichnung: string;
|
||||
beschreibung?: string | null;
|
||||
pflicht?: boolean;
|
||||
sort_order?: number;
|
||||
}) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO fahrzeug_checklist_items (fahrzeug_id, bezeichnung, beschreibung, pflicht, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[fahrzeugId, data.bezeichnung, data.beschreibung ?? null, data.pflicht ?? true, data.sort_order ?? 0]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.addVehicleItem failed', { error, fahrzeugId });
|
||||
throw new Error('Fahrzeug-Item konnte nicht erstellt werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateVehicleItem(id: number, data: {
|
||||
bezeichnung?: string;
|
||||
beschreibung?: string | null;
|
||||
pflicht?: boolean;
|
||||
sort_order?: number;
|
||||
aktiv?: boolean;
|
||||
}) {
|
||||
try {
|
||||
const setClauses: string[] = [];
|
||||
const values: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (data.bezeichnung !== undefined) { setClauses.push(`bezeichnung = $${idx}`); values.push(data.bezeichnung); idx++; }
|
||||
if ('beschreibung' in data) { setClauses.push(`beschreibung = $${idx}`); values.push(data.beschreibung); idx++; }
|
||||
if (data.pflicht !== undefined) { setClauses.push(`pflicht = $${idx}`); values.push(data.pflicht); idx++; }
|
||||
if (data.sort_order !== undefined) { setClauses.push(`sort_order = $${idx}`); values.push(data.sort_order); idx++; }
|
||||
if (data.aktiv !== undefined) { setClauses.push(`aktiv = $${idx}`); values.push(data.aktiv); idx++; }
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
const r = await pool.query(`SELECT * FROM fahrzeug_checklist_items WHERE id = $1`, [id]);
|
||||
return r.rows[0] || null;
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE fahrzeug_checklist_items SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`,
|
||||
values
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.updateVehicleItem failed', { error, id });
|
||||
throw new Error('Fahrzeug-Item konnte nicht aktualisiert werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVehicleItem(id: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE fahrzeug_checklist_items SET aktiv = false WHERE id = $1 RETURNING *`,
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.deleteVehicleItem failed', { error, id });
|
||||
throw new Error('Fahrzeug-Item konnte nicht deaktiviert werden');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Templates for a specific vehicle (via type junction)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getTemplatesForVehicle(fahrzeugId: string) {
|
||||
try {
|
||||
// Templates that match the vehicle's types, or global templates (no type)
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT v.*, ft.name AS fahrzeug_typ_name
|
||||
FROM checklist_vorlagen v
|
||||
LEFT JOIN fahrzeug_typen ft ON ft.id = v.fahrzeug_typ_id
|
||||
WHERE v.aktiv = true
|
||||
AND (
|
||||
v.fahrzeug_typ_id IS NULL
|
||||
OR v.fahrzeug_typ_id IN (
|
||||
SELECT fahrzeug_typ_id FROM fahrzeug_fahrzeug_typen WHERE fahrzeug_id = $1
|
||||
)
|
||||
)
|
||||
ORDER BY v.name ASC`,
|
||||
[fahrzeugId]
|
||||
);
|
||||
|
||||
// Attach items to each template
|
||||
for (const vorlage of result.rows) {
|
||||
const items = await pool.query(
|
||||
`SELECT * FROM checklist_vorlage_items WHERE vorlage_id = $1 ORDER BY sort_order ASC, id ASC`,
|
||||
[vorlage.id]
|
||||
);
|
||||
vorlage.items = items.rows;
|
||||
}
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.getTemplatesForVehicle failed', { error, fahrzeugId });
|
||||
throw new Error('Checklisten für Fahrzeug konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ausführungen (Executions)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function startExecution(fahrzeugId: string, vorlageId: number, userId: string) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Create the execution record
|
||||
const execResult = await client.query(
|
||||
`INSERT INTO checklist_ausfuehrungen (fahrzeug_id, vorlage_id, ausgefuehrt_von)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *`,
|
||||
[fahrzeugId, vorlageId, userId]
|
||||
);
|
||||
const execution = execResult.rows[0];
|
||||
|
||||
// Copy template items into execution items
|
||||
const vorlageItems = await client.query(
|
||||
`SELECT * FROM checklist_vorlage_items WHERE vorlage_id = $1 ORDER BY sort_order ASC, id ASC`,
|
||||
[vorlageId]
|
||||
);
|
||||
for (const item of vorlageItems.rows) {
|
||||
await client.query(
|
||||
`INSERT INTO checklist_ausfuehrung_items (ausfuehrung_id, vorlage_item_id, bezeichnung)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[execution.id, item.id, item.bezeichnung]
|
||||
);
|
||||
}
|
||||
|
||||
// Copy vehicle-specific items
|
||||
const vehicleItems = await client.query(
|
||||
`SELECT * FROM fahrzeug_checklist_items WHERE fahrzeug_id = $1 AND aktiv = true ORDER BY sort_order ASC, id ASC`,
|
||||
[fahrzeugId]
|
||||
);
|
||||
for (const item of vehicleItems.rows) {
|
||||
await client.query(
|
||||
`INSERT INTO checklist_ausfuehrung_items (ausfuehrung_id, fahrzeug_item_id, bezeichnung)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[execution.id, item.id, item.bezeichnung]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Fetch the complete execution with items
|
||||
return getExecutionById(execution.id);
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK').catch(() => {});
|
||||
logger.error('ChecklistService.startExecution failed', { error, fahrzeugId, vorlageId });
|
||||
throw new Error('Checklist-Ausführung konnte nicht gestartet werden');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function getExecutionById(id: string) {
|
||||
try {
|
||||
const execResult = await pool.query(
|
||||
`SELECT a.*,
|
||||
f.bezeichnung AS fahrzeug_name, f.kurzname AS fahrzeug_kurzname,
|
||||
v.name AS vorlage_name,
|
||||
u1.name AS ausgefuehrt_von_name,
|
||||
u2.name AS freigegeben_von_name
|
||||
FROM checklist_ausfuehrungen a
|
||||
LEFT JOIN fahrzeuge f ON f.id = a.fahrzeug_id
|
||||
LEFT JOIN checklist_vorlagen v ON v.id = a.vorlage_id
|
||||
LEFT JOIN users u1 ON u1.id = a.ausgefuehrt_von
|
||||
LEFT JOIN users u2 ON u2.id = a.freigegeben_von
|
||||
WHERE a.id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (execResult.rows.length === 0) return null;
|
||||
|
||||
const execution = execResult.rows[0];
|
||||
const itemsResult = await pool.query(
|
||||
`SELECT * FROM checklist_ausfuehrung_items WHERE ausfuehrung_id = $1 ORDER BY id ASC`,
|
||||
[id]
|
||||
);
|
||||
execution.items = itemsResult.rows;
|
||||
return execution;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.getExecutionById failed', { error, id });
|
||||
throw new Error('Ausführung konnte nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitExecution(
|
||||
id: string,
|
||||
items: Array<{ itemId: number; ergebnis: string; kommentar?: string }>,
|
||||
notizen: string | null,
|
||||
userId: string,
|
||||
) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Update each item's result
|
||||
for (const item of items) {
|
||||
await client.query(
|
||||
`UPDATE checklist_ausfuehrung_items SET ergebnis = $1, kommentar = $2 WHERE id = $3 AND ausfuehrung_id = $4`,
|
||||
[item.ergebnis, item.kommentar ?? null, item.itemId, id]
|
||||
);
|
||||
}
|
||||
|
||||
// Check if all pflicht items have ergebnis = 'ok'
|
||||
const pflichtCheck = await client.query(
|
||||
`SELECT ai.id, ai.ergebnis, ai.vorlage_item_id, ai.fahrzeug_item_id
|
||||
FROM checklist_ausfuehrung_items ai
|
||||
LEFT JOIN checklist_vorlage_items vi ON vi.id = ai.vorlage_item_id
|
||||
LEFT JOIN fahrzeug_checklist_items fi ON fi.id = ai.fahrzeug_item_id
|
||||
WHERE ai.ausfuehrung_id = $1
|
||||
AND (COALESCE(vi.pflicht, fi.pflicht, true) = true)`,
|
||||
[id]
|
||||
);
|
||||
|
||||
const allPflichtOk = pflichtCheck.rows.every((r: any) => r.ergebnis === 'ok');
|
||||
const newStatus = allPflichtOk ? 'abgeschlossen' : 'unvollstaendig';
|
||||
|
||||
await client.query(
|
||||
`UPDATE checklist_ausfuehrungen SET status = $1, ausgefuehrt_am = NOW(), notizen = $2 WHERE id = $3`,
|
||||
[newStatus, notizen, id]
|
||||
);
|
||||
|
||||
// Update checklist_faelligkeit if completed
|
||||
if (allPflichtOk) {
|
||||
const exec = await client.query(`SELECT vorlage_id, fahrzeug_id FROM checklist_ausfuehrungen WHERE id = $1`, [id]);
|
||||
if (exec.rows.length > 0) {
|
||||
const { vorlage_id, fahrzeug_id } = exec.rows[0];
|
||||
const vorlage = await client.query(`SELECT intervall, intervall_tage FROM checklist_vorlagen WHERE id = $1`, [vorlage_id]);
|
||||
if (vorlage.rows.length > 0) {
|
||||
const nextDue = calculateNextDueDate(vorlage.rows[0].intervall, vorlage.rows[0].intervall_tage);
|
||||
if (nextDue) {
|
||||
await client.query(
|
||||
`INSERT INTO checklist_faelligkeit (fahrzeug_id, vorlage_id, naechste_faellig_am, letzte_ausfuehrung_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (fahrzeug_id, vorlage_id) DO UPDATE
|
||||
SET naechste_faellig_am = $3, letzte_ausfuehrung_id = $4`,
|
||||
[fahrzeug_id, vorlage_id, nextDue, id]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return getExecutionById(id);
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK').catch(() => {});
|
||||
logger.error('ChecklistService.submitExecution failed', { error, id });
|
||||
throw new Error('Ausführung konnte nicht abgeschlossen werden');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function approveExecution(id: string, userId: string) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE checklist_ausfuehrungen
|
||||
SET status = 'freigegeben', freigegeben_von = $1, freigegeben_am = NOW()
|
||||
WHERE id = $2 AND status IN ('abgeschlossen', 'unvollstaendig')
|
||||
RETURNING *`,
|
||||
[userId, id]
|
||||
);
|
||||
if (result.rows.length === 0) return null;
|
||||
return getExecutionById(id);
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.approveExecution failed', { error, id });
|
||||
throw new Error('Freigabe konnte nicht erteilt werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function getExecutions(filter?: { fahrzeugId?: string; vorlageId?: number; status?: string }) {
|
||||
try {
|
||||
const conditions: string[] = [];
|
||||
const values: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (filter?.fahrzeugId) {
|
||||
conditions.push(`a.fahrzeug_id = $${idx}`);
|
||||
values.push(filter.fahrzeugId);
|
||||
idx++;
|
||||
}
|
||||
if (filter?.vorlageId) {
|
||||
conditions.push(`a.vorlage_id = $${idx}`);
|
||||
values.push(filter.vorlageId);
|
||||
idx++;
|
||||
}
|
||||
if (filter?.status) {
|
||||
conditions.push(`a.status = $${idx}`);
|
||||
values.push(filter.status);
|
||||
idx++;
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const result = await pool.query(
|
||||
`SELECT a.*,
|
||||
f.bezeichnung AS fahrzeug_name, f.kurzname AS fahrzeug_kurzname,
|
||||
v.name AS vorlage_name,
|
||||
u1.name AS ausgefuehrt_von_name,
|
||||
u2.name AS freigegeben_von_name
|
||||
FROM checklist_ausfuehrungen a
|
||||
LEFT JOIN fahrzeuge f ON f.id = a.fahrzeug_id
|
||||
LEFT JOIN checklist_vorlagen v ON v.id = a.vorlage_id
|
||||
LEFT JOIN users u1 ON u1.id = a.ausgefuehrt_von
|
||||
LEFT JOIN users u2 ON u2.id = a.freigegeben_von
|
||||
${where}
|
||||
ORDER BY a.created_at DESC`,
|
||||
values
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.getExecutions failed', { error });
|
||||
throw new Error('Ausführungen konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fälligkeiten (Due dates)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getOverdueChecklists() {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT cf.*, f.bezeichnung AS fahrzeug_name, f.kurzname AS fahrzeug_kurzname,
|
||||
v.name AS vorlage_name
|
||||
FROM checklist_faelligkeit cf
|
||||
JOIN fahrzeuge f ON f.id = cf.fahrzeug_id AND f.deleted_at IS NULL
|
||||
JOIN checklist_vorlagen v ON v.id = cf.vorlage_id AND v.aktiv = true
|
||||
WHERE cf.naechste_faellig_am <= CURRENT_DATE
|
||||
ORDER BY cf.naechste_faellig_am ASC
|
||||
`);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.getOverdueChecklists failed', { error });
|
||||
throw new Error('Überfällige Checklisten konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function getDueChecklists(fahrzeugId: string) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT cf.*, v.name AS vorlage_name, v.intervall, v.intervall_tage
|
||||
FROM checklist_faelligkeit cf
|
||||
JOIN checklist_vorlagen v ON v.id = cf.vorlage_id AND v.aktiv = true
|
||||
WHERE cf.fahrzeug_id = $1
|
||||
ORDER BY cf.naechste_faellig_am ASC`,
|
||||
[fahrzeugId]
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('ChecklistService.getDueChecklists failed', { error, fahrzeugId });
|
||||
throw new Error('Fälligkeiten konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getVorlagen,
|
||||
getVorlageById,
|
||||
createVorlage,
|
||||
updateVorlage,
|
||||
deleteVorlage,
|
||||
getVorlageItems,
|
||||
addVorlageItem,
|
||||
updateVorlageItem,
|
||||
deleteVorlageItem,
|
||||
getVehicleItems,
|
||||
addVehicleItem,
|
||||
updateVehicleItem,
|
||||
deleteVehicleItem,
|
||||
getTemplatesForVehicle,
|
||||
startExecution,
|
||||
getExecutionById,
|
||||
submitExecution,
|
||||
approveExecution,
|
||||
getExecutions,
|
||||
getOverdueChecklists,
|
||||
getDueChecklists,
|
||||
};
|
||||
142
backend/src/services/fahrzeugTyp.service.ts
Normal file
142
backend/src/services/fahrzeugTyp.service.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import pool from '../config/database';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
class FahrzeugTypService {
|
||||
async getAll() {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM fahrzeug_typen ORDER BY name ASC`
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('FahrzeugTypService.getAll failed', { error });
|
||||
throw new Error('Fahrzeug-Typen konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async getById(id: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM fahrzeug_typen WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('FahrzeugTypService.getById failed', { error, id });
|
||||
throw new Error('Fahrzeug-Typ konnte nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async create(data: { name: string; beschreibung?: string; icon?: string }) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO fahrzeug_typen (name, beschreibung, icon)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *`,
|
||||
[data.name, data.beschreibung ?? null, data.icon ?? null]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('FahrzeugTypService.create failed', { error });
|
||||
throw new Error('Fahrzeug-Typ konnte nicht erstellt werden');
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: number, data: { name?: string; beschreibung?: string; icon?: string }) {
|
||||
try {
|
||||
const setClauses: string[] = [];
|
||||
const values: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (data.name !== undefined) {
|
||||
setClauses.push(`name = $${idx}`);
|
||||
values.push(data.name);
|
||||
idx++;
|
||||
}
|
||||
if (data.beschreibung !== undefined) {
|
||||
setClauses.push(`beschreibung = $${idx}`);
|
||||
values.push(data.beschreibung);
|
||||
idx++;
|
||||
}
|
||||
if (data.icon !== undefined) {
|
||||
setClauses.push(`icon = $${idx}`);
|
||||
values.push(data.icon);
|
||||
idx++;
|
||||
}
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
return this.getById(id);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const result = await pool.query(
|
||||
`UPDATE fahrzeug_typen SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`,
|
||||
values
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('FahrzeugTypService.update failed', { error, id });
|
||||
throw new Error('Fahrzeug-Typ konnte nicht aktualisiert werden');
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`DELETE FROM fahrzeug_typen WHERE id = $1 RETURNING *`,
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('FahrzeugTypService.delete failed', { error, id });
|
||||
throw new Error('Fahrzeug-Typ konnte nicht gelöscht werden');
|
||||
}
|
||||
}
|
||||
|
||||
async getTypesForVehicle(fahrzeugId: string) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT ft.* FROM fahrzeug_typen ft
|
||||
JOIN fahrzeug_fahrzeug_typen fft ON fft.fahrzeug_typ_id = ft.id
|
||||
WHERE fft.fahrzeug_id = $1
|
||||
ORDER BY ft.name ASC`,
|
||||
[fahrzeugId]
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('FahrzeugTypService.getTypesForVehicle failed', { error, fahrzeugId });
|
||||
throw new Error('Fahrzeug-Typen konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async setTypesForVehicle(fahrzeugId: string, typIds: number[]) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
await client.query(
|
||||
`DELETE FROM fahrzeug_fahrzeug_typen WHERE fahrzeug_id = $1`,
|
||||
[fahrzeugId]
|
||||
);
|
||||
|
||||
for (const typId of typIds) {
|
||||
await client.query(
|
||||
`INSERT INTO fahrzeug_fahrzeug_typen (fahrzeug_id, fahrzeug_typ_id) VALUES ($1, $2)`,
|
||||
[fahrzeugId, typId]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
return this.getTypesForVehicle(fahrzeugId);
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK').catch(() => {});
|
||||
logger.error('FahrzeugTypService.setTypesForVehicle failed', { error, fahrzeugId });
|
||||
throw new Error('Fahrzeug-Typen konnten nicht gesetzt werden');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new FahrzeugTypService();
|
||||
@@ -1,5 +1,6 @@
|
||||
import pool from '../config/database';
|
||||
import logger from '../utils/logger';
|
||||
import fs from 'fs';
|
||||
|
||||
interface IssueFilters {
|
||||
typ_id?: number[];
|
||||
@@ -92,13 +93,13 @@ async function getIssueById(id: number) {
|
||||
}
|
||||
|
||||
async function createIssue(
|
||||
data: { titel: string; beschreibung?: string; typ_id?: number; prioritaet?: string },
|
||||
data: { titel: string; beschreibung?: string; typ_id?: number; prioritaet?: string; faellig_am?: string | null },
|
||||
userId: string
|
||||
) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO issues (titel, beschreibung, typ_id, prioritaet, erstellt_von)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`INSERT INTO issues (titel, beschreibung, typ_id, prioritaet, erstellt_von, faellig_am)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *`,
|
||||
[
|
||||
data.titel,
|
||||
@@ -106,6 +107,7 @@ async function createIssue(
|
||||
data.typ_id || 3,
|
||||
data.prioritaet || 'mittel',
|
||||
userId,
|
||||
data.faellig_am || null,
|
||||
]
|
||||
);
|
||||
return getIssueById(result.rows[0].id);
|
||||
@@ -127,6 +129,7 @@ async function updateIssue(
|
||||
prioritaet?: string;
|
||||
status?: string;
|
||||
zugewiesen_an?: string | null;
|
||||
faellig_am?: string | null;
|
||||
}
|
||||
) {
|
||||
try {
|
||||
@@ -164,6 +167,11 @@ async function updateIssue(
|
||||
values.push(data.zugewiesen_an ?? null);
|
||||
idx++;
|
||||
}
|
||||
if ('faellig_am' in data) {
|
||||
setClauses.push(`faellig_am = $${idx}`);
|
||||
values.push(data.faellig_am ?? null);
|
||||
idx++;
|
||||
}
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
return getIssueById(id);
|
||||
@@ -594,6 +602,92 @@ async function deleteIssuePriority(id: number) {
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Files (Issue Dateien)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function addFile(
|
||||
issueId: number,
|
||||
fileData: { dateiname: string; dateipfad: string; dateityp: string; dateigroesse: number },
|
||||
userId: string
|
||||
) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO issue_dateien (issue_id, dateiname, dateipfad, dateityp, dateigroesse, hochgeladen_von)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *`,
|
||||
[issueId, fileData.dateiname, fileData.dateipfad, fileData.dateityp, fileData.dateigroesse, userId]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('IssueService.addFile failed', { error, issueId });
|
||||
throw new Error('Datei konnte nicht gespeichert werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function getFiles(issueId: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM issue_dateien WHERE issue_id = $1 ORDER BY hochgeladen_am DESC`,
|
||||
[issueId]
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('IssueService.getFiles failed', { error, issueId });
|
||||
throw new Error('Dateien konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile(fileId: string, userId: string) {
|
||||
try {
|
||||
const fileResult = await pool.query(
|
||||
`SELECT * FROM issue_dateien WHERE id = $1`,
|
||||
[fileId]
|
||||
);
|
||||
if (fileResult.rows.length === 0) return null;
|
||||
|
||||
const file = fileResult.rows[0];
|
||||
await pool.query(`DELETE FROM issue_dateien WHERE id = $1`, [fileId]);
|
||||
|
||||
// Remove from disk
|
||||
try {
|
||||
if (file.dateipfad && fs.existsSync(file.dateipfad)) {
|
||||
fs.unlinkSync(file.dateipfad);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Failed to delete issue file from disk', { path: file.dateipfad, error: err });
|
||||
}
|
||||
|
||||
return { dateipfad: file.dateipfad, dateiname: file.dateiname };
|
||||
} catch (error) {
|
||||
logger.error('IssueService.deleteFile failed', { error, fileId });
|
||||
throw new Error('Datei konnte nicht gelöscht werden');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Overdue Issues
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getOverdueIssues() {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT i.*, u1.name AS erstellt_von_name, u2.name AS zugewiesen_an_name
|
||||
FROM issues i
|
||||
LEFT JOIN users u1 ON u1.id = i.erstellt_von
|
||||
LEFT JOIN users u2 ON u2.id = i.zugewiesen_an
|
||||
LEFT JOIN issue_statuses s ON s.schluessel = i.status
|
||||
WHERE i.faellig_am < NOW()
|
||||
AND (s.ist_abschluss = false OR s.ist_abschluss IS NULL)
|
||||
ORDER BY i.faellig_am ASC
|
||||
`);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('IssueService.getOverdueIssues failed', { error });
|
||||
throw new Error('Überfällige Issues konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getIssues,
|
||||
getIssueById,
|
||||
@@ -618,5 +712,9 @@ export default {
|
||||
createIssuePriority,
|
||||
updateIssuePriority,
|
||||
deleteIssuePriority,
|
||||
addFile,
|
||||
getFiles,
|
||||
deleteFile,
|
||||
getOverdueIssues,
|
||||
UNASSIGN,
|
||||
};
|
||||
|
||||
@@ -137,6 +137,16 @@ class VehicleService {
|
||||
? { titel: lehrgang.rows[0].titel, beginn: lehrgang.rows[0].beginn, ende: lehrgang.rows[0].ende }
|
||||
: null;
|
||||
|
||||
// Fetch vehicle types via junction table
|
||||
const typenResult = await pool.query(
|
||||
`SELECT ft.* FROM fahrzeug_typen ft
|
||||
JOIN fahrzeug_fahrzeug_typen fft ON fft.fahrzeug_typ_id = ft.id
|
||||
WHERE fft.fahrzeug_id = $1
|
||||
ORDER BY ft.name ASC`,
|
||||
[id]
|
||||
);
|
||||
(vehicle as any).typen = typenResult.rows;
|
||||
|
||||
return vehicle;
|
||||
} catch (error) {
|
||||
logger.error('VehicleService.getVehicleById failed', { error, id });
|
||||
|
||||
Reference in New Issue
Block a user