diff --git a/backend/services/matchService.js b/backend/services/matchService.js index d1b61581..3995c272 100755 --- a/backend/services/matchService.js +++ b/backend/services/matchService.js @@ -69,6 +69,50 @@ class MatchService { return `${seasonStartYear}/${seasonEndYear}`; } + normalizeTeamNameForMatch(name) { + return String(name || '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .replace(/\s*\((?:j|m)\d+\)\s*/gi, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + async findExistingMatchForCSV(clubId, date, homeTeamName, guestTeamName) { + if (Number.isNaN(date?.getTime?.())) { + return null; + } + + const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0); + const endOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999); + const candidates = await Match.findAll({ + where: { + clubId, + date: { [Op.between]: [startOfDay, endOfDay] } + }, + attributes: ['id', 'homeTeamId', 'guestTeamId', 'leagueId', 'locationId'] + }); + + const normalizedHome = this.normalizeTeamNameForMatch(homeTeamName); + const normalizedGuest = this.normalizeTeamNameForMatch(guestTeamName); + + for (const candidate of candidates) { + const [homeTeam, guestTeam] = await Promise.all([ + Team.findByPk(candidate.homeTeamId, { attributes: ['name'] }), + Team.findByPk(candidate.guestTeamId, { attributes: ['name'] }) + ]); + const candidateHome = this.normalizeTeamNameForMatch(homeTeam?.name); + const candidateGuest = this.normalizeTeamNameForMatch(guestTeam?.name); + + if (candidateHome === normalizedHome && candidateGuest === normalizedGuest) { + return candidate; + } + } + + return null; + } + async importCSV(userToken, clubId, filePath) { await checkAccess(userToken, clubId); let seasonString = ''; @@ -92,6 +136,43 @@ class MatchService { seasonId: season.id, }, }); + const homeTeamName = this.formatTeamNameWithAgeClass( + row['HeimMannschaft'], + row['HeimMannschaftAltersklasse'] + ); + const guestTeamName = this.formatTeamNameWithAgeClass( + row['GastMannschaft'], + row['GastMannschaftAltersklasse'] + ); + let location = null; + if (row['HalleName']?.trim()) { + [location] = await Location.findOrCreate({ + where: { + name: row['HalleName'], + address: row['HalleStrasse'], + city: row['HalleOrt'], + zip: row['HallePLZ'], + }, + }); + } + + // Club teams already configured through MyTischtennis can be linked to + // leagues with a different display name than the click-TT CSV "Staffel". + // Update the existing match in that league so the schedule view receives + // the venue instead of creating a disconnected duplicate in a CSV league. + const existingMatch = await this.findExistingMatchForCSV( + clubId, + parsedDate, + homeTeamName, + guestTeamName + ); + if (existingMatch && existingMatch.leagueId !== league.id) { + if (location && existingMatch.locationId !== location.id) { + await existingMatch.update({ locationId: location.id }); + } + continue; + } + const homeTeamId = await this.getOrCreateTeamId( row['HeimMannschaft'], row['HeimMannschaftAltersklasse'], @@ -106,21 +187,13 @@ class MatchService { league.id, season.id ); - const [location] = await Location.findOrCreate({ - where: { - name: row['HalleName'], - address: row['HalleStrasse'], - city: row['HalleOrt'], - zip: row['HallePLZ'], - }, - }); matches.push({ date: parsedDate, time: row['Termin'].split(' ')[1], homeTeamId: homeTeamId, guestTeamId: guestTeamId, leagueId: league.id, - locationId: location.id, + locationId: location?.id || null, clubId: clubId, }); }