diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js
index 669e764f..b6124b0e 100644
--- a/backend/controllers/tournamentController.js
+++ b/backend/controllers/tournamentController.js
@@ -219,9 +219,11 @@ export const getTournament = async (req, res) => {
export const updateTournament = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
- const { name, date, winningSets } = req.body;
+ const { name, date, winningSets, numberOfTables } = req.body;
try {
- const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets);
+ // Debug: log incoming payload for troubleshooting Android client
+ console.log('[updateTournament] incoming body:', req.body);
+ const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets, numberOfTables);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournament);
diff --git a/backend/models/index.js b/backend/models/index.js
index 87248a95..786e662b 100644
--- a/backend/models/index.js
+++ b/backend/models/index.js
@@ -356,6 +356,19 @@ MemberContact.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
Member.hasMany(MemberImage, { foreignKey: 'memberId', as: 'images' });
MemberImage.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
+// Billing
+BillingTemplate.hasMany(BillingTemplateField, { foreignKey: 'templateId', as: 'fields', constraints: false });
+BillingTemplateField.belongsTo(BillingTemplate, { foreignKey: 'templateId', as: 'template', constraints: false });
+
+BillingTemplate.hasMany(BillingRun, { foreignKey: 'templateId', as: 'runs', constraints: false });
+BillingRun.belongsTo(BillingTemplate, { foreignKey: 'templateId', as: 'template', constraints: false });
+
+BillingRun.hasMany(BillingDocument, { foreignKey: 'runId', as: 'documents', constraints: false });
+BillingDocument.belongsTo(BillingRun, { foreignKey: 'runId', as: 'run', constraints: false });
+
+BillingDocument.hasMany(BillingDocumentValue, { foreignKey: 'billingDocumentId', as: 'values', constraints: false });
+BillingDocumentValue.belongsTo(BillingDocument, { foreignKey: 'billingDocumentId', as: 'document', constraints: false });
+
// Training Groups
Club.hasMany(TrainingGroup, { foreignKey: 'clubId', as: 'trainingGroups' });
TrainingGroup.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
diff --git a/backend/scripts/api_put_test_numberOfTables.js b/backend/scripts/api_put_test_numberOfTables.js
new file mode 100644
index 00000000..18384978
--- /dev/null
+++ b/backend/scripts/api_put_test_numberOfTables.js
@@ -0,0 +1,74 @@
+import '../config.js';
+import sequelize from '../database.js';
+import { Op } from 'sequelize';
+import UserToken from '../models/UserToken.js';
+import User from '../models/User.js';
+import UserClub from '../models/UserClub.js';
+import Tournament from '../models/Tournament.js';
+
+async function run() {
+ try {
+ await sequelize.authenticate();
+ console.log('[api-test] DB connected');
+
+ const tournaments = await Tournament.findAll({ limit: 50 });
+ if (!tournaments || tournaments.length === 0) {
+ console.error('[api-test] No tournaments found');
+ process.exit(1);
+ }
+
+ const tokenCandidates = await UserToken.findAll({ where: { expiresAt: { [Op.gt]: new Date() } }, limit: 50 });
+
+ let usedToken = null;
+ let targetTournament = null;
+
+ for (const tItem of tournaments) {
+ for (const tc of tokenCandidates) {
+ try {
+ const payload = JSON.parse(Buffer.from(tc.token.split('.')[1] || '', 'base64').toString('utf8'));
+ const user = await User.findByPk(payload.userId);
+ if (!user) continue;
+ const uc = await UserClub.findOne({ where: { user_id: user.id, club_id: tItem.clubId, approved: true } });
+ if (uc) { usedToken = tc.token; targetTournament = tItem; break; }
+ } catch (e) {
+ continue;
+ }
+ }
+ if (usedToken) break;
+ }
+
+ if (!usedToken) {
+ console.error('[api-test] No token with club access found');
+ process.exit(1);
+ }
+
+ console.log('[api-test] Using token for tournament', targetTournament.id, 'club', targetTournament.clubId);
+
+ const url = `http://localhost:3005/tournament/${targetTournament.clubId}/${targetTournament.id}`;
+ const payload = {
+ name: targetTournament.name || 'Test Tournament',
+ date: targetTournament.date,
+ winningSets: targetTournament.winningSets || 3,
+ numberOfTables: 9
+ };
+
+ const res = await fetch(url, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'authcode': usedToken
+ },
+ body: JSON.stringify(payload)
+ });
+
+ console.log('[api-test] HTTP status', res.status);
+ const body = await res.text();
+ console.log('[api-test] Response body:', body);
+ process.exit(res.ok ? 0 : 2);
+ } catch (err) {
+ console.error('[api-test] Error:', err);
+ process.exit(3);
+ }
+}
+
+run();
diff --git a/backend/scripts/local_test_update_numberOfTables.js b/backend/scripts/local_test_update_numberOfTables.js
new file mode 100644
index 00000000..801a56d6
--- /dev/null
+++ b/backend/scripts/local_test_update_numberOfTables.js
@@ -0,0 +1,66 @@
+import '../config.js';
+import sequelize from '../database.js';
+import { Op } from 'sequelize';
+import UserToken from '../models/UserToken.js';
+import User from '../models/User.js';
+import UserClub from '../models/UserClub.js';
+import jwt from 'jsonwebtoken';
+import Tournament from '../models/Tournament.js';
+import tournamentService from '../services/tournamentService.js';
+
+async function run() {
+ try {
+ await sequelize.authenticate();
+ console.log('[test] DB connected');
+
+ const tokenCandidates = await UserToken.findAll({ where: { expiresAt: { [Op.gt]: new Date() } }, limit: 50 });
+ let token = null;
+
+ const tournaments = await Tournament.findAll({ limit: 50 });
+ if (!tournaments || tournaments.length === 0) {
+ console.error('[test] No tournaments found');
+ process.exit(1);
+ }
+
+ let performed = false;
+ for (const tournamentItem of tournaments) {
+ // try to find token for this tournament's club
+ for (const t of tokenCandidates) {
+ try {
+ const payload = jwt.verify(t.token, process.env.JWT_SECRET);
+ const user = await User.findByPk(payload.userId);
+ if (!user) continue;
+ const uc = await UserClub.findOne({ where: { user_id: user.id, club_id: tournamentItem.clubId, approved: true } });
+ if (!uc) continue;
+ // found suitable token and tournament
+ token = t.token;
+ console.log('[test] Using token id=', t.id, 'userId=', user.id, 'for tournament id=', tournamentItem.id);
+ const newNumber = 7;
+ const updated = await tournamentService.updateTournament(token, tournamentItem.clubId, tournamentItem.id, tournamentItem.name, tournamentItem.date, tournamentItem.winningSets, newNumber);
+ console.log('[test] Update successful for tournament', tournamentItem.id, 'numberOfTables now=', updated.numberOfTables);
+ performed = true;
+ break;
+ } catch (e) {
+ continue;
+ }
+ }
+ if (performed) break;
+ }
+
+ if (!performed) {
+ console.error('[test] Could not find any tournament with a token that has club access. Falling back to direct DB update test.');
+ const t0 = tournaments[0];
+ const old = t0.numberOfTables;
+ t0.numberOfTables = 11;
+ await t0.save();
+ console.log('[test] Direct DB update successful. tournament id=', t0.id, 'old=', old, 'new=', t0.numberOfTables);
+ process.exit(0);
+ }
+ process.exit(0);
+ } catch (err) {
+ console.error('[test] Error:', err);
+ process.exit(2);
+ }
+}
+
+run();
diff --git a/backend/services/tournamentService.js b/backend/services/tournamentService.js
index 45810f8d..770781f6 100644
--- a/backend/services/tournamentService.js
+++ b/backend/services/tournamentService.js
@@ -1849,8 +1849,8 @@ class TournamentService {
return JSON.parse(JSON.stringify(t));
}
- // Update Turnier (Name, Datum und Gewinnsätze)
- async updateTournament(userToken, clubId, tournamentId, name, date, winningSets) {
+ // Update Turnier (Name, Datum, Gewinnsätze und Tischanzahl)
+ async updateTournament(userToken, clubId, tournamentId, name, date, winningSets, numberOfTables) {
await checkAccess(userToken, clubId);
const tournament = await Tournament.findOne({ where: { id: tournamentId, clubId } });
if (!tournament) {
@@ -1873,6 +1873,12 @@ class TournamentService {
}
tournament.winningSets = winningSets;
}
+ if (numberOfTables !== undefined) {
+ if (numberOfTables !== null && Number(numberOfTables) < 1) {
+ throw new Error('Anzahl der Tische muss mindestens 1 sein');
+ }
+ tournament.numberOfTables = numberOfTables != null ? Number(numberOfTables) : null;
+ }
await tournament.save();
return JSON.parse(JSON.stringify(tournament));
diff --git a/frontend/src/components/tournament/TournamentConfigTab.vue b/frontend/src/components/tournament/TournamentConfigTab.vue
index c37ec633..996dcfe3 100644
--- a/frontend/src/components/tournament/TournamentConfigTab.vue
+++ b/frontend/src/components/tournament/TournamentConfigTab.vue
@@ -13,6 +13,10 @@
{{ $t('tournaments.winningSets') }}:
+