feat(TournamentStats): enhance internal tournament statistics with member profile integration
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 37s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 37s
- Integrated member profile data (birth date and gender) into the internal tournament statistics calculations for more accurate age class filtering. - Removed deprecated age class filtering functions and streamlined the statistics computation process. - Updated the InternalTournamentStats component to reflect changes in age class options and improved sorting logic. - Enhanced localization strings across multiple languages to support new terminology related to age classes and gender, improving user accessibility and understanding.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { memberToCanonicalTtBucket } from '../utils/ttInternalStatsBuckets.js';
|
||||
|
||||
/**
|
||||
* Statistik-Werte für interne Einzel-Turniere (Gruppenphase + K.-o.-Runde).
|
||||
*
|
||||
@@ -6,6 +8,7 @@
|
||||
* bei N = 1: 100 %. Nur Vereinsmitglieder werden gewertet; N zählt alle Platzierten.
|
||||
* K.-o.: Wer die K.-o.-Runde erreicht: höchster Gruppen-Prozentwert dieser Klasse + 1,
|
||||
* danach +1 pro gewonnenes K.-o.-Spiel (wie bisher, jetzt auf %-Skala).
|
||||
* Alters-/Geschlechtsfilter: pro Spieler aus Vereinsmitglied Geburtsdatum + Geschlecht (nicht aus Turnierklassen-Name).
|
||||
*/
|
||||
|
||||
export function parseWinnerFromMatch(match) {
|
||||
@@ -81,35 +84,6 @@ export function groupPercentFromRankings(rankings, nInGroup) {
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Schlüssel für Gruppen/Spiele ohne zugeordnete Klasse (classId null) */
|
||||
export const NO_CLASS_AGE_KEY = 'ak|__noclass__';
|
||||
|
||||
/**
|
||||
* Stabiler Filter-Schlüssel aus einer Einzel-Klasse (Geburtsjahre + Geschlecht).
|
||||
* @param {object} classRow – TournamentClass-Plain
|
||||
* @returns {string|null} null bei Doppel
|
||||
*/
|
||||
export function ageClassFilterKey(classRow) {
|
||||
if (!classRow || classRow.isDoubles) return null;
|
||||
const min = classRow.minBirthYear ?? classRow.min_birth_year ?? null;
|
||||
const max = classRow.maxBirthYear ?? classRow.max_birth_year ?? null;
|
||||
const g = classRow.gender ?? '';
|
||||
return `ak|${min ?? ''}|${max ?? ''}|${g}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number|null|undefined} classIdNum
|
||||
* @param {Array<object>} classes
|
||||
* @returns {string|null} null wenn unbekannte ID oder Doppel-Klasse
|
||||
*/
|
||||
export function ageKeyForClassSlice(classIdNum, classes) {
|
||||
if (classIdNum == null || classIdNum === undefined) return NO_CLASS_AGE_KEY;
|
||||
const c = (classes || []).find((x) => Number(x.id) === Number(classIdNum));
|
||||
if (!c) return null;
|
||||
if (c.isDoubles) return null;
|
||||
return ageClassFilterKey(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {Array} opts.groups – Aus getGroupsWithParticipants (participants: flache Objekte mit id, position, isExternal, …)
|
||||
@@ -117,7 +91,9 @@ export function ageKeyForClassSlice(classIdNum, classes) {
|
||||
* @param {Array<{ id: number, isDoubles?: boolean }>} opts.classes
|
||||
* @param {Map<number, number>} opts.tmToMemberId – Turnier-Mitglied-ID -> Vereins-Mitglied-ID
|
||||
* @param {Map<number, { firstName: string, lastName: string }>} [opts.tmToName] – optional Namen
|
||||
* @param {Set<string>|null} [opts.allowedAgeKeys] – nur diese Altersklassen-Schlüssel werten; null = alle
|
||||
* @param {Map<number, { birthDate?: string|null, gender?: string|null }>} [opts.tmToMemberProfile] – pro TM: Mitglied Geburtsdatum + Geschlecht (für Filter)
|
||||
* @param {Set<string>|null} [opts.allowedAgeKeys] – tt|9|female … tt|adult|open; null = alle
|
||||
* @param {string} [opts.tournamentDateIso] – Turnierdatum für J/Erwachsenen-Zuordnung
|
||||
* @returns {Map<number, { points: number, firstName: string, lastName: string }>} Vereins-Mitglied-ID -> Aggregation
|
||||
*/
|
||||
export function computeInternalSinglesStatsForTournament({
|
||||
@@ -126,7 +102,9 @@ export function computeInternalSinglesStatsForTournament({
|
||||
classes,
|
||||
tmToMemberId,
|
||||
tmToName,
|
||||
tmToMemberProfile,
|
||||
allowedAgeKeys = null,
|
||||
tournamentDateIso,
|
||||
}) {
|
||||
const doublesClassIds = new Set(
|
||||
(classes || []).filter((c) => c.isDoubles).map((c) => Number(c.id))
|
||||
@@ -164,18 +142,12 @@ export function computeInternalSinglesStatsForTournament({
|
||||
return { firstName: n?.firstName || '', lastName: n?.lastName || '' };
|
||||
};
|
||||
|
||||
const profileForTm = (tmId) => tmToMemberProfile?.get(Number(tmId)) ?? null;
|
||||
|
||||
for (const classKey of classKeys) {
|
||||
const classIdNum = classKey === 'null' ? null : Number(classKey);
|
||||
if (classIdNum != null && doublesClassIds.has(classIdNum)) continue;
|
||||
|
||||
const sliceAgeKey = ageKeyForClassSlice(classIdNum, classes);
|
||||
if (sliceAgeKey == null) continue;
|
||||
|
||||
if (allowedAgeKeys != null) {
|
||||
if (allowedAgeKeys.size === 0) continue;
|
||||
if (!allowedAgeKeys.has(sliceAgeKey)) continue;
|
||||
}
|
||||
|
||||
const classGroups = (groups || []).filter((g) => {
|
||||
const gc = g.classId != null ? Number(g.classId) : null;
|
||||
const key = gc != null ? String(gc) : 'null';
|
||||
@@ -235,6 +207,12 @@ export function computeInternalSinglesStatsForTournament({
|
||||
const mid = tmToMemberId.get(Number(tmId));
|
||||
if (mid == null) continue;
|
||||
|
||||
const bucket = memberToCanonicalTtBucket(profileForTm(tmId), tournamentDateIso);
|
||||
if (allowedAgeKeys != null) {
|
||||
if (allowedAgeKeys.size === 0) continue;
|
||||
if (bucket == null || !allowedAgeKeys.has(bucket)) continue;
|
||||
}
|
||||
|
||||
const gPts = groupPointsByTm.get(tmId) ?? 0;
|
||||
const wins = koWins.get(tmId) || 0;
|
||||
const inKo = playedKo.has(tmId);
|
||||
@@ -258,7 +236,4 @@ export default {
|
||||
groupPercentFromRankings,
|
||||
computeInternalSinglesStatsForTournament,
|
||||
parseWinnerFromMatch,
|
||||
ageClassFilterKey,
|
||||
ageKeyForClassSlice,
|
||||
NO_CLASS_AGE_KEY,
|
||||
};
|
||||
|
||||
@@ -15,11 +15,8 @@ import { Op, literal } from 'sequelize';
|
||||
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
import {
|
||||
computeInternalSinglesStatsForTournament,
|
||||
ageClassFilterKey,
|
||||
NO_CLASS_AGE_KEY,
|
||||
} from './internalTournamentStatsService.js';
|
||||
import { computeInternalSinglesStatsForTournament } from './internalTournamentStatsService.js';
|
||||
import { getCanonicalTtAgeClassOptions } from '../utils/ttInternalStatsBuckets.js';
|
||||
|
||||
/** @param {unknown} val Query ageClassKeys: fehlend = alle; leer = keine; sonst kommagetrennte Schlüssel */
|
||||
function parseInternalStatsAgeClassKeys(val) {
|
||||
@@ -4326,8 +4323,6 @@ Ve // 2. Neues Turnier anlegen
|
||||
});
|
||||
const list = JSON.parse(JSON.stringify(tournaments));
|
||||
|
||||
const ageClassOptionsByKey = new Map();
|
||||
|
||||
const memberAgg = new Map();
|
||||
|
||||
const rawKeys = parseInternalStatsAgeClassKeys(ageClassKeysQuery);
|
||||
@@ -4338,48 +4333,6 @@ Ve // 2. Neues Turnier anlegen
|
||||
const groups = await this.getGroupsWithParticipants(userToken, clubId, t.id);
|
||||
const matches = await this.getTournamentMatches(userToken, clubId, t.id);
|
||||
|
||||
for (const c of classesJson) {
|
||||
if (c.isDoubles) continue;
|
||||
const k = ageClassFilterKey(c);
|
||||
if (!k) continue;
|
||||
if (!ageClassOptionsByKey.has(k)) {
|
||||
ageClassOptionsByKey.set(k, {
|
||||
key: k,
|
||||
name: c.name || '',
|
||||
minBirthYear: c.minBirthYear ?? c.min_birth_year ?? null,
|
||||
maxBirthYear: c.maxBirthYear ?? c.max_birth_year ?? null,
|
||||
gender: c.gender ?? null,
|
||||
isNoClass: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
let hasNoClassSlice = false;
|
||||
for (const g of groups || []) {
|
||||
if (g.classId == null || g.classId === undefined) {
|
||||
hasNoClassSlice = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasNoClassSlice) {
|
||||
for (const ma of matches || []) {
|
||||
if (ma.round === 'group') continue;
|
||||
if (ma.classId == null || ma.classId === undefined) {
|
||||
hasNoClassSlice = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasNoClassSlice && !ageClassOptionsByKey.has(NO_CLASS_AGE_KEY)) {
|
||||
ageClassOptionsByKey.set(NO_CLASS_AGE_KEY, {
|
||||
key: NO_CLASS_AGE_KEY,
|
||||
name: '',
|
||||
minBirthYear: null,
|
||||
maxBirthYear: null,
|
||||
gender: null,
|
||||
isNoClass: true,
|
||||
});
|
||||
}
|
||||
|
||||
let allowedAgeKeys = null;
|
||||
if (rawKeys !== null) {
|
||||
allowedAgeKeys = new Set(rawKeys);
|
||||
@@ -4398,10 +4351,11 @@ Ve // 2. Neues Turnier anlegen
|
||||
const tmList = [...tmIds];
|
||||
const tmToMemberId = new Map();
|
||||
const tmToName = new Map();
|
||||
const tmToMemberProfile = new Map();
|
||||
if (tmList.length > 0) {
|
||||
const tms = await TournamentMember.findAll({
|
||||
where: { tournamentId: t.id, id: { [Op.in]: tmList } },
|
||||
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }],
|
||||
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName', 'birthDate', 'gender'] }],
|
||||
});
|
||||
for (const row of tms) {
|
||||
const plain = row.toJSON();
|
||||
@@ -4411,6 +4365,10 @@ Ve // 2. Neues Turnier anlegen
|
||||
firstName: plain.member.firstName || '',
|
||||
lastName: plain.member.lastName || '',
|
||||
});
|
||||
tmToMemberProfile.set(plain.id, {
|
||||
birthDate: plain.member.birthDate ?? null,
|
||||
gender: plain.member.gender ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4421,7 +4379,9 @@ Ve // 2. Neues Turnier anlegen
|
||||
classes: classesJson,
|
||||
tmToMemberId,
|
||||
tmToName,
|
||||
tmToMemberProfile,
|
||||
allowedAgeKeys,
|
||||
tournamentDateIso: t.date,
|
||||
});
|
||||
|
||||
for (const [mid, row] of memberTotals) {
|
||||
@@ -4458,13 +4418,7 @@ Ve // 2. Neues Turnier anlegen
|
||||
b.averagePoints - a.averagePoints || (a.lastName || '').localeCompare(b.lastName || '', 'de')
|
||||
);
|
||||
|
||||
const ageClassOptions = [...ageClassOptionsByKey.values()].sort((a, b) => {
|
||||
if (a.isNoClass && !b.isNoClass) return 1;
|
||||
if (!a.isNoClass && b.isNoClass) return -1;
|
||||
const an = (a.name || '').localeCompare(b.name || '', 'de');
|
||||
if (an !== 0) return an;
|
||||
return (a.key || '').localeCompare(b.key || '', 'de');
|
||||
});
|
||||
const ageClassOptions = getCanonicalTtAgeClassOptions();
|
||||
|
||||
return {
|
||||
months: monthsNum,
|
||||
|
||||
Reference in New Issue
Block a user