feat(kaisertisch): implement Kaisertisch tournament functionality with model, routes, and controller
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 57s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 57s
This commit is contained in:
59
backend/controllers/kaisertischController.js
Normal file
59
backend/controllers/kaisertischController.js
Normal 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.' });
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
18
backend/models/KaisertischTournament.js
Normal file
18
backend/models/KaisertischTournament.js
Normal 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;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
10
backend/routes/kaisertischRoutes.js
Normal file
10
backend/routes/kaisertischRoutes.js
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user