feat(TournamentStats): enhance internal tournament statistics with member profile integration
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:
Torsten Schulz (local)
2026-04-08 13:48:41 +02:00
parent 30994adee8
commit bbd9f08e97
19 changed files with 213 additions and 138 deletions

View File

@@ -0,0 +1,147 @@
/**
* Feste TT-Einzel-Filter (J9J19, Erwachsene × Weiblich/Alle).
* Zuordnung über **Teilnehmer/Mitglied**: Geburtsdatum + Geschlecht, Saison-Stichtag 01.01.
* (vgl. frontend/src/utils/ttAgeClass.js)
*/
const YOUTH_BANDS = [9, 11, 13, 15, 19];
/**
* @param {string|undefined} isoDateStr Turnierdatum (DATEONLY)
* @returns {number} erstes Saisonjahr (01.08.31.07.)
*/
export function getSeasonStartYearFromTournamentDate(isoDateStr) {
if (!isoDateStr) {
const n = new Date();
const month = n.getMonth() + 1;
const year = n.getFullYear();
return month >= 8 ? year : year - 1;
}
const d = new Date(String(isoDateStr).length <= 10 ? `${isoDateStr}T12:00:00` : isoDateStr);
if (Number.isNaN(d.getTime())) {
const n = new Date();
const month = n.getMonth() + 1;
const year = n.getFullYear();
return month >= 8 ? year : year - 1;
}
const month = d.getMonth() + 1;
const year = d.getFullYear();
return month >= 8 ? year : year - 1;
}
function getStichtagDate(seasonStartYear, classNum) {
const y = seasonStartYear - (classNum - 1);
return new Date(y, 0, 1);
}
/**
* @param {number} ms Geburtszeitpunkt
* @param {number} seasonStartYear
* @returns {'J9'|'J11'|'J13'|'J15'|'J19'|'adult'}
*/
function getExclusiveJugendLabelFromBirthTime(ms, seasonStartYear) {
const c = (k) => getStichtagDate(seasonStartYear, k).getTime();
const c9 = c(9);
const c11 = c(11);
const c13 = c(13);
const c15 = c(15);
const c19 = c(19);
if (ms < c19) return 'adult';
if (ms >= c9) return 'J9';
if (ms >= c11 && ms < c9) return 'J11';
if (ms >= c13 && ms < c11) return 'J13';
if (ms >= c15 && ms < c13) return 'J15';
if (ms >= c19 && ms < c15) return 'J19';
return 'adult';
}
/**
* @param {string|Date|null|undefined} birthDate wie Member.birthDate (Klartext nach get)
* @returns {number|null} ms seit Epoch
*/
export function parseMemberBirthDateToMs(birthDate) {
if (birthDate == null || birthDate === '') return null;
if (birthDate instanceof Date && !Number.isNaN(birthDate.getTime())) {
return birthDate.getTime();
}
const t = String(birthDate).trim();
if (/^\d{4}-\d{2}-\d{2}/.test(t)) {
const [a, b, c] = t.split(/[T\s]/)[0].split('-').map(Number);
if (Number.isFinite(a) && Number.isFinite(b) && Number.isFinite(c)) {
const d = new Date(a, b - 1, c);
return Number.isNaN(d.getTime()) ? null : d.getTime();
}
}
if (t.includes('.')) {
const parts = t.split('.');
if (parts.length >= 3) {
const day = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10);
const year = parseInt(parts[2], 10);
if (Number.isFinite(day) && Number.isFinite(month) && Number.isFinite(year)) {
const d = new Date(year, month - 1, day);
return Number.isNaN(d.getTime()) ? null : d.getTime();
}
}
}
return null;
}
/** TT: nur Weiblich vs. Alle (alles außer female → open) */
function memberGenderMode(gender) {
return gender === 'female' ? 'female' : 'open';
}
/**
* Kanonischer Filter-Schlüssel aus Vereinsmitglied (Teilnehmer).
* @param {{ birthDate?: string|Date|null, gender?: string|null }|null|undefined} memberProfile
* @param {string|undefined} tournamentDateIso
* @returns {string|null} z. B. tt|13|open; null ohne auswertbares Geburtsdatum
*/
export function memberToCanonicalTtBucket(memberProfile, tournamentDateIso) {
if (!memberProfile) return null;
const ms = parseMemberBirthDateToMs(memberProfile.birthDate);
if (ms == null) return null;
const seasonStartYear = getSeasonStartYearFromTournamentDate(tournamentDateIso);
const jc = getExclusiveJugendLabelFromBirthTime(ms, seasonStartYear);
const gMode = memberGenderMode(memberProfile.gender);
if (jc === 'adult') return `tt|adult|${gMode}`;
const n = parseInt(jc.slice(1), 10);
if (!YOUTH_BANDS.includes(n)) return `tt|adult|${gMode}`;
return `tt|${n}|${gMode}`;
}
export function getCanonicalTtAgeClassOptions() {
const out = [];
for (const n of YOUTH_BANDS) {
out.push({
key: `tt|${n}|female`,
band: 'youth',
bandNum: n,
genderMode: 'female',
isNoClass: false,
});
out.push({
key: `tt|${n}|open`,
band: 'youth',
bandNum: n,
genderMode: 'open',
isNoClass: false,
});
}
out.push({
key: 'tt|adult|female',
band: 'adult',
bandNum: null,
genderMode: 'female',
isNoClass: false,
});
out.push({
key: 'tt|adult|open',
band: 'adult',
bandNum: null,
genderMode: 'open',
isNoClass: false,
});
return out;
}