Implement cross-club friendly match concept with invitations and shared matches
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 49s

- Added controllers for handling friendly match invitations and shared matches.
- Created migration scripts for `friendly_match_invitation` and `friendly_match_shared` tables.
- Developed models for `FriendlyMatchInvitation` and `FriendlyMatchShared`.
- Established routes for managing invitations and shared matches.
- Implemented services for business logic related to invitations and shared matches.
- Documented the concept plan for the new feature including API endpoints and data models.
This commit is contained in:
Torsten Schulz (local)
2026-05-30 17:50:35 +02:00
parent 359527eb5b
commit 0ff67dae80
21 changed files with 1795 additions and 17 deletions

View File

@@ -0,0 +1,96 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import User from './User.js';
const FriendlyMatchInvitation = sequelize.define('FriendlyMatchInvitation', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
fromClubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
field: 'from_club_id',
},
toClubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
field: 'to_club_id',
},
proposedDate: {
type: DataTypes.DATEONLY,
allowNull: false,
field: 'proposed_date',
},
proposedStartTime: {
type: DataTypes.TIME,
allowNull: true,
field: 'proposed_start_time',
},
proposedMatchName: {
type: DataTypes.STRING(255),
allowNull: false,
field: 'proposed_match_name',
},
message: {
type: DataTypes.TEXT,
allowNull: true,
},
status: {
type: DataTypes.STRING(32),
allowNull: false,
defaultValue: 'pending',
},
createdByUserId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: User,
key: 'id',
},
onDelete: 'SET NULL',
field: 'created_by_user_id',
},
acceptedByUserId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: User,
key: 'id',
},
onDelete: 'SET NULL',
field: 'accepted_by_user_id',
},
acceptedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'accepted_at',
},
}, {
tableName: 'friendly_match_invitation',
underscored: true,
timestamps: true,
indexes: [
{
fields: ['to_club_id', 'status', 'proposed_date'],
},
{
fields: ['from_club_id', 'status', 'proposed_date'],
},
],
});
export default FriendlyMatchInvitation;

View File

@@ -0,0 +1,186 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import User from './User.js';
const FriendlyMatchShared = sequelize.define('FriendlyMatchShared', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
homeClubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
field: 'home_club_id',
},
guestClubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
field: 'guest_club_id',
},
date: {
type: DataTypes.DATEONLY,
allowNull: false,
},
startTime: {
type: DataTypes.TIME,
allowNull: true,
field: 'start_time',
},
matchName: {
type: DataTypes.STRING(255),
allowNull: true,
field: 'match_name',
},
homeTeamName: {
type: DataTypes.STRING(255),
allowNull: false,
field: 'home_team_name',
},
guestTeamName: {
type: DataTypes.STRING(255),
allowNull: false,
field: 'guest_team_name',
},
locationName: {
type: DataTypes.STRING(255),
allowNull: true,
field: 'location_name',
},
locationAddress: {
type: DataTypes.STRING(255),
allowNull: true,
field: 'location_address',
},
locationCity: {
type: DataTypes.STRING(255),
allowNull: true,
field: 'location_city',
},
locationZip: {
type: DataTypes.STRING(32),
allowNull: true,
field: 'location_zip',
},
matchSystem: {
type: DataTypes.STRING(120),
allowNull: false,
defaultValue: 'Braunschweiger System',
field: 'match_system',
},
singlesCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 12,
field: 'singles_count',
},
doublesCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 4,
field: 'doubles_count',
},
winningSets: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 3,
field: 'winning_sets',
},
homeMatchPoints: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'home_match_points',
},
guestMatchPoints: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'guest_match_points',
},
isCompleted: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'is_completed',
},
homeParticipants: {
type: DataTypes.JSON,
allowNull: true,
field: 'home_participants',
},
guestParticipants: {
type: DataTypes.JSON,
allowNull: true,
field: 'guest_participants',
},
resultDetails: {
type: DataTypes.JSON,
allowNull: true,
field: 'result_details',
},
playersReady: {
type: DataTypes.JSON,
allowNull: true,
field: 'players_ready',
},
playersPlanned: {
type: DataTypes.JSON,
allowNull: true,
field: 'players_planned',
},
playersPlayed: {
type: DataTypes.JSON,
allowNull: true,
field: 'players_played',
},
status: {
type: DataTypes.STRING(32),
allowNull: false,
defaultValue: 'active',
},
createdByUserId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: User,
key: 'id',
},
onDelete: 'SET NULL',
field: 'created_by_user_id',
},
createdFromInvitationId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'created_from_invitation_id',
},
}, {
tableName: 'friendly_match_shared',
underscored: true,
timestamps: true,
indexes: [
{
fields: ['home_club_id', 'date', 'start_time'],
},
{
fields: ['guest_club_id', 'date', 'start_time'],
},
{
fields: ['status'],
},
],
});
export default FriendlyMatchShared;

