feat(MemberPlayInterest): implement play interest management for members
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 38s

- Added new endpoints to get and set member play interests in the memberController.
- Integrated MemberPlayInterest model into the application, establishing relationships with Member and Club models.
- Updated memberRoutes to include routes for managing member play interests.
- Enhanced memberService to handle play interest retrieval and updates.
- Updated localization files to include new terms related to member play interests.
- Refactored server.js to include MemberPlayInterest in the synchronization process.
This commit is contained in:
Torsten Schulz (local)
2026-04-15 10:48:10 +02:00
parent 45c701b149
commit 2dff5221e3
15 changed files with 1226 additions and 14 deletions

View File

@@ -47,6 +47,43 @@ const setClubMembers = async (req, res) => {
}
}
const getMemberPlayInterests = async (req, res) => {
try {
const { clubId } = req.params;
const { seasonId, lineupHalf } = req.query;
const { authcode: userToken } = req.headers;
const result = await MemberService.getMemberPlayInterests(userToken, Number(clubId), Number(seasonId), String(lineupHalf || ''));
res.status(result.status || 500).json(result.response);
} catch (error) {
console.error('[getMemberPlayInterests] - Error:', error);
res.status(500).json({ error: 'Failed to load member play interests' });
}
};
const setMemberPlayInterest = async (req, res) => {
try {
const { clubId } = req.params;
const { memberId, seasonId, lineupHalf, interested = true } = req.body;
const { authcode: userToken } = req.headers;
const normalizedInterested = interested === true || interested === 'true' || interested === 1 || interested === '1';
const result = await MemberService.setMemberPlayInterest(
userToken,
Number(clubId),
Number(memberId),
Number(seasonId),
String(lineupHalf || ''),
normalizedInterested
);
if (result.status === 200) {
emitMemberChanged(clubId);
}
res.status(result.status || 500).json(result.response);
} catch (error) {
console.error('[setMemberPlayInterest] - Error:', error);
res.status(500).json({ error: 'Failed to save member play interest' });
}
};
const uploadMemberImage = async (req, res) => {
try {
const { clubId, memberId } = req.params;
@@ -290,6 +327,8 @@ export {
getClubMembers,
getWaitingApprovals,
setClubMembers,
getMemberPlayInterests,
setMemberPlayInterest,
uploadMemberImage,
getMemberImage,
updateRatingsFromMyTischtennis,

View File

@@ -0,0 +1,16 @@
-- Halbserienbasierte Spielinteressen (pro Mitglied, Club, Saison und Halbserie)
CREATE TABLE IF NOT EXISTS `member_play_interest` (
`id` INT NOT NULL AUTO_INCREMENT,
`club_id` INT NOT NULL,
`member_id` INT NOT NULL,
`season_id` INT NOT NULL,
`lineup_half` ENUM('first_half', 'second_half') NOT NULL,
`interested` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_member_play_interest_half` (`club_id`, `member_id`, `season_id`, `lineup_half`),
KEY `idx_member_play_interest_member` (`member_id`),
KEY `idx_member_play_interest_season` (`season_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -137,8 +137,7 @@ const Member = sequelize.define('Member', {
type: DataTypes.BOOLEAN,
allowNull: false,
default: false,
}
,
},
gender: {
type: DataTypes.ENUM('male','female','diverse','unknown'),
allowNull: true,

View File

@@ -0,0 +1,44 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const MemberPlayInterest = sequelize.define('MemberPlayInterest', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
clubId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'club_id'
},
memberId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'member_id'
},
seasonId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'season_id'
},
lineupHalf: {
type: DataTypes.ENUM('first_half', 'second_half'),
allowNull: false,
field: 'lineup_half'
},
interested: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
}
}, {
underscored: true,
sequelize,
modelName: 'MemberPlayInterest',
tableName: 'member_play_interest',
timestamps: true
});
export default MemberPlayInterest;

View File

