feat(kaisertisch): implement Kaisertisch tournament functionality with model, routes, and controller
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 57s

This commit is contained in:
Torsten Schulz (local)
2026-08-19 14:02:12 +02:00
parent d251d40868
commit 18ba120927
7 changed files with 191 additions and 27 deletions

View File

@@ -0,0 +1,59 @@
import { DiaryDate, KaisertischTournament } from '../models/index.js';
import HttpError from '../exceptions/HttpError.js';
const normalizeState = (state) => {
if (!state || typeof state !== 'object' || Array.isArray(state)) throw new HttpError('Ungültiger Kaisertisch-Stand', 400);
const tableCount = Math.min(30, Math.max(1, Number(state.tableCount) || 4));
const tables = Array.isArray(state.tables) ? state.tables.slice(0, tableCount).map((table) => ({ left: table?.left || null, right: table?.right || null })) : [];
return {
tableCount,
tables: tables.length === tableCount ? tables : Array.from({ length: tableCount }, () => ({ left: null, right: null })),
history: Array.isArray(state.history) ? state.history.slice(0, 500) : [],
round: Math.max(0, Number(state.round) || 0),
pendingWinners: state.pendingWinners && typeof state.pendingWinners === 'object' ? state.pendingWinners : {},
};
};
export const listKaisertischTournaments = async (req, res) => {
try {
const tournaments = await KaisertischTournament.findAll({
where: { clubId: req.params.clubId },
include: [{ model: DiaryDate, as: 'diaryDate', attributes: ['id', 'date'] }],
order: [[{ model: DiaryDate, as: 'diaryDate' }, 'date', 'DESC'], ['updatedAt', 'DESC']],
});
res.json(tournaments.map((tournament) => ({
diaryDateId: tournament.diaryDateId,
date: tournament.diaryDate?.date || null,
roundCount: Array.isArray(tournament.state?.history) ? tournament.state.history.length : 0,
updatedAt: tournament.updatedAt,
})));
} catch (error) {
console.error('[listKaisertischTournaments]', error);
res.status(error.statusCode || 500).json({ error: error.message || 'Kaisertisch-Turniere konnten nicht geladen werden.' });
}
};
export const getKaisertischTournament = async (req, res) => {
try {
const tournament = await KaisertischTournament.findOne({ where: { clubId: req.params.clubId, diaryDateId: req.params.dateId } });
if (!tournament) return res.status(404).json({ error: 'Kein Kaisertisch-Turnier für diesen Trainingstag gefunden.' });
res.json({ diaryDateId: tournament.diaryDateId, state: tournament.state, updatedAt: tournament.updatedAt });
} catch (error) {
console.error('[getKaisertischTournament]', error);
res.status(error.statusCode || 500).json({ error: error.message || 'Kaisertisch-Turnier konnte nicht geladen werden.' });
}
};
export const saveKaisertischTournament = async (req, res) => {
try {
const { clubId, dateId } = req.params;
const diaryDate = await DiaryDate.findOne({ where: { id: dateId, clubId } });
if (!diaryDate) throw new HttpError('Trainingstag nicht gefunden.', 404);
const state = normalizeState(req.body?.state);
const [tournament] = await KaisertischTournament.upsert({ clubId, diaryDateId: dateId, state });
res.status(200).json({ diaryDateId: Number(dateId), state: tournament?.state || state });
} catch (error) {
console.error('[saveKaisertischTournament]', error);
res.status(error.statusCode || 500).json({ error: error.message || 'Kaisertisch-Turnier konnte nicht gespeichert werden.' });
}
};

View File

@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS kaisertisch_tournaments (
id INT AUTO_INCREMENT PRIMARY KEY,
club_id INT NOT NULL,
diary_date_id INT NOT NULL,
state JSON NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uniq_kaisertisch_tournament_club_date (club_id, diary_date_id),
CONSTRAINT fk_kaisertisch_tournament_club FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE,
CONSTRAINT fk_kaisertisch_tournament_date FOREIGN KEY (diary_date_id) REFERENCES diary_dates(id) ON DELETE CASCADE
);

