Aktualisiere die Version auf 1.8.10 und füge Vereinsrang (QTTR / TTR) zur QTTR-Tabelle hinzu. Implementiere die Logik zur Berechnung und Sortierung der Vereinsränge in den API- und Import-Skripten.
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 5m18s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

This commit is contained in:
Torsten Schulz (local)
2026-09-23 16:00:06 +02:00
parent 645e4b9655
commit 1a7ba583d8
4 changed files with 66 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "harheimertc-website", "name": "harheimertc-website",
"version": "1.8.9", "version": "1.8.10",
"description": "Moderne Webseite für den Harheimer Tischtennis Club", "description": "Moderne Webseite für den Harheimer Tischtennis Club",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -46,7 +46,7 @@
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500"> <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
Rang Vereinsrang<br>(QTTR / TTR)
</th> </th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500"> <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">
Spieler Spieler
@@ -69,7 +69,9 @@
:class="isOwnRow(row.playerName) ? 'bg-primary-100' : ''" :class="isOwnRow(row.playerName) ? 'bg-primary-100' : ''"
> >
<td class="px-4 py-3 text-sm text-gray-600"> <td class="px-4 py-3 text-sm text-gray-600">
{{ row.rank ?? '' }} <span :title="clubRankTitle(row)">
{{ formatClubRank(row) }}
</span>
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<div :class="['font-medium', getPlayerNameClass(row)]"> <div :class="['font-medium', getPlayerNameClass(row)]">
@@ -133,6 +135,20 @@ function isOwnRow(playerName) {
return normalizeName(playerName) === current return normalizeName(playerName) === current
} }
function formatClubRank(row) {
const qttrRank = row.clubQttrRank
const ttrRank = row.clubTtrRank
if (qttrRank == null) return ''
return ttrRank == null ? String(qttrRank) : `${qttrRank} (${ttrRank})`
}
function clubRankTitle(row) {
if (row.clubQttrRank == null) return 'Keine QTTR-Platzierung verfügbar'
return row.clubTtrRank == null
? `QTTR-Platzierung im Verein: ${row.clubQttrRank}`
: `QTTR-Platzierung im Verein: ${row.clubQttrRank}; TTR-Platzierung im Verein: ${row.clubTtrRank}`
}
function getPlayerNameClass(row) { function getPlayerNameClass(row) {
const minor = isMinor(row.birthdate) const minor = isMinor(row.birthdate)
if (minor && isMaleGender(row.gender)) return 'text-blue-400' if (minor && isMaleGender(row.gender)) return 'text-blue-400'

View File

@@ -37,6 +37,26 @@ function buildBirthdateLookup(entries) {
return lookup return lookup
} }
function addClubRanks(rows) {
const addRankFor = (valueKey, rankKey) => {
rows
.filter((row) => row[valueKey] != null)
.sort((a, b) => b[valueKey] - a[valueKey] || String(a.playerName || '').localeCompare(String(b.playerName || ''), 'de'))
.forEach((row, index) => {
row[rankKey] = index + 1
})
}
addRankFor('currentQttr', 'clubQttrRank')
addRankFor('currentTtr', 'clubTtrRank')
return rows.sort((a, b) =>
(a.clubQttrRank ?? Number.POSITIVE_INFINITY) - (b.clubQttrRank ?? Number.POSITIVE_INFINITY)
|| (a.clubTtrRank ?? Number.POSITIVE_INFINITY) - (b.clubTtrRank ?? Number.POSITIVE_INFINITY)
|| String(a.playerName || '').localeCompare(String(b.playerName || ''), 'de')
)
}
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '') const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
if (!token || !verifyToken(token)) { if (!token || !verifyToken(token)) {
@@ -73,16 +93,16 @@ export default defineEventHandler(async (event) => {
].flatMap(entry => [entry?.name, `${entry?.firstName || ''} ${entry?.lastName || ''}`.trim()]).map(normalizeName).filter(Boolean)) ].flatMap(entry => [entry?.name, `${entry?.firstName || ''} ${entry?.lastName || ''}`.trim()]).map(normalizeName).filter(Boolean))
const birthdateLookup = buildBirthdateLookup([...visibleManualMembers, ...visibleUsers]) const birthdateLookup = buildBirthdateLookup([...visibleManualMembers, ...visibleUsers])
const rankedRows = addClubRanks(Array.isArray(payload.rows) ? payload.rows.map(row => ({ ...row })) : [])
return { return {
...payload, ...payload,
rows: Array.isArray(payload.rows) rows: rankedRows
? payload.rows
.filter(row => !hiddenNames.has(normalizeName(row.playerName))) .filter(row => !hiddenNames.has(normalizeName(row.playerName)))
.map((row) => ({ .map((row) => ({
...row, ...row,
birthdate: birthdateLookup.get(normalizeName(row.playerName)) || row.birthdate || '' birthdate: birthdateLookup.get(normalizeName(row.playerName)) || row.birthdate || ''
})) }))
: []
} }
} catch (error) { } catch (error) {
if (error?.code === 'ENOENT') { if (error?.code === 'ENOENT') {

View File

@@ -67,6 +67,26 @@ function normalizeGender(value) {
return normalized || null return normalized || null
} }
function addClubRanks(rows) {
const addRankFor = (valueKey, rankKey) => {
rows
.filter((row) => row[valueKey] != null)
.sort((a, b) => b[valueKey] - a[valueKey] || String(a.playerName || '').localeCompare(String(b.playerName || ''), 'de'))
.forEach((row, index) => {
row[rankKey] = index + 1
})
}
addRankFor('currentQttr', 'clubQttrRank')
addRankFor('currentTtr', 'clubTtrRank')
return rows.sort((a, b) =>
(a.clubQttrRank ?? Number.POSITIVE_INFINITY) - (b.clubQttrRank ?? Number.POSITIVE_INFINITY)
|| (a.clubTtrRank ?? Number.POSITIVE_INFINITY) - (b.clubTtrRank ?? Number.POSITIVE_INFINITY)
|| String(a.playerName || '').localeCompare(String(b.playerName || ''), 'de')
)
}
function extractTableBlocks(html) { function extractTableBlocks(html) {
return [...String(html || '').matchAll(/<table\b[^>]*>[\s\S]*?<\/table>/gi)].map((match) => match[0]) return [...String(html || '').matchAll(/<table\b[^>]*>[\s\S]*?<\/table>/gi)].map((match) => match[0])
} }
@@ -190,7 +210,7 @@ export async function importQttrValues(options = {}) {
throw new Error('QTTR-Tabelle ist leer oder unvollständig') throw new Error('QTTR-Tabelle ist leer oder unvollständig')
} }
const parsedRows = rows.map((cells) => deriveQttrFields(headers, cells)) const parsedRows = addClubRanks(rows.map((cells) => deriveQttrFields(headers, cells)))
const payload = { const payload = {
format: 'harheimertc.qttr.v1', format: 'harheimertc.qttr.v1',
importedAt: new Date().toISOString(), importedAt: new Date().toISOString(),
@@ -243,6 +263,8 @@ async function importAuthenticatedTtrValues(connection) {
}) })
}).filter(row => row.playerName && (row.currentTtr != null || row.currentQttr != null)) }).filter(row => row.playerName && (row.currentTtr != null || row.currentQttr != null))
addClubRanks(rows)
if (!rows.length) throw new Error('Keine verwertbaren TTR-Werte in der myTischtennis-Rangliste gefunden.') if (!rows.length) throw new Error('Keine verwertbaren TTR-Werte in der myTischtennis-Rangliste gefunden.')
const previousQttrSignature = qttrListSignature(previousPayload.rows) const previousQttrSignature = qttrListSignature(previousPayload.rows)
const nextQttrSignature = qttrListSignature(rows) const nextQttrSignature = qttrListSignature(rows)