calendar and vehicle booking rework
This commit is contained in:
@@ -103,6 +103,7 @@ import bannerRoutes from './routes/banner.routes';
|
||||
import permissionRoutes from './routes/permission.routes';
|
||||
import ausruestungsanfrageRoutes from './routes/ausruestungsanfrage.routes';
|
||||
import issueRoutes from './routes/issue.routes';
|
||||
import buchungskategorieRoutes from './routes/buchungskategorie.routes';
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/user', userRoutes);
|
||||
@@ -128,6 +129,7 @@ app.use('/api/banners', bannerRoutes);
|
||||
app.use('/api/permissions', permissionRoutes);
|
||||
app.use('/api/ausruestungsanfragen', ausruestungsanfrageRoutes);
|
||||
app.use('/api/issues', issueRoutes);
|
||||
app.use('/api/buchungskategorien', buchungskategorieRoutes);
|
||||
|
||||
// Static file serving for uploads (authenticated)
|
||||
const uploadsDir = process.env.NODE_ENV === 'production' ? '/app/uploads' : path.resolve(__dirname, '../../uploads');
|
||||
|
||||
101
backend/src/controllers/buchungskategorie.controller.ts
Normal file
101
backend/src/controllers/buchungskategorie.controller.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Request, Response } from 'express';
|
||||
import buchungskategorieService from '../services/buchungskategorie.service';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const param = (req: Request, key: string): string => req.params[key] as string;
|
||||
|
||||
class BuchungsKategorieController {
|
||||
async list(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const kategorien = await buchungskategorieService.getAll();
|
||||
res.status(200).json({ success: true, data: kategorien });
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieController.list error', { error });
|
||||
res.status(500).json({ success: false, message: 'Buchungskategorien konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async listActive(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const kategorien = await buchungskategorieService.getActive();
|
||||
res.status(200).json({ success: true, data: kategorien });
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieController.listActive error', { error });
|
||||
res.status(500).json({ success: false, message: 'Buchungskategorien konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response): Promise<void> {
|
||||
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 kategorie = await buchungskategorieService.create(req.body);
|
||||
res.status(201).json({ success: true, data: kategorie });
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieController.create error', { error });
|
||||
res.status(500).json({ success: false, message: 'Buchungskategorie 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 kategorie = await buchungskategorieService.update(id, req.body);
|
||||
if (!kategorie) {
|
||||
res.status(404).json({ success: false, message: 'Buchungskategorie nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: kategorie });
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieController.update error', { error });
|
||||
res.status(500).json({ success: false, message: 'Buchungskategorie konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async deactivate(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 kategorie = await buchungskategorieService.deactivate(id);
|
||||
if (!kategorie) {
|
||||
res.status(404).json({ success: false, message: 'Buchungskategorie nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: kategorie });
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieController.deactivate error', { error });
|
||||
res.status(500).json({ success: false, message: 'Buchungskategorie konnte nicht deaktiviert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async remove(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 kategorie = await buchungskategorieService.remove(id);
|
||||
if (!kategorie) {
|
||||
res.status(404).json({ success: false, message: 'Buchungskategorie nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: kategorie });
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieController.remove error', { error });
|
||||
res.status(500).json({ success: false, message: 'Buchungskategorie konnte nicht gelöscht werden' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new BuchungsKategorieController();
|
||||
22
backend/src/database/migrations/062_buchungs_kategorien.sql
Normal file
22
backend/src/database/migrations/062_buchungs_kategorien.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- =============================================================================
|
||||
-- Migration 062: Buchungskategorien (Booking Categories)
|
||||
-- Replaces the fahrzeug_buchung_art ENUM with a configurable categories table.
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS buchungs_kategorien (
|
||||
id SERIAL PRIMARY KEY,
|
||||
bezeichnung VARCHAR(100) NOT NULL UNIQUE,
|
||||
farbe VARCHAR(7) DEFAULT '#607D8B',
|
||||
aktiv BOOLEAN DEFAULT TRUE,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
erstellt_am TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
INSERT INTO buchungs_kategorien (bezeichnung, farbe, sort_order) VALUES
|
||||
('intern', '#1976D2', 1),
|
||||
('extern', '#388E3C', 2),
|
||||
('wartung', '#F57C00', 3),
|
||||
('reservierung', '#7B1FA2', 4),
|
||||
('lehrgang', '#D32F2F', 5),
|
||||
('sonstiges', '#607D8B', 6)
|
||||
ON CONFLICT (bezeichnung) DO NOTHING;
|
||||
56
backend/src/routes/buchungskategorie.routes.ts
Normal file
56
backend/src/routes/buchungskategorie.routes.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Router } from 'express';
|
||||
import buchungskategorieController from '../controllers/buchungskategorie.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET / — all categories (admin view, includes inactive)
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
requirePermission('kalender:view_bookings'),
|
||||
buchungskategorieController.list.bind(buchungskategorieController)
|
||||
);
|
||||
|
||||
// GET /active — only active categories (for booking forms)
|
||||
router.get(
|
||||
'/active',
|
||||
authenticate,
|
||||
requirePermission('kalender:view_bookings'),
|
||||
buchungskategorieController.listActive.bind(buchungskategorieController)
|
||||
);
|
||||
|
||||
// POST / — create new category
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
requirePermission('kalender:manage_bookings'),
|
||||
buchungskategorieController.create.bind(buchungskategorieController)
|
||||
);
|
||||
|
||||
// PATCH /:id — update category
|
||||
router.patch(
|
||||
'/:id',
|
||||
authenticate,
|
||||
requirePermission('kalender:manage_bookings'),
|
||||
buchungskategorieController.update.bind(buchungskategorieController)
|
||||
);
|
||||
|
||||
// DELETE /:id — soft-delete (deactivate)
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
requirePermission('kalender:manage_bookings'),
|
||||
buchungskategorieController.deactivate.bind(buchungskategorieController)
|
||||
);
|
||||
|
||||
// DELETE /:id/permanent — hard delete
|
||||
router.delete(
|
||||
'/:id/permanent',
|
||||
authenticate,
|
||||
requirePermission('kalender:manage_bookings'),
|
||||
buchungskategorieController.remove.bind(buchungskategorieController)
|
||||
);
|
||||
|
||||
export default router;
|
||||
105
backend/src/services/buchungskategorie.service.ts
Normal file
105
backend/src/services/buchungskategorie.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
// =============================================================================
|
||||
// BuchungsKategorie (Booking Category) Service
|
||||
// =============================================================================
|
||||
|
||||
import pool from '../config/database';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
interface CreateKategorieData {
|
||||
bezeichnung: string;
|
||||
farbe?: string;
|
||||
sort_order?: number;
|
||||
}
|
||||
|
||||
interface UpdateKategorieData {
|
||||
bezeichnung?: string;
|
||||
farbe?: string;
|
||||
aktiv?: boolean;
|
||||
sort_order?: number;
|
||||
}
|
||||
|
||||
async function getAll() {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM buchungs_kategorien ORDER BY sort_order, bezeichnung`
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieService.getAll failed', { error });
|
||||
throw new Error('Buchungskategorien konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function getActive() {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM buchungs_kategorien WHERE aktiv = TRUE ORDER BY sort_order, bezeichnung`
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieService.getActive failed', { error });
|
||||
throw new Error('Buchungskategorien konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function create(data: CreateKategorieData) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO buchungs_kategorien (bezeichnung, farbe, sort_order)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *`,
|
||||
[data.bezeichnung, data.farbe || '#607D8B', data.sort_order || 0]
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieService.create failed', { error });
|
||||
throw new Error('Buchungskategorie konnte nicht erstellt werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function update(id: number, data: UpdateKategorieData) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE buchungs_kategorien
|
||||
SET bezeichnung = COALESCE($1, bezeichnung),
|
||||
farbe = COALESCE($2, farbe),
|
||||
aktiv = COALESCE($3, aktiv),
|
||||
sort_order = COALESCE($4, sort_order)
|
||||
WHERE id = $5
|
||||
RETURNING *`,
|
||||
[data.bezeichnung, data.farbe, data.aktiv, data.sort_order, id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieService.update failed', { error, id });
|
||||
throw new Error('Buchungskategorie konnte nicht aktualisiert werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function deactivate(id: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE buchungs_kategorien SET aktiv = FALSE WHERE id = $1 RETURNING *`,
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieService.deactivate failed', { error, id });
|
||||
throw new Error('Buchungskategorie konnte nicht deaktiviert werden');
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: number) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`DELETE FROM buchungs_kategorien WHERE id = $1 RETURNING *`,
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('BuchungsKategorieService.remove failed', { error, id });
|
||||
throw new Error('Buchungskategorie konnte nicht gelöscht werden');
|
||||
}
|
||||
}
|
||||
|
||||
export default { getAll, getActive, create, update, deactivate, remove };
|
||||
Reference in New Issue
Block a user