View File

@@ -0,0 +1,18 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import DiaryDate from './DiaryDates.js';
const KaisertischTournament = sequelize.define('KaisertischTournament', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
clubId: { type: DataTypes.INTEGER, allowNull: false, field: 'club_id', references: { model: Club, key: 'id' }, onDelete: 'CASCADE' },
diaryDateId: { type: DataTypes.INTEGER, allowNull: false, field: 'diary_date_id', references: { model: DiaryDate, key: 'id' }, onDelete: 'CASCADE' },
state: { type: DataTypes.JSON, allowNull: false },
}, {
tableName: 'kaisertisch_tournaments',
underscored: true,
timestamps: true,
indexes: [{ unique: true, fields: ['club_id', 'diary_date_id'] }],
});
export default KaisertischTournament;

View File

@@ -98,6 +98,7 @@ import MemberProfileChangeRequest from './MemberProfileChangeRequest.js';
import MemberEventResponse from './MemberEventResponse.js';
import NotificationEvent from './NotificationEvent.js';
import NotificationRecipient from './NotificationRecipient.js';
import KaisertischTournament from './KaisertischTournament.js';
import TournamentSuggestion from './TournamentSuggestion.js';
// Official tournaments relations
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
@@ -131,6 +132,10 @@ TournamentSuggestion.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
DiaryDate.belongsTo(Club, { foreignKey: 'clubId' });
Club.hasMany(DiaryDate, { foreignKey: 'clubId' });
DiaryDate.hasOne(KaisertischTournament, { foreignKey: 'diaryDateId', as: 'kaisertischTournament' });
KaisertischTournament.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDate' });
Club.hasMany(KaisertischTournament, { foreignKey: 'clubId', as: 'kaisertischTournaments' });
KaisertischTournament.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
DiaryDate.belongsToMany(Member, { through: Participant, as: 'participants', foreignKey: 'diaryDateId' });
Member.belongsToMany(DiaryDate, { through: Participant, as: 'diaryDates', foreignKey: 'memberId' });
@@ -726,6 +731,7 @@ export {
MemberEventResponse,
NotificationEvent,
NotificationRecipient,
KaisertischTournament,
TournamentSuggestion,
ClubDistributionGroupMember,
};

View File

@@ -0,0 +1,10 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { authorize } from '../middleware/authorizationMiddleware.js';
import { getKaisertischTournament, listKaisertischTournaments, saveKaisertischTournament } from '../controllers/kaisertischController.js';
const router = express.Router();
router.get('/:clubId', authenticate, authorize('diary', 'read'), listKaisertischTournaments);
router.get('/:clubId/:dateId', authenticate, authorize('diary', 'read'), getKaisertischTournament);
router.put('/:clubId/:dateId', authenticate, authorize('diary', 'write'), saveKaisertischTournament);
export default router;

View File

@@ -21,6 +21,7 @@ import {
import authRoutes from './routes/authRoutes.js';
import clubRoutes from './routes/clubRoutes.js';
import diaryRoutes from './routes/diaryRoutes.js';
import kaisertischRoutes from './routes/kaisertischRoutes.js';
import memberRoutes from './routes/memberRoutes.js';
import participantRoutes from './routes/participantRoutes.js';
import activityRoutes from './routes/activityRoutes.js';
@@ -339,6 +340,7 @@ app.use('/api/auth', authRoutes);
app.use('/api/clubs', clubRoutes);
app.use('/api/clubmembers', memberRoutes);
app.use('/api/diary', diaryRoutes);
app.use('/api/kaisertisch', kaisertischRoutes);
app.use('/api/participants', participantRoutes);
app.use('/api/activities', activityRoutes);
app.use('/api/membernotes', memberNoteRoutes);