feat(MemberService): enhance member ranking matching with improved name normalization and indexing
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s

This commit is contained in:
Torsten Schulz (local)
2026-10-02 09:40:15 +02:00
parent 8feaf8335e
commit f07415b877

View File

@@ -742,22 +742,51 @@ class MemberService {
const notFound = [];
const matched = [];
// Indizes für schnelleres Matching (nach personId und Name)
// Indizes für schnelleres Matching (nach personId und Name).
// Namen aus der Mitgliederverwaltung können unsichtbare Unicode-Zeichen,
// abweichende Bindestriche oder Akzente enthalten. Die Rangliste liefert
// dagegen eine normalisierte Schreibweise. Deshalb zusätzlich einen
// fehlertoleranten Namensschlüssel führen.
const buildNameKey = (firstName, lastName, { loose = false } = {}) => {
const name = `${firstName || ''} ${lastName || ''}`
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.toLocaleLowerCase('de-DE')
.trim();
return loose
? name.replace(/[^\p{L}\p{N}]+/gu, '')
: name.replace(/\s+/g, ' ');
};
const createIndex = (entries) => {
const byName = new Map();
const byLooseName = new Map();
const byPersonId = new Map();
for (const e of (entries || [])) {
const key = `${(e.firstname||'').toLowerCase().trim()}|${(e.lastname||'').toLowerCase().trim()}`;
if (!byName.has(key)) byName.set(key, e);
const key = buildNameKey(e.firstname, e.lastname);
const looseKey = buildNameKey(e.firstname, e.lastname, { loose: true });
if (key) {
if (!byName.has(key)) byName.set(key, e);
else byName.set(key, null);
}
// Ein lockerer Schlüssel ist nur sicher, wenn er eindeutig ist.
if (looseKey) {
if (!byLooseName.has(looseKey)) byLooseName.set(looseKey, e);
else byLooseName.set(looseKey, null);
}
if (e.personId && !byPersonId.has(String(e.personId))) {
byPersonId.set(String(e.personId), e);
}
}
return { byName, byPersonId };
return { byName, byLooseName, byPersonId };
};
const currentIdx = createIndex(rankingsCurrent.entries);
const quarterIdx = rankingsQuarter.success ? createIndex(rankingsQuarter.entries) : { byName: new Map(), byPersonId: new Map() };
const quarterIdx = rankingsQuarter.success
? createIndex(rankingsQuarter.entries)
: { byName: new Map(), byLooseName: new Map(), byPersonId: new Map() };
// 4. Für jedes Mitglied TTR und QTTR aktualisieren
let updatedTtr = 0;
@@ -765,22 +794,30 @@ class MemberService {
for (const member of members) {
const firstName = member.firstName;
const lastName = member.lastName;
const key = `${(firstName||'').toLowerCase().trim()}|${(lastName||'').toLowerCase().trim()}`;
const key = buildNameKey(firstName, lastName);
const looseKey = buildNameKey(firstName, lastName, { loose: true });
const personId = member.myTischtennisPlayerId ? String(member.myTischtennisPlayerId).trim() : null;
const rankingEntry = personId ? (currentIdx.byPersonId.get(personId) || currentIdx.byName.get(key)) : currentIdx.byName.get(key);
const rankingQuarterEntry = personId ? (quarterIdx.byPersonId.get(personId) || quarterIdx.byName.get(key)) : quarterIdx.byName.get(key);
const findRankingEntry = (index) =>
(personId && index.byPersonId.get(personId))
|| index.byName.get(key)
|| index.byLooseName.get(looseKey)
|| null;
const rankingEntry = findRankingEntry(currentIdx);
const rankingQuarterEntry = findRankingEntry(quarterIdx);
if (rankingEntry || rankingQuarterEntry) {
try {
const oldTtr = member.ttr;
const oldQttr = member.qttr;
const historyPlayerId = this._extractTtrHistoryPlayerId(rankingEntry) || this._extractTtrHistoryPlayerId(rankingQuarterEntry);
if (rankingEntry && typeof rankingEntry.fedRank === 'number') {
member.ttr = rankingEntry.fedRank;
const ttr = Number(rankingEntry?.fedRank);
const qttr = Number(rankingQuarterEntry?.fedRank);
if (Number.isFinite(ttr)) {
member.ttr = ttr;
if (member.ttr !== oldTtr) updatedTtr++;
}
if (rankingQuarterEntry && typeof rankingQuarterEntry.fedRank === 'number') {
member.qttr = rankingQuarterEntry.fedRank;
if (Number.isFinite(qttr)) {
member.qttr = qttr;
if (member.qttr !== oldQttr) updatedQttr++;
} else if (!rankingsQuarter.success && (member.qttr == null)) {
// Fallback: wenn QTTR-Abruf fehlgeschlagen ist und kein Wert vorhanden war,
@@ -793,6 +830,13 @@ class MemberService {
if (historyPlayerId && member.myTischtennisHistoryPlayerId !== historyPlayerId) {
member.myTischtennisHistoryPlayerId = historyPlayerId;
}
// Nach einer eindeutigen Namenszuordnung die offizielle ID
// übernehmen. Künftige Abrufe sind damit unabhängig von
// Schreibweisen im Mitgliederbestand.
const resolvedPlayerId = rankingEntry?.personId || rankingQuarterEntry?.personId;
if (resolvedPlayerId && member.myTischtennisPlayerId !== String(resolvedPlayerId)) {
member.myTischtennisPlayerId = String(resolvedPlayerId);
}
await member.save();
const ratingChanged = member.ttr !== oldTtr || member.qttr !== oldQttr;
const oldQttrNumber = Number(oldQttr);