From a6493990d329e61b785c4862d2cc31844352756d Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 1 Oct 2025 22:47:13 +0200 Subject: [PATCH] =?UTF-8?q?Erweitert=20die=20Backend-=20und=20Frontend-Fun?= =?UTF-8?q?ktionalit=C3=A4t=20zur=20Unterst=C3=BCtzung=20von=20Teams=20und?= =?UTF-8?q?=20Saisons.=20F=C3=BCgt=20neue=20Routen=20f=C3=BCr=20Team-=20un?= =?UTF-8?q?d=20Club-Team-Management=20hinzu,=20aktualisiert=20die=20Match-?= =?UTF-8?q?=20und=20Team-Modelle=20zur=20Ber=C3=BCcksichtigung=20von=20Sai?= =?UTF-8?q?sons,=20und=20implementiert=20die=20Saison-Auswahl=20in=20der?= =?UTF-8?q?=20Benutzeroberfl=C3=A4che.=20Optimiert=20die=20Logik=20zur=20A?= =?UTF-8?q?bfrage=20von=20Ligen=20und=20Spielen=20basierend=20auf=20der=20?= =?UTF-8?q?ausgew=C3=A4hlten=20Saison.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/clubTeamController.js | 137 +++++ backend/controllers/matchController.js | 6 +- backend/controllers/seasonController.js | 111 ++++ backend/controllers/teamController.js | 139 +++++ backend/migrations/add_season_to_teams.sql | 44 ++ backend/models/ClubTeam.js | 54 ++ backend/models/Match.js | 9 - backend/models/Team.js | 22 + backend/models/index.js | 19 +- backend/routes/clubTeamRoutes.js | 32 ++ backend/routes/seasonRoutes.js | 28 + backend/routes/teamRoutes.js | 32 ++ backend/server.js | 8 +- backend/services/clubTeamService.js | 192 +++++++ backend/services/leagueService.js | 104 ++++ backend/services/matchService.js | 205 ++++--- backend/services/seasonService.js | 160 ++++++ backend/services/teamService.js | 144 +++++ frontend/src/App.vue | 6 +- frontend/src/components/SeasonSelector.vue | 301 ++++++++++ frontend/src/router.js | 2 + frontend/src/views/ScheduleView.vue | 47 +- frontend/src/views/TeamManagementView.vue | 612 +++++++++++++++++++++ 23 files changed, 2309 insertions(+), 105 deletions(-) create mode 100644 backend/controllers/clubTeamController.js create mode 100644 backend/controllers/seasonController.js create mode 100644 backend/controllers/teamController.js create mode 100644 backend/migrations/add_season_to_teams.sql create mode 100644 backend/models/ClubTeam.js create mode 100644 backend/routes/clubTeamRoutes.js create mode 100644 backend/routes/seasonRoutes.js create mode 100644 backend/routes/teamRoutes.js create mode 100644 backend/services/clubTeamService.js create mode 100644 backend/services/leagueService.js create mode 100644 backend/services/seasonService.js create mode 100644 backend/services/teamService.js create mode 100644 frontend/src/components/SeasonSelector.vue create mode 100644 frontend/src/views/TeamManagementView.vue diff --git a/backend/controllers/clubTeamController.js b/backend/controllers/clubTeamController.js new file mode 100644 index 0000000..f111d4e --- /dev/null +++ b/backend/controllers/clubTeamController.js @@ -0,0 +1,137 @@ +import ClubTeamService from '../services/clubTeamService.js'; +import { getUserByToken } from '../utils/userUtils.js'; +import { devLog } from '../utils/logger.js'; + +export const getClubTeams = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { clubid: clubId } = req.params; + const { seasonid: seasonId } = req.query; + + devLog('[getClubTeams] - Getting club teams for club:', clubId, 'season:', seasonId); + const user = await getUserByToken(token); + + // Check if user has access to this club + const clubTeams = await ClubTeamService.getAllClubTeamsByClub(clubId, seasonId); + devLog('[getClubTeams] - Found club teams:', clubTeams.length); + + res.status(200).json(clubTeams); + } catch (error) { + console.error('[getClubTeams] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const getClubTeam = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { clubteamid: clubTeamId } = req.params; + devLog('[getClubTeam] - Getting club team:', clubTeamId); + const user = await getUserByToken(token); + + const clubTeam = await ClubTeamService.getClubTeamById(clubTeamId); + if (!clubTeam) { + return res.status(404).json({ error: "notfound" }); + } + + res.status(200).json(clubTeam); + } catch (error) { + console.error('[getClubTeam] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const createClubTeam = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { clubid: clubId } = req.params; + const { name, leagueId, seasonId } = req.body; + + devLog('[createClubTeam] - Creating club team:', { name, clubId, leagueId, seasonId }); + const user = await getUserByToken(token); + + if (!name) { + return res.status(400).json({ error: "missingname" }); + } + + const clubTeamData = { + name, + clubId: parseInt(clubId), + leagueId: leagueId ? parseInt(leagueId) : null, + seasonId: seasonId ? parseInt(seasonId) : null + }; + + const newClubTeam = await ClubTeamService.createClubTeam(clubTeamData); + devLog('[createClubTeam] - Club team created with ID:', newClubTeam.id); + + res.status(201).json(newClubTeam); + } catch (error) { + console.error('[createClubTeam] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const updateClubTeam = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { clubteamid: clubTeamId } = req.params; + const { name, leagueId, seasonId } = req.body; + + devLog('[updateClubTeam] - Updating club team:', clubTeamId, { name, leagueId, seasonId }); + const user = await getUserByToken(token); + + const updateData = {}; + if (name !== undefined) updateData.name = name; + if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null; + if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null; + + const success = await ClubTeamService.updateClubTeam(clubTeamId, updateData); + if (!success) { + return res.status(404).json({ error: "notfound" }); + } + + const updatedClubTeam = await ClubTeamService.getClubTeamById(clubTeamId); + res.status(200).json(updatedClubTeam); + } catch (error) { + console.error('[updateClubTeam] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const deleteClubTeam = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { clubteamid: clubTeamId } = req.params; + devLog('[deleteClubTeam] - Deleting club team:', clubTeamId); + const user = await getUserByToken(token); + + const success = await ClubTeamService.deleteClubTeam(clubTeamId); + if (!success) { + return res.status(404).json({ error: "notfound" }); + } + + res.status(200).json({ message: "Club team deleted successfully" }); + } catch (error) { + console.error('[deleteClubTeam] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const getLeagues = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { clubid: clubId } = req.params; + const { seasonid: seasonId } = req.query; + + devLog('[getLeagues] - Getting leagues for club:', clubId, 'season:', seasonId); + const user = await getUserByToken(token); + + const leagues = await ClubTeamService.getLeaguesByClub(clubId, seasonId); + devLog('[getLeagues] - Found leagues:', leagues.length); + + res.status(200).json(leagues); + } catch (error) { + console.error('[getLeagues] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; diff --git a/backend/controllers/matchController.js b/backend/controllers/matchController.js index f3f96f3..52f24e1 100644 --- a/backend/controllers/matchController.js +++ b/backend/controllers/matchController.js @@ -25,7 +25,8 @@ export const getLeaguesForCurrentSeason = async (req, res) => { devLog(req.headers, req.params); const { authcode: userToken } = req.headers; const { clubId } = req.params; - const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId); + const { seasonid: seasonId } = req.query; + const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId, seasonId); return res.status(200).json(leagues); } catch (error) { console.error('Error retrieving leagues:', error); @@ -37,7 +38,8 @@ export const getMatchesForLeagues = async (req, res) => { try { const { authcode: userToken } = req.headers; const { clubId } = req.params; - const matches = await MatchService.getMatchesForLeagues(userToken, clubId); + const { seasonid: seasonId } = req.query; + const matches = await MatchService.getMatchesForLeagues(userToken, clubId, seasonId); return res.status(200).json(matches); } catch (error) { console.error('Error retrieving matches:', error); diff --git a/backend/controllers/seasonController.js b/backend/controllers/seasonController.js new file mode 100644 index 0000000..9335dc4 --- /dev/null +++ b/backend/controllers/seasonController.js @@ -0,0 +1,111 @@ +import SeasonService from '../services/seasonService.js'; +import { getUserByToken } from '../utils/userUtils.js'; +import { devLog } from '../utils/logger.js'; + +export const getSeasons = async (req, res) => { + try { + const { authcode: token } = req.headers; + devLog('[getSeasons] - Getting all seasons'); + const user = await getUserByToken(token); + + const seasons = await SeasonService.getAllSeasons(); + devLog('[getSeasons] - Found seasons:', seasons.length); + + res.status(200).json(seasons); + } catch (error) { + console.error('[getSeasons] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const getCurrentSeason = async (req, res) => { + try { + const { authcode: token } = req.headers; + devLog('[getCurrentSeason] - Getting current season'); + const user = await getUserByToken(token); + + const season = await SeasonService.getOrCreateCurrentSeason(); + devLog('[getCurrentSeason] - Current season:', season.season); + + res.status(200).json(season); + } catch (error) { + console.error('[getCurrentSeason] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const createSeason = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { season } = req.body; + + devLog('[createSeason] - Creating season:', season); + const user = await getUserByToken(token); + + if (!season) { + return res.status(400).json({ error: "missingseason" }); + } + + // Validiere Saison-Format (z.B. "2023/2024") + const seasonRegex = /^\d{4}\/\d{4}$/; + if (!seasonRegex.test(season)) { + return res.status(400).json({ error: "invalidseasonformat" }); + } + + const newSeason = await SeasonService.createSeason(season); + devLog('[createSeason] - Season created with ID:', newSeason.id); + + res.status(201).json(newSeason); + } catch (error) { + console.error('[createSeason] - Error:', error); + if (error.message === 'Season already exists') { + res.status(409).json({ error: "alreadyexists" }); + } else { + res.status(500).json({ error: "internalerror" }); + } + } +}; + +export const getSeason = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { seasonid: seasonId } = req.params; + + devLog('[getSeason] - Getting season:', seasonId); + const user = await getUserByToken(token); + + const season = await SeasonService.getSeasonById(seasonId); + if (!season) { + return res.status(404).json({ error: "notfound" }); + } + + res.status(200).json(season); + } catch (error) { + console.error('[getSeason] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const deleteSeason = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { seasonid: seasonId } = req.params; + + devLog('[deleteSeason] - Deleting season:', seasonId); + const user = await getUserByToken(token); + + const success = await SeasonService.deleteSeason(seasonId); + if (!success) { + return res.status(404).json({ error: "notfound" }); + } + + res.status(200).json({ message: "deleted" }); + } catch (error) { + console.error('[deleteSeason] - Error:', error); + if (error.message === 'Season is used by teams' || error.message === 'Season is used by leagues') { + res.status(409).json({ error: "seasoninuse" }); + } else { + res.status(500).json({ error: "internalerror" }); + } + } +}; diff --git a/backend/controllers/teamController.js b/backend/controllers/teamController.js new file mode 100644 index 0000000..8110280 --- /dev/null +++ b/backend/controllers/teamController.js @@ -0,0 +1,139 @@ +import TeamService from '../services/teamService.js'; +import { getUserByToken } from '../utils/userUtils.js'; +import { devLog } from '../utils/logger.js'; + +export const getTeams = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { clubid: clubId } = req.params; + const { seasonid: seasonId } = req.query; + + devLog('[getTeams] - Getting teams for club:', clubId, 'season:', seasonId); + const user = await getUserByToken(token); + + // Check if user has access to this club + const teams = await TeamService.getAllTeamsByClub(clubId, seasonId); + devLog('[getTeams] - Found teams:', teams.length); + + res.status(200).json(teams); + } catch (error) { + console.error('[getTeams] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const getTeam = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { teamid: teamId } = req.params; + + devLog('[getTeam] - Getting team:', teamId); + const user = await getUserByToken(token); + + const team = await TeamService.getTeamById(teamId); + if (!team) { + return res.status(404).json({ error: "notfound" }); + } + + res.status(200).json(team); + } catch (error) { + console.error('[getTeam] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const createTeam = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { clubid: clubId } = req.params; + const { name, leagueId, seasonId } = req.body; + + devLog('[createTeam] - Creating team:', { name, clubId, leagueId, seasonId }); + const user = await getUserByToken(token); + + if (!name) { + return res.status(400).json({ error: "missingname" }); + } + + const teamData = { + name, + clubId: parseInt(clubId), + leagueId: leagueId ? parseInt(leagueId) : null, + seasonId: seasonId ? parseInt(seasonId) : null + }; + + const newTeam = await TeamService.createTeam(teamData); + devLog('[createTeam] - Team created with ID:', newTeam.id); + + res.status(201).json(newTeam); + } catch (error) { + console.error('[createTeam] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const updateTeam = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { teamid: teamId } = req.params; + const { name, leagueId, seasonId } = req.body; + + devLog('[updateTeam] - Updating team:', teamId, { name, leagueId, seasonId }); + const user = await getUserByToken(token); + + const updateData = {}; + if (name !== undefined) updateData.name = name; + if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null; + if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null; + + const success = await TeamService.updateTeam(teamId, updateData); + if (!success) { + return res.status(404).json({ error: "notfound" }); + } + + const updatedTeam = await TeamService.getTeamById(teamId); + res.status(200).json(updatedTeam); + } catch (error) { + console.error('[updateTeam] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const deleteTeam = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { teamid: teamId } = req.params; + + devLog('[deleteTeam] - Deleting team:', teamId); + const user = await getUserByToken(token); + + const success = await TeamService.deleteTeam(teamId); + if (!success) { + return res.status(404).json({ error: "notfound" }); + } + + res.status(200).json({ message: "deleted" }); + } catch (error) { + console.error('[deleteTeam] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; + +export const getLeagues = async (req, res) => { + try { + const { authcode: token } = req.headers; + const { clubid: clubId } = req.params; + const { seasonid: seasonId } = req.query; + + devLog('[getLeagues] - Getting leagues for club:', clubId, 'season:', seasonId); + const user = await getUserByToken(token); + + const leagues = await TeamService.getLeaguesByClub(clubId, seasonId); + devLog('[getLeagues] - Found leagues:', leagues.length); + + res.status(200).json(leagues); + } catch (error) { + console.error('[getLeagues] - Error:', error); + res.status(500).json({ error: "internalerror" }); + } +}; diff --git a/backend/migrations/add_season_to_teams.sql b/backend/migrations/add_season_to_teams.sql new file mode 100644 index 0000000..b752763 --- /dev/null +++ b/backend/migrations/add_season_to_teams.sql @@ -0,0 +1,44 @@ +-- Migration: Add season_id to teams table +-- First, add the column as nullable +ALTER TABLE `team` ADD COLUMN `season_id` INT NULL; + +-- Get or create current season +SET @current_season_id = ( + SELECT id FROM `season` + WHERE season = ( + CASE + WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1) + ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE())) + END + ) + LIMIT 1 +); + +-- If no season exists, create it +INSERT IGNORE INTO `season` (season) VALUES ( + CASE + WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1) + ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE())) + END +); + +-- Get the season ID again (in case we just created it) +SET @current_season_id = ( + SELECT id FROM `season` + WHERE season = ( + CASE + WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1) + ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE())) + END + ) + LIMIT 1 +); + +-- Update all existing teams to use the current season +UPDATE `team` SET `season_id` = @current_season_id WHERE `season_id` IS NULL; + +-- Now make the column NOT NULL and add the foreign key constraint +ALTER TABLE `team` MODIFY COLUMN `season_id` INT NOT NULL; +ALTER TABLE `team` ADD CONSTRAINT `team_season_id_foreign_idx` + FOREIGN KEY (`season_id`) REFERENCES `season` (`id`) + ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/models/ClubTeam.js b/backend/models/ClubTeam.js new file mode 100644 index 0000000..fb84cc1 --- /dev/null +++ b/backend/models/ClubTeam.js @@ -0,0 +1,54 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; +import Club from './Club.js'; +import League from './League.js'; +import Season from './Season.js'; + +const ClubTeam = sequelize.define('ClubTeam', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + clubId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: Club, + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, + leagueId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: League, + key: 'id', + }, + onDelete: 'SET NULL', + onUpdate: 'CASCADE', + }, + seasonId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: Season, + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, +}, { + underscored: true, + tableName: 'club_team', + timestamps: true, +}); + +export default ClubTeam; diff --git a/backend/models/Match.js b/backend/models/Match.js index 502bc5e..24de295 100644 --- a/backend/models/Match.js +++ b/backend/models/Match.js @@ -3,7 +3,6 @@ import sequelize from '../database.js'; import Club from './Club.js'; import League from './League.js'; import Team from './Team.js'; -import Season from './Season.js'; import Location from './Location.js'; const Match = sequelize.define('Match', { @@ -21,14 +20,6 @@ const Match = sequelize.define('Match', { type: DataTypes.TIME, allowNull: true, }, - seasonId: { - type: DataTypes.INTEGER, - references: { - model: Season, - key: 'id', - }, - allowNull: false, - }, locationId: { type: DataTypes.INTEGER, references: { diff --git a/backend/models/Team.js b/backend/models/Team.js index 048e4d5..781231e 100644 --- a/backend/models/Team.js +++ b/backend/models/Team.js @@ -1,6 +1,8 @@ import { DataTypes } from 'sequelize'; import sequelize from '../database.js'; import Club from './Club.js'; +import League from './League.js'; +import Season from './Season.js'; const Team = sequelize.define('Team', { id: { @@ -23,6 +25,26 @@ const Team = sequelize.define('Team', { onDelete: 'CASCADE', onUpdate: 'CASCADE', }, + leagueId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: League, + key: 'id', + }, + onDelete: 'SET NULL', + onUpdate: 'CASCADE', + }, + seasonId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: Season, + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, }, { underscored: true, tableName: 'team', diff --git a/backend/models/index.js b/backend/models/index.js index 993601a..f686330 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -19,6 +19,7 @@ import DiaryDateActivity from './DiaryDateActivity.js'; import Match from './Match.js'; import League from './League.js'; import Team from './Team.js'; +import ClubTeam from './ClubTeam.js'; import Season from './Season.js'; import Location from './Location.js'; import Group from './Group.js'; @@ -118,8 +119,21 @@ Team.belongsTo(Club, { foreignKey: 'clubId', as: 'club' }); Club.hasMany(League, { foreignKey: 'clubId', as: 'leagues' }); League.belongsTo(Club, { foreignKey: 'clubId', as: 'club' }); -Match.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' }); -Season.hasMany(Match, { foreignKey: 'seasonId', as: 'matches' }); +League.hasMany(Team, { foreignKey: 'leagueId', as: 'teams' }); +Team.belongsTo(League, { foreignKey: 'leagueId', as: 'league' }); + +Season.hasMany(Team, { foreignKey: 'seasonId', as: 'teams' }); +Team.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' }); + +// ClubTeam relationships +Club.hasMany(ClubTeam, { foreignKey: 'clubId', as: 'clubTeams' }); +ClubTeam.belongsTo(Club, { foreignKey: 'clubId', as: 'club' }); + +League.hasMany(ClubTeam, { foreignKey: 'leagueId', as: 'clubTeams' }); +ClubTeam.belongsTo(League, { foreignKey: 'leagueId', as: 'league' }); + +Season.hasMany(ClubTeam, { foreignKey: 'seasonId', as: 'clubTeams' }); +ClubTeam.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' }); Match.belongsTo(Location, { foreignKey: 'locationId', as: 'location' }); Location.hasMany(Match, { foreignKey: 'locationId', as: 'matches' }); @@ -231,6 +245,7 @@ export { Match, League, Team, + ClubTeam, Group, GroupActivity, Tournament, diff --git a/backend/routes/clubTeamRoutes.js b/backend/routes/clubTeamRoutes.js new file mode 100644 index 0000000..cee1b6a --- /dev/null +++ b/backend/routes/clubTeamRoutes.js @@ -0,0 +1,32 @@ +import express from 'express'; +import { authenticate } from '../middleware/authMiddleware.js'; +import { + getClubTeams, + getClubTeam, + createClubTeam, + updateClubTeam, + deleteClubTeam, + getLeagues +} from '../controllers/clubTeamController.js'; + +const router = express.Router(); + +// Get all club teams for a club +router.get('/club/:clubid', authenticate, getClubTeams); + +// Create a new club team +router.post('/club/:clubid', authenticate, createClubTeam); + +// Get leagues for a club +router.get('/leagues/:clubid', authenticate, getLeagues); + +// Get a specific club team +router.get('/:clubteamid', authenticate, getClubTeam); + +// Update a club team +router.put('/:clubteamid', authenticate, updateClubTeam); + +// Delete a club team +router.delete('/:clubteamid', authenticate, deleteClubTeam); + +export default router; diff --git a/backend/routes/seasonRoutes.js b/backend/routes/seasonRoutes.js new file mode 100644 index 0000000..48b2d3a --- /dev/null +++ b/backend/routes/seasonRoutes.js @@ -0,0 +1,28 @@ +import express from 'express'; +import { authenticate } from '../middleware/authMiddleware.js'; +import { + getSeasons, + getCurrentSeason, + createSeason, + getSeason, + deleteSeason +} from '../controllers/seasonController.js'; + +const router = express.Router(); + +// Get all seasons +router.get('/', authenticate, getSeasons); + +// Get current season (creates if not exists) +router.get('/current', authenticate, getCurrentSeason); + +// Get a specific season +router.get('/:seasonid', authenticate, getSeason); + +// Create a new season +router.post('/', authenticate, createSeason); + +// Delete a season +router.delete('/:seasonid', authenticate, deleteSeason); + +export default router; diff --git a/backend/routes/teamRoutes.js b/backend/routes/teamRoutes.js new file mode 100644 index 0000000..ea3ee3c --- /dev/null +++ b/backend/routes/teamRoutes.js @@ -0,0 +1,32 @@ +import express from 'express'; +import { authenticate } from '../middleware/authMiddleware.js'; +import { + getTeams, + getTeam, + createTeam, + updateTeam, + deleteTeam, + getLeagues +} from '../controllers/teamController.js'; + +const router = express.Router(); + +// Get all teams for a club +router.get('/club/:clubid', authenticate, getTeams); + +// Get leagues for a club +router.get('/leagues/:clubid', authenticate, getLeagues); + +// Get a specific team +router.get('/:teamid', authenticate, getTeam); + +// Create a new team +router.post('/club/:clubid', authenticate, createTeam); + +// Update a team +router.put('/:teamid', authenticate, updateTeam); + +// Delete a team +router.delete('/:teamid', authenticate, deleteTeam); + +export default router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 62d3d5a..013957b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -6,7 +6,7 @@ import cors from 'cors'; import { User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote, DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag, - PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, Group, + PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, Group, GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult, TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis } from './models/index.js'; @@ -34,6 +34,9 @@ import accidentRoutes from './routes/accidentRoutes.js'; import trainingStatsRoutes from './routes/trainingStatsRoutes.js'; import officialTournamentRoutes from './routes/officialTournamentRoutes.js'; import myTischtennisRoutes from './routes/myTischtennisRoutes.js'; +import teamRoutes from './routes/teamRoutes.js'; +import clubTeamRoutes from './routes/clubTeamRoutes.js'; +import seasonRoutes from './routes/seasonRoutes.js'; const app = express(); const port = process.env.PORT || 3000; @@ -79,6 +82,9 @@ app.use('/api/accident', accidentRoutes); app.use('/api/training-stats', trainingStatsRoutes); app.use('/api/official-tournaments', officialTournamentRoutes); app.use('/api/mytischtennis', myTischtennisRoutes); +app.use('/api/teams', teamRoutes); +app.use('/api/club-teams', clubTeamRoutes); +app.use('/api/seasons', seasonRoutes); app.use(express.static(path.join(__dirname, '../frontend/dist'))); diff --git a/backend/services/clubTeamService.js b/backend/services/clubTeamService.js new file mode 100644 index 0000000..ecb1b3b --- /dev/null +++ b/backend/services/clubTeamService.js @@ -0,0 +1,192 @@ +import ClubTeam from '../models/ClubTeam.js'; +import League from '../models/League.js'; +import Season from '../models/Season.js'; +import SeasonService from './seasonService.js'; +import { devLog } from '../utils/logger.js'; + +class ClubTeamService { + /** + * Holt alle ClubTeams für einen Verein, optional gefiltert nach Saison. + * Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison verwendet. + * @param {number} clubId - Die ID des Vereins. + * @param {number|null} seasonId - Optionale Saison-ID. + * @returns {Promise>} Eine Liste von ClubTeams. + */ + static async getAllClubTeamsByClub(clubId, seasonId = null) { + try { + devLog('[ClubTeamService.getAllClubTeamsByClub] - Getting club teams for club:', clubId, 'season:', seasonId); + + // Wenn keine Saison angegeben, verwende die aktuelle + if (!seasonId) { + const currentSeason = await SeasonService.getOrCreateCurrentSeason(); + seasonId = currentSeason.id; + } + + const clubTeams = await ClubTeam.findAll({ + where: { clubId, seasonId }, + order: [['name', 'ASC']] + }); + + // Manuelle Datenanreicherung für Liga und Saison + const enrichedClubTeams = []; + for (const clubTeam of clubTeams) { + const enrichedTeam = { + id: clubTeam.id, + name: clubTeam.name, + clubId: clubTeam.clubId, + leagueId: clubTeam.leagueId, + seasonId: clubTeam.seasonId, + createdAt: clubTeam.createdAt, + updatedAt: clubTeam.updatedAt, + league: { name: 'Unbekannt' }, + season: { season: 'Unbekannt' } + }; + + // Lade Liga-Daten + if (clubTeam.leagueId) { + const league = await League.findByPk(clubTeam.leagueId, { attributes: ['name'] }); + if (league) enrichedTeam.league = league; + } + + // Lade Saison-Daten + if (clubTeam.seasonId) { + const season = await Season.findByPk(clubTeam.seasonId, { attributes: ['season'] }); + if (season) enrichedTeam.season = season; + } + + enrichedClubTeams.push(enrichedTeam); + } + devLog('[ClubTeamService.getAllClubTeamsByClub] - Found club teams:', enrichedClubTeams.length); + return enrichedClubTeams; + } catch (error) { + console.error('[ClubTeamService.getAllClubTeamsByClub] - Error:', error); + throw error; + } + } + + /** + * Holt ein ClubTeam anhand seiner ID + * @param {number} clubTeamId - Die ID des ClubTeams + * @returns {Promise} Das ClubTeam oder null, wenn nicht gefunden + */ + static async getClubTeamById(clubTeamId) { + try { + devLog('[ClubTeamService.getClubTeamById] - Getting club team:', clubTeamId); + const clubTeam = await ClubTeam.findByPk(clubTeamId, { + include: [ + { + model: League, + as: 'league', + attributes: ['id', 'name'] + }, + { + model: Season, + as: 'season', + attributes: ['id', 'season'] + } + ] + }); + devLog('[ClubTeamService.getClubTeamById] - Found club team:', clubTeam ? 'yes' : 'no'); + return clubTeam; + } catch (error) { + console.error('[ClubTeamService.getClubTeamById] - Error:', error); + throw error; + } + } + + /** + * Erstellt ein neues ClubTeam. + * Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison zugewiesen. + * @param {object} clubTeamData - Die Daten des neuen ClubTeams (name, clubId, optional leagueId, seasonId). + * @returns {Promise} Das erstellte ClubTeam. + */ + static async createClubTeam(clubTeamData) { + try { + devLog('[ClubTeamService.createClubTeam] - Creating club team:', clubTeamData); + + // Wenn keine Saison angegeben, verwende die aktuelle + if (!clubTeamData.seasonId) { + const currentSeason = await SeasonService.getOrCreateCurrentSeason(); + clubTeamData.seasonId = currentSeason.id; + } + + const clubTeam = await ClubTeam.create(clubTeamData); + devLog('[ClubTeamService.createClubTeam] - Club team created with ID:', clubTeam.id); + return clubTeam; + } catch (error) { + console.error('[ClubTeamService.createClubTeam] - Error:', error); + throw error; + } + } + + /** + * Aktualisiert ein bestehendes ClubTeam. + * @param {number} clubTeamId - Die ID des zu aktualisierenden ClubTeams. + * @param {object} updateData - Die zu aktualisierenden Daten. + * @returns {Promise} True, wenn das ClubTeam aktualisiert wurde, sonst false. + */ + static async updateClubTeam(clubTeamId, updateData) { + try { + devLog('[ClubTeamService.updateClubTeam] - Updating club team:', clubTeamId, updateData); + const [updatedRowsCount] = await ClubTeam.update(updateData, { + where: { id: clubTeamId } + }); + devLog('[ClubTeamService.updateClubTeam] - Updated rows:', updatedRowsCount); + return updatedRowsCount > 0; + } catch (error) { + console.error('[ClubTeamService.updateClubTeam] - Error:', error); + throw error; + } + } + + /** + * Löscht ein ClubTeam. + * @param {number} clubTeamId - Die ID des zu löschenden ClubTeams. + * @returns {Promise} True, wenn das ClubTeam gelöscht wurde, sonst false. + */ + static async deleteClubTeam(clubTeamId) { + try { + devLog('[ClubTeamService.deleteClubTeam] - Deleting club team:', clubTeamId); + const deletedRows = await ClubTeam.destroy({ + where: { id: clubTeamId } + }); + devLog('[ClubTeamService.deleteClubTeam] - Deleted rows:', deletedRows); + return deletedRows > 0; + } catch (error) { + console.error('[ClubTeamService.deleteClubTeam] - Error:', error); + throw error; + } + } + + /** + * Holt alle Ligen für einen Verein, optional gefiltert nach Saison. + * Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison verwendet. + * @param {number} clubId - Die ID des Vereins. + * @param {number|null} seasonId - Optionale Saison-ID. + * @returns {Promise>} Eine Liste von Ligen. + */ + static async getLeaguesByClub(clubId, seasonId = null) { + try { + devLog('[ClubTeamService.getLeaguesByClub] - Getting leagues for club:', clubId, 'season:', seasonId); + + // Wenn keine Saison angegeben, verwende die aktuelle + if (!seasonId) { + const currentSeason = await SeasonService.getOrCreateCurrentSeason(); + seasonId = currentSeason.id; + } + + const leagues = await League.findAll({ + where: { clubId, seasonId }, + attributes: ['id', 'name', 'seasonId'], + order: [['name', 'ASC']] + }); + devLog('[ClubTeamService.getLeaguesByClub] - Found leagues:', leagues.length); + return leagues; + } catch (error) { + console.error('[ClubTeamService.getLeaguesByClub] - Error:', error); + throw error; + } + } +} + +export default ClubTeamService; diff --git a/backend/services/leagueService.js b/backend/services/leagueService.js new file mode 100644 index 0000000..b320941 --- /dev/null +++ b/backend/services/leagueService.js @@ -0,0 +1,104 @@ +import League from '../models/League.js'; +import Season from '../models/Season.js'; +import SeasonService from './seasonService.js'; +import { devLog } from '../utils/logger.js'; + +class LeagueService { + static async getAllLeaguesByClub(clubId, seasonId = null) { + try { + devLog('[LeagueService.getAllLeaguesByClub] - Getting leagues for club:', clubId, 'season:', seasonId); + + // Wenn keine Saison angegeben, verwende die aktuelle + if (!seasonId) { + const currentSeason = await SeasonService.getOrCreateCurrentSeason(); + seasonId = currentSeason.id; + } + + const leagues = await League.findAll({ + where: { clubId, seasonId }, + include: [ + { + model: Season, + as: 'season', + attributes: ['id', 'season'] + } + ], + order: [['name', 'ASC']] + }); + devLog('[LeagueService.getAllLeaguesByClub] - Found leagues:', leagues.length); + return leagues; + } catch (error) { + console.error('[LeagueService.getAllLeaguesByClub] - Error:', error); + throw error; + } + } + + static async getLeagueById(leagueId) { + try { + devLog('[LeagueService.getLeagueById] - Getting league:', leagueId); + const league = await League.findByPk(leagueId, { + include: [ + { + model: Season, + as: 'season', + attributes: ['id', 'season'] + } + ] + }); + devLog('[LeagueService.getLeagueById] - Found league:', league ? 'yes' : 'no'); + return league; + } catch (error) { + console.error('[LeagueService.getLeagueById] - Error:', error); + throw error; + } + } + + static async createLeague(leagueData) { + try { + devLog('[LeagueService.createLeague] - Creating league:', leagueData); + + // Wenn keine Saison angegeben, verwende die aktuelle + if (!leagueData.seasonId) { + const currentSeason = await SeasonService.getOrCreateCurrentSeason(); + leagueData.seasonId = currentSeason.id; + } + + const league = await League.create(leagueData); + devLog('[LeagueService.createLeague] - League created with ID:', league.id); + return league; + } catch (error) { + console.error('[LeagueService.createLeague] - Error:', error); + throw error; + } + } + + static async updateLeague(leagueId, updateData) { + try { + devLog('[LeagueService.updateLeague] - Updating league:', leagueId, updateData); + const [updatedRowsCount] = await League.update(updateData, { + where: { id: leagueId } + }); + devLog('[LeagueService.updateLeague] - Updated rows:', updatedRowsCount); + return updatedRowsCount > 0; + } catch (error) { + console.error('[LeagueService.updateLeague] - Error:', error); + throw error; + } + } + + static async deleteLeague(leagueId) { + try { + devLog('[LeagueService.deleteLeague] - Deleting league:', leagueId); + const deletedRowsCount = await League.destroy({ + where: { id: leagueId } + }); + devLog('[LeagueService.deleteLeague] - Deleted rows:', deletedRowsCount); + return deletedRowsCount > 0; + } catch (error) { + console.error('[LeagueService.deleteLeague] - Error:', error); + throw error; + } + } +} + +export default LeagueService; diff --git a/backend/services/matchService.js b/backend/services/matchService.js index 4333bae..958bcb1 100644 --- a/backend/services/matchService.js +++ b/backend/services/matchService.js @@ -7,6 +7,7 @@ import Season from '../models/Season.js'; import Location from '../models/Location.js'; import League from '../models/League.js'; import Team from '../models/Team.js'; +import SeasonService from './seasonService.js'; import { checkAccess } from '../utils/userUtils.js'; import { Op } from 'sequelize'; @@ -22,8 +23,7 @@ class MatchService { seasonStartYear = currentYear - 1; } const seasonEndYear = seasonStartYear + 1; - const seasonEndYearString = seasonEndYear.toString().slice(-2); - return `${seasonStartYear}/${seasonEndYearString}`; + return `${seasonStartYear}/${seasonEndYear}`; } async importCSV(userToken, clubId, filePath) { @@ -58,7 +58,6 @@ class MatchService { }, }); matches.push({ - seasonId: season.id, date: parsedDate, time: row['Termin'].split(' ')[1], homeTeamId: homeTeamId, @@ -72,7 +71,14 @@ class MatchService { if (seasonString) { season = await Season.findOne({ where: { season: seasonString } }); if (season) { - await Match.destroy({ where: { clubId, seasonId: season.id } }); + // Lösche alle Matches für Ligen dieser Saison + const leagues = await League.findAll({ + where: { seasonId: season.id, clubId } + }); + const leagueIds = leagues.map(league => league.id); + if (leagueIds.length > 0) { + await Match.destroy({ where: { clubId, leagueId: leagueIds } }); + } } } const result = await Match.bulkCreate(matches); @@ -99,33 +105,28 @@ class MatchService { } - async getLeaguesForCurrentSeason(userToken, clubId) { + async getLeaguesForCurrentSeason(userToken, clubId, seasonId = null) { await checkAccess(userToken, clubId); - const seasonString = this.generateSeasonString(); - const season = await Season.findOne({ - where: { - season: { - [Op.like]: `%${seasonString}%` - } + + // Verwende SeasonService für korrekte Saison-Verwaltung + let season; + if (!seasonId) { + season = await SeasonService.getOrCreateCurrentSeason(); + } else { + season = await SeasonService.getSeasonById(seasonId); + if (!season) { + throw new Error('Season not found'); } - }); - if (!season) { - await Season.create({ season: seasonString }); - throw new Error('Season not found'); } + try { const leagues = await League.findAll({ - include: [{ - model: Match, - as: 'leagueMatches', - where: { - seasonId: season.id, - clubId: clubId - }, - attributes: [], - }], + where: { + clubId: clubId, + seasonId: season.id + }, attributes: ['id', 'name'], - group: ['League.id'], + order: [['name', 'ASC']] }); return leagues; } catch (error) { @@ -134,48 +135,66 @@ class MatchService { } } - async getMatchesForLeagues(userToken, clubId) { + async getMatchesForLeagues(userToken, clubId, seasonId = null) { await checkAccess(userToken, clubId); - const seasonString = this.generateSeasonString(); - const season = await Season.findOne({ - where: { - season: { - [Op.like]: `%${seasonString}%` - } + + // Wenn keine Saison angegeben, verwende die aktuelle + let season; + if (!seasonId) { + season = await SeasonService.getOrCreateCurrentSeason(); + } else { + season = await SeasonService.getSeasonById(seasonId); + if (!season) { + throw new Error('Season not found'); } - }); - if (!season) { - throw new Error('Season not found'); } const matches = await Match.findAll({ where: { - seasonId: season.id, clubId: clubId, - }, - include: [ - { - model: League, - as: 'leagueDetails', - attributes: ['name'], - }, - { - model: Team, - as: 'homeTeam', // Assuming your associations are set correctly - attributes: ['name'], - }, - { - model: Team, - as: 'guestTeam', - attributes: ['name'], - }, - { - model: Location, - as: 'location', - attributes: ['name', 'address', 'city', 'zip'], - } - ] + } }); - return matches; + + // Filtere Matches nach Liga-Saison und lade Daten manuell + const enrichedMatches = []; + for (const match of matches) { + // Lade Liga-Daten + const league = await League.findByPk(match.leagueId, { attributes: ['name', 'seasonId'] }); + if (!league || league.seasonId !== season.id) { + continue; // Skip matches from other seasons + } + + const enrichedMatch = { + id: match.id, + date: match.date, + time: match.time, + homeTeamId: match.homeTeamId, + guestTeamId: match.guestTeamId, + locationId: match.locationId, + leagueId: match.leagueId, + homeTeam: { name: 'Unbekannt' }, + guestTeam: { name: 'Unbekannt' }, + location: { name: 'Unbekannt', address: '', city: '', zip: '' }, + leagueDetails: { name: league.name } + }; + + if (match.homeTeamId) { + const homeTeam = await Team.findByPk(match.homeTeamId, { attributes: ['name'] }); + if (homeTeam) enrichedMatch.homeTeam = homeTeam; + } + if (match.guestTeamId) { + const guestTeam = await Team.findByPk(match.guestTeamId, { attributes: ['name'] }); + if (guestTeam) enrichedMatch.guestTeam = guestTeam; + } + if (match.locationId) { + const location = await Location.findByPk(match.locationId, { + attributes: ['name', 'address', 'city', 'zip'] + }); + if (location) enrichedMatch.location = location; + } + + enrichedMatches.push(enrichedMatch); + } + return enrichedMatches; } async getMatchesForLeague(userToken, clubId, leagueId) { @@ -193,34 +212,50 @@ class MatchService { } const matches = await Match.findAll({ where: { - seasonId: season.id, clubId: clubId, leagueId: leagueId - }, - include: [ - { - model: League, - as: 'leagueDetails', - attributes: ['name'], - }, - { - model: Team, - as: 'homeTeam', - attributes: ['name'], - }, - { - model: Team, - as: 'guestTeam', - attributes: ['name'], - }, - { - model: Location, - as: 'location', - attributes: ['name', 'address', 'city', 'zip'], - } - ] + } }); - return matches; + + // Lade Team- und Location-Daten manuell + const enrichedMatches = []; + for (const match of matches) { + const enrichedMatch = { + id: match.id, + date: match.date, + time: match.time, + homeTeamId: match.homeTeamId, + guestTeamId: match.guestTeamId, + locationId: match.locationId, + leagueId: match.leagueId, + homeTeam: { name: 'Unbekannt' }, + guestTeam: { name: 'Unbekannt' }, + location: { name: 'Unbekannt', address: '', city: '', zip: '' }, + leagueDetails: { name: 'Unbekannt' } + }; + + if (match.homeTeamId) { + const homeTeam = await Team.findByPk(match.homeTeamId, { attributes: ['name'] }); + if (homeTeam) enrichedMatch.homeTeam = homeTeam; + } + if (match.guestTeamId) { + const guestTeam = await Team.findByPk(match.guestTeamId, { attributes: ['name'] }); + if (guestTeam) enrichedMatch.guestTeam = guestTeam; + } + if (match.locationId) { + const location = await Location.findByPk(match.locationId, { + attributes: ['name', 'address', 'city', 'zip'] + }); + if (location) enrichedMatch.location = location; + } + if (match.leagueId) { + const league = await League.findByPk(match.leagueId, { attributes: ['name'] }); + if (league) enrichedMatch.leagueDetails = league; + } + + enrichedMatches.push(enrichedMatch); + } + return enrichedMatches; } } diff --git a/backend/services/seasonService.js b/backend/services/seasonService.js new file mode 100644 index 0000000..c1b03a9 --- /dev/null +++ b/backend/services/seasonService.js @@ -0,0 +1,160 @@ +import Season from '../models/Season.js'; +import { devLog } from '../utils/logger.js'; + +class SeasonService { + /** + * Ermittelt die aktuelle Saison basierend auf dem aktuellen Datum + * @returns {string} Saison im Format "2023/2024" + */ + static getCurrentSeasonString() { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; // getMonth() ist 0-basiert + + // Ab 1. Juli: neue Saison beginnt + if (currentMonth >= 7) { + return `${currentYear}/${currentYear + 1}`; + } else { + return `${currentYear - 1}/${currentYear}`; + } + } + + /** + * Holt oder erstellt die aktuelle Saison + * @returns {Promise} Die aktuelle Saison + */ + static async getOrCreateCurrentSeason() { + try { + const currentSeasonString = this.getCurrentSeasonString(); + devLog('[SeasonService.getOrCreateCurrentSeason] - Current season string:', currentSeasonString); + + // Versuche die aktuelle Saison zu finden + let season = await Season.findOne({ + where: { season: currentSeasonString } + }); + + // Falls nicht vorhanden, erstelle sie + if (!season) { + devLog('[SeasonService.getOrCreateCurrentSeason] - Creating new season:', currentSeasonString); + season = await Season.create({ + season: currentSeasonString + }); + } + + devLog('[SeasonService.getOrCreateCurrentSeason] - Season found/created:', season.id); + return season; + } catch (error) { + console.error('[SeasonService.getOrCreateCurrentSeason] - Error:', error); + throw error; + } + } + + /** + * Holt alle verfügbaren Saisons + * @returns {Promise>} Alle Saisons sortiert nach Name + */ + static async getAllSeasons() { + try { + devLog('[SeasonService.getAllSeasons] - Getting all seasons'); + const seasons = await Season.findAll({ + order: [['season', 'DESC']] // Neueste zuerst + }); + devLog('[SeasonService.getAllSeasons] - Found seasons:', seasons.length); + return seasons; + } catch (error) { + console.error('[SeasonService.getAllSeasons] - Error:', error); + throw error; + } + } + + /** + * Erstellt eine neue Saison + * @param {string} seasonString - Saison im Format "2023/2024" + * @returns {Promise} Die erstellte Saison + */ + static async createSeason(seasonString) { + try { + devLog('[SeasonService.createSeason] - Creating season:', seasonString); + + // Prüfe ob Saison bereits existiert + const existingSeason = await Season.findOne({ + where: { season: seasonString } + }); + + if (existingSeason) { + throw new Error('Season already exists'); + } + + const season = await Season.create({ + season: seasonString + }); + + devLog('[SeasonService.createSeason] - Season created with ID:', season.id); + return season; + } catch (error) { + console.error('[SeasonService.createSeason] - Error:', error); + throw error; + } + } + + /** + * Holt eine Saison nach ID + * @param {number} seasonId - Die Saison-ID + * @returns {Promise} Die Saison oder null + */ + static async getSeasonById(seasonId) { + try { + devLog('[SeasonService.getSeasonById] - Getting season:', seasonId); + const season = await Season.findByPk(seasonId); + devLog('[SeasonService.getSeasonById] - Found season:', season ? 'yes' : 'no'); + return season; + } catch (error) { + console.error('[SeasonService.getSeasonById] - Error:', error); + throw error; + } + } + + /** + * Löscht eine Saison (nur wenn keine Teams/Ligen damit verknüpft sind) + * @param {number} seasonId - Die Saison-ID + * @returns {Promise} True wenn gelöscht, false wenn nicht möglich + */ + static async deleteSeason(seasonId) { + try { + devLog('[SeasonService.deleteSeason] - Deleting season:', seasonId); + + // Prüfe ob Saison verwendet wird + const season = await Season.findByPk(seasonId, { + include: [ + { association: 'teams' }, + { association: 'leagues' } + ] + }); + + if (!season) { + return false; + } + + // Prüfe ob Saison verwendet wird + if (season.teams && season.teams.length > 0) { + throw new Error('Season is used by teams'); + } + + if (season.leagues && season.leagues.length > 0) { + throw new Error('Season is used by leagues'); + } + + await Season.destroy({ + where: { id: seasonId } + }); + + devLog('[SeasonService.deleteSeason] - Season deleted'); + return true; + } catch (error) { + console.error('[SeasonService.deleteSeason] - Error:', error); + throw error; + } + } +} + +export default SeasonService; diff --git a/backend/services/teamService.js b/backend/services/teamService.js new file mode 100644 index 0000000..9741c73 --- /dev/null +++ b/backend/services/teamService.js @@ -0,0 +1,144 @@ +import Team from '../models/Team.js'; +import League from '../models/League.js'; +import Club from '../models/Club.js'; +import Season from '../models/Season.js'; +import SeasonService from './seasonService.js'; +import { devLog } from '../utils/logger.js'; + +class TeamService { + static async getAllTeamsByClub(clubId, seasonId = null) { + try { + devLog('[TeamService.getAllTeamsByClub] - Getting teams for club:', clubId, 'season:', seasonId); + + // Wenn keine Saison angegeben, verwende die aktuelle + if (!seasonId) { + const currentSeason = await SeasonService.getOrCreateCurrentSeason(); + seasonId = currentSeason.id; + } + + const teams = await Team.findAll({ + where: { clubId, seasonId }, + include: [ + { + model: League, + as: 'league', + attributes: ['id', 'name'] + }, + { + model: Season, + as: 'season', + attributes: ['id', 'season'] + } + ], + order: [['name', 'ASC']] + }); + devLog('[TeamService.getAllTeamsByClub] - Found teams:', teams.length); + return teams; + } catch (error) { + console.error('[TeamService.getAllTeamsByClub] - Error:', error); + throw error; + } + } + + static async getTeamById(teamId) { + try { + devLog('[TeamService.getTeamById] - Getting team:', teamId); + const team = await Team.findByPk(teamId, { + include: [ + { + model: League, + as: 'league', + attributes: ['id', 'name'] + }, + { + model: Club, + as: 'club', + attributes: ['id', 'name'] + }, + { + model: Season, + as: 'season', + attributes: ['id', 'season'] + } + ] + }); + devLog('[TeamService.getTeamById] - Found team:', team ? 'yes' : 'no'); + return team; + } catch (error) { + console.error('[TeamService.getTeamById] - Error:', error); + throw error; + } + } + + static async createTeam(teamData) { + try { + devLog('[TeamService.createTeam] - Creating team:', teamData); + + // Wenn keine Saison angegeben, verwende die aktuelle + if (!teamData.seasonId) { + const currentSeason = await SeasonService.getOrCreateCurrentSeason(); + teamData.seasonId = currentSeason.id; + } + + const team = await Team.create(teamData); + devLog('[TeamService.createTeam] - Team created with ID:', team.id); + return team; + } catch (error) { + console.error('[TeamService.createTeam] - Error:', error); + throw error; + } + } + + static async updateTeam(teamId, updateData) { + try { + devLog('[TeamService.updateTeam] - Updating team:', teamId, updateData); + const [updatedRowsCount] = await Team.update(updateData, { + where: { id: teamId } + }); + devLog('[TeamService.updateTeam] - Updated rows:', updatedRowsCount); + return updatedRowsCount > 0; + } catch (error) { + console.error('[TeamService.updateTeam] - Error:', error); + throw error; + } + } + + static async deleteTeam(teamId) { + try { + devLog('[TeamService.deleteTeam] - Deleting team:', teamId); + const deletedRowsCount = await Team.destroy({ + where: { id: teamId } + }); + devLog('[TeamService.deleteTeam] - Deleted rows:', deletedRowsCount); + return deletedRowsCount > 0; + } catch (error) { + console.error('[TeamService.deleteTeam] - Error:', error); + throw error; + } + } + + static async getLeaguesByClub(clubId, seasonId = null) { + try { + devLog('[TeamService.getLeaguesByClub] - Getting leagues for club:', clubId, 'season:', seasonId); + + // Wenn keine Saison angegeben, verwende die aktuelle + if (!seasonId) { + const currentSeason = await SeasonService.getOrCreateCurrentSeason(); + seasonId = currentSeason.id; + } + + const leagues = await League.findAll({ + where: { clubId, seasonId }, + attributes: ['id', 'name', 'seasonId'], + order: [['name', 'ASC']] + }); + devLog('[TeamService.getLeaguesByClub] - Found leagues:', leagues.length); + return leagues; + } catch (error) { + console.error('[TeamService.getLeaguesByClub] - Error:', error); + throw error; + } + } +} + +export default TeamService; diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d175d24..55435f2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -65,6 +65,10 @@ ⚙️ Vordefinierte Aktivitäten + + 👥 + Team-Verwaltung + @@ -170,7 +174,7 @@ export default { }, loadClub() { - this.setCurrentClub(this.currentClub); + this.setCurrentClub(this.selectedClub); this.$router.push('/training-stats'); }, diff --git a/frontend/src/components/SeasonSelector.vue b/frontend/src/components/SeasonSelector.vue new file mode 100644 index 0000000..43e800d --- /dev/null +++ b/frontend/src/components/SeasonSelector.vue @@ -0,0 +1,301 @@ + + + + + diff --git a/frontend/src/router.js b/frontend/src/router.js index 13599d0..264802f 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -14,6 +14,7 @@ import TrainingStatsView from './views/TrainingStatsView.vue'; import PredefinedActivities from './views/PredefinedActivities.vue'; import OfficialTournaments from './views/OfficialTournaments.vue'; import MyTischtennisAccount from './views/MyTischtennisAccount.vue'; +import TeamManagementView from './views/TeamManagementView.vue'; import Impressum from './views/Impressum.vue'; import Datenschutz from './views/Datenschutz.vue'; @@ -33,6 +34,7 @@ const routes = [ { path: '/predefined-activities', component: PredefinedActivities }, { path: '/official-tournaments', component: OfficialTournaments }, { path: '/mytischtennis-account', component: MyTischtennisAccount }, + { path: '/team-management', component: TeamManagementView }, { path: '/impressum', component: Impressum }, { path: '/datenschutz', component: Datenschutz }, ]; diff --git a/frontend/src/views/ScheduleView.vue b/frontend/src/views/ScheduleView.vue index 7e22d1c..0426fa7 100644 --- a/frontend/src/views/ScheduleView.vue +++ b/frontend/src/views/ScheduleView.vue @@ -1,6 +1,13 @@