feat: Implement friendly match management features
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
- Added backend support for managing friendly matches including listing, creating, updating, and deleting matches. - Introduced a new database table `friendly_match` with relevant fields for match details. - Created a service layer to handle business logic related to friendly matches. - Developed API routes for friendly match operations with appropriate authentication and authorization. - Added a Vue component for managing participants in friendly matches, allowing selection of members and manual entry of names. - Updated existing tournament editor screens to integrate friendly match functionalities.
This commit is contained in:
214
backend/services/friendlyMatchService.js
Normal file
214
backend/services/friendlyMatchService.js
Normal file
@@ -0,0 +1,214 @@
|
||||
import FriendlyMatch from '../models/FriendlyMatch.js';
|
||||
import Member from '../models/Member.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
function cleanString(value, fallback = '') {
|
||||
const text = String(value ?? '').trim();
|
||||
return text || fallback;
|
||||
}
|
||||
|
||||
function cleanOptionalString(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
function normalizeParticipantList(list) {
|
||||
if (typeof list === 'string') {
|
||||
try {
|
||||
list = JSON.parse(list);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list
|
||||
.map((entry) => {
|
||||
const type = entry?.type === 'member' ? 'member' : 'manual';
|
||||
if (type === 'member') {
|
||||
const memberId = Number.parseInt(entry?.memberId, 10);
|
||||
if (!Number.isInteger(memberId)) return null;
|
||||
return { type, memberId };
|
||||
}
|
||||
const firstName = cleanString(entry?.firstName);
|
||||
const lastName = cleanString(entry?.lastName);
|
||||
if (!firstName && !lastName) return null;
|
||||
return { type, firstName, lastName };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeArrayValue(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeIdList(list) {
|
||||
if (typeof list === 'string') {
|
||||
try {
|
||||
list = JSON.parse(list);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(list)) return null;
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const value of list) {
|
||||
const id = Number.parseInt(value, 10);
|
||||
if (!Number.isInteger(id) || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
result.push(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function toScheduleRow(match) {
|
||||
return {
|
||||
id: match.id,
|
||||
friendlyMatchId: match.id,
|
||||
isFriendly: true,
|
||||
date: match.date,
|
||||
time: match.time,
|
||||
homeTeam: { name: match.homeTeamName },
|
||||
guestTeam: { name: match.guestTeamName },
|
||||
location: {
|
||||
name: match.locationName || 'N/A',
|
||||
address: match.locationAddress || '',
|
||||
city: match.locationCity || '',
|
||||
zip: match.locationZip || '',
|
||||
},
|
||||
leagueDetails: { name: 'Freundschaftsspiel' },
|
||||
homeMatchPoints: match.homeMatchPoints || 0,
|
||||
guestMatchPoints: match.guestMatchPoints || 0,
|
||||
isCompleted: match.isCompleted || false,
|
||||
matchSystem: match.matchSystem,
|
||||
singlesCount: match.singlesCount,
|
||||
doublesCount: match.doublesCount,
|
||||
winningSets: match.winningSets,
|
||||
homeParticipants: normalizeParticipantList(match.homeParticipants),
|
||||
guestParticipants: normalizeParticipantList(match.guestParticipants),
|
||||
resultDetails: normalizeArrayValue(match.resultDetails),
|
||||
playersReady: normalizeArrayValue(match.playersReady),
|
||||
playersPlanned: normalizeArrayValue(match.playersPlanned),
|
||||
playersPlayed: normalizeArrayValue(match.playersPlayed),
|
||||
};
|
||||
}
|
||||
|
||||
class FriendlyMatchService {
|
||||
async list(userToken, clubId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const matches = await FriendlyMatch.findAll({
|
||||
where: { clubId },
|
||||
order: [['date', 'ASC'], ['time', 'ASC'], ['id', 'ASC']],
|
||||
});
|
||||
return matches.map(toScheduleRow);
|
||||
}
|
||||
|
||||
async create(userToken, clubId, payload = {}) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const homeTeamName = cleanString(payload.homeTeamName);
|
||||
const guestTeamName = cleanString(payload.guestTeamName);
|
||||
const date = cleanString(payload.date);
|
||||
if (!homeTeamName || !guestTeamName || !date) {
|
||||
throw new HttpError('Datum, Heimteam und Gastteam sind Pflichtfelder.', 400);
|
||||
}
|
||||
|
||||
const match = await FriendlyMatch.create({
|
||||
clubId,
|
||||
date,
|
||||
time: cleanOptionalString(payload.time),
|
||||
homeTeamName,
|
||||
guestTeamName,
|
||||
locationName: cleanOptionalString(payload.locationName),
|
||||
locationAddress: cleanOptionalString(payload.locationAddress),
|
||||
locationCity: cleanOptionalString(payload.locationCity),
|
||||
locationZip: cleanOptionalString(payload.locationZip),
|
||||
matchSystem: cleanString(payload.matchSystem, 'Braunschweiger System'),
|
||||
singlesCount: Number.parseInt(payload.singlesCount, 10) || 12,
|
||||
doublesCount: Number.parseInt(payload.doublesCount, 10) || 4,
|
||||
winningSets: Number.parseInt(payload.winningSets, 10) || 3,
|
||||
homeMatchPoints: Number.parseInt(payload.homeMatchPoints, 10) || 0,
|
||||
guestMatchPoints: Number.parseInt(payload.guestMatchPoints, 10) || 0,
|
||||
isCompleted: Boolean(payload.isCompleted),
|
||||
homeParticipants: normalizeParticipantList(payload.homeParticipants),
|
||||
guestParticipants: normalizeParticipantList(payload.guestParticipants),
|
||||
resultDetails: Array.isArray(payload.resultDetails) ? payload.resultDetails : [],
|
||||
playersReady: [],
|
||||
playersPlanned: [],
|
||||
playersPlayed: [],
|
||||
});
|
||||
return toScheduleRow(match);
|
||||
}
|
||||
|
||||
async update(userToken, clubId, matchId, payload = {}) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const match = await FriendlyMatch.findOne({ where: { id: matchId, clubId } });
|
||||
if (!match) throw new HttpError('Freundschaftsspiel nicht gefunden.', 404);
|
||||
|
||||
const updates = {};
|
||||
for (const field of ['date', 'time', 'homeTeamName', 'guestTeamName', 'locationName', 'locationAddress', 'locationCity', 'locationZip', 'matchSystem']) {
|
||||
if (Object.prototype.hasOwnProperty.call(payload, field)) {
|
||||
updates[field] = field === 'date' || field === 'homeTeamName' || field === 'guestTeamName' || field === 'matchSystem'
|
||||
? cleanString(payload[field])
|
||||
: cleanOptionalString(payload[field]);
|
||||
}
|
||||
}
|
||||
for (const field of ['singlesCount', 'doublesCount', 'winningSets', 'homeMatchPoints', 'guestMatchPoints']) {
|
||||
if (Object.prototype.hasOwnProperty.call(payload, field)) {
|
||||
updates[field] = Number.parseInt(payload[field], 10) || 0;
|
||||
}
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'isCompleted')) updates.isCompleted = Boolean(payload.isCompleted);
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'homeParticipants')) updates.homeParticipants = normalizeParticipantList(payload.homeParticipants);
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'guestParticipants')) updates.guestParticipants = normalizeParticipantList(payload.guestParticipants);
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'resultDetails')) {
|
||||
updates.resultDetails = Array.isArray(payload.resultDetails) ? payload.resultDetails : [];
|
||||
}
|
||||
|
||||
await match.update(updates);
|
||||
return toScheduleRow(match);
|
||||
}
|
||||
|
||||
async remove(userToken, clubId, matchId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const deleted = await FriendlyMatch.destroy({ where: { id: matchId, clubId } });
|
||||
if (!deleted) throw new HttpError('Freundschaftsspiel nicht gefunden.', 404);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async updatePlayers(userToken, clubId, matchId, payload = {}) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const match = await FriendlyMatch.findOne({ where: { id: matchId, clubId } });
|
||||
if (!match) throw new HttpError('Freundschaftsspiel nicht gefunden.', 404);
|
||||
|
||||
const ready = normalizeIdList(payload.playersReady);
|
||||
const planned = normalizeIdList(payload.playersPlanned);
|
||||
const played = normalizeIdList(payload.playersPlayed);
|
||||
await match.update({
|
||||
playersReady: ready ?? (match.playersReady || []),
|
||||
playersPlanned: planned ?? (match.playersPlanned || []),
|
||||
playersPlayed: played ?? (match.playersPlayed || []),
|
||||
});
|
||||
return toScheduleRow(match);
|
||||
}
|
||||
|
||||
async members(userToken, clubId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
return Member.findAll({
|
||||
where: { clubId, active: true },
|
||||
attributes: ['id', 'firstName', 'lastName', 'gender'],
|
||||
order: [['lastName', 'ASC'], ['firstName', 'ASC']],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new FriendlyMatchService();
|
||||
Reference in New Issue
Block a user