feat: implement team name normalization and enhance CSV import match detection
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 51s

This commit is contained in:
Torsten Schulz (local)
2026-07-17 12:25:58 +02:00
parent 94385231d3
commit 370df07c2a

View File

@@ -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,
});
}