diff --git a/backend/controllers/officialTournamentController.js b/backend/controllers/officialTournamentController.js index ff516a0..feb6281 100644 --- a/backend/controllers/officialTournamentController.js +++ b/backend/controllers/officialTournamentController.js @@ -4,6 +4,7 @@ const pdfParse = require('pdf-parse/lib/pdf-parse.js'); import { checkAccess } from '../utils/userUtils.js'; import OfficialTournament from '../models/OfficialTournament.js'; import OfficialCompetition from '../models/OfficialCompetition.js'; +import OfficialCompetitionMember from '../models/OfficialCompetitionMember.js'; // In-Memory Store (einfacher Start); später DB-Modell const parsedTournaments = new Map(); // key: id, value: { id, clubId, rawText, parsedData } @@ -67,6 +68,7 @@ export const getParsedTournament = async (req, res) => { const t = await OfficialTournament.findOne({ where: { id, clubId } }); if (!t) return res.status(404).json({ error: 'not found' }); const comps = await OfficialCompetition.findAll({ where: { tournamentId: id } }); + const entries = await OfficialCompetitionMember.findAll({ where: { tournamentId: id } }); const competitions = comps.map((c) => { const j = c.toJSON(); return { @@ -109,12 +111,53 @@ export const getParsedTournament = async (req, res) => { meldeschluesse: JSON.parse(t.registrationDeadlines || '[]'), competitions, }, + participation: entries.map(e => ({ + id: e.id, + tournamentId: e.tournamentId, + competitionId: e.competitionId, + memberId: e.memberId, + wants: !!e.wants, + registered: !!e.registered, + participated: !!e.participated, + placement: e.placement || null, + })), }); } catch (e) { res.status(500).json({ error: 'Failed to fetch parsed tournament' }); } }; +export const upsertCompetitionMember = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId, id } = req.params; // id = tournamentId + await checkAccess(userToken, clubId); + const { competitionId, memberId, wants, registered, participated, placement } = req.body; + if (!competitionId || !memberId) return res.status(400).json({ error: 'competitionId and memberId required' }); + const [row] = await OfficialCompetitionMember.findOrCreate({ + where: { competitionId, memberId }, + defaults: { + tournamentId: id, + competitionId, + memberId, + wants: !!wants, + registered: !!registered, + participated: !!participated, + placement: placement || null, + } + }); + row.wants = wants !== undefined ? !!wants : row.wants; + row.registered = registered !== undefined ? !!registered : row.registered; + row.participated = participated !== undefined ? !!participated : row.participated; + if (placement !== undefined) row.placement = placement; + await row.save(); + return res.status(200).json({ success: true, id: row.id }); + } catch (e) { + console.error('[upsertCompetitionMember] Error:', e); + res.status(500).json({ error: 'Failed to save participation' }); + } +}; + export const listOfficialTournaments = async (req, res) => { try { const { authcode: userToken } = req.headers; diff --git a/backend/models/OfficialCompetitionMember.js b/backend/models/OfficialCompetitionMember.js new file mode 100644 index 0000000..b8ee7bf --- /dev/null +++ b/backend/models/OfficialCompetitionMember.js @@ -0,0 +1,25 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; + +const OfficialCompetitionMember = sequelize.define('OfficialCompetitionMember', { + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + tournamentId: { type: DataTypes.INTEGER, allowNull: false }, + competitionId: { type: DataTypes.INTEGER, allowNull: false }, + memberId: { type: DataTypes.INTEGER, allowNull: false }, + wants: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, + registered: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, + participated: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, + placement: { type: DataTypes.STRING, allowNull: true }, +}, { + tableName: 'official_competition_members', + timestamps: true, + underscored: true, + indexes: [ + { unique: true, fields: ['competition_id', 'member_id'] }, + { fields: ['tournament_id'] }, + ], +}); + +export default OfficialCompetitionMember; + + diff --git a/backend/models/index.js b/backend/models/index.js index 0473078..7f747b0 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -32,9 +32,17 @@ import Accident from './Accident.js'; import UserToken from './UserToken.js'; import OfficialTournament from './OfficialTournament.js'; import OfficialCompetition from './OfficialCompetition.js'; +import OfficialCompetitionMember from './OfficialCompetitionMember.js'; // Official tournaments relations OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' }); OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' }); +// Official competition participations +OfficialCompetition.hasMany(OfficialCompetitionMember, { foreignKey: 'competitionId', as: 'members' }); +OfficialCompetitionMember.belongsTo(OfficialCompetition, { foreignKey: 'competitionId', as: 'competition' }); +OfficialTournament.hasMany(OfficialCompetitionMember, { foreignKey: 'tournamentId', as: 'competitionMembers' }); +OfficialCompetitionMember.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' }); +Member.hasMany(OfficialCompetitionMember, { foreignKey: 'memberId', as: 'officialCompetitionEntries' }); +OfficialCompetitionMember.belongsTo(Member, { foreignKey: 'memberId', as: 'member' }); User.hasMany(Log, { foreignKey: 'userId' }); Log.belongsTo(User, { foreignKey: 'userId' }); @@ -230,4 +238,5 @@ export { UserToken, OfficialTournament, OfficialCompetition, + OfficialCompetitionMember, }; diff --git a/backend/routes/officialTournamentRoutes.js b/backend/routes/officialTournamentRoutes.js index ebdd60d..e2fad46 100644 --- a/backend/routes/officialTournamentRoutes.js +++ b/backend/routes/officialTournamentRoutes.js @@ -1,7 +1,7 @@ import express from 'express'; import multer from 'multer'; import { authenticate } from '../middleware/authMiddleware.js'; -import { uploadTournamentPdf, getParsedTournament, listOfficialTournaments, deleteOfficialTournament } from '../controllers/officialTournamentController.js'; +import { uploadTournamentPdf, getParsedTournament, listOfficialTournaments, deleteOfficialTournament, upsertCompetitionMember } from '../controllers/officialTournamentController.js'; const router = express.Router(); const upload = multer({ storage: multer.memoryStorage() }); @@ -12,6 +12,7 @@ router.get('/:clubId', listOfficialTournaments); router.post('/:clubId/upload', upload.single('pdf'), uploadTournamentPdf); router.get('/:clubId/:id', getParsedTournament); router.delete('/:clubId/:id', deleteOfficialTournament); +router.post('/:clubId/:id/participation', upsertCompetitionMember); export default router; diff --git a/backend/server.js b/backend/server.js index 03f073b..3b61367 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,7 +8,7 @@ import { DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag, PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, Group, GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult, - TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition + TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember } from './models/index.js'; import authRoutes from './routes/authRoutes.js'; import clubRoutes from './routes/clubRoutes.js'; @@ -117,41 +117,60 @@ app.get('*', (req, res) => { await renameColumnIfExists('official_tournaments', 'konkurrenztypen', 'competition_types', 'TEXT NULL'); await renameColumnIfExists('official_tournaments', 'meldeschluesse', 'registration_deadlines', 'TEXT NULL'); - await User.sync(); - await Club.sync({ alter: true }); - await UserClub.sync({ alter: true }); - await Log.sync({ alter: true }); - await Member.sync({ alter: true }); - await DiaryDate.sync({ alter: true }); - await Participant.sync({ alter: true }); - await Activity.sync({ alter: true }); - await MemberNote.sync({ alter: true }); - await DiaryNote.sync({ alter: true }); - await DiaryTag.sync({ alter: true }); - await MemberDiaryTag.sync({ alter: true }); - await DiaryDateTag.sync({ alter: true }); - await DiaryMemberTag.sync({ alter: true }); - await DiaryMemberNote.sync({ alter: true }); - await PredefinedActivity.sync({ alter: true }); - await PredefinedActivityImage.sync({ alter: true }); - await DiaryDateActivity.sync({ alter: true }); - await DiaryMemberActivity.sync({ alter: true }); - await OfficialTournament.sync({ alter: true }); - await OfficialCompetition.sync({ alter: true }); - await Season.sync({ alter: true }); - await League.sync({ alter: true }); - await Team.sync({ alter: true }); - await Location.sync({ alter: true }); - await Match.sync({ alter: true }); - await Group.sync({ alter: true }); - await GroupActivity.sync({ alter: true }); - await Tournament.sync({ alter: true }); - await TournamentGroup.sync({ alter: true }); - await TournamentMember.sync({ alter: true }); - await TournamentMatch.sync({ alter: true }); - await TournamentResult.sync({ alter: true }); - await Accident.sync({ alter: true }); - await UserToken.sync(); + const isDev = process.env.STAGE === 'dev'; + const safeSync = async (model) => { + try { + if (isDev) { + await model.sync({ alter: true }); + } else { + await model.sync(); + } + } catch (e) { + try { + console.error(`[sync] ${model?.name || 'model'} alter failed:`, e?.message || e); + await model.sync(); + } catch (e2) { + console.error(`[sync] fallback failed for ${model?.name || 'model'}:`, e2?.message || e2); + } + } + }; + + await safeSync(User); + await safeSync(Club); + await safeSync(UserClub); + await safeSync(Log); + await safeSync(Member); + await safeSync(DiaryDate); + await safeSync(Participant); + await safeSync(Activity); + await safeSync(MemberNote); + await safeSync(DiaryNote); + await safeSync(DiaryTag); + await safeSync(MemberDiaryTag); + await safeSync(DiaryDateTag); + await safeSync(DiaryMemberTag); + await safeSync(DiaryMemberNote); + await safeSync(PredefinedActivity); + await safeSync(PredefinedActivityImage); + await safeSync(DiaryDateActivity); + await safeSync(DiaryMemberActivity); + await safeSync(OfficialTournament); + await safeSync(OfficialCompetition); + await safeSync(OfficialCompetitionMember); + await safeSync(Season); + await safeSync(League); + await safeSync(Team); + await safeSync(Location); + await safeSync(Match); + await safeSync(Group); + await safeSync(GroupActivity); + await safeSync(Tournament); + await safeSync(TournamentGroup); + await safeSync(TournamentMember); + await safeSync(TournamentMatch); + await safeSync(TournamentResult); + await safeSync(Accident); + await safeSync(UserToken); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6978e75..3e3e03e 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -50,7 +50,7 @@ - Turniere + Interne Turniere diff --git a/frontend/src/components/PDFGenerator.js b/frontend/src/components/PDFGenerator.js index 195eeec..c9014c1 100644 --- a/frontend/src/components/PDFGenerator.js +++ b/frontend/src/components/PDFGenerator.js @@ -264,7 +264,7 @@ class PDFGenerator { }); } - addMemberCompetitions(tournamentTitle, memberName, recommendedRows = [], otherRows = []) { + addMemberCompetitions(tournamentTitle, memberName, recommendedRows = [], otherRows = [], venues = []) { let y = this.margin; this.pdf.setFont('helvetica', 'bold'); this.pdf.setFontSize(14); @@ -319,6 +319,37 @@ class PDFGenerator { } } } + // Austragungsort(e) direkt vor den Hinweisen + const venueLines = Array.isArray(venues) ? venues.filter(Boolean) : []; + if (venueLines.length) { + const heading = venueLines.length === 1 ? 'Austragungsort' : 'Austragungsorte'; + const maxWidth = 210 - this.margin * 2; + if (y + 20 + venueLines.length * 6 > this.pageHeight) { + this.addNewPage(); + y = this.margin; + } else { + y += 6; + } + this.pdf.setFont('helvetica', 'bold'); + this.pdf.setFontSize(13); + this.pdf.text(`${heading}:`, this.margin, y); + y += 7; + this.pdf.setFont('helvetica', 'normal'); + this.pdf.setFontSize(12); + for (const v of venueLines) { + const wrapped = this.pdf.splitTextToSize(String(v), maxWidth); + for (const line of wrapped) { + this.pdf.text(line, this.margin, y); + y += 6; + if (y > this.pageHeight) { + this.addNewPage(); + y = this.margin; + this.pdf.setFont('helvetica', 'normal'); + this.pdf.setFontSize(12); + } + } + } + } // Hinweise-Sektion const remainingForHints = 60; // Platz für Überschrift + Liste abschätzen if (y + remainingForHints > this.pageHeight) { diff --git a/frontend/src/views/OfficialTournaments.vue b/frontend/src/views/OfficialTournaments.vue index 4969dec..0c89b28 100644 --- a/frontend/src/views/OfficialTournaments.vue +++ b/frontend/src/views/OfficialTournaments.vue @@ -87,13 +87,42 @@