Freundschaftsspiele korrigiert
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 52s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 52s
This commit is contained in:
58
backend/services/clubVenueService.js
Normal file
58
backend/services/clubVenueService.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import ClubVenue from '../models/ClubVenue.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
const clean = (value) => String(value || '').trim();
|
||||
const cleanNullable = (value) => clean(value) || null;
|
||||
|
||||
class ClubVenueService {
|
||||
async list(userToken, clubId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
return ClubVenue.findAll({
|
||||
where: { clubId },
|
||||
order: [['sortOrder', 'ASC'], ['name', 'ASC'], ['id', 'ASC']],
|
||||
});
|
||||
}
|
||||
|
||||
async create(userToken, clubId, payload = {}) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const name = clean(payload.name);
|
||||
if (!name) throw new HttpError('Name ist erforderlich', 400);
|
||||
const maxSortOrder = await ClubVenue.max('sortOrder', { where: { clubId } }) || 0;
|
||||
return ClubVenue.create({
|
||||
clubId,
|
||||
name,
|
||||
address: cleanNullable(payload.address),
|
||||
zip: cleanNullable(payload.zip),
|
||||
city: cleanNullable(payload.city),
|
||||
sortOrder: Number.isFinite(Number(payload.sortOrder)) ? Number(payload.sortOrder) : maxSortOrder + 1,
|
||||
});
|
||||
}
|
||||
|
||||
async update(userToken, clubId, venueId, payload = {}) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const venue = await ClubVenue.findOne({ where: { id: venueId, clubId } });
|
||||
if (!venue) throw new HttpError('Spiellokal nicht gefunden', 404);
|
||||
const updates = {};
|
||||
if (payload.name !== undefined) {
|
||||
const name = clean(payload.name);
|
||||
if (!name) throw new HttpError('Name ist erforderlich', 400);
|
||||
updates.name = name;
|
||||
}
|
||||
if (payload.address !== undefined) updates.address = cleanNullable(payload.address);
|
||||
if (payload.zip !== undefined) updates.zip = cleanNullable(payload.zip);
|
||||
if (payload.city !== undefined) updates.city = cleanNullable(payload.city);
|
||||
if (payload.sortOrder !== undefined && Number.isFinite(Number(payload.sortOrder))) updates.sortOrder = Number(payload.sortOrder);
|
||||
return venue.update(updates);
|
||||
}
|
||||
|
||||
async delete(userToken, clubId, venueId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const venue = await ClubVenue.findOne({ where: { id: venueId, clubId } });
|
||||
if (!venue) throw new HttpError('Spiellokal nicht gefunden', 404);
|
||||
await venue.destroy();
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export default new ClubVenueService();
|
||||
@@ -71,6 +71,21 @@ function normalizeIdList(list) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function isMatchLocked(match) {
|
||||
const dateText = cleanString(match?.date);
|
||||
if (!dateText) return false;
|
||||
const [year, month, day] = dateText.split('-').map((value) => Number.parseInt(value, 10));
|
||||
if (!year || !month || !day) return false;
|
||||
const endsAt = new Date(year, month - 1, day, 23, 59, 59, 999);
|
||||
return endsAt.getTime() <= Date.now();
|
||||
}
|
||||
|
||||
function assertMatchEditable(match) {
|
||||
if (isMatchLocked(match)) {
|
||||
throw new HttpError('Der Termin ist verstrichen. Das Freundschaftsspiel ist nur noch sichtbar.', 409);
|
||||
}
|
||||
}
|
||||
|
||||
function toScheduleRow(match) {
|
||||
return {
|
||||
id: match.id,
|
||||
@@ -100,6 +115,7 @@ function toScheduleRow(match) {
|
||||
playersReady: normalizeArrayValue(match.playersReady),
|
||||
playersPlanned: normalizeArrayValue(match.playersPlanned),
|
||||
playersPlayed: normalizeArrayValue(match.playersPlayed),
|
||||
isLocked: isMatchLocked(match),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -153,6 +169,7 @@ class FriendlyMatchService {
|
||||
await checkAccess(userToken, clubId);
|
||||
const match = await FriendlyMatch.findOne({ where: { id: matchId, clubId } });
|
||||
if (!match) throw new HttpError('Freundschaftsspiel nicht gefunden.', 404);
|
||||
assertMatchEditable(match);
|
||||
|
||||
const updates = {};
|
||||
for (const field of ['date', 'time', 'homeTeamName', 'guestTeamName', 'locationName', 'locationAddress', 'locationCity', 'locationZip', 'matchSystem']) {
|
||||
@@ -189,6 +206,7 @@ class FriendlyMatchService {
|
||||
await checkAccess(userToken, clubId);
|
||||
const match = await FriendlyMatch.findOne({ where: { id: matchId, clubId } });
|
||||
if (!match) throw new HttpError('Freundschaftsspiel nicht gefunden.', 404);
|
||||
assertMatchEditable(match);
|
||||
|
||||
const ready = normalizeIdList(payload.playersReady);
|
||||
const planned = normalizeIdList(payload.playersPlanned);
|
||||
|
||||
@@ -4,6 +4,7 @@ import FriendlyMatchInvitation from '../models/FriendlyMatchInvitation.js';
|
||||
import UserClub from '../models/UserClub.js';
|
||||
import User from '../models/User.js';
|
||||
import Club from '../models/Club.js';
|
||||
import Member from '../models/Member.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import { checkAccess, getUserByToken } from '../utils/userUtils.js';
|
||||
import { sendFriendlyMatchInvitationEmail } from './emailService.js';
|
||||
@@ -66,7 +67,41 @@ function isClubInvolved(clubId, match) {
|
||||
return Number(match.homeClubId) === id || Number(match.guestClubId) === id;
|
||||
}
|
||||
|
||||
function toSharedScheduleRow(match) {
|
||||
function isSharedMatchLocked(match) {
|
||||
const dateText = cleanString(match?.date);
|
||||
if (!dateText) return false;
|
||||
const [year, month, day] = dateText.split('-').map((value) => Number.parseInt(value, 10));
|
||||
if (!year || !month || !day) return false;
|
||||
const endsAt = new Date(year, month - 1, day, 23, 59, 59, 999);
|
||||
return endsAt.getTime() <= Date.now();
|
||||
}
|
||||
|
||||
function assertSharedMatchEditable(match) {
|
||||
if (isSharedMatchLocked(match)) {
|
||||
throw new HttpError('Der Termin ist verstrichen. Das Freundschaftsspiel ist nur noch sichtbar.', 409);
|
||||
}
|
||||
}
|
||||
|
||||
function canShowOpponentMembers(match) {
|
||||
return String(match?.status || '') === 'active' && !isSharedMatchLocked(match);
|
||||
}
|
||||
|
||||
function toSharedScheduleRow(match, viewerClubId = null) {
|
||||
const hideOpponent = viewerClubId != null && !canShowOpponentMembers(match);
|
||||
const viewerIsHome = Number(viewerClubId) === Number(match.homeClubId);
|
||||
const rawHomeParticipants = normalizeArrayValue(match.homeParticipants);
|
||||
const rawGuestParticipants = normalizeArrayValue(match.guestParticipants);
|
||||
const rawResultDetails = normalizeArrayValue(match.resultDetails);
|
||||
const homeParticipants = hideOpponent && !viewerIsHome ? [] : rawHomeParticipants;
|
||||
const guestParticipants = hideOpponent && viewerIsHome ? [] : rawGuestParticipants;
|
||||
const resultDetails = hideOpponent
|
||||
? rawResultDetails.map((row) => ({
|
||||
...row,
|
||||
homeName: viewerIsHome ? row?.homeName : '',
|
||||
guestName: viewerIsHome ? '' : row?.guestName,
|
||||
}))
|
||||
: rawResultDetails;
|
||||
|
||||
return {
|
||||
id: match.id,
|
||||
friendlyMatchId: match.id,
|
||||
@@ -92,13 +127,14 @@ function toSharedScheduleRow(match) {
|
||||
singlesCount: match.singlesCount,
|
||||
doublesCount: match.doublesCount,
|
||||
winningSets: match.winningSets,
|
||||
homeParticipants: normalizeArrayValue(match.homeParticipants),
|
||||
guestParticipants: normalizeArrayValue(match.guestParticipants),
|
||||
resultDetails: normalizeArrayValue(match.resultDetails),
|
||||
homeParticipants,
|
||||
guestParticipants,
|
||||
resultDetails,
|
||||
playersReady: normalizeArrayValue(match.playersReady),
|
||||
playersPlanned: normalizeArrayValue(match.playersPlanned),
|
||||
playersPlayed: normalizeArrayValue(match.playersPlayed),
|
||||
status: match.status,
|
||||
isLocked: isSharedMatchLocked(match),
|
||||
matchName: match.matchName,
|
||||
createdFromInvitationId: match.createdFromInvitationId,
|
||||
};
|
||||
@@ -143,7 +179,7 @@ class FriendlyMatchSharedService {
|
||||
|
||||
const out = matches
|
||||
.map((match) => {
|
||||
const row = toSharedScheduleRow(match);
|
||||
const row = toSharedScheduleRow(match, clubId);
|
||||
const combined = normalizeTextForSearch([
|
||||
row.matchName,
|
||||
row.homeTeam?.name,
|
||||
@@ -180,7 +216,7 @@ class FriendlyMatchSharedService {
|
||||
},
|
||||
order: [['date', 'ASC'], ['startTime', 'ASC'], ['id', 'ASC']],
|
||||
});
|
||||
return matches.map(toSharedScheduleRow);
|
||||
return matches.map((match) => toSharedScheduleRow(match, clubId));
|
||||
}
|
||||
|
||||
async getSharedById(userToken, clubId, matchId) {
|
||||
@@ -189,7 +225,7 @@ class FriendlyMatchSharedService {
|
||||
if (!match || !isClubInvolved(clubId, match)) {
|
||||
throw new HttpError('Gemeinsames Freundschaftsspiel nicht gefunden.', 404);
|
||||
}
|
||||
return toSharedScheduleRow(match);
|
||||
return toSharedScheduleRow(match, clubId);
|
||||
}
|
||||
|
||||
async updateShared(userToken, clubId, matchId, payload = {}) {
|
||||
@@ -199,8 +235,13 @@ class FriendlyMatchSharedService {
|
||||
if (!match || !isClubInvolved(clubId, match)) {
|
||||
throw new HttpError('Gemeinsames Freundschaftsspiel nicht gefunden.', 404);
|
||||
}
|
||||
assertSharedMatchEditable(match);
|
||||
|
||||
const updates = {};
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'time') && !Object.prototype.hasOwnProperty.call(payload, 'startTime')) {
|
||||
payload.startTime = payload.time;
|
||||
}
|
||||
|
||||
for (const field of ['date', 'startTime', 'matchName', 'homeTeamName', 'guestTeamName', 'locationName', 'locationAddress', 'locationCity', 'locationZip', 'matchSystem', 'status']) {
|
||||
if (Object.prototype.hasOwnProperty.call(payload, field)) {
|
||||
updates[field] = ['date', 'homeTeamName', 'guestTeamName', 'matchSystem', 'status'].includes(field)
|
||||
@@ -229,7 +270,28 @@ class FriendlyMatchSharedService {
|
||||
}
|
||||
|
||||
await match.update(updates);
|
||||
return toSharedScheduleRow(match);
|
||||
return toSharedScheduleRow(match, clubId);
|
||||
}
|
||||
|
||||
async membersForSide(userToken, clubId, matchId, side) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const match = await FriendlyMatchShared.findByPk(matchId);
|
||||
if (!match || !isClubInvolved(clubId, match)) {
|
||||
throw new HttpError('Gemeinsames Freundschaftsspiel nicht gefunden.', 404);
|
||||
}
|
||||
|
||||
const requestedSide = side === 'guest' ? 'guest' : 'home';
|
||||
const targetClubId = requestedSide === 'guest' ? match.guestClubId : match.homeClubId;
|
||||
const isOpponent = Number(targetClubId) !== Number(clubId);
|
||||
if (isOpponent && !canShowOpponentMembers(match)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Member.findAll({
|
||||
where: { clubId: targetClubId, active: true },
|
||||
attributes: ['id', 'firstName', 'lastName', 'gender', 'clubId'],
|
||||
order: [['lastName', 'ASC'], ['firstName', 'ASC']],
|
||||
});
|
||||
}
|
||||
|
||||
async updateSharedPlayers(userToken, clubId, matchId, payload = {}) {
|
||||
@@ -239,18 +301,34 @@ class FriendlyMatchSharedService {
|
||||
if (!match || !isClubInvolved(clubId, match)) {
|
||||
throw new HttpError('Gemeinsames Freundschaftsspiel nicht gefunden.', 404);
|
||||
}
|
||||
assertSharedMatchEditable(match);
|
||||
|
||||
const ready = normalizeIdList(payload.playersReady);
|
||||
const planned = normalizeIdList(payload.playersPlanned);
|
||||
const played = normalizeIdList(payload.playersPlayed);
|
||||
|
||||
const currentClubMembers = await Member.findAll({
|
||||
where: { clubId },
|
||||
attributes: ['id'],
|
||||
});
|
||||
const currentClubMemberIds = new Set(currentClubMembers.map((member) => Number(member.id)));
|
||||
const mergeForCurrentClub = (existingValue, nextValue) => {
|
||||
if (nextValue == null) return normalizeArrayValue(existingValue);
|
||||
const existing = normalizeArrayValue(existingValue)
|
||||
.map((id) => Number.parseInt(id, 10))
|
||||
.filter((id) => Number.isInteger(id));
|
||||
const preservedOtherClub = existing.filter((id) => !currentClubMemberIds.has(id));
|
||||
const nextCurrentClub = nextValue.filter((id) => currentClubMemberIds.has(id));
|
||||
return [...new Set([...preservedOtherClub, ...nextCurrentClub])];
|
||||
};
|
||||
|
||||
await match.update({
|
||||
playersReady: ready ?? (match.playersReady || []),
|
||||
playersPlanned: planned ?? (match.playersPlanned || []),
|
||||
playersPlayed: played ?? (match.playersPlayed || []),
|
||||
playersReady: mergeForCurrentClub(match.playersReady, ready),
|
||||
playersPlanned: mergeForCurrentClub(match.playersPlanned, planned),
|
||||
playersPlayed: mergeForCurrentClub(match.playersPlayed, played),
|
||||
});
|
||||
|
||||
return toSharedScheduleRow(match);
|
||||
return toSharedScheduleRow(match, clubId);
|
||||
}
|
||||
|
||||
async removeShared(userToken, clubId, matchId) {
|
||||
@@ -367,7 +445,7 @@ class FriendlyMatchSharedService {
|
||||
|
||||
return {
|
||||
invitation: toInvitationDto(invitation),
|
||||
sharedMatch: toSharedScheduleRow(shared),
|
||||
sharedMatch: toSharedScheduleRow(shared, clubId),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user