diff --git a/backend/services/memberService.js b/backend/services/memberService.js index 7620df18..e765c921 100755 --- a/backend/services/memberService.js +++ b/backend/services/memberService.js @@ -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);