feat(ClubTeam): enhance club team management with lineup features and member eligibility
- Added teamGender and teamAgeGroup fields to ClubTeam model for better categorization. - Updated create and update club team endpoints to handle new fields and default values. - Implemented getClubTeamLineup and updateClubTeamLineup functions for managing team lineups. - Enhanced member management with adultReleaseApproved and adultReserveApproved fields in Member model. - Updated frontend views to support new lineup features and member eligibility flags. - Improved localization for new terms related to team management and member eligibility across multiple languages.
This commit is contained in:
@@ -42,7 +42,7 @@ export const createClubTeam = async (req, res) => {
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubid: clubId } = req.params;
|
||||
const { name, leagueId, seasonId } = req.body;
|
||||
const { name, leagueId, seasonId, teamGender, teamAgeGroup } = req.body;
|
||||
|
||||
const user = await getUserByToken(token);
|
||||
|
||||
@@ -54,7 +54,9 @@ export const createClubTeam = async (req, res) => {
|
||||
name,
|
||||
clubId: parseInt(clubId),
|
||||
leagueId: leagueId ? parseInt(leagueId) : null,
|
||||
seasonId: seasonId ? parseInt(seasonId) : null
|
||||
seasonId: seasonId ? parseInt(seasonId) : null,
|
||||
teamGender: teamGender || 'open',
|
||||
teamAgeGroup: teamAgeGroup || 'adult'
|
||||
};
|
||||
|
||||
const newClubTeam = await ClubTeamService.createClubTeam(clubTeamData);
|
||||
@@ -70,7 +72,7 @@ export const updateClubTeam = async (req, res) => {
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubteamid: clubTeamId } = req.params;
|
||||
const { name, leagueId, seasonId } = req.body;
|
||||
const { name, leagueId, seasonId, teamGender, teamAgeGroup } = req.body;
|
||||
|
||||
const user = await getUserByToken(token);
|
||||
|
||||
@@ -78,6 +80,8 @@ export const updateClubTeam = async (req, res) => {
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null;
|
||||
if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null;
|
||||
if (teamGender !== undefined) updateData.teamGender = teamGender || 'open';
|
||||
if (teamAgeGroup !== undefined) updateData.teamAgeGroup = teamAgeGroup || 'adult';
|
||||
|
||||
const success = await ClubTeamService.updateClubTeam(clubTeamId, updateData);
|
||||
if (!success) {
|
||||
@@ -126,3 +130,47 @@ export const getLeagues = async (req, res) => {
|
||||
res.status(500).json({ error: "internalerror" });
|
||||
}
|
||||
};
|
||||
|
||||
export const getClubTeamLineup = async (req, res) => {
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubteamid: clubTeamId } = req.params;
|
||||
const lineupHalf = req.query.half === 'second_half' ? 'second_half' : 'first_half';
|
||||
await getUserByToken(token);
|
||||
|
||||
const clubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
|
||||
if (!clubTeam) {
|
||||
return res.status(404).json({ error: "notfound" });
|
||||
}
|
||||
|
||||
const lineup = await ClubTeamService.getTeamLineup(clubTeamId, lineupHalf);
|
||||
res.status(200).json(lineup);
|
||||
} catch (error) {
|
||||
console.error('[getClubTeamLineup] - Error:', error);
|
||||
res.status(500).json({ error: "internalerror" });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubTeamLineup = async (req, res) => {
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubteamid: clubTeamId } = req.params;
|
||||
const { assignments, lineupHalf: requestLineupHalf } = req.body;
|
||||
const lineupHalf = requestLineupHalf === 'second_half' || req.query.half === 'second_half' ? 'second_half' : 'first_half';
|
||||
await getUserByToken(token);
|
||||
|
||||
const clubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
|
||||
if (!clubTeam) {
|
||||
return res.status(404).json({ error: "notfound" });
|
||||
}
|
||||
|
||||
const lineup = await ClubTeamService.replaceTeamLineup(clubTeamId, assignments, lineupHalf);
|
||||
res.status(200).json(lineup);
|
||||
} catch (error) {
|
||||
console.error('[updateClubTeamLineup] - Error:', error);
|
||||
if (error?.code === 'TEAM_LINEUP_TABLE_MISSING') {
|
||||
return res.status(500).json({ error: 'teamlineuptablemissing' });
|
||||
}
|
||||
res.status(500).json({ error: "internalerror" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,11 +29,11 @@ const getWaitingApprovals = async(req, res) => {
|
||||
const setClubMembers = async (req, res) => {
|
||||
try {
|
||||
const { id: memberId, firstname: firstName, lastname: lastName, street, city, postalCode, birthdate, phone, email, active,
|
||||
testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts } = req.body;
|
||||
testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, adultReleaseApproved, adultReserveApproved, contacts } = req.body;
|
||||
const { id: clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, postalCode, birthdate,
|
||||
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts);
|
||||
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, adultReleaseApproved, adultReserveApproved, contacts);
|
||||
|
||||
// Emit Socket-Event wenn Member erfolgreich erstellt/aktualisiert wurde
|
||||
if (addResult.status === 200) {
|
||||
|
||||
@@ -51,6 +51,18 @@ const ClubTeam = sequelize.define('ClubTeam', {
|
||||
comment: 'Team ID from myTischtennis (e.g. 2995094)',
|
||||
field: 'my_tischtennis_team_id'
|
||||
},
|
||||
teamGender: {
|
||||
type: DataTypes.ENUM('open', 'female'),
|
||||
allowNull: false,
|
||||
defaultValue: 'open',
|
||||
field: 'team_gender'
|
||||
},
|
||||
teamAgeGroup: {
|
||||
type: DataTypes.ENUM('adult', 'J19', 'J17', 'J15', 'J13', 'J11'),
|
||||
allowNull: false,
|
||||
defaultValue: 'adult',
|
||||
field: 'team_age_group'
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'club_team',
|
||||
|
||||
47
backend/models/ClubTeamMember.js
Normal file
47
backend/models/ClubTeamMember.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const ClubTeamMember = sequelize.define('ClubTeamMember', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
clubTeamId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'club_team_id',
|
||||
},
|
||||
memberId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'member_id',
|
||||
},
|
||||
lineupHalf: {
|
||||
type: DataTypes.ENUM('first_half', 'second_half'),
|
||||
allowNull: false,
|
||||
defaultValue: 'first_half',
|
||||
field: 'lineup_half',
|
||||
},
|
||||
position: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'club_team_member',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['club_team_id', 'lineup_half', 'member_id'],
|
||||
},
|
||||
{
|
||||
unique: true,
|
||||
fields: ['club_team_id', 'lineup_half', 'position'],
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default ClubTeamMember;
|
||||
@@ -161,6 +161,20 @@ const Member = sequelize.define('Member', {
|
||||
field: 'member_form_handed_over',
|
||||
comment: 'Mitgliedsformular ausgehändigt'
|
||||
},
|
||||
adultReleaseApproved: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'adult_release_approved',
|
||||
comment: 'Jugendspieler mit Freigabe fuer Erwachsene'
|
||||
},
|
||||
adultReserveApproved: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'adult_reserve_approved',
|
||||
comment: 'Jugendspieler als Ersatz bei Erwachsenen zugelassen'
|
||||
},
|
||||
myTischtennisPlayerId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
|
||||
@@ -20,6 +20,7 @@ import Match from './Match.js';
|
||||
import League from './League.js';
|
||||
import Team from './Team.js';
|
||||
import ClubTeam from './ClubTeam.js';
|
||||
import ClubTeamMember from './ClubTeamMember.js';
|
||||
import TeamDocument from './TeamDocument.js';
|
||||
import Season from './Season.js';
|
||||
import Location from './Location.js';
|
||||
@@ -168,6 +169,11 @@ ClubTeam.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
|
||||
Season.hasMany(ClubTeam, { foreignKey: 'seasonId', as: 'clubTeams' });
|
||||
ClubTeam.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
|
||||
|
||||
ClubTeam.hasMany(ClubTeamMember, { foreignKey: 'clubTeamId', as: 'lineupEntries' });
|
||||
ClubTeamMember.belongsTo(ClubTeam, { foreignKey: 'clubTeamId', as: 'clubTeam' });
|
||||
Member.hasMany(ClubTeamMember, { foreignKey: 'memberId', as: 'clubTeamAssignments' });
|
||||
ClubTeamMember.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
|
||||
|
||||
// TeamDocument relationships
|
||||
ClubTeam.hasMany(TeamDocument, { foreignKey: 'clubTeamId', as: 'documents' });
|
||||
TeamDocument.belongsTo(ClubTeam, { foreignKey: 'clubTeamId', as: 'clubTeam' });
|
||||
@@ -406,6 +412,7 @@ export {
|
||||
League,
|
||||
Team,
|
||||
ClubTeam,
|
||||
ClubTeamMember,
|
||||
TeamDocument,
|
||||
Group,
|
||||
GroupActivity,
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
createClubTeam,
|
||||
updateClubTeam,
|
||||
deleteClubTeam,
|
||||
getLeagues
|
||||
getLeagues,
|
||||
getClubTeamLineup,
|
||||
updateClubTeamLineup
|
||||
} from '../controllers/clubTeamController.js';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -23,6 +25,10 @@ router.get('/leagues/:clubid', authenticate, getLeagues);
|
||||
// Get a specific club team
|
||||
router.get('/:clubteamid', authenticate, getClubTeam);
|
||||
|
||||
// Get/save lineup for a specific club team
|
||||
router.get('/:clubteamid/lineup', authenticate, getClubTeamLineup);
|
||||
router.put('/:clubteamid/lineup', authenticate, updateClubTeamLineup);
|
||||
|
||||
// Update a club team
|
||||
router.put('/:clubteamid', authenticate, updateClubTeam);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { initializeSocketIO } from './services/socketService.js';
|
||||
import {
|
||||
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
|
||||
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
|
||||
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, TeamDocument, Group,
|
||||
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
|
||||
, MemberOrder, MemberOrderHistory
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import ClubTeam from '../models/ClubTeam.js';
|
||||
import ClubTeamMember from '../models/ClubTeamMember.js';
|
||||
import League from '../models/League.js';
|
||||
import Member from '../models/Member.js';
|
||||
import Season from '../models/Season.js';
|
||||
import SeasonService from './seasonService.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
|
||||
class ClubTeamService {
|
||||
static isMissingTeamLineupTable(error) {
|
||||
return error?.original?.code === 'ER_NO_SUCH_TABLE'
|
||||
&& String(error?.original?.sqlMessage || '').includes('club_team_member');
|
||||
}
|
||||
/**
|
||||
* Holt alle ClubTeams für einen Verein, optional gefiltert nach Saison.
|
||||
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison verwendet.
|
||||
@@ -36,6 +42,8 @@ class ClubTeamService {
|
||||
leagueId: clubTeam.leagueId,
|
||||
seasonId: clubTeam.seasonId,
|
||||
myTischtennisTeamId: clubTeam.myTischtennisTeamId,
|
||||
teamGender: clubTeam.teamGender,
|
||||
teamAgeGroup: clubTeam.teamAgeGroup,
|
||||
createdAt: clubTeam.createdAt,
|
||||
updatedAt: clubTeam.updatedAt,
|
||||
league: { name: 'Unbekannt' },
|
||||
@@ -178,6 +186,63 @@ class ClubTeamService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getTeamLineup(clubTeamId, lineupHalf = 'first_half') {
|
||||
try {
|
||||
return await ClubTeamMember.findAll({
|
||||
where: { clubTeamId, lineupHalf },
|
||||
include: [
|
||||
{
|
||||
model: Member,
|
||||
as: 'member'
|
||||
}
|
||||
],
|
||||
order: [['position', 'ASC']]
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.isMissingTeamLineupTable(error)) {
|
||||
return [];
|
||||
}
|
||||
console.error('[ClubTeamService.getTeamLineup] - Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async replaceTeamLineup(clubTeamId, assignments, lineupHalf = 'first_half') {
|
||||
try {
|
||||
await ClubTeamMember.destroy({
|
||||
where: { clubTeamId, lineupHalf }
|
||||
});
|
||||
|
||||
if (!Array.isArray(assignments) || assignments.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedAssignments = assignments
|
||||
.filter((entry) => entry && entry.memberId)
|
||||
.map((entry, index) => ({
|
||||
clubTeamId,
|
||||
lineupHalf,
|
||||
memberId: Number(entry.memberId),
|
||||
position: Number(entry.position) || (index + 1)
|
||||
}));
|
||||
|
||||
if (normalizedAssignments.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await ClubTeamMember.bulkCreate(normalizedAssignments);
|
||||
return await this.getTeamLineup(clubTeamId, lineupHalf);
|
||||
} catch (error) {
|
||||
if (this.isMissingTeamLineupTable(error)) {
|
||||
const tableMissingError = new Error('teamlineuptablemissing');
|
||||
tableMissingError.code = 'TEAM_LINEUP_TABLE_MISSING';
|
||||
throw tableMissingError;
|
||||
}
|
||||
console.error('[ClubTeamService.replaceTeamLineup] - Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ClubTeamService;
|
||||
|
||||
@@ -106,7 +106,7 @@ class MemberService {
|
||||
}
|
||||
|
||||
async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, postalCode, birthdate, phone, email, active = true, testMembership = false,
|
||||
picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null, memberFormHandedOver = false, contacts = []) {
|
||||
picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null, memberFormHandedOver = false, adultReleaseApproved = false, adultReserveApproved = false, contacts = []) {
|
||||
try {
|
||||
await checkAccess(userToken, clubId);
|
||||
let member = null;
|
||||
@@ -130,6 +130,8 @@ class MemberService {
|
||||
if (ttr !== undefined) member.ttr = ttr;
|
||||
if (qttr !== undefined) member.qttr = qttr;
|
||||
member.memberFormHandedOver = !!memberFormHandedOver;
|
||||
member.adultReleaseApproved = !!adultReleaseApproved;
|
||||
member.adultReserveApproved = !!adultReserveApproved;
|
||||
await member.save();
|
||||
|
||||
// Update contacts if provided
|
||||
@@ -173,6 +175,8 @@ class MemberService {
|
||||
ttr: ttr,
|
||||
qttr: qttr,
|
||||
memberFormHandedOver: !!memberFormHandedOver,
|
||||
adultReleaseApproved: !!adultReleaseApproved,
|
||||
adultReserveApproved: !!adultReserveApproved,
|
||||
});
|
||||
|
||||
// Create contacts if provided
|
||||
|
||||
Reference in New Issue
Block a user