@@ -50,6 +50,7 @@ import MemberTransferConfig from './MemberTransferConfig.js';
import MemberContact from './MemberContact.js';
import MemberImage from './MemberImage.js';
import MemberTtrHistory from './MemberTtrHistory.js';
import MemberPlayInterest from './MemberPlayInterest.js';
import MemberOrder from './MemberOrder.js';
import MemberOrderHistory from './MemberOrderHistory.js';
import TrainingGroup from './TrainingGroup.js';
@@ -96,6 +97,10 @@ MemberNote.belongsTo(Member, { foreignKey: 'memberId' });
Member.hasMany(MemberTtrHistory, { as: 'ttrHistoryEntries', foreignKey: 'memberId' });
MemberTtrHistory.belongsTo(Member, { as: 'member', foreignKey: 'memberId' });
Member.hasMany(MemberPlayInterest, { as: 'playInterests', foreignKey: 'memberId' });
MemberPlayInterest.belongsTo(Member, { as: 'member', foreignKey: 'memberId' });
Club.hasMany(MemberPlayInterest, { as: 'memberPlayInterests', foreignKey: 'clubId' });
MemberPlayInterest.belongsTo(Club, { as: 'club', foreignKey: 'clubId' });
Member.hasMany(MemberOrder, { as: 'orders', foreignKey: 'memberId' });
MemberOrder.belongsTo(Member, { as: 'member', foreignKey: 'memberId' });
@@ -438,6 +443,7 @@ export {
MemberContact,
MemberImage,
MemberTtrHistory,
MemberPlayInterest,
MemberOrder,
MemberOrderHistory,
TrainingGroup,

View File

@@ -2,6 +2,8 @@ import {
getClubMembers,
getWaitingApprovals,
setClubMembers,
getMemberPlayInterests,
setMemberPlayInterest,
uploadMemberImage,
getMemberImage,
updateRatingsFromMyTischtennis,
@@ -35,6 +37,8 @@ router.post('/image/:clubId/:memberId/:imageId/primary', authenticate, authorize
router.get('/get/:id/:showAll', authenticate, authorize('members', 'read'), getClubMembers);
router.get('/gallery/:clubId', authenticate, authorize('members', 'read'), generateMemberGallery);
router.post('/set/:id', authenticate, authorize('members', 'write'), setClubMembers);
router.get('/play-interest/:clubId', authenticate, authorize('members', 'read'), getMemberPlayInterests);
router.post('/play-interest/:clubId', authenticate, authorize('members', 'write'), setMemberPlayInterest);
router.get('/notapproved/:id', authenticate, authorize('members', 'read'), getWaitingApprovals);
router.post('/update-ratings/:id', authenticate, authorize('mytischtennis', 'write'), updateRatingsFromMyTischtennis);
router.get('/ttr-history/:clubId/:memberId', authenticate, authorize('members', 'read'), getMemberTtrHistory);

View File

@@ -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
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, ClickTtAccount, MyTischtennisUpdateHistory, MyTischtennisFetchLog, ApiLog, MemberTransferConfig, MemberContact, MemberTtrHistory, MemberPlayInterest
, MemberOrder, MemberOrderHistory
} from './models/index.js';
import authRoutes from './routes/authRoutes.js';
@@ -543,6 +543,7 @@ app.use((err, req, res, next) => {
await safeSync(MemberTransferConfig);
await safeSync(MemberContact);
await safeSync(MemberTtrHistory);
await safeSync(MemberPlayInterest);
await safeSync(MemberOrder);
await safeSync(MemberOrderHistory);
await safeSync(ClubTeam);

View File

@@ -4,6 +4,7 @@ import { checkAccess, getUserByToken, hasUserClubAccess } from "../utils/userUti
import Member from "../models/Member.js";
import MemberImage from "../models/MemberImage.js";
import MemberTtrHistory from "../models/MemberTtrHistory.js";
import MemberPlayInterest from "../models/MemberPlayInterest.js";
import Participant from "../models/Participant.js";
import DiaryDate from "../models/DiaryDates.js";
import { Op, fn, col } from 'sequelize';
@@ -14,6 +15,64 @@ import sharp from 'sharp';
import { devLog } from '../utils/logger.js';
import { standardizePhoneNumber } from '../utils/phoneUtils.js';
class MemberService {
async getMemberPlayInterests(userToken, clubId, seasonId, lineupHalf) {
await checkAccess(userToken, clubId);
if (!seasonId || !['first_half', 'second_half'].includes(String(lineupHalf || ''))) {
return {
status: 400,
response: { error: 'invalidplayinterestparams' }
};
}
const rows = await MemberPlayInterest.findAll({
where: {
clubId,
seasonId,
lineupHalf,
interested: true
},
attributes: ['memberId', 'seasonId', 'lineupHalf', 'interested']
});
return {
status: 200,
response: rows.map((row) => row.toJSON())
};
}
async setMemberPlayInterest(userToken, clubId, memberId, seasonId, lineupHalf, interested = true) {
await checkAccess(userToken, clubId);
if (!memberId || !seasonId || !['first_half', 'second_half'].includes(String(lineupHalf || ''))) {
return {
status: 400,
response: { error: 'invalidplayinterestparams' }
};
}
const member = await Member.findOne({ where: { id: memberId, clubId } });
if (!member) {
return {
status: 404,
response: { error: 'membernotfound' }
};
}
const [row] = await MemberPlayInterest.findOrCreate({
where: { clubId, memberId, seasonId, lineupHalf },
defaults: { interested: !!interested }
});
if (row.interested !== !!interested) {
row.interested = !!interested;
await row.save();
}
return {
status: 200,
response: { result: 'success' }
};
}
async getApprovalRequests(userToken, clubId) {
await checkAccess(userToken, clubId);
const user = await getUserByToken(userToken);