View File

@@ -58,6 +58,8 @@ import BillingDocument from './BillingDocument.js';
import BillingDocumentValue from './BillingDocumentValue.js';
import BillingUserSetting from './BillingUserSetting.js';
import FriendlyMatch from './FriendlyMatch.js';
import FriendlyMatchShared from './FriendlyMatchShared.js';
import FriendlyMatchInvitation from './FriendlyMatchInvitation.js';
import MemberTtrHistory from './MemberTtrHistory.js';
import MemberPlayInterest from './MemberPlayInterest.js';
import ClickTtAccount from './ClickTtAccount.js';
@@ -410,6 +412,37 @@ ClubDisabledPresetGroup.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
TrainingGroup.hasMany(TrainingTime, { foreignKey: 'trainingGroupId', as: 'trainingTimes' });
TrainingTime.belongsTo(TrainingGroup, { foreignKey: 'trainingGroupId', as: 'trainingGroup' });
// Friendly shared matches
FriendlyMatchShared.belongsTo(Club, { foreignKey: 'homeClubId', as: 'homeClub' });
FriendlyMatchShared.belongsTo(Club, { foreignKey: 'guestClubId', as: 'guestClub' });
Club.hasMany(FriendlyMatchShared, { foreignKey: 'homeClubId', as: 'homeSharedFriendlyMatches' });
Club.hasMany(FriendlyMatchShared, { foreignKey: 'guestClubId', as: 'guestSharedFriendlyMatches' });
FriendlyMatchShared.belongsTo(User, { foreignKey: 'createdByUserId', as: 'createdByUser' });
User.hasMany(FriendlyMatchShared, { foreignKey: 'createdByUserId', as: 'createdSharedFriendlyMatches' });
// Friendly invitations
FriendlyMatchInvitation.belongsTo(Club, { foreignKey: 'fromClubId', as: 'fromClub' });
FriendlyMatchInvitation.belongsTo(Club, { foreignKey: 'toClubId', as: 'toClub' });
Club.hasMany(FriendlyMatchInvitation, { foreignKey: 'fromClubId', as: 'sentFriendlyMatchInvitations' });
Club.hasMany(FriendlyMatchInvitation, { foreignKey: 'toClubId', as: 'receivedFriendlyMatchInvitations' });
FriendlyMatchInvitation.belongsTo(User, { foreignKey: 'createdByUserId', as: 'createdByUser' });
FriendlyMatchInvitation.belongsTo(User, { foreignKey: 'acceptedByUserId', as: 'acceptedByUser' });
User.hasMany(FriendlyMatchInvitation, { foreignKey: 'createdByUserId', as: 'createdFriendlyMatchInvitations' });
User.hasMany(FriendlyMatchInvitation, { foreignKey: 'acceptedByUserId', as: 'acceptedFriendlyMatchInvitations' });
FriendlyMatchShared.belongsTo(FriendlyMatchInvitation, {
foreignKey: 'createdFromInvitationId',
as: 'sourceInvitation',
constraints: false,
});
FriendlyMatchInvitation.hasOne(FriendlyMatchShared, {
foreignKey: 'createdFromInvitationId',
as: 'createdSharedMatch',
constraints: false,
});
export {
User,
Log,
@@ -468,6 +501,8 @@ export {
BillingDocumentValue,
BillingUserSetting,
FriendlyMatch,
FriendlyMatchShared,
FriendlyMatchInvitation,
MemberTtrHistory,
MemberPlayInterest,
ClickTtAccount,