Implement external participant management and tournament class features
This commit enhances the tournament management system by introducing functionality for handling external participants and tournament classes. New methods are added to the `tournamentController` and `tournamentService` for adding, retrieving, updating, and removing external participants, as well as managing tournament classes. The backend models are updated to support these features, including new relationships and attributes. The frontend is also updated to allow users to manage external participants and classes, improving the overall user experience and interactivity in tournament management.
This commit is contained in:
@@ -18,9 +18,9 @@ export const getTournaments = async (req, res) => {
|
||||
// 2. Neues Turnier anlegen
|
||||
export const addTournament = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentName, date, winningSets } = req.body;
|
||||
const { clubId, tournamentName, date, winningSets, allowsExternal } = req.body;
|
||||
try {
|
||||
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets);
|
||||
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal);
|
||||
// Emit Socket-Event
|
||||
if (clubId && tournament && tournament.id) {
|
||||
emitTournamentChanged(clubId, tournament.id);
|
||||
@@ -82,9 +82,24 @@ export const setModus = async (req, res) => {
|
||||
// 6. Gruppen-Strukturen anlegen (leere Gruppen)
|
||||
export const createGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
const { clubId, tournamentId, numberOfGroups } = req.body;
|
||||
try {
|
||||
await tournamentService.createGroups(token, clubId, tournamentId);
|
||||
await tournamentService.createGroups(token, clubId, tournamentId, numberOfGroups);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 6b. Gruppen-Strukturen pro Klasse anlegen
|
||||
export const createGroupsPerClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, groupsPerClass } = req.body;
|
||||
try {
|
||||
await tournamentService.createGroupsPerClass(token, clubId, tournamentId, groupsPerClass);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
@@ -360,4 +375,128 @@ export const setMatchActive = async (req, res) => {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Externe Teilnehmer hinzufügen
|
||||
export const addExternalParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, firstName, lastName, club, birthDate } = req.body;
|
||||
try {
|
||||
await tournamentService.addExternalParticipant(token, clubId, tournamentId, firstName, lastName, club, birthDate);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Externer Teilnehmer hinzugefügt' });
|
||||
} catch (error) {
|
||||
console.error('[addExternalParticipant] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Externe Teilnehmer abrufen
|
||||
export const getExternalParticipants = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
const participants = await tournamentService.getExternalParticipants(token, clubId, tournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (error) {
|
||||
console.error('[getExternalParticipants] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Externe Teilnehmer löschen
|
||||
export const removeExternalParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.body;
|
||||
try {
|
||||
await tournamentService.removeExternalParticipant(token, clubId, tournamentId, participantId);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Externer Teilnehmer entfernt' });
|
||||
} catch (error) {
|
||||
console.error('[removeExternalParticipant] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Gesetzt-Status für externe Teilnehmer aktualisieren
|
||||
export const updateExternalParticipantSeeded = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.params;
|
||||
const { seeded } = req.body;
|
||||
try {
|
||||
await tournamentService.updateExternalParticipantSeeded(token, clubId, tournamentId, participantId, seeded);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Gesetzt-Status aktualisiert' });
|
||||
} catch (error) {
|
||||
console.error('[updateExternalParticipantSeeded] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Tournament Classes
|
||||
export const getTournamentClasses = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.params;
|
||||
try {
|
||||
const classes = await tournamentService.getTournamentClasses(token, clubId, tournamentId);
|
||||
res.status(200).json(classes);
|
||||
} catch (error) {
|
||||
console.error('[getTournamentClasses] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const addTournamentClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.params;
|
||||
const { name } = req.body;
|
||||
try {
|
||||
const tournamentClass = await tournamentService.addTournamentClass(token, clubId, tournamentId, name);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(tournamentClass);
|
||||
} catch (error) {
|
||||
console.error('[addTournamentClass] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTournamentClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId } = req.params;
|
||||
const { name, sortOrder } = req.body;
|
||||
try {
|
||||
const tournamentClass = await tournamentService.updateTournamentClass(token, clubId, tournamentId, classId, name, sortOrder);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(tournamentClass);
|
||||
} catch (error) {
|
||||
console.error('[updateTournamentClass] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTournamentClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId } = req.params;
|
||||
try {
|
||||
await tournamentService.deleteTournamentClass(token, clubId, tournamentId, classId);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Klasse gelöscht' });
|
||||
} catch (error) {
|
||||
console.error('[deleteTournamentClass] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateParticipantClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.params;
|
||||
const { classId, isExternal } = req.body;
|
||||
try {
|
||||
await tournamentService.updateParticipantClass(token, clubId, tournamentId, participantId, classId, isExternal);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Klasse aktualisiert' });
|
||||
} catch (error) {
|
||||
console.error('[updateParticipantClass] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
22
backend/migrations/add_allows_external_to_tournament.sql
Normal file
22
backend/migrations/add_allows_external_to_tournament.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Migration: Add 'allows_external' column to tournament table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament';
|
||||
SET @columnname = 'allows_external';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `winning_sets`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Migration: Add 'class_id' column to external_tournament_participant table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'external_tournament_participant';
|
||||
SET @columnname = 'class_id';
|
||||
|
||||
-- Check if column exists
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
);
|
||||
|
||||
-- Add column if it doesn't exist
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE `external_tournament_participant` ADD COLUMN `class_id` INT(11) NULL AFTER `seeded`',
|
||||
'SELECT 1 AS column_already_exists'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
27
backend/migrations/add_class_id_to_tournament_group.sql
Normal file
27
backend/migrations/add_class_id_to_tournament_group.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Migration: Add 'class_id' column to tournament_group table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_group';
|
||||
SET @columnname = 'class_id';
|
||||
|
||||
-- Check if column exists
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
);
|
||||
|
||||
-- Add column if it doesn't exist
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE `tournament_group` ADD COLUMN `class_id` INT(11) NULL AFTER `tournament_id`',
|
||||
'SELECT 1 AS column_already_exists'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
27
backend/migrations/add_class_id_to_tournament_match.sql
Normal file
27
backend/migrations/add_class_id_to_tournament_match.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Migration: Add 'class_id' column to tournament_match table
|
||||
-- Date: 2025-01-16
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_match';
|
||||
SET @columnname = 'class_id';
|
||||
|
||||
-- Check if column exists
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
);
|
||||
|
||||
-- Add column if it doesn't exist
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE `tournament_match` ADD COLUMN `class_id` INT(11) NULL AFTER `group_id`',
|
||||
'SELECT 1 AS column_already_exists'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
22
backend/migrations/add_class_id_to_tournament_member.sql
Normal file
22
backend/migrations/add_class_id_to_tournament_member.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Migration: Add 'class_id' column to tournament_member table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_member';
|
||||
SET @columnname = 'class_id';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` INT(11) NULL AFTER `seeded`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
-- Migration: Change 'ttr' column to 'birth_date' in external_tournament_participant table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'external_tournament_participant';
|
||||
SET @oldcolumnname = 'ttr';
|
||||
SET @newcolumnname = 'birth_date';
|
||||
|
||||
-- Check if old column exists
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @oldcolumnname)
|
||||
) > 0,
|
||||
CONCAT('ALTER TABLE `', @tablename, '` CHANGE COLUMN `', @oldcolumnname, '` `', @newcolumnname, '` VARCHAR(255) NULL AFTER `club`'),
|
||||
'SELECT 1'
|
||||
));
|
||||
PREPARE alterIfExists FROM @preparedStatement;
|
||||
EXECUTE alterIfExists;
|
||||
DEALLOCATE PREPARE alterIfExists;
|
||||
|
||||
-- If old column didn't exist, check if new column exists and add it if not
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @newcolumnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @newcolumnname, '` VARCHAR(255) NULL AFTER `club`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Migration: Create external_tournament_participant table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `external_tournament_participant` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`tournament_id` INT(11) NOT NULL,
|
||||
`group_id` INT(11) NULL,
|
||||
`first_name` VARCHAR(255) NOT NULL,
|
||||
`last_name` VARCHAR(255) NOT NULL,
|
||||
`club` VARCHAR(255) NULL,
|
||||
`birth_date` VARCHAR(255) NULL,
|
||||
`seeded` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_tournament_id` (`tournament_id`),
|
||||
INDEX `idx_group_id` (`group_id`),
|
||||
CONSTRAINT `fk_external_participant_tournament` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_external_participant_group` FOREIGN KEY (`group_id`) REFERENCES `tournament_group` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
16
backend/migrations/create_tournament_class_table.sql
Normal file
16
backend/migrations/create_tournament_class_table.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Migration: Create tournament_class table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `tournament_class` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`tournament_id` INT(11) NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `tournament_id` (`tournament_id`),
|
||||
CONSTRAINT `tournament_class_ibfk_1` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
89
backend/models/ExternalTournamentParticipant.js
Normal file
89
backend/models/ExternalTournamentParticipant.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import { encryptData, decryptData } from '../utils/encrypt.js';
|
||||
|
||||
const ExternalTournamentParticipant = sequelize.define('ExternalTournamentParticipant', {
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
groupId: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: false,
|
||||
allowNull: true
|
||||
},
|
||||
firstName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
set(value) {
|
||||
const encryptedValue = encryptData(value);
|
||||
this.setDataValue('firstName', encryptedValue);
|
||||
},
|
||||
get() {
|
||||
const encryptedValue = this.getDataValue('firstName');
|
||||
return decryptData(encryptedValue);
|
||||
}
|
||||
},
|
||||
lastName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
set(value) {
|
||||
const encryptedValue = encryptData(value);
|
||||
this.setDataValue('lastName', encryptedValue);
|
||||
},
|
||||
get() {
|
||||
const encryptedValue = this.getDataValue('lastName');
|
||||
return decryptData(encryptedValue);
|
||||
}
|
||||
},
|
||||
club: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
set(value) {
|
||||
if (!value) {
|
||||
this.setDataValue('club', null);
|
||||
return;
|
||||
}
|
||||
const encryptedValue = encryptData(value);
|
||||
this.setDataValue('club', encryptedValue);
|
||||
},
|
||||
get() {
|
||||
const encryptedValue = this.getDataValue('club');
|
||||
if (!encryptedValue) return null;
|
||||
return decryptData(encryptedValue);
|
||||
}
|
||||
},
|
||||
birthDate: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
set(value) {
|
||||
if (!value) {
|
||||
this.setDataValue('birthDate', null);
|
||||
return;
|
||||
}
|
||||
const encryptedValue = encryptData(value || '');
|
||||
this.setDataValue('birthDate', encryptedValue);
|
||||
},
|
||||
get() {
|
||||
const encryptedValue = this.getDataValue('birthDate');
|
||||
if (!encryptedValue) return null;
|
||||
return decryptData(encryptedValue);
|
||||
}
|
||||
},
|
||||
seeded: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'external_tournament_participant',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default ExternalTournamentParticipant;
|
||||
|
||||
@@ -34,6 +34,11 @@ const Tournament = sequelize.define('Tournament', {
|
||||
allowNull: false,
|
||||
defaultValue: 3,
|
||||
},
|
||||
allowsExternal: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament',
|
||||
|
||||
38
backend/models/TournamentClass.js
Normal file
38
backend/models/TournamentClass.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Tournament from './Tournament.js';
|
||||
|
||||
const TournamentClass = sequelize.define('TournamentClass', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Tournament,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
sortOrder: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_class',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
export default TournamentClass;
|
||||
|
||||
@@ -12,6 +12,10 @@ const TournamentGroup = sequelize.define('TournamentGroup', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_group',
|
||||
|
||||
@@ -25,6 +25,10 @@ const TournamentMatch = sequelize.define('TournamentMatch', {
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
groupRound: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
|
||||
@@ -21,6 +21,10 @@ const TournamentMember = sequelize.define('TournamentMember', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
|
||||
@@ -27,9 +27,11 @@ import Group from './Group.js';
|
||||
import GroupActivity from './GroupActivity.js';
|
||||
import Tournament from './Tournament.js';
|
||||
import TournamentGroup from './TournamentGroup.js';
|
||||
import TournamentClass from './TournamentClass.js';
|
||||
import TournamentMember from './TournamentMember.js';
|
||||
import TournamentMatch from './TournamentMatch.js';
|
||||
import TournamentResult from './TournamentResult.js';
|
||||
import ExternalTournamentParticipant from './ExternalTournamentParticipant.js';
|
||||
import Accident from './Accident.js';
|
||||
import UserToken from './UserToken.js';
|
||||
import OfficialTournament from './OfficialTournament.js';
|
||||
@@ -201,6 +203,15 @@ Member.hasMany(TournamentMember, { foreignKey: 'clubMemberId', as: 'tournamentGr
|
||||
|
||||
TournamentMember.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentMember, { foreignKey: 'tournamentId', as: 'tournamentMembers' });
|
||||
TournamentMember.belongsTo(TournamentClass, {
|
||||
foreignKey: 'classId',
|
||||
as: 'class',
|
||||
constraints: false
|
||||
});
|
||||
TournamentClass.hasMany(TournamentMember, {
|
||||
foreignKey: 'classId',
|
||||
as: 'members'
|
||||
});
|
||||
|
||||
TournamentMatch.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentMatch, { foreignKey: 'tournamentId', as: 'tournamentMatches' });
|
||||
@@ -227,6 +238,33 @@ TournamentMatch.belongsTo(TournamentMember, { foreignKey: 'player2Id', as: 'play
|
||||
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player1Id', as: 'player1Matches' });
|
||||
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player2Id', as: 'player2Matches' });
|
||||
|
||||
// Tournament Classes
|
||||
TournamentClass.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentClass, { foreignKey: 'tournamentId', as: 'classes' });
|
||||
|
||||
// External Tournament Participants
|
||||
ExternalTournamentParticipant.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(ExternalTournamentParticipant, { foreignKey: 'tournamentId', as: 'externalParticipants' });
|
||||
ExternalTournamentParticipant.belongsTo(TournamentGroup, {
|
||||
foreignKey: 'groupId',
|
||||
targetKey: 'id',
|
||||
as: 'group',
|
||||
constraints: false
|
||||
});
|
||||
TournamentGroup.hasMany(ExternalTournamentParticipant, {
|
||||
foreignKey: 'groupId',
|
||||
as: 'externalGroupMembers'
|
||||
});
|
||||
ExternalTournamentParticipant.belongsTo(TournamentClass, {
|
||||
foreignKey: 'classId',
|
||||
as: 'class',
|
||||
constraints: false
|
||||
});
|
||||
TournamentClass.hasMany(ExternalTournamentParticipant, {
|
||||
foreignKey: 'classId',
|
||||
as: 'externalParticipants'
|
||||
});
|
||||
|
||||
Accident.belongsTo(Member, { foreignKey: 'memberId', as: 'members' });
|
||||
Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
|
||||
|
||||
@@ -283,9 +321,11 @@ export {
|
||||
GroupActivity,
|
||||
Tournament,
|
||||
TournamentGroup,
|
||||
TournamentClass,
|
||||
TournamentMember,
|
||||
TournamentMatch,
|
||||
TournamentResult,
|
||||
ExternalTournamentParticipant,
|
||||
Accident,
|
||||
UserToken,
|
||||
OfficialTournament,
|
||||
|
||||
@@ -23,6 +23,16 @@ import {
|
||||
reopenMatch,
|
||||
deleteKnockoutMatches,
|
||||
setMatchActive,
|
||||
addExternalParticipant,
|
||||
getExternalParticipants,
|
||||
removeExternalParticipant,
|
||||
updateExternalParticipantSeeded,
|
||||
getTournamentClasses,
|
||||
addTournamentClass,
|
||||
updateTournamentClass,
|
||||
deleteTournamentClass,
|
||||
updateParticipantClass,
|
||||
createGroupsPerClass,
|
||||
} from '../controllers/tournamentController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
@@ -36,11 +46,12 @@ router.post('/modus', authenticate, setModus);
|
||||
router.post('/groups/reset', authenticate, resetGroups);
|
||||
router.post('/matches/reset', authenticate, resetMatches);
|
||||
router.put('/groups', authenticate, createGroups);
|
||||
router.post('/groups/create', authenticate, createGroupsPerClass);
|
||||
router.post('/groups', authenticate, fillGroups);
|
||||
router.get('/groups', authenticate, getGroups);
|
||||
router.post('/match/result', authenticate, addMatchResult);
|
||||
router.delete('/match/result', authenticate, deleteMatchResult);
|
||||
router.post("/match/reopen", reopenMatch);
|
||||
router.post("/match/reopen", authenticate, reopenMatch);
|
||||
router.post('/match/finish', authenticate, finishMatch);
|
||||
router.put('/match/:clubId/:tournamentId/:matchId/active', authenticate, setMatchActive);
|
||||
router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches);
|
||||
@@ -48,8 +59,21 @@ router.put('/:clubId/:tournamentId', authenticate, updateTournament);
|
||||
router.get('/:clubId/:tournamentId', authenticate, getTournament);
|
||||
router.get('/:clubId', authenticate, getTournaments);
|
||||
router.post('/knockout', authenticate, startKnockout);
|
||||
router.delete("/matches/knockout", deleteKnockoutMatches);
|
||||
router.delete("/matches/knockout", authenticate, deleteKnockoutMatches);
|
||||
router.post('/groups/manual', authenticate, manualAssignGroups);
|
||||
router.post('/', authenticate, addTournament);
|
||||
|
||||
// Externe Teilnehmer
|
||||
router.post('/external-participant', authenticate, addExternalParticipant);
|
||||
router.post('/external-participants', authenticate, getExternalParticipants);
|
||||
router.delete('/external-participant', authenticate, removeExternalParticipant);
|
||||
router.put('/external-participant/:clubId/:tournamentId/:participantId/seeded', authenticate, updateExternalParticipantSeeded);
|
||||
|
||||
// Tournament Classes
|
||||
router.get('/classes/:clubId/:tournamentId', authenticate, getTournamentClasses);
|
||||
router.post('/class/:clubId/:tournamentId', authenticate, addTournamentClass);
|
||||
router.put('/class/:clubId/:tournamentId/:classId', authenticate, updateTournamentClass);
|
||||
router.delete('/class/:clubId/:tournamentId/:classId', authenticate, deleteTournamentClass);
|
||||
router.put('/participant/:clubId/:tournamentId/:participantId/class', authenticate, updateParticipantClass);
|
||||
|
||||
export default router;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -95,9 +95,13 @@
|
||||
<span class="nav-icon">🏆</span>
|
||||
Interne Turniere
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('tournaments', 'read')" to="/official-tournaments" class="nav-link" title="Offizielle Turniere">
|
||||
<router-link v-if="hasPermission('tournaments', 'read')" to="/external-tournaments" class="nav-link" title="Turniere mit Externen">
|
||||
<span class="nav-icon">🌐</span>
|
||||
Offene Turniere
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('tournaments', 'read')" to="/official-tournaments" class="nav-link" title="Turnierteilnahmen">
|
||||
<span class="nav-icon">📄</span>
|
||||
Offizielle Turniere
|
||||
Turnierteilnahmen
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('predefined_activities', 'read')" to="/predefined-activities" class="nav-link" title="Vordefinierte Aktivitäten">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
|
||||
@@ -58,6 +58,7 @@ class PDFGenerator {
|
||||
this.pdf.addPage();
|
||||
this.xPos = this.margin;
|
||||
this.yPos = this.position;
|
||||
this.cursorY = this.margin;
|
||||
this.isLeftColumn = true;
|
||||
}
|
||||
|
||||
@@ -606,6 +607,552 @@ class PDFGenerator {
|
||||
this.cursorY = y + 10;
|
||||
}
|
||||
|
||||
addTournamentPDF(tournamentName, tournamentDate, groupsByClass, groupRankings, matchesByClassAndGroup, getPlayerName, knockoutRanking, participants, hasKnockoutMatches, knockoutMatches) {
|
||||
// Header
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(16);
|
||||
this.pdf.text(tournamentName || 'Turnier', this.margin, this.cursorY);
|
||||
this.cursorY += 8;
|
||||
|
||||
if (tournamentDate) {
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(12);
|
||||
const formattedDate = new Date(tournamentDate).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
this.pdf.text(`Datum: ${formattedDate}`, this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
}
|
||||
|
||||
// 1. Gesamt-Ranking nach Klassen (nur Platz und Spieler)
|
||||
// knockoutRanking und participants werden als zusätzliche Parameter übergeben
|
||||
// Verwende K.O.-Ranking nur wenn es vorhanden ist UND K.O.-Runden existieren
|
||||
const useKnockoutRanking = knockoutRanking && knockoutRanking.length > 0 && hasKnockoutMatches;
|
||||
this.addTournamentClassRankings(groupsByClass, groupRankings, getPlayerName, useKnockoutRanking ? knockoutRanking : null, participants);
|
||||
|
||||
// 2. Gruppen-Matrizen mit Ergebnissen (neue Seite)
|
||||
this.addTournamentGroupMatrices(groupsByClass, groupRankings, getPlayerName);
|
||||
|
||||
// 4. Alle Spiele nach Klasse und Gruppe sortiert (inkl. K.O.-Runden)
|
||||
this.addTournamentMatches(matchesByClassAndGroup, getPlayerName, knockoutMatches);
|
||||
}
|
||||
|
||||
addTournamentClassRankings(groupsByClass, groupRankings, getPlayerName, knockoutRanking, participants) {
|
||||
// Wenn K.O.-Runden vorhanden sind, verwende diese für das Ranking
|
||||
if (knockoutRanking && knockoutRanking.length > 0) {
|
||||
// Erstelle Mapping von Member-ID zu classId
|
||||
// Für interne Teilnehmer: member.id -> classId
|
||||
const memberClassMap = {};
|
||||
if (participants) {
|
||||
participants.forEach(p => {
|
||||
// Interne Teilnehmer haben ein member-Objekt
|
||||
if (p.member && p.member.id) {
|
||||
memberClassMap[p.member.id] = p.classId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Gruppiere K.O.-Ranking nach Klassen
|
||||
// entry.classId sollte bereits vorhanden sein (aus extendedRankingList)
|
||||
const rankingByClass = {};
|
||||
knockoutRanking.forEach(entry => {
|
||||
// Verwende classId direkt aus entry, falls vorhanden
|
||||
let classId = entry.classId;
|
||||
|
||||
// Fallback: Suche über memberId, falls classId nicht vorhanden
|
||||
if (classId == null && entry.member) {
|
||||
const memberId = entry.member.id;
|
||||
if (memberId && participants) {
|
||||
// Suche im Mapping
|
||||
classId = memberClassMap[memberId] || null;
|
||||
|
||||
// Falls nicht gefunden, suche direkt in participants
|
||||
if (!classId) {
|
||||
const participant = participants.find(p =>
|
||||
p.member && p.member.id === memberId
|
||||
);
|
||||
if (participant) {
|
||||
classId = participant.classId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const classKey = classId != null ? String(classId) : 'null';
|
||||
|
||||
if (!rankingByClass[classKey]) {
|
||||
rankingByClass[classKey] = [];
|
||||
}
|
||||
|
||||
const playerName = entry.member
|
||||
? `${entry.member.firstName || ''} ${entry.member.lastName || ''}`.trim()
|
||||
: getPlayerName(entry.player || entry);
|
||||
|
||||
rankingByClass[classKey].push({
|
||||
position: entry.position,
|
||||
name: playerName
|
||||
});
|
||||
});
|
||||
|
||||
// Sortiere innerhalb jeder Klasse nach Position
|
||||
Object.keys(rankingByClass).forEach(classKey => {
|
||||
rankingByClass[classKey].sort((a, b) => a.position - b.position);
|
||||
});
|
||||
|
||||
// Zeige Rankings nach Klassen (sortiert nach classId)
|
||||
Object.entries(rankingByClass).sort((a, b) => {
|
||||
const aNum = a[0] === 'null' ? 999999 : parseInt(a[0]);
|
||||
const bNum = b[0] === 'null' ? 999999 : parseInt(b[0]);
|
||||
return aNum - bNum;
|
||||
}).forEach(([classId, players]) => {
|
||||
const className = classId !== 'null' && classId !== 'undefined' && this.getClassNameForId ? this.getClassNameForId(classId) : null;
|
||||
|
||||
// Prüfe ob neue Seite nötig
|
||||
if (this.cursorY > 250) {
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
}
|
||||
|
||||
// Klassen-Überschrift
|
||||
if (className) {
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text(className, this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
}
|
||||
|
||||
// Zeige Spieler
|
||||
players.forEach(player => {
|
||||
if (this.cursorY > 280) {
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
// Klassen-Überschrift erneut anzeigen bei Seitenwechsel
|
||||
if (className) {
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text(className, this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
}
|
||||
}
|
||||
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(11);
|
||||
const playerText = `${player.position}. ${player.name}`;
|
||||
this.pdf.text(playerText, this.margin, this.cursorY);
|
||||
this.cursorY += 7;
|
||||
});
|
||||
|
||||
// Abstand nach Klasse
|
||||
this.cursorY += 5;
|
||||
});
|
||||
} else {
|
||||
// Fallback: Verwende Gruppen-Rankings (nur wenn keine K.O.-Runden)
|
||||
// Aber: Wenn es K.O.-Runden gibt, aber rankingList leer ist, bedeutet das,
|
||||
// dass die K.O.-Runden noch nicht abgeschlossen sind
|
||||
// In diesem Fall sollten wir trotzdem die Gruppen-Rankings verwenden
|
||||
Object.entries(groupsByClass).forEach(([classId, groups]) => {
|
||||
const className = classId !== 'null' && classId !== 'undefined' && this.getClassNameForId ? this.getClassNameForId(classId) : null;
|
||||
|
||||
// Prüfe ob neue Seite nötig
|
||||
if (this.cursorY > 250) {
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
}
|
||||
|
||||
// Klassen-Überschrift
|
||||
if (className) {
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text(className, this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
}
|
||||
|
||||
// Sammle alle Spieler aus allen Gruppen dieser Klasse
|
||||
// WICHTIG: Zeige alle Spieler, nicht nur die ersten Plätze
|
||||
const allPlayers = [];
|
||||
groups.forEach(group => {
|
||||
const rankings = groupRankings[group.groupId] || [];
|
||||
rankings.forEach(player => {
|
||||
allPlayers.push({
|
||||
position: player.position,
|
||||
name: player.name,
|
||||
seeded: player.seeded
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Sortiere nach Position (1, 1, 2, 2, 3, 3, etc.)
|
||||
allPlayers.sort((a, b) => {
|
||||
if (a.position !== b.position) {
|
||||
return a.position - b.position;
|
||||
}
|
||||
// Bei gleicher Position alphabetisch sortieren
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
// Erstelle einfache Liste: nur Platz und Spieler
|
||||
allPlayers.forEach(player => {
|
||||
if (this.cursorY > 280) {
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
// Klassen-Überschrift erneut anzeigen bei Seitenwechsel
|
||||
if (className) {
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text(className, this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
}
|
||||
}
|
||||
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(11);
|
||||
const playerText = `${player.position}. ${player.seeded ? '★ ' : ''}${player.name}`;
|
||||
this.pdf.text(playerText, this.margin, this.cursorY);
|
||||
this.cursorY += 7;
|
||||
});
|
||||
|
||||
// Abstand nach Klasse
|
||||
this.cursorY += 5;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addTournamentTables(groupsByClass, groupRankings, getPlayerName) {
|
||||
// Für jede Klasse
|
||||
Object.entries(groupsByClass).forEach(([classId, groups]) => {
|
||||
const className = classId !== 'null' && classId !== 'undefined' && this.getClassNameForId ? this.getClassNameForId(classId) : null;
|
||||
|
||||
// Für jede Gruppe
|
||||
groups.forEach(group => {
|
||||
if (this.cursorY > 250) {
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
}
|
||||
|
||||
// Überschrift
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(12);
|
||||
let title = `Gruppe ${group.groupNumber}`;
|
||||
if (className) {
|
||||
title = `${className} - ${title}`;
|
||||
}
|
||||
this.pdf.text(title, this.margin, this.cursorY);
|
||||
this.cursorY += 8;
|
||||
|
||||
// Tabelle mit Rankings
|
||||
const rankings = groupRankings[group.groupId] || [];
|
||||
if (rankings.length > 0) {
|
||||
const head = [['Platz', 'Spieler', 'Punkte', 'Sätze', 'Diff']];
|
||||
const body = rankings.map(p => [
|
||||
`${p.position}.`,
|
||||
(p.seeded ? '★ ' : '') + p.name,
|
||||
p.points.toString(),
|
||||
`${p.setsWon}:${p.setsLost}`,
|
||||
(p.setDiff >= 0 ? '+' : '') + p.setDiff.toString()
|
||||
]);
|
||||
|
||||
autoTable(this.pdf, {
|
||||
startY: this.cursorY,
|
||||
margin: { left: this.margin, right: this.margin },
|
||||
head,
|
||||
body,
|
||||
theme: 'grid',
|
||||
styles: { fontSize: 10 },
|
||||
headStyles: { fillColor: [220, 220, 220], textColor: 0, halign: 'left', fontStyle: 'bold' },
|
||||
didDrawPage: (data) => {
|
||||
this.cursorY = data.cursor.y + 10;
|
||||
}
|
||||
});
|
||||
this.cursorY += 5;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addTournamentKnockoutRanking(knockoutRanking, getPlayerName) {
|
||||
// Prüfe ob neue Seite nötig
|
||||
if (this.cursorY > 200) {
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
}
|
||||
|
||||
// Überschrift
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text('Gesamt-Ranking (K.O.-Runden)', this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
|
||||
// Ranking-Tabelle
|
||||
const head = [['Platz', 'Spieler']];
|
||||
const body = knockoutRanking.map(entry => {
|
||||
const playerName = entry.member
|
||||
? `${entry.member.firstName || ''} ${entry.member.lastName || ''}`.trim()
|
||||
: getPlayerName(entry.player || entry);
|
||||
return [
|
||||
`${entry.position}.`,
|
||||
playerName
|
||||
];
|
||||
});
|
||||
|
||||
autoTable(this.pdf, {
|
||||
startY: this.cursorY,
|
||||
margin: { left: this.margin, right: this.margin },
|
||||
head,
|
||||
body,
|
||||
theme: 'grid',
|
||||
styles: { fontSize: 11 },
|
||||
headStyles: { fillColor: [220, 220, 220], textColor: 0, halign: 'left', fontStyle: 'bold' },
|
||||
didDrawPage: (data) => {
|
||||
this.cursorY = data.cursor.y + 10;
|
||||
}
|
||||
});
|
||||
this.cursorY += 5;
|
||||
}
|
||||
|
||||
addTournamentGroupMatrices(groupsByClass, groupRankings, getPlayerName) {
|
||||
// Neue Seite für Matrizen
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text('Gruppen-Matrizen', this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
|
||||
// Für jede Klasse
|
||||
Object.entries(groupsByClass).forEach(([classId, groups]) => {
|
||||
const className = classId !== 'null' && classId !== 'undefined' && this.getClassNameForId ? this.getClassNameForId(classId) : null;
|
||||
|
||||
// Für jede Gruppe
|
||||
groups.forEach(group => {
|
||||
if (this.cursorY > 200) {
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
}
|
||||
|
||||
const rankings = groupRankings[group.groupId] || [];
|
||||
if (rankings.length === 0) return;
|
||||
|
||||
// Überschrift
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(11);
|
||||
let title = `Gruppe ${group.groupNumber}`;
|
||||
if (className) {
|
||||
title = `${className} - ${title}`;
|
||||
}
|
||||
this.pdf.text(title, this.margin, this.cursorY);
|
||||
this.cursorY += 7;
|
||||
|
||||
// Matrix erstellen
|
||||
const head = [['', ...rankings.map((p, idx) => `G${String.fromCharCode(96 + group.groupNumber)}${idx + 1}`)]];
|
||||
const body = rankings.map((player, idx) => {
|
||||
const row = [`G${String.fromCharCode(96 + group.groupNumber)}${idx + 1} ${(player.seeded ? '★ ' : '') + player.name}`];
|
||||
rankings.forEach((opponent, oppIdx) => {
|
||||
if (idx === oppIdx) {
|
||||
row.push('-');
|
||||
} else {
|
||||
// Finde Match-Ergebnis
|
||||
const match = this.findMatchResult ? this.findMatchResult(player.id, opponent.id, group.groupId) : null;
|
||||
row.push(match || '-');
|
||||
}
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
autoTable(this.pdf, {
|
||||
startY: this.cursorY,
|
||||
margin: { left: this.margin, right: this.margin },
|
||||
head,
|
||||
body,
|
||||
theme: 'grid',
|
||||
styles: { fontSize: 8 },
|
||||
headStyles: { fillColor: [220, 220, 220], textColor: 0, halign: 'left', fontStyle: 'bold' },
|
||||
columnStyles: {
|
||||
0: { cellWidth: 50 }
|
||||
},
|
||||
didDrawPage: (data) => {
|
||||
this.cursorY = data.cursor.y + 10;
|
||||
}
|
||||
});
|
||||
this.cursorY += 5;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addTournamentMatches(matchesByClassAndGroup, getPlayerName, knockoutMatches = []) {
|
||||
// Neue Seite für Spiele
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
this.pdf.text('Alle Spiele', this.margin, this.cursorY);
|
||||
this.cursorY += 10;
|
||||
|
||||
// Für jede Klasse
|
||||
Object.entries(matchesByClassAndGroup).forEach(([classId, groups]) => {
|
||||
const className = classId !== 'null' && classId !== 'undefined' && this.getClassNameForId ? this.getClassNameForId(classId) : null;
|
||||
|
||||
// Für jede Gruppe
|
||||
Object.entries(groups).forEach(([groupId, matches]) => {
|
||||
if (matches.length === 0) return;
|
||||
|
||||
if (this.cursorY > 200) {
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
}
|
||||
|
||||
// Überschrift
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(11);
|
||||
let title = `Gruppe ${matches[0].groupNumber || groupId}`;
|
||||
if (className) {
|
||||
title = `${className} - ${title}`;
|
||||
}
|
||||
this.pdf.text(title, this.margin, this.cursorY);
|
||||
this.cursorY += 7;
|
||||
|
||||
// Spiele-Tabelle
|
||||
const head = [['Runde', 'Spieler 1', 'Spieler 2', 'Ergebnis', 'Sätze']];
|
||||
const body = matches.map(m => [
|
||||
m.groupRound?.toString() || '-',
|
||||
getPlayerName(m.player1),
|
||||
getPlayerName(m.player2),
|
||||
m.isFinished ? (m.result || '-') : '-',
|
||||
this.formatSets(m)
|
||||
]);
|
||||
|
||||
autoTable(this.pdf, {
|
||||
startY: this.cursorY,
|
||||
margin: { left: this.margin, right: this.margin },
|
||||
head,
|
||||
body,
|
||||
theme: 'grid',
|
||||
styles: { fontSize: 9 },
|
||||
headStyles: { fillColor: [220, 220, 220], textColor: 0, halign: 'left', fontStyle: 'bold' },
|
||||
didDrawPage: (data) => {
|
||||
this.cursorY = data.cursor.y + 10;
|
||||
}
|
||||
});
|
||||
this.cursorY += 5;
|
||||
});
|
||||
});
|
||||
|
||||
// K.O.-Runden hinzufügen (wenn vorhanden)
|
||||
if (knockoutMatches && knockoutMatches.length > 0) {
|
||||
// Gruppiere K.O.-Matches nach Klassen
|
||||
const knockoutMatchesByClass = {};
|
||||
knockoutMatches.forEach(match => {
|
||||
const classKey = match.classId != null ? String(match.classId) : 'null';
|
||||
if (!knockoutMatchesByClass[classKey]) {
|
||||
knockoutMatchesByClass[classKey] = [];
|
||||
}
|
||||
knockoutMatchesByClass[classKey].push(match);
|
||||
});
|
||||
|
||||
// Sortiere Klassen
|
||||
const sortedClasses = Object.keys(knockoutMatchesByClass).sort((a, b) => {
|
||||
const aNum = a === 'null' ? 999999 : parseInt(a);
|
||||
const bNum = b === 'null' ? 999999 : parseInt(b);
|
||||
return aNum - bNum;
|
||||
});
|
||||
|
||||
// Für jede Klasse
|
||||
sortedClasses.forEach(classKey => {
|
||||
const classMatches = knockoutMatchesByClass[classKey];
|
||||
const className = classKey !== 'null' && classKey !== 'undefined' && this.getClassNameForId ? this.getClassNameForId(classKey) : null;
|
||||
|
||||
// Sortiere Matches nach Runde (frühere Runden zuerst: Achtelfinale, Viertelfinale, Halbfinale, Finale)
|
||||
const getRoundType = (roundName) => {
|
||||
if (!roundName) return 999;
|
||||
if (roundName.includes('Achtelfinale')) return 0;
|
||||
if (roundName.includes('Viertelfinale')) return 1;
|
||||
if (roundName.includes('Halbfinale')) return 2;
|
||||
if (roundName.includes('Finale')) return 3;
|
||||
// Für Runden wie "6-Runde", "8-Runde" etc. - extrahiere die Zahl
|
||||
const numberMatch = roundName.match(/(\d+)-Runde/);
|
||||
if (numberMatch) {
|
||||
const num = parseInt(numberMatch[1]);
|
||||
// Größere Zahlen = frühere Runden, also umgekehrt sortieren
|
||||
return -num; // Negativ, damit größere Zahlen zuerst kommen
|
||||
}
|
||||
return 999; // Unbekannte Runden zuletzt
|
||||
};
|
||||
|
||||
classMatches.sort((a, b) => {
|
||||
const aRoundType = getRoundType(a.round);
|
||||
const bRoundType = getRoundType(b.round);
|
||||
if (aRoundType !== bRoundType) {
|
||||
return aRoundType - bRoundType;
|
||||
}
|
||||
// Wenn gleicher Typ, alphabetisch sortieren
|
||||
return (a.round || '').localeCompare(b.round || '');
|
||||
});
|
||||
|
||||
if (this.cursorY > 200) {
|
||||
this.addNewPage();
|
||||
this.cursorY = this.margin;
|
||||
}
|
||||
|
||||
// Überschrift
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(11);
|
||||
let title = 'K.-o.-Runde';
|
||||
if (className) {
|
||||
title = `${className} - ${title}`;
|
||||
}
|
||||
this.pdf.text(title, this.margin, this.cursorY);
|
||||
this.cursorY += 7;
|
||||
|
||||
// Spiele-Tabelle
|
||||
const head = [['Runde', 'Spieler 1', 'Spieler 2', 'Ergebnis', 'Sätze']];
|
||||
const body = classMatches.map(m => [
|
||||
m.round || '-',
|
||||
getPlayerName(m.player1),
|
||||
getPlayerName(m.player2),
|
||||
m.isFinished ? (m.result || '-') : '-',
|
||||
this.formatSets(m)
|
||||
]);
|
||||
|
||||
autoTable(this.pdf, {
|
||||
startY: this.cursorY,
|
||||
margin: { left: this.margin, right: this.margin },
|
||||
head,
|
||||
body,
|
||||
theme: 'grid',
|
||||
styles: { fontSize: 9 },
|
||||
headStyles: { fillColor: [220, 220, 220], textColor: 0, halign: 'left', fontStyle: 'bold' },
|
||||
didDrawPage: (data) => {
|
||||
this.cursorY = data.cursor.y + 10;
|
||||
}
|
||||
});
|
||||
this.cursorY += 5;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getClassNameForId(classId, groupsByClass) {
|
||||
// Versuche, den Klassennamen zu finden
|
||||
// Dies sollte von außen übergeben werden, aber als Fallback:
|
||||
return `Klasse ${classId}`;
|
||||
}
|
||||
|
||||
findMatchResult(player1Id, player2Id, groupId) {
|
||||
// Diese Methode sollte von außen übergeben werden
|
||||
// Für jetzt: Rückgabe von null
|
||||
return null;
|
||||
}
|
||||
|
||||
formatSets(match) {
|
||||
if (!match.tournamentResults || match.tournamentResults.length === 0) {
|
||||
return '-';
|
||||
}
|
||||
return match.tournamentResults
|
||||
.sort((a, b) => a.set - b.set)
|
||||
.map(r => `${Math.abs(r.pointsPlayer1)}:${Math.abs(r.pointsPlayer2)}`)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PDFGenerator;
|
||||
|
||||
@@ -10,6 +10,7 @@ import DiaryView from './views/DiaryView.vue';
|
||||
import PendingApprovalsView from './views/PendingApprovalsView.vue';
|
||||
import ScheduleView from './views/ScheduleView.vue';
|
||||
import TournamentsView from './views/TournamentsView.vue';
|
||||
import ExternalTournamentsView from './views/ExternalTournamentsView.vue';
|
||||
import TrainingStatsView from './views/TrainingStatsView.vue';
|
||||
import ClubSettings from './views/ClubSettings.vue';
|
||||
import PredefinedActivities from './views/PredefinedActivities.vue';
|
||||
@@ -33,7 +34,8 @@ const routes = [
|
||||
{ path: '/diary', component: DiaryView },
|
||||
{ path: '/pending-approvals', component: PendingApprovalsView},
|
||||
{ path: '/schedule', component: ScheduleView},
|
||||
{ path: '/tournaments', component: TournamentsView },
|
||||
{ path: '/tournaments', component: TournamentsView, props: { allowsExternal: false } },
|
||||
{ path: '/external-tournaments', component: ExternalTournamentsView },
|
||||
{ path: '/training-stats', component: TrainingStatsView },
|
||||
{ path: '/club-settings', component: ClubSettings },
|
||||
{ path: '/predefined-activities', component: PredefinedActivities },
|
||||
|
||||
14
frontend/src/views/ExternalTournamentsView.vue
Normal file
14
frontend/src/views/ExternalTournamentsView.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<TournamentsView :allowsExternal="true" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TournamentsView from './TournamentsView.vue';
|
||||
|
||||
export default {
|
||||
name: 'ExternalTournamentsView',
|
||||
components: {
|
||||
TournamentsView
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="official-tournaments">
|
||||
<h2>Offizielle Turniere</h2>
|
||||
<h2>Turnierteilnahmen</h2>
|
||||
<div v-if="list && list.length" class="list">
|
||||
<div class="tabs">
|
||||
<button :class="['tab', topActiveTab==='events' ? 'active' : '']" @click="switchTopTab('events')" title="Gespeicherte Veranstaltungen anzeigen">Veranstaltungen</button>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user