feat: add tournament suggestions job and notification system
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 56s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 56s
- Implemented a scheduled job for fetching tournament suggestions in the scheduler service. - Added a new service for handling tournament suggestions, including fetching and updating suggestions. - Created notification system with models, controllers, and services for managing user notifications. - Introduced a notification bell component in the frontend to display unread notifications. - Added a notifications view for users to see all notifications and mark them as read. - Updated API client and socket service to handle notifications and authentication. - Created database migrations for notifications and tournament suggestions.
This commit is contained in:
23
backend/models/NotificationEvent.js
Normal file
23
backend/models/NotificationEvent.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const NotificationEvent = sequelize.define('NotificationEvent', {
|
||||
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
clubId: { type: DataTypes.INTEGER, allowNull: true, field: 'club_id' },
|
||||
type: { type: DataTypes.STRING(80), allowNull: false },
|
||||
priority: { type: DataTypes.ENUM('low', 'normal', 'high', 'critical'), allowNull: false, defaultValue: 'normal' },
|
||||
resourceType: { type: DataTypes.STRING(80), allowNull: true, field: 'resource_type' },
|
||||
resourceId: { type: DataTypes.STRING(80), allowNull: true, field: 'resource_id' },
|
||||
title: { type: DataTypes.STRING(255), allowNull: false },
|
||||
body: { type: DataTypes.TEXT, allowNull: true },
|
||||
route: { type: DataTypes.STRING(500), allowNull: true },
|
||||
payload: { type: DataTypes.JSON, allowNull: true },
|
||||
dedupeKey: { type: DataTypes.STRING(255), allowNull: true, unique: true, field: 'dedupe_key' },
|
||||
}, {
|
||||
tableName: 'notification_events',
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
indexes: [{ fields: ['club_id', 'created_at'] }],
|
||||
});
|
||||
|
||||
export default NotificationEvent;
|
||||
20
backend/models/NotificationRecipient.js
Normal file
20
backend/models/NotificationRecipient.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const NotificationRecipient = sequelize.define('NotificationRecipient', {
|
||||
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
eventId: { type: DataTypes.BIGINT, allowNull: false, field: 'event_id' },
|
||||
userId: { type: DataTypes.INTEGER, allowNull: false, field: 'user_id' },
|
||||
readAt: { type: DataTypes.DATE, allowNull: true, field: 'read_at' },
|
||||
archivedAt: { type: DataTypes.DATE, allowNull: true, field: 'archived_at' },
|
||||
}, {
|
||||
tableName: 'notification_recipients',
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ unique: true, fields: ['event_id', 'user_id'] },
|
||||
{ fields: ['user_id', 'read_at', 'created_at'] },
|
||||
],
|
||||
});
|
||||
|
||||
export default NotificationRecipient;
|
||||
18
backend/models/TournamentSuggestion.js
Normal file
18
backend/models/TournamentSuggestion.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const TournamentSuggestion = sequelize.define('TournamentSuggestion', {
|
||||
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
clubId: { type: DataTypes.INTEGER, allowNull: false, field: 'club_id' },
|
||||
federation: { type: DataTypes.STRING(32), allowNull: false },
|
||||
title: { type: DataTypes.STRING(255), allowNull: false },
|
||||
eventDate: { type: DataTypes.STRING(32), allowNull: true, field: 'event_date' },
|
||||
organizer: { type: DataTypes.STRING(255), allowNull: true },
|
||||
location: { type: DataTypes.STRING(255), allowNull: true },
|
||||
sourceUrl: { type: DataTypes.STRING(1000), allowNull: false, field: 'source_url' },
|
||||
sourceFingerprint: { type: DataTypes.STRING(128), allowNull: false, unique: true, field: 'source_fingerprint' },
|
||||
status: { type: DataTypes.ENUM('new', 'reviewed', 'dismissed'), allowNull: false, defaultValue: 'new' },
|
||||
lastSeenAt: { type: DataTypes.DATE, allowNull: false, field: 'last_seen_at' },
|
||||
}, { tableName: 'tournament_suggestions', underscored: true, timestamps: true, indexes: [{ fields: ['club_id', 'status', 'event_date'] }] });
|
||||
|
||||
export default TournamentSuggestion;
|
||||
@@ -95,6 +95,9 @@ import ClubDistributionGroup from './ClubDistributionGroup.js';
|
||||
import ClubDistributionGroupMember from './ClubDistributionGroupMember.js';
|
||||
import MemberProfileChangeRequest from './MemberProfileChangeRequest.js';
|
||||
import MemberEventResponse from './MemberEventResponse.js';
|
||||
import NotificationEvent from './NotificationEvent.js';
|
||||
import NotificationRecipient from './NotificationRecipient.js';
|
||||
import TournamentSuggestion from './TournamentSuggestion.js';
|
||||
// Official tournaments relations
|
||||
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
|
||||
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
@@ -112,6 +115,15 @@ Log.belongsTo(User, { foreignKey: 'userId' });
|
||||
User.belongsToMany(Club, { through: UserClub, foreignKey: 'userId' });
|
||||
Club.belongsToMany(User, { through: UserClub, foreignKey: 'clubId' });
|
||||
|
||||
NotificationEvent.hasMany(NotificationRecipient, { foreignKey: 'eventId', as: 'recipients' });
|
||||
NotificationRecipient.belongsTo(NotificationEvent, { foreignKey: 'eventId', as: 'event' });
|
||||
User.hasMany(NotificationRecipient, { foreignKey: 'userId', as: 'notificationRecipients' });
|
||||
NotificationRecipient.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
Club.hasMany(NotificationEvent, { foreignKey: 'clubId', as: 'notificationEvents' });
|
||||
NotificationEvent.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
|
||||
Club.hasMany(TournamentSuggestion, { foreignKey: 'clubId', as: 'tournamentSuggestions' });
|
||||
TournamentSuggestion.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
|
||||
|
||||
DiaryDate.belongsTo(Club, { foreignKey: 'clubId' });
|
||||
Club.hasMany(DiaryDate, { foreignKey: 'clubId' });
|
||||
|
||||
@@ -706,5 +718,8 @@ export {
|
||||
ClubDistributionGroup,
|
||||
MemberProfileChangeRequest,
|
||||
MemberEventResponse,
|
||||
NotificationEvent,
|
||||
NotificationRecipient,
|
||||
TournamentSuggestion,
|
||||
ClubDistributionGroupMember,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user