Füge Unterstützung für TTR-Änderungen hinzu: Erweitere Benachrichtigungseinstellungen und implementiere Push-Benachrichtigungen für TTR-Wertänderungen.
Some checks failed
Code Analysis and Production Deploy / deploy-production (push) Has been cancelled
Code Analysis and Production Deploy / deploy-test (push) Has been cancelled
Code Analysis and Production Deploy / analyze (push) Has been cancelled

This commit is contained in:
Torsten Schulz (local)
2026-09-08 14:54:20 +02:00
parent 5ca4253b44
commit 9bc9501219
12 changed files with 117 additions and 22 deletions

View File

@@ -8,6 +8,7 @@ export const DEFAULT_NOTIFICATION_SETTINGS = Object.freeze({
birthdays: false,
newContactRequest: false,
newUserRegistration: false,
ownTtrChanges: false,
selectedTeamSlugs: [],
selectedTeamSeason: null,
notificationTime: '09:00'
@@ -41,6 +42,7 @@ export function sanitizeNotificationSettings(input = {}) {
birthdays: coerceBoolean(input.birthdays),
newContactRequest: coerceBoolean(input.newContactRequest),
newUserRegistration: coerceBoolean(input.newUserRegistration),
ownTtrChanges: coerceBoolean(input.ownTtrChanges),
selectedTeamSlugs: [...new Set(selectedTeamSlugs)],
selectedTeamSeason,
notificationTime

View File

@@ -150,6 +150,26 @@ function isVorstandUser(user) {
return roles.includes('admin') || roles.includes('vorstand')
}
function normalizePersonName(value) {
return String(value || '')
.trim()
.toLowerCase()
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/['`]/g, '')
.replace(/\s+/g, ' ')
}
function userMatchesPlayer(user, playerName) {
const player = normalizePersonName(playerName)
if (!player) return false
const candidates = [
user?.name,
`${user?.firstName || ''} ${user?.lastName || ''}`.trim()
].map(normalizePersonName).filter(Boolean)
return candidates.includes(player)
}
export async function sendPushToUsers({ title, body, data = {}, predicate, bodyForUser, dataForUser, failureLabel = 'FCM-Push' }) {
const serviceAccount = await readServiceAccount()
if (serviceAccount == null) {
@@ -275,3 +295,44 @@ export async function sendNewUserRegistrationPush(registration) {
failureLabel: 'FCM Registrierungs-Push'
})
}
export async function sendOwnTtrChangePush(changes = []) {
const byPlayer = new Map(
changes
.filter(change => change?.playerName && Number.isFinite(change?.previousTtr) && Number.isFinite(change?.currentTtr))
.map(change => [normalizePersonName(change.playerName), change])
)
if (!byPlayer.size) return { sent: 0, failed: 0, removed: 0, recipients: 0, tokenCount: 0, skipped: false }
return sendPushToUsers({
title: 'TTR-Wert geändert',
data: { type: 'ttr_change' },
predicate: (user, settings) => settings.ownTtrChanges && [...byPlayer.values()].some(change => userMatchesPlayer(user, change.playerName)),
bodyForUser: (user) => {
const change = [...byPlayer.values()].find(entry => userMatchesPlayer(user, entry.playerName))
return `Dein TTR-Wert hat sich von ${change.previousTtr} auf ${change.currentTtr} geändert.`
},
dataForUser: (user) => {
const change = [...byPlayer.values()].find(entry => userMatchesPlayer(user, entry.playerName))
return {
previousTtr: change.previousTtr,
currentTtr: change.currentTtr,
notificationId: notificationIdFor(`ttr:${change.playerName}:${change.previousTtr}:${change.currentTtr}`)
}
},
failureLabel: 'FCM TTR-Änderungs-Push'
})
}
export async function sendTestPushToUser(userId) {
return sendPushToUsers({
title: 'Harheimer TC: Push-Test',
body: 'Diese Test-Benachrichtigung wurde erfolgreich vom CMS ausgelöst.',
data: {
type: 'test',
notificationId: notificationIdFor(`test:${userId}:${Date.now()}`)
},
predicate: user => user?.id === userId,
failureLabel: 'FCM Test-Push'
})
}

View File

@@ -1,5 +1,6 @@
import { promises as fs } from 'fs'
import { getServerDataPath } from './paths.js'
import { sendOwnTtrChangePush } from './push-notifications.js'
import { fetchClubRankings } from './mytischtennis-client.js'
import { readMyTischtennisConnection, saveMyTischtennisConnection } from './mytischtennis-connection.js'
@@ -215,6 +216,9 @@ export async function importQttrValues(options = {}) {
}
async function importAuthenticatedTtrValues(connection) {
const previousPayload = await fs.readFile(OUTPUT_FILE, 'utf8')
.then(JSON.parse)
.catch(() => ({ rows: [] }))
const [ttrEntries, qttrEntries] = await Promise.all([
fetchClubRankings(connection, 'yes'),
fetchClubRankings(connection, 'no')
@@ -248,6 +252,16 @@ async function importAuthenticatedTtrValues(connection) {
}
await fs.mkdir(getServerDataPath(), { recursive: true })
await fs.writeFile(OUTPUT_FILE, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
const previousTtrByPlayer = new Map((previousPayload.rows || []).map(row => [
String(row?.playerName || '').trim().toLowerCase(), toNumberOrNull(row?.currentTtr)
]))
const changes = rows.flatMap(row => {
const previousTtr = previousTtrByPlayer.get(String(row.playerName || '').trim().toLowerCase())
return previousTtr != null && row.currentTtr != null && previousTtr !== row.currentTtr
? [{ playerName: row.playerName, previousTtr, currentTtr: row.currentTtr }]
: []
})
await sendOwnTtrChangePush(changes)
await saveMyTischtennisConnection({ ...connection, lastSuccessfulImportAt: importedAt, lastImportError: null })
return { outputFile: OUTPUT_FILE, tableCount: 1, ...payload }
}