Resolve external opponent TTR ratings and show lookup diagnostics
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s
This commit is contained in:
@@ -869,6 +869,15 @@ class MyTischtennisClient {
|
||||
* @param {string} fedNickname - Federation nickname (e.g., "HeTTV")
|
||||
* @returns {Promise<Object>} Rankings with player entries (all pages)
|
||||
*/
|
||||
async searchClubs(cookie, query) {
|
||||
const result = await this.authenticatedRequest('/api/search/clubs', cookie, {
|
||||
method: 'POST', data: new URLSearchParams({ query }).toString()
|
||||
});
|
||||
if (!result.success) return result;
|
||||
if (!Array.isArray(result.data?.results)) return { success: false, error: 'Ungültige Antwort der myTischtennis-Vereinssuche' };
|
||||
return { success: true, clubs: result.data.results };
|
||||
}
|
||||
|
||||
async getClubRankings(cookie, clubId, fedNickname, currentRanking = 'yes', options = {}) {
|
||||
const { includeHistoryPlayerIds = false } = options;
|
||||
const allEntries = [];
|
||||
@@ -877,7 +886,7 @@ class MyTischtennisClient {
|
||||
|
||||
|
||||
while (hasMorePages) {
|
||||
const endpoint = `/rankings/andro-rangliste?all-players=on&clubnr=${clubId}&fednickname=${fedNickname}¤t-ranking=${currentRanking}&results-per-page=100&page=${currentPage}&_data=routes%2F%24`;
|
||||
const endpoint = `/rankings/andro-rangliste?all-players=on&continent=all&country=all&clubnr=${encodeURIComponent(clubId)}&fednickname=${encodeURIComponent(fedNickname)}¤t-ranking=${currentRanking}&results-per-page=100&page=${currentPage}&_data=routes%2F%24`;
|
||||
const result = await this.authenticatedRequest(endpoint, cookie, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
@@ -10,21 +10,8 @@ import myTischtennisService from '../services/myTischtennisService.js';
|
||||
import myTischtennisClient from '../clients/myTischtennisClient.js';
|
||||
import { Op } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import { resolveTtrPreview } from '../services/ttrPreviewService.js';
|
||||
|
||||
const normalizeTtrName = (value) => String(value || '')
|
||||
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ').trim().toLocaleLowerCase('de-DE');
|
||||
|
||||
const playerName = (player) => [player?.firstname || player?.firstName, player?.lastname || player?.lastName]
|
||||
.filter(Boolean).join(' ').trim();
|
||||
|
||||
const findClubForTeam = (clubs, teamName) => {
|
||||
const teamKey = normalizeTtrName(teamName);
|
||||
return clubs.find((club) => {
|
||||
const clubKey = normalizeTtrName(club.name);
|
||||
return clubKey && (teamKey === clubKey || teamKey.startsWith(`${clubKey} `) || clubKey.startsWith(`${teamKey} `));
|
||||
}) || null;
|
||||
};
|
||||
export const uploadCSV = async (req, res) => {
|
||||
try {
|
||||
const clubId = req.body?.clubId ?? req.query?.clubId;
|
||||
@@ -210,8 +197,7 @@ export const getMatchPlayers = async (req, res) => {
|
||||
|
||||
/**
|
||||
* Resolves the current TTR snapshots for a nuScore lineup. Local member data
|
||||
* is preferred. Missing opponent data is fetched from the club ranking when
|
||||
* the opposing club is also known in the database.
|
||||
* is preferred. Missing opponents are resolved through myTischtennis club search.
|
||||
*/
|
||||
export const getTtrPreviewPlayers = async (req, res) => {
|
||||
try {
|
||||
@@ -224,6 +210,7 @@ export const getTtrPreviewPlayers = async (req, res) => {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const snapshotDate = new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin' }).format(new Date());
|
||||
let cachedPlayers = [];
|
||||
if (match) {
|
||||
const [cachedRows] = await sequelize.query(
|
||||
'SELECT payload FROM match_ttr_preview_cache WHERE match_id = :matchId AND snapshot_date = :snapshotDate LIMIT 1',
|
||||
@@ -231,65 +218,26 @@ export const getTtrPreviewPlayers = async (req, res) => {
|
||||
);
|
||||
if (cachedRows.length) {
|
||||
const payload = typeof cachedRows[0].payload === 'string' ? JSON.parse(cachedRows[0].payload) : cachedRows[0].payload;
|
||||
return res.json({ ...payload, cached: true, snapshotDate });
|
||||
// Keep successful daily lookups, but retry missing players instead
|
||||
// of caching an unsuccessful response until tomorrow.
|
||||
cachedPlayers = Array.isArray(payload.players) ? payload.players : [];
|
||||
}
|
||||
}
|
||||
|
||||
const sides = ['home', 'guest'];
|
||||
const lineup = Object.fromEntries(sides.map((side) => [side, Array.isArray(req.body?.[`${side}Players`]) ? req.body[`${side}Players`] : []]));
|
||||
const allPlayers = sides.flatMap((side) => lineup[side].map((player) => ({ side, player })));
|
||||
const members = await Member.findAll({ where: { ttr: { [Op.ne]: null } } });
|
||||
const byExternalId = new Map();
|
||||
const byName = new Map();
|
||||
members.forEach((member) => {
|
||||
if (member.myTischtennisPlayerId) byExternalId.set(String(member.myTischtennisPlayerId), member);
|
||||
const key = normalizeTtrName(`${member.firstName} ${member.lastName}`);
|
||||
if (key && !byName.has(key)) byName.set(key, member);
|
||||
else if (key) byName.set(key, null);
|
||||
});
|
||||
|
||||
const resolved = new Map();
|
||||
for (const { side, player } of allPlayers) {
|
||||
const id = player?.nuLigaPersonId || player?.personId || player?.id;
|
||||
const member = (id && byExternalId.get(String(id))) || byName.get(normalizeTtrName(playerName(player)));
|
||||
if (member?.ttr != null) {
|
||||
resolved.set(`${side}:${normalizeTtrName(playerName(player))}`, {
|
||||
ttr: Number(member.ttr), adjustmentConstant: Number(member.ttrAdjustmentConstant || 16), source: 'database'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const clubs = await Club.findAll({ where: { associationMemberNumber: { [Op.ne]: null }, myTischtennisFedNickname: { [Op.ne]: null } } });
|
||||
let session = null;
|
||||
for (const side of sides) {
|
||||
const pending = lineup[side].filter((player) => !resolved.has(`${side}:${normalizeTtrName(playerName(player))}`));
|
||||
if (!pending.length) continue;
|
||||
const club = findClubForTeam(clubs, req.body?.[`${side}TeamName`]);
|
||||
if (!club) continue;
|
||||
try {
|
||||
session ||= await myTischtennisService.getSession(req.user.id);
|
||||
const rankings = await myTischtennisClient.getClubRankings(session.cookie, club.associationMemberNumber, club.myTischtennisFedNickname);
|
||||
if (!rankings.success) continue;
|
||||
const rankingByName = new Map(rankings.entries.map((entry) => [normalizeTtrName(`${entry.firstname} ${entry.lastname}`), entry]));
|
||||
pending.forEach((player) => {
|
||||
const entry = rankingByName.get(normalizeTtrName(playerName(player)));
|
||||
if (Number.isFinite(Number(entry?.fedRank))) {
|
||||
resolved.set(`${side}:${normalizeTtrName(playerName(player))}`, { ttr: Number(entry.fedRank), adjustmentConstant: 16, source: 'myTischtennis' });
|
||||
}
|
||||
});
|
||||
} catch (_error) {
|
||||
// Keep the player explicitly unresolved; the report remains usable.
|
||||
}
|
||||
}
|
||||
|
||||
const payload = { players: allPlayers.map(({ side, player }) => {
|
||||
const value = resolved.get(`${side}:${normalizeTtrName(playerName(player))}`);
|
||||
return { side, name: playerName(player), nuLigaPersonId: player?.nuLigaPersonId || null, ttr: value?.ttr ?? null, adjustmentConstant: value?.adjustmentConstant ?? null, source: value?.source || 'unavailable' };
|
||||
}) };
|
||||
const clubs = await Club.findAll();
|
||||
const payload = await resolveTtrPreview({
|
||||
homePlayers: Array.isArray(req.body?.homePlayers) ? req.body.homePlayers : [],
|
||||
guestPlayers: Array.isArray(req.body?.guestPlayers) ? req.body.guestPlayers : [],
|
||||
homeTeamName: req.body?.homeTeamName, guestTeamName: req.body?.guestTeamName,
|
||||
homeClubName: req.body?.homeClubName, guestClubName: req.body?.guestClubName,
|
||||
members, clubs, cachedPlayers,
|
||||
}, { getSession: () => myTischtennisService.getSession(req.user.id), client: myTischtennisClient });
|
||||
if (match) {
|
||||
await sequelize.query(
|
||||
`INSERT INTO match_ttr_preview_cache (match_id, snapshot_date, payload, created_at, updated_at)
|
||||
VALUES (:matchId, :snapshotDate, :payload, NOW(), NOW())`,
|
||||
VALUES (:matchId, :snapshotDate, :payload, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE payload = VALUES(payload), updated_at = NOW()`,
|
||||
{ replacements: { matchId: match.id, snapshotDate, payload: JSON.stringify(payload) } }
|
||||
);
|
||||
}
|
||||
|
||||
69
backend/services/ttrPreviewService.js
Normal file
69
backend/services/ttrPreviewService.js
Normal file
@@ -0,0 +1,69 @@
|
||||
export const normalizeTtrName = value => String(value || '').normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '').replace(/ß/g, 'ss')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ').trim().toLocaleLowerCase('de-DE');
|
||||
|
||||
const nameOf = player => [player?.firstname || player?.firstName, player?.lastname || player?.lastName].filter(Boolean).join(' ').trim() || String(player?.name || '').trim();
|
||||
const hasRating = value => value !== null && value !== undefined && String(value).trim() !== '' && Number.isFinite(Number(value));
|
||||
export const clubNameFromTeam = name => String(name || '').replace(/\s*\([^)]*\)/g, '')
|
||||
.replace(/\s+(?:[IVXL]+|[1-9])\s*$/i, '').trim();
|
||||
const matchesClub = (name, club) => normalizeTtrName(clubNameFromTeam(name)) === normalizeTtrName(club);
|
||||
|
||||
/** Dependencies are injected so the real lookup path can be tested without credentials. */
|
||||
export async function resolveTtrPreview({ homePlayers = [], guestPlayers = [], homeTeamName, guestTeamName,
|
||||
homeClubName, guestClubName, members = [], clubs = [], cachedPlayers = [] }, { getSession, client }) {
|
||||
const players = [];
|
||||
const diagnostics = [];
|
||||
let sessionPromise;
|
||||
const session = () => (sessionPromise ||= getSession());
|
||||
for (const side of ['home', 'guest']) {
|
||||
const lineup = side === 'home' ? homePlayers : guestPlayers;
|
||||
const teamName = side === 'home' ? homeTeamName : guestTeamName;
|
||||
const clubName = (side === 'home' ? homeClubName : guestClubName) || clubNameFromTeam(teamName);
|
||||
const localClub = clubs.find(club => matchesClub(teamName, club.name) || matchesClub(clubName, club.name));
|
||||
const sidePlayers = lineup.map(player => {
|
||||
const name = nameOf(player);
|
||||
// nuLigaPersonId and myTischtennisPlayerId are different ID namespaces.
|
||||
const candidates = members.filter(member => hasRating(member.ttr) &&
|
||||
normalizeTtrName(`${member.firstName} ${member.lastName}`) === normalizeTtrName(name) &&
|
||||
(!localClub || Number(member.clubId) === Number(localClub.id)));
|
||||
const member = candidates.length === 1 ? candidates[0] : null;
|
||||
const cached = cachedPlayers.find(entry => entry.side === side && normalizeTtrName(entry.name) === normalizeTtrName(name) &&
|
||||
String(entry.nuLigaPersonId || '') === String(player.nuLigaPersonId || '') && hasRating(entry.ttr));
|
||||
return { side, name, nuLigaPersonId: player.nuLigaPersonId || null,
|
||||
ttr: member ? Number(member.ttr) : cached ? Number(cached.ttr) : null,
|
||||
adjustmentConstant: Number(member?.ttrAdjustmentConstant || cached?.adjustmentConstant || 16),
|
||||
source: member ? 'database' : cached ? cached.source : 'unavailable' };
|
||||
});
|
||||
const pending = sidePlayers.filter(player => !hasRating(player.ttr));
|
||||
if (pending.length) {
|
||||
try {
|
||||
const activeSession = await session();
|
||||
let clubNr = localClub?.associationMemberNumber;
|
||||
let federation = localClub?.myTischtennisFedNickname;
|
||||
if (!clubNr || !federation) {
|
||||
if (!clubName) throw new Error('Vereinsname fehlt in den Spielberichtsdaten.');
|
||||
const search = await client.searchClubs(activeSession.cookie, clubNameFromTeam(clubName));
|
||||
if (!search.success) throw new Error(search.error || 'Vereinssuche fehlgeschlagen.');
|
||||
const matches = search.clubs.filter(club => matchesClub(clubName, club.clubname) || matchesClub(teamName, club.clubname));
|
||||
if (matches.length !== 1) throw new Error(matches.length ? 'Verein nicht eindeutig gefunden.' : 'Verein bei myTischtennis nicht gefunden.');
|
||||
clubNr = matches[0].clubnr;
|
||||
federation = matches[0].organization_short;
|
||||
if (!clubNr || !federation) throw new Error('Vereinsnummer oder Verbandskürzel fehlt im Suchergebnis.');
|
||||
}
|
||||
const ranking = await client.getClubRankings(activeSession.cookie, clubNr, federation);
|
||||
if (!ranking.success) throw new Error(ranking.error || 'Ranglistenabruf fehlgeschlagen.');
|
||||
for (const player of pending) {
|
||||
const entries = ranking.entries.filter(entry => normalizeTtrName(nameOf(entry)) === normalizeTtrName(player.name) && hasRating(entry.fedRank));
|
||||
if (entries.length === 1) Object.assign(player, { ttr: Number(entries[0].fedRank), source: 'myTischtennis' });
|
||||
}
|
||||
const missing = pending.filter(player => !hasRating(player.ttr));
|
||||
if (missing.length) diagnostics.push({ side, code: 'players_not_found',
|
||||
message: `${clubName || teamName}: Kein eindeutiger aktueller TTR für ${missing.map(player => player.name).join(', ')} in der Rangliste gefunden.` });
|
||||
} catch (error) {
|
||||
diagnostics.push({ side, code: 'lookup_failed', message: `${clubName || teamName || side}: ${error.message}` });
|
||||
}
|
||||
}
|
||||
players.push(...sidePlayers);
|
||||
}
|
||||
return { players, diagnostics };
|
||||
}
|
||||
98
backend/tests/ttrPreview.node.test.mjs
Normal file
98
backend/tests/ttrPreview.node.test.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { resolveTtrPreview, clubNameFromTeam } from '../services/ttrPreviewService.js';
|
||||
|
||||
const anna = { firstname: 'Anna', lastname: 'Wolf', nuLigaPersonId: 123 };
|
||||
function setup(overrides = {}) {
|
||||
const calls = [];
|
||||
const input = { homePlayers: [anna], guestPlayers: [], homeTeamName: 'TSV 1875 Bonames (J15)', ...overrides };
|
||||
const dependencies = {
|
||||
getSession: async () => ({ cookie: 'test-cookie' }),
|
||||
client: {
|
||||
searchClubs: async (_cookie, query) => {
|
||||
calls.push(['search', query]);
|
||||
return { success: true, clubs: [{ clubname: 'TSV 1875 Bonames', clubnr: '43002', organization_short: 'HeTTV' }] };
|
||||
},
|
||||
getClubRankings: async (_cookie, club, fed) => {
|
||||
calls.push(['rankings', club, fed]);
|
||||
return { success: true, entries: [{ ...anna, fedRank: 680 }] };
|
||||
},
|
||||
},
|
||||
};
|
||||
return { input, dependencies, calls };
|
||||
}
|
||||
|
||||
test('resolves an external opponent without any local club through search and ranking', async () => {
|
||||
const { input, dependencies, calls } = setup();
|
||||
const result = await resolveTtrPreview(input, dependencies);
|
||||
assert.equal(result.players[0].ttr, 680);
|
||||
assert.equal(result.players[0].source, 'myTischtennis');
|
||||
assert.deepEqual(calls, [['search', 'TSV 1875 Bonames'], ['rankings', '43002', 'HeTTV']]);
|
||||
assert.deepEqual(result.diagnostics, []);
|
||||
assert.equal(clubNameFromTeam('Eintracht Frankfurt VI (J15)'), 'Eintracht Frankfurt');
|
||||
});
|
||||
|
||||
test('uses stored ratings from both clubs without requiring a myTischtennis session', async () => {
|
||||
const { input, dependencies, calls } = setup({ guestPlayers: [{ firstname: 'Yumin', lastname: 'Kim' }],
|
||||
guestTeamName: 'Eintracht Frankfurt VI (J15)',
|
||||
clubs: [{ id: 1, name: 'TSV 1875 Bonames' }, { id: 2, name: 'Eintracht Frankfurt' }],
|
||||
members: [{ clubId: 1, firstName: 'Anna', lastName: 'Wolf', ttr: 680 },
|
||||
{ clubId: 2, firstName: 'Yumin', lastName: 'Kim', ttr: 900 }] });
|
||||
dependencies.getSession = () => { throw new Error('must not log in'); };
|
||||
const result = await resolveTtrPreview(input, dependencies);
|
||||
assert.deepEqual(result.players.map(p => p.ttr), [680, 900]);
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
|
||||
test('an expired session is visible and does not erase already resolved local players', async () => {
|
||||
const { input, dependencies } = setup({ guestPlayers: [{ firstname: 'Yumin', lastname: 'Kim' }],
|
||||
members: [{ firstName: 'Anna', lastName: 'Wolf', ttr: 680 }] });
|
||||
dependencies.getSession = async () => { throw new Error('Session abgelaufen. Bitte erneut einloggen.'); };
|
||||
const result = await resolveTtrPreview(input, dependencies);
|
||||
assert.equal(result.players[0].ttr, 680);
|
||||
assert.equal(result.players[1].ttr, null);
|
||||
assert.match(result.diagnostics[0].message, /Session abgelaufen/);
|
||||
});
|
||||
|
||||
test('missing or ambiguous ranking values are not converted to zero or guessed', async () => {
|
||||
const { input, dependencies } = setup();
|
||||
dependencies.client.getClubRankings = async () => ({ success: true, entries: [{ ...anna, fedRank: null }] });
|
||||
let result = await resolveTtrPreview(input, dependencies);
|
||||
assert.equal(result.players[0].ttr, null);
|
||||
assert.equal(result.diagnostics[0].code, 'players_not_found');
|
||||
dependencies.client.getClubRankings = async () => ({ success: true, entries: [{ ...anna, fedRank: 680 }, { ...anna, fedRank: 900 }] });
|
||||
result = await resolveTtrPreview(input, dependencies);
|
||||
assert.equal(result.players[0].ttr, null);
|
||||
});
|
||||
|
||||
test('daily cached values skip external calls but missing cached players can be retried', async () => {
|
||||
const { input, dependencies, calls } = setup({ cachedPlayers: [{ side: 'home', name: 'Anna Wolf', nuLigaPersonId: 123, ttr: 670, source: 'myTischtennis' }] });
|
||||
let result = await resolveTtrPreview(input, dependencies);
|
||||
assert.equal(result.players[0].ttr, 670);
|
||||
assert.equal(calls.length, 0);
|
||||
input.cachedPlayers[0].ttr = null;
|
||||
result = await resolveTtrPreview(input, dependencies);
|
||||
assert.equal(result.players[0].ttr, 680);
|
||||
assert.equal(calls.length, 2);
|
||||
});
|
||||
|
||||
test('nuLiga and myTischtennis numeric IDs cannot accidentally match different players', async () => {
|
||||
const { input, dependencies } = setup({ members: [{ myTischtennisPlayerId: 123, firstName: 'Other', lastName: 'Player', ttr: 2000 }] });
|
||||
const result = await resolveTtrPreview(input, dependencies);
|
||||
assert.equal(result.players[0].ttr, 680);
|
||||
});
|
||||
|
||||
test('configured clubs bypass search and failures report their actual cause', async () => {
|
||||
const { input, dependencies, calls } = setup({ clubs: [{ name: 'TSV 1875 Bonames', associationMemberNumber: '43002', myTischtennisFedNickname: 'HeTTV' }] });
|
||||
dependencies.client.getClubRankings = async () => ({ success: false, error: 'Zugriff auf aktuelle Rangliste verweigert' });
|
||||
const result = await resolveTtrPreview(input, dependencies);
|
||||
assert.equal(calls.length, 0);
|
||||
assert.match(result.diagnostics[0].message, /Zugriff auf aktuelle Rangliste verweigert/);
|
||||
});
|
||||
|
||||
test('displayed pairings can resolve ratings without nuScore lineup metadata', async () => {
|
||||
const { input, dependencies } = setup({ homePlayers: [{ name: 'Anna Wolf' }] });
|
||||
const result = await resolveTtrPreview(input, dependencies);
|
||||
assert.equal(result.players[0].ttr, 680);
|
||||
assert.equal(result.players[0].name, 'Anna Wolf');
|
||||
});
|
||||
@@ -640,6 +640,8 @@
|
||||
</tbody></table>
|
||||
</div>
|
||||
<p v-if="ttrUnavailablePlayers.length" class="ttr-preview-empty">Noch ohne Wert: {{ ttrUnavailablePlayers.join(', ') }}.</p>
|
||||
<p v-if="ttrPlayers.length" class="ttr-preview-empty">{{ ttrPlayers.length - ttrUnavailablePlayers.length }} von {{ ttrPlayers.length }} TTR-Werten geladen.</p>
|
||||
<p v-for="(diagnostic, index) in ttrDiagnostics" :key="`ttr-diagnostic-${index}`" class="ttr-preview-empty" role="status">{{ diagnostic.message }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeSection === 'completion'" class="completion-content">
|
||||
@@ -954,6 +956,7 @@ export default {
|
||||
ttrPlayers: [],
|
||||
ttrLoading: false,
|
||||
ttrError: '',
|
||||
ttrDiagnostics: [],
|
||||
// Fehlermeldungen für Satzeingaben
|
||||
errors: [],
|
||||
// Aktive Zelle der schwebenden Satz-Tastatur: { matchIndex, setIndex } oder null
|
||||
@@ -3351,7 +3354,15 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
ttrLineupPlayers(side) {
|
||||
const key = side === 'home' ? 'teamLineupHomePlayers' : 'teamLineupGuestPlayers';
|
||||
return (this.meetingDetails?.[key] || []).filter((player) => player?.isSelected && player?.nuLigaPersonId > 0 && this.isSingleLineupPlayer(player));
|
||||
const lineup = this.meetingDetails?.[key] || [];
|
||||
// The displayed singles are authoritative, including older nuScore
|
||||
// lineups without positionSingle/isSelected metadata.
|
||||
const names = [...new Set(this.results.filter(match => !this.isTtrDouble(match))
|
||||
.map(match => side === 'home' ? match.homeName : match.guestName).filter(Boolean))];
|
||||
if (names.length) return names.map(name => lineup.find(player =>
|
||||
this.normalizeTtrName([player.firstname || player.firstName, player.lastname || player.lastName].filter(Boolean).join(' ')) === this.normalizeTtrName(name)
|
||||
) || { name });
|
||||
return lineup.filter((player) => player?.isSelected && player?.nuLigaPersonId > 0 && this.isSingleLineupPlayer(player));
|
||||
},
|
||||
async openTtrSection() {
|
||||
if (!this.canOpenTtrPreview) return;
|
||||
@@ -3362,6 +3373,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
if (!this.canOpenTtrPreview || !this.meetingDetails) return;
|
||||
this.ttrLoading = true;
|
||||
this.ttrError = '';
|
||||
this.ttrDiagnostics = [];
|
||||
try {
|
||||
if (this.standaloneTest) {
|
||||
const response = await apiClient.post('/matches/ttr-preview/standalone', {
|
||||
@@ -3370,7 +3382,10 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
guestPlayers: this.ttrLineupPlayers('guest'),
|
||||
homeTeamName: this.meetingData?.homeTeamname,
|
||||
guestTeamName: this.meetingData?.guestTeamname,
|
||||
homeClubName: this.meetingData?.homeClub,
|
||||
guestClubName: this.meetingData?.guestClub,
|
||||
});
|
||||
this.ttrDiagnostics = response.data?.diagnostics || [];
|
||||
this.applyTtrPreviewPlayers(Array.isArray(response.data?.players) ? response.data.players : []);
|
||||
return;
|
||||
}
|
||||
@@ -3383,7 +3398,10 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
guestPlayers: this.ttrLineupPlayers('guest'),
|
||||
homeTeamName: this.meetingData?.homeTeamname,
|
||||
guestTeamName: this.meetingData?.guestTeamname,
|
||||
homeClubName: this.meetingData?.homeClub,
|
||||
guestClubName: this.meetingData?.guestClub,
|
||||
});
|
||||
this.ttrDiagnostics = response.data?.diagnostics || [];
|
||||
this.applyTtrPreviewPlayers(Array.isArray(response.data?.players) ? response.data.players : []);
|
||||
} catch (error) {
|
||||
this.ttrError = error.response?.data?.error || 'TTR-Werte konnten nicht geladen werden.';
|
||||
|
||||
@@ -84,3 +84,40 @@ test('standalone test mode calls the API and keeps real pairings without inventi
|
||||
assert.equal(rows[0].homeTtr, null);
|
||||
assert.equal(rows[0].preview, null);
|
||||
});
|
||||
|
||||
test('TTR requests include displayed singles even when legacy lineup metadata is missing', () => {
|
||||
const report = dialog();
|
||||
report.meetingDetails = { teamLineupHomePlayers: [{ firstname: 'Anton', lastname: 'Schottdorf', nuLigaPersonId: 123 }] };
|
||||
const home = report.ttrLineupPlayers('home');
|
||||
assert.equal(home.length, 1);
|
||||
assert.equal(home[0].nuLigaPersonId, 123);
|
||||
const guest = report.ttrLineupPlayers('guest');
|
||||
assert.equal(guest.length, 1);
|
||||
assert.equal(guest[0].name, 'Yumin Kim');
|
||||
});
|
||||
|
||||
test('test mode on a saved report calls the real endpoint and exposes lookup diagnostics', async () => {
|
||||
const report = dialog();
|
||||
Object.assign(report, {
|
||||
standaloneTest: false,
|
||||
isTestMode: true,
|
||||
canOpenTtrPreview: true,
|
||||
match: { id: 42 },
|
||||
meetingDetails: {},
|
||||
meetingData: { homeClub: 'TSV 1875 Bonames', guestClub: 'Eintracht Frankfurt' },
|
||||
});
|
||||
const originalPost = context.apiClient.post;
|
||||
context.apiClient.post = async (url, body) => {
|
||||
assert.equal(url, '/matches/42/ttr-preview');
|
||||
assert.equal(body.homeClubName, 'TSV 1875 Bonames');
|
||||
assert.equal(body.homePlayers[0].name, 'Anton Schottdorf');
|
||||
return { data: { players: [], diagnostics: [{ side: 'home', message: 'Session abgelaufen' }] } };
|
||||
};
|
||||
try {
|
||||
await report.loadTtrPreview();
|
||||
assert.equal(report.ttrDiagnostics[0].message, 'Session abgelaufen');
|
||||
assert.equal(report.ttrLoading, false);
|
||||
} finally {
|
||||
context.apiClient.post = originalPost;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user