feat: add TTR simulator and calculation engine, enhance member model with TTR adjustment constant
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 59s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 59s
This commit is contained in:
@@ -176,7 +176,7 @@ export const getMatchPlayers = async (req, res) => {
|
||||
const Member = (await import('../models/Member.js')).default;
|
||||
const members = await Member.findAll({
|
||||
where: { clubId: clubId, active: true },
|
||||
attributes: ['id', 'firstName', 'lastName', 'gender']
|
||||
attributes: ['id', 'firstName', 'lastName', 'gender', 'ttr', 'qttr', 'ttrAdjustmentConstant']
|
||||
});
|
||||
return res.status(200).json(members);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Hidden application-review users do not appear in club member/user lists
|
||||
-- and must never receive outgoing emails.
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `is_hidden` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN `suppress_emails` TINYINT(1) NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `member`
|
||||
ADD COLUMN `ttr_adjustment_constant` DECIMAL(6,2) NOT NULL DEFAULT 16;
|
||||
@@ -214,6 +214,13 @@ const Member = sequelize.define('Member', {
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
},
|
||||
ttrAdjustmentConstant: {
|
||||
type: DataTypes.DECIMAL(6, 2),
|
||||
allowNull: false,
|
||||
defaultValue: 16,
|
||||
field: 'ttr_adjustment_constant',
|
||||
comment: 'Individuelle Änderungskonstante für TTR-Prognosen'
|
||||
},
|
||||
qttr: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
|
||||
@@ -34,6 +34,20 @@ const User = sequelize.define('User', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
isHidden: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false,
|
||||
field: 'is_hidden',
|
||||
comment: 'System accounts hidden from member and user lists',
|
||||
},
|
||||
suppressEmails: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false,
|
||||
field: 'suppress_emails',
|
||||
comment: 'Do not deliver outgoing emails to this account',
|
||||
},
|
||||
authCode: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
|
||||
@@ -12,7 +12,9 @@ const TEST_USERS = [
|
||||
email: 'admin@test.de',
|
||||
password: 'test123',
|
||||
role: 'admin',
|
||||
isOwner: false
|
||||
isOwner: false,
|
||||
isHidden: true,
|
||||
suppressEmails: true
|
||||
},
|
||||
{
|
||||
email: 'trainer@test.de',
|
||||
@@ -75,11 +77,21 @@ async function createTestUsers() {
|
||||
user = await User.create({
|
||||
email: userData.email,
|
||||
password: userData.password,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
isHidden: Boolean(userData.isHidden),
|
||||
suppressEmails: Boolean(userData.suppressEmails),
|
||||
});
|
||||
report.push(' ✓ User created');
|
||||
}
|
||||
|
||||
if (user.isHidden !== Boolean(userData.isHidden) || user.suppressEmails !== Boolean(userData.suppressEmails)) {
|
||||
await user.update({
|
||||
isHidden: Boolean(userData.isHidden),
|
||||
suppressEmails: Boolean(userData.suppressEmails),
|
||||
});
|
||||
report.push(' ✓ Updated test account visibility and email settings');
|
||||
}
|
||||
|
||||
// Check if user is already in club
|
||||
let userClub = await UserClub.findOne({
|
||||
where: {
|
||||
@@ -145,4 +157,3 @@ createTestUsers().catch(err => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const migrations = [
|
||||
{
|
||||
id: '20261005_add_hidden_test_user_flags',
|
||||
async up() {
|
||||
const [hiddenColumns] = await sequelize.query("SHOW COLUMNS FROM `user` LIKE 'is_hidden'");
|
||||
if (hiddenColumns.length === 0) {
|
||||
await sequelize.query('ALTER TABLE `user` ADD COLUMN `is_hidden` TINYINT(1) NOT NULL DEFAULT 0');
|
||||
}
|
||||
|
||||
const [emailSuppressionColumns] = await sequelize.query("SHOW COLUMNS FROM `user` LIKE 'suppress_emails'");
|
||||
if (emailSuppressionColumns.length === 0) {
|
||||
await sequelize.query('ALTER TABLE `user` ADD COLUMN `suppress_emails` TINYINT(1) NOT NULL DEFAULT 0');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '20261005_add_member_ttr_adjustment_constant',
|
||||
async up() {
|
||||
const [columns] = await sequelize.query("SHOW COLUMNS FROM `member` LIKE 'ttr_adjustment_constant'");
|
||||
if (columns.length === 0) {
|
||||
await sequelize.query('ALTER TABLE `member` ADD COLUMN `ttr_adjustment_constant` DECIMAL(6,2) NOT NULL DEFAULT 16');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '20260922_add_match_player_cancellations',
|
||||
async up() {
|
||||
|
||||
@@ -59,6 +59,8 @@ class ClubService {
|
||||
{
|
||||
model: User,
|
||||
as: 'Users',
|
||||
where: { isHidden: false },
|
||||
required: false,
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
@@ -224,6 +226,8 @@ class ClubService {
|
||||
{
|
||||
model: User,
|
||||
as: 'user',
|
||||
where: { isHidden: false },
|
||||
required: true,
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import User from '../models/User.js';
|
||||
|
||||
let transporter = null;
|
||||
|
||||
@@ -38,7 +39,32 @@ function getEmailTransportStatus() {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRecipientList(recipients) {
|
||||
const values = Array.isArray(recipients) ? recipients : [recipients];
|
||||
return values
|
||||
.flatMap((value) => String(value || '').split(','))
|
||||
.map((value) => value.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
async function sendMail(mailOptions) {
|
||||
const recipients = normalizeRecipientList(mailOptions.to);
|
||||
if (recipients.length) {
|
||||
const suppressedUsers = await User.findAll({
|
||||
where: { email: recipients, suppressEmails: true },
|
||||
attributes: ['email'],
|
||||
});
|
||||
const suppressedEmails = new Set(suppressedUsers.map((user) => String(user.email).toLowerCase()));
|
||||
const allowedRecipients = recipients.filter((email) => !suppressedEmails.has(email));
|
||||
|
||||
if (!allowedRecipients.length) {
|
||||
console.info('[email] Delivery suppressed for hidden test account');
|
||||
return { suppressed: true, accepted: [], rejected: [] };
|
||||
}
|
||||
|
||||
mailOptions = { ...mailOptions, to: allowedRecipients.join(', ') };
|
||||
}
|
||||
|
||||
return getTransporter().sendMail({
|
||||
from: getDefaultFrom(),
|
||||
...mailOptions,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import UserClub from "../models/UserClub.js";
|
||||
import User from "../models/User.js";
|
||||
import Club from "../models/Club.js";
|
||||
import { checkAccess, getUserByToken, hasUserClubAccess } from "../utils/userUtils.js";
|
||||
import Member from "../models/Member.js";
|
||||
@@ -221,6 +222,17 @@ class MemberService {
|
||||
if (!showAll || showAll === 'false') {
|
||||
where['active'] = true;
|
||||
}
|
||||
const hiddenUsers = await User.findAll({
|
||||
where: { isHidden: true },
|
||||
attributes: ['id'],
|
||||
});
|
||||
const hiddenUserIds = hiddenUsers.map((user) => user.id);
|
||||
if (hiddenUserIds.length) {
|
||||
where[Op.or] = [
|
||||
{ userId: null },
|
||||
{ userId: { [Op.notIn]: hiddenUserIds } },
|
||||
];
|
||||
}
|
||||
const MemberContact = (await import('../models/MemberContact.js')).default;
|
||||
try {
|
||||
const members = await Member.findAll({
|
||||
|
||||
@@ -474,7 +474,13 @@ class PermissionService {
|
||||
|
||||
const userClubs = await UserClub.findAll({
|
||||
where: { clubId },
|
||||
include: [{ model: User, as: 'user', attributes: ['id', 'email'] }],
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'email'],
|
||||
where: { isHidden: false },
|
||||
required: true,
|
||||
}],
|
||||
order: [[{ model: User, as: 'user' }, 'email', 'ASC']],
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user