diff --git a/backend/services/clickTtPlayerRegistrationService.js b/backend/services/clickTtPlayerRegistrationService.js index f726ca8e..84e69f86 100644 --- a/backend/services/clickTtPlayerRegistrationService.js +++ b/backend/services/clickTtPlayerRegistrationService.js @@ -1,5 +1,6 @@ import { chromium } from 'playwright'; import Member from '../models/Member.js'; +import Club from '../models/Club.js'; import ClickTtAccount from '../models/ClickTtAccount.js'; import { checkAccess } from '../utils/userUtils.js'; import HttpError from '../exceptions/HttpError.js'; @@ -46,6 +47,12 @@ class ClickTtPlayerRegistrationService { throw new HttpError('Mitglied nicht gefunden', 404); } + const club = await Club.findByPk(clubId); + const associationMemberNumber = String(club?.associationMemberNumber || '').trim(); + if (!associationMemberNumber) { + throw new HttpError('Für den Verein ist keine Verbands-Mitgliedsnummer hinterlegt', 400); + } + const account = await ClickTtAccount.findOne({ where: { userId } }); if (!account?.username) { throw new HttpError('Kein HTTV-/click-TT-Account für diesen Benutzer hinterlegt', 400); @@ -82,6 +89,8 @@ class ClickTtPlayerRegistrationService { this._attachNetworkLogging(page, trace); await this._openAuthenticatedClickTt(page, { username, password, trace }); + await this._selectClubContext(page, associationMemberNumber, trace); + await this._ensureApplicationEntryVisible(page, trace); await this._clickByText(page, 'Spielberechtigungen beantragen', trace); await this._fillSearchForm(page, memberJson); await this._clickByText(page, 'Personen suchen', trace); @@ -271,10 +280,13 @@ class ClickTtPlayerRegistrationService { async _dismissConsentOverlays(page, trace) { const candidates = [ + page.getByRole('button', { name: /verstanden/i }).first(), page.getByRole('button', { name: /akzeptieren|accept|zustimmen/i }).first(), + page.getByRole('link', { name: /verstanden/i }).first(), page.locator('#cmpwelcomebtnyes').first(), page.locator('[id*="cmp"][class*="button"]:has-text("Akzeptieren")').first(), - page.locator('button:has-text("Akzeptieren")').first() + page.locator('button:has-text("Akzeptieren")').first(), + page.locator('a:has-text("Verstanden")').first() ]; for (const locator of candidates) { @@ -293,6 +305,47 @@ class ClickTtPlayerRegistrationService { return false; } + async _ensureApplicationEntryVisible(page, trace) { + await this._dismissConsentOverlays(page, trace); + + const directEntry = await this._hasTextTarget(page, 'Spielberechtigungen beantragen'); + if (directEntry) { + return; + } + + if (await this._hasTextTarget(page, 'Persönlicher Bereich')) { + this._trace(trace, 'step', { + name: 'open-personal-area' + }); + await this._clickByText(page, 'Persönlicher Bereich', trace); + await page.waitForLoadState('domcontentloaded'); + await this._dismissConsentOverlays(page, trace); + } + } + + async _selectClubContext(page, associationMemberNumber, trace) { + await this._dismissConsentOverlays(page, trace); + + const clubLink = page.locator(`a:has-text("(${associationMemberNumber})")`).first(); + if (await clubLink.count()) { + this._trace(trace, 'step', { + name: 'select-club-context', + associationMemberNumber + }); + await clubLink.click(); + await page.waitForLoadState('domcontentloaded'); + await this._dismissConsentOverlays(page, trace); + return true; + } + + this._trace(trace, 'step', { + name: 'club-context-not-found-on-page', + associationMemberNumber, + url: page.url() + }); + return false; + } + async _fillSearchForm(page, member) { await this._fillFirstAvailable(page, [ 'input[name*=".1"]', @@ -384,6 +437,26 @@ class ClickTtPlayerRegistrationService { return false; } + async _hasTextTarget(page, text) { + const escaped = escapeRegExp(text); + const selectors = [ + `input[type="submit"][value*="${text}"]`, + `input[type="button"][value*="${text}"]`, + `button:has-text("${text}")`, + `a:has-text("${text}")`, + `text=/${escaped}/i` + ]; + + for (const selector of selectors) { + const locator = page.locator(selector).first(); + if (await locator.count()) { + return true; + } + } + + return false; + } + async _clickByText(page, text, trace) { const escaped = escapeRegExp(text); const selectors = [