feat(tactics): add functionality for managing tactic plans in official tournaments
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 55s
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:
@@ -17,6 +17,35 @@ export const updateOfficialTournament = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const listTacticPlans = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers; const { clubId, tournamentId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const plans = await officialTournamentService.listTacticPlans(clubId, tournamentId);
|
||||
if (!plans) return res.status(404).json({ error: 'not found' });
|
||||
res.status(200).json(plans);
|
||||
} catch (e) { res.status(e.status || 500).json({ error: e.message || 'Failed to list tactic plans' }); }
|
||||
};
|
||||
|
||||
export const saveTacticPlan = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers; const { clubId, tournamentId, planId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const plan = await officialTournamentService.saveTacticPlan(clubId, tournamentId, planId, req.body);
|
||||
res.status(planId ? 200 : 201).json(plan);
|
||||
} catch (e) { res.status(e.status || 500).json({ error: e.message || 'Failed to save tactic plan' }); }
|
||||
};
|
||||
|
||||
export const deleteTacticPlan = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers; const { clubId, tournamentId, planId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const deleted = await officialTournamentService.deleteTacticPlan(clubId, tournamentId, planId);
|
||||
if (!deleted) return res.status(404).json({ error: 'not found' });
|
||||
res.status(204).send();
|
||||
} catch (e) { res.status(e.status || 500).json({ error: e.message || 'Failed to delete tactic plan' }); }
|
||||
};
|
||||
|
||||
export const uploadTournamentPdf = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
|
||||
23
backend/models/OfficialTournamentTacticPlan.js
Normal file
23
backend/models/OfficialTournamentTacticPlan.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const OfficialTournamentTacticPlan = sequelize.define('OfficialTournamentTacticPlan', {
|
||||
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 },
|
||||
round: { type: DataTypes.STRING, allowNull: true },
|
||||
opponentName: { type: DataTypes.STRING, allowNull: true },
|
||||
ownStrengths: { type: DataTypes.TEXT, allowNull: true },
|
||||
ownWeaknesses: { type: DataTypes.TEXT, allowNull: true },
|
||||
opponentStrengths: { type: DataTypes.TEXT, allowNull: true },
|
||||
opponentWeaknesses: { type: DataTypes.TEXT, allowNull: true },
|
||||
drawingData: { type: DataTypes.TEXT('medium'), allowNull: true },
|
||||
}, {
|
||||
tableName: 'official_tournament_tactic_plans',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [{ fields: ['tournament_id'] }, { fields: ['competition_id', 'member_id'] }],
|
||||
});
|
||||
|
||||
export default OfficialTournamentTacticPlan;
|
||||
@@ -41,6 +41,7 @@ import UserToken from './UserToken.js';
|
||||
import OfficialTournament from './OfficialTournament.js';
|
||||
import OfficialCompetition from './OfficialCompetition.js';
|
||||
import OfficialCompetitionMember from './OfficialCompetitionMember.js';
|
||||
import OfficialTournamentTacticPlan from './OfficialTournamentTacticPlan.js';
|
||||
import MyTischtennis from './MyTischtennis.js';
|
||||
import MyTischtennisUpdateHistory from './MyTischtennisUpdateHistory.js';
|
||||
import MyTischtennisFetchLog from './MyTischtennisFetchLog.js';
|
||||
@@ -108,6 +109,10 @@ OfficialTournament.hasMany(OfficialCompetitionMember, { foreignKey: 'tournamentI
|
||||
OfficialCompetitionMember.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Member.hasMany(OfficialCompetitionMember, { foreignKey: 'memberId', as: 'officialCompetitionEntries' });
|
||||
OfficialCompetitionMember.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
|
||||
OfficialTournament.hasMany(OfficialTournamentTacticPlan, { foreignKey: 'tournamentId', as: 'tacticPlans' });
|
||||
OfficialTournamentTacticPlan.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
OfficialTournamentTacticPlan.belongsTo(OfficialCompetition, { foreignKey: 'competitionId', as: 'competition' });
|
||||
OfficialTournamentTacticPlan.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
|
||||
|
||||
User.hasMany(Log, { foreignKey: 'userId' });
|
||||
Log.belongsTo(User, { foreignKey: 'userId' });
|
||||
@@ -665,6 +670,7 @@ export {
|
||||
OfficialTournament,
|
||||
OfficialCompetition,
|
||||
OfficialCompetitionMember,
|
||||
OfficialTournamentTacticPlan,
|
||||
MyTischtennis,
|
||||
MyTischtennisUpdateHistory,
|
||||
MyTischtennisFetchLog,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
listClubParticipations,
|
||||
updateParticipantStatus,
|
||||
autoRegisterOfficialTournamentParticipants
|
||||
, listTacticPlans, saveTacticPlan, deleteTacticPlan
|
||||
} from '../controllers/officialTournamentController.js';
|
||||
import { fetchTournamentSuggestions, listTournamentSuggestions, updateTournamentSuggestion } from '../controllers/tournamentSuggestionController.js';
|
||||
|
||||
@@ -25,6 +26,10 @@ router.post('/:clubId/suggestions/fetch', fetchTournamentSuggestions);
|
||||
router.patch('/:clubId/suggestions/:id', updateTournamentSuggestion);
|
||||
router.get('/:clubId/participations/summary', listClubParticipations);
|
||||
router.post('/:clubId/upload', upload.single('pdf'), uploadTournamentPdf);
|
||||
router.get('/:clubId/:tournamentId/tactics', listTacticPlans);
|
||||
router.post('/:clubId/:tournamentId/tactics', saveTacticPlan);
|
||||
router.patch('/:clubId/:tournamentId/tactics/:planId', saveTacticPlan);
|
||||
router.delete('/:clubId/:tournamentId/tactics/:planId', deleteTacticPlan);
|
||||
router.get('/:clubId/:id', getParsedTournament);
|
||||
router.patch('/:clubId/:id', updateOfficialTournament);
|
||||
router.delete('/:clubId/:id', deleteOfficialTournament);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
|
||||
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, ClubTeamMember, TeamDocument, Group,
|
||||
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
|
||||
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, ClickTtAccount, MyTischtennisUpdateHistory, MyTischtennisFetchLog, ApiLog, MemberTransferConfig, MemberContact, MemberTtrHistory, MemberPlayInterest,
|
||||
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, OfficialTournamentTacticPlan, MyTischtennis, ClickTtAccount, MyTischtennisUpdateHistory, MyTischtennisFetchLog, ApiLog, MemberTransferConfig, MemberContact, MemberTtrHistory, MemberPlayInterest,
|
||||
MemberOrder, MemberOrderHistory, MemberGroupPhoto, BillingTemplate, BillingTemplateField, BillingRun, BillingDocument, BillingDocumentValue, BillingUserSetting, FriendlyMatch, TrainingCancellation
|
||||
, FriendlyMatchShared, FriendlyMatchInvitation
|
||||
, CalendarEvent, ClubVenue, ClubRequest, ClubRequestNote, ClubSepaMandate, ClubPaymentClaim, ClubAccount, ClubAccountTransaction, ClubInvoiceParty, ClubInvoice, ClubInvoiceItem, ClubRole, ClubUserRole, ClubCommunicationThread, ClubCommunicationMessage, ClubCommunicationRecipient, ClubCommunicationDeliveryLog, ClubCommunicationTemplate, ClubDistributionGroup, ClubDistributionGroupMember, MemberProfileChangeRequest, MemberEventResponse, NotificationEvent, NotificationRecipient, TournamentSuggestion
|
||||
@@ -630,6 +630,7 @@ app.use((err, req, res, next) => {
|
||||
await safeSync(OfficialTournament);
|
||||
await safeSync(OfficialCompetition);
|
||||
await safeSync(OfficialCompetitionMember);
|
||||
await safeSync(OfficialTournamentTacticPlan);
|
||||
await safeSync(Season);
|
||||
await safeSync(League);
|
||||
await safeSync(Team);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user