feat(tactics): add functionality for managing tactic plans in official tournaments
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 55s

- Implemented routes for listing, saving, updating, and deleting tactic plans in officialTournamentRoutes.js.
- Created OfficialTournamentTacticPlan model to handle tactic plan data.
- Developed OfficialTournamentService methods for tactic plan operations including validation and error handling.
- Enhanced OfficialTournaments.vue to include a new tactics tab with UI for managing tactic plans.
- Added TacticBoard component for visualizing and editing tactic paths.
- Updated server.js to synchronize the new tactic plan model.
This commit is contained in:
Torsten Schulz (local)
2026-08-14 13:15:26 +02:00
parent 3320884cef
commit f59c7391b8
8 changed files with 519 additions and 16 deletions

View File

@@ -6,10 +6,53 @@ import { Op } from 'sequelize';
import OfficialTournament from '../models/OfficialTournament.js';
import OfficialCompetition from '../models/OfficialCompetition.js';
import OfficialCompetitionMember from '../models/OfficialCompetitionMember.js';
import OfficialTournamentTacticPlan from '../models/OfficialTournamentTacticPlan.js';
import Member from '../models/Member.js';
import OfficialTournamentParserService from './officialTournamentParserService.js';
class OfficialTournamentService {
async listTacticPlans(clubId, tournamentId) {
const tournament = await OfficialTournament.findOne({ where: { id: tournamentId, clubId } });
if (!tournament) return null;
return OfficialTournamentTacticPlan.findAll({
where: { tournamentId },
include: [
{ model: OfficialCompetition, as: 'competition', attributes: ['id', 'ageClassCompetition'] },
{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] },
],
order: [['updatedAt', 'DESC']],
});
}
async saveTacticPlan(clubId, tournamentId, planId, payload) {
const { competitionId, memberId } = payload;
if (!competitionId || !memberId) {
const error = new Error('Bitte Spieler/in und Konkurrenz auswählen.'); error.status = 400; throw error;
}
const tournament = await OfficialTournament.findOne({ where: { id: tournamentId, clubId } });
if (!tournament) { const error = new Error('Turnier nicht gefunden.'); error.status = 404; throw error; }
const competition = await OfficialCompetition.findOne({ where: { id: competitionId, tournamentId } });
if (!competition) { const error = new Error('Die Konkurrenz gehört nicht zu diesem Turnier.'); error.status = 400; throw error; }
const entry = await OfficialCompetitionMember.findOne({ where: { tournamentId, competitionId, memberId } });
if (!entry || (!entry.registered && !entry.participated)) {
const error = new Error('Taktikpläne sind nur für angemeldete oder teilnehmende Spieler/innen möglich.'); error.status = 400; throw error;
}
const fields = ['competitionId', 'memberId', 'round', 'opponentName', 'ownStrengths', 'ownWeaknesses', 'opponentStrengths', 'opponentWeaknesses', 'drawingData'];
const values = Object.fromEntries(fields.map((key) => [key, payload[key] ?? null]));
if (planId) {
const plan = await OfficialTournamentTacticPlan.findOne({ where: { id: planId, tournamentId } });
if (!plan) { const error = new Error('Matchplan nicht gefunden.'); error.status = 404; throw error; }
await plan.update(values);
return plan;
}
return OfficialTournamentTacticPlan.create({ tournamentId, ...values });
}
async deleteTacticPlan(clubId, tournamentId, planId) {
const tournament = await OfficialTournament.findOne({ where: { id: tournamentId, clubId } });
if (!tournament) return null;
return OfficialTournamentTacticPlan.destroy({ where: { id: planId, tournamentId } });
}
async uploadTournamentPdf(clubId, pdfBuffer) {
const data = await pdfParse(pdfBuffer);
const parsed = OfficialTournamentParserService.parseTournamentText(data.text);
@@ -306,6 +349,7 @@ class OfficialTournamentService {
async deleteOfficialTournament(clubId, id) {
const t = await OfficialTournament.findOne({ where: { id, clubId } });
if (!t) return false;
await OfficialTournamentTacticPlan.destroy({ where: { tournamentId: id } });
await OfficialCompetition.destroy({ where: { tournamentId: id } });
await OfficialTournament.destroy({ where: { id } });
return true;