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:
52
backend/controllers/clubVenueController.js
Normal file
52
backend/controllers/clubVenueController.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import clubVenueService from '../services/clubVenueService.js';
|
||||
|
||||
const handleError = (res, label, error) => {
|
||||
if (error.message === 'noaccess') return res.status(403).json({ error: 'noaccess' });
|
||||
if (error.statusCode || error.status) return res.status(error.statusCode || error.status).json({ error: error.message });
|
||||
console.error(`[${label}] - error:`, error);
|
||||
return res.status(500).json({ error: 'internalerror' });
|
||||
};
|
||||
|
||||
export const listClubVenues = async (req, res) => {
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const venues = await clubVenueService.list(token, clubId);
|
||||
res.status(200).json(venues);
|
||||
} catch (error) {
|
||||
handleError(res, 'listClubVenues', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const createClubVenue = async (req, res) => {
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const venue = await clubVenueService.create(token, clubId, req.body);
|
||||
res.status(201).json(venue);
|
||||
} catch (error) {
|
||||
handleError(res, 'createClubVenue', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubVenue = async (req, res) => {
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, venueId } = req.params;
|
||||
const venue = await clubVenueService.update(token, clubId, venueId, req.body);
|
||||
res.status(200).json(venue);
|
||||
} catch (error) {
|
||||
handleError(res, 'updateClubVenue', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteClubVenue = async (req, res) => {
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, venueId } = req.params;
|
||||
const result = await clubVenueService.delete(token, clubId, venueId);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
handleError(res, 'deleteClubVenue', error);
|
||||
}
|
||||
};
|
||||
@@ -37,6 +37,21 @@ export const listSharedFriendlyMatches = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getSharedFriendlyMatchMembers = async (req, res) => {
|
||||
try {
|
||||
const members = await friendlyMatchSharedService.membersForSide(
|
||||
userTokenFrom(req),
|
||||
req.params.clubId,
|
||||
req.params.matchId,
|
||||
req.params.side,
|
||||
);
|
||||
res.status(200).json(members);
|
||||
} catch (error) {
|
||||
console.error('[getSharedFriendlyMatchMembers] Error:', error);
|
||||
res.status(error.statusCode || 500).json({ error: error.message || 'Mitglieder konnten nicht geladen werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSharedFriendlyMatch = async (req, res) => {
|
||||
try {
|
||||
const match = await friendlyMatchSharedService.updateShared(
|
||||
|
||||
15
backend/migrations/20260605_create_club_venue.sql
Normal file
15
backend/migrations/20260605_create_club_venue.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS `club_venue` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`club_id` INT NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`address` VARCHAR(255) NULL,
|
||||
`zip` VARCHAR(32) NULL,
|
||||
`city` VARCHAR(255) NULL,
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_club_venue_club_sort` (`club_id`, `sort_order`),
|
||||
KEY `idx_club_venue_club_name` (`club_id`, `name`),
|
||||
CONSTRAINT `fk_club_venue_club` FOREIGN KEY (`club_id`) REFERENCES `clubs` (`id`) ON DELETE CASCADE
|
||||
);
|
||||
54
backend/models/ClubVenue.js
Normal file
54
backend/models/ClubVenue.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Club from './Club.js';
|
||||
|
||||
const ClubVenue = sequelize.define('ClubVenue', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Club,
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
field: 'club_id',
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
},
|
||||
address: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
zip: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: true,
|
||||
},
|
||||
city: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
sortOrder: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: 'sort_order',
|
||||
},
|
||||
}, {
|
||||
tableName: 'club_venue',
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['club_id', 'sort_order'] },
|
||||
{ fields: ['club_id', 'name'] },
|
||||
],
|
||||
});
|
||||
|
||||
export default ClubVenue;
|
||||
@@ -69,6 +69,7 @@ import TrainingGroup from './TrainingGroup.js';
|
||||
import MemberTrainingGroup from './MemberTrainingGroup.js';
|
||||
import ClubDisabledPresetGroup from './ClubDisabledPresetGroup.js';
|
||||
import TrainingTime from './TrainingTime.js';
|
||||
import ClubVenue from './ClubVenue.js';
|
||||
// Official tournaments relations
|
||||
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
|
||||
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
@@ -443,6 +444,9 @@ FriendlyMatchInvitation.hasOne(FriendlyMatchShared, {
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
Club.hasMany(ClubVenue, { foreignKey: 'clubId', as: 'venues' });
|
||||
ClubVenue.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
|
||||
|
||||
export {
|
||||
User,
|
||||
Log,
|
||||
@@ -512,4 +516,5 @@ export {
|
||||
MemberTrainingGroup,
|
||||
ClubDisabledPresetGroup,
|
||||
TrainingTime,
|
||||
ClubVenue,
|
||||
};
|
||||
|
||||
18
backend/routes/clubVenueRoutes.js
Normal file
18
backend/routes/clubVenueRoutes.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { authorize } from '../middleware/authorizationMiddleware.js';
|
||||
import {
|
||||
createClubVenue,
|
||||
deleteClubVenue,
|
||||
listClubVenues,
|
||||
updateClubVenue,
|
||||
} from '../controllers/clubVenueController.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/:clubId', authenticate, authorize('settings', 'read'), listClubVenues);
|
||||
router.post('/:clubId', authenticate, authorize('settings', 'write'), createClubVenue);
|
||||
router.put('/:clubId/:venueId', authenticate, authorize('settings', 'write'), updateClubVenue);
|
||||
router.delete('/:clubId/:venueId', authenticate, authorize('settings', 'write'), deleteClubVenue);
|
||||
|
||||
export default router;
|
||||
@@ -2,6 +2,7 @@ import express from 'express';
|
||||
import {
|
||||
deleteSharedFriendlyMatch,
|
||||
findSharedFriendlyMatches,
|
||||
getSharedFriendlyMatchMembers,
|
||||
listSharedFriendlyMatches,
|
||||
updateSharedFriendlyMatch,
|
||||
updateSharedFriendlyMatchPlayers,
|
||||
@@ -13,6 +14,7 @@ const router = express.Router();
|
||||
|
||||
router.get('/find', authenticate, authorize('schedule', 'read'), findSharedFriendlyMatches);
|
||||
router.get('/shared/:clubId', authenticate, authorize('schedule', 'read'), listSharedFriendlyMatches);
|
||||
router.get('/shared/:clubId/:matchId/members/:side', authenticate, authorize('schedule', 'read'), getSharedFriendlyMatchMembers);
|
||||
router.put('/shared/:clubId/:matchId', authenticate, authorize('schedule', 'write'), updateSharedFriendlyMatch);
|
||||
router.patch('/shared/:clubId/:matchId/players', authenticate, authorize('schedule', 'write'), updateSharedFriendlyMatchPlayers);
|
||||
router.delete('/shared/:clubId/:matchId', authenticate, authorize('schedule', 'write'), deleteSharedFriendlyMatch);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, ClickTtAccount, MyTischtennisUpdateHistory, MyTischtennisFetchLog, ApiLog, MemberTransferConfig, MemberContact, MemberTtrHistory, MemberPlayInterest,
|
||||
MemberOrder, MemberOrderHistory, MemberGroupPhoto, BillingTemplate, BillingTemplateField, BillingRun, BillingDocument, BillingDocumentValue, BillingUserSetting, FriendlyMatch, TrainingCancellation
|
||||
, FriendlyMatchShared, FriendlyMatchInvitation
|
||||
, CalendarEvent
|
||||
, CalendarEvent, ClubVenue
|
||||
} from './models/index.js';
|
||||
import authRoutes from './routes/authRoutes.js';
|
||||
import clubRoutes from './routes/clubRoutes.js';
|
||||
@@ -57,6 +57,7 @@ import clickTtHttpPageRoutes from './routes/clickTtHttpPageRoutes.js';
|
||||
import memberTransferConfigRoutes from './routes/memberTransferConfigRoutes.js';
|
||||
import trainingGroupRoutes from './routes/trainingGroupRoutes.js';
|
||||
import trainingTimeRoutes from './routes/trainingTimeRoutes.js';
|
||||
import clubVenueRoutes from './routes/clubVenueRoutes.js';
|
||||
import trainingCancellationRoutes from './routes/trainingCancellationRoutes.js';
|
||||
import memberOrderRoutes from './routes/memberOrderRoutes.js';
|
||||
import memberGroupPhotoRoutes from './routes/memberGroupPhotoRoutes.js';
|
||||
@@ -355,6 +356,7 @@ app.use('/api/clicktt', clickTtHttpPageRoutes);
|
||||
app.use('/api/member-transfer-config', memberTransferConfigRoutes);
|
||||
app.use('/api/training-groups', trainingGroupRoutes);
|
||||
app.use('/api/training-times', trainingTimeRoutes);
|
||||
app.use('/api/club-venues', clubVenueRoutes);
|
||||
app.use('/api/training-cancellations', trainingCancellationRoutes);
|
||||
app.use('/api/member-orders', memberOrderRoutes);
|
||||
app.use('/api/member-group-photos', memberGroupPhotoRoutes);
|
||||
@@ -565,6 +567,7 @@ app.use((err, req, res, next) => {
|
||||
|
||||
await safeSync(User);
|
||||
await safeSync(Club);
|
||||
await safeSync(ClubVenue);
|
||||
await safeSync(UserClub);
|
||||
await safeSync(Log);
|
||||
await safeSync(Member);
|
||||
|
||||
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