Refactor code structure for improved readability and maintainability
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 53s

This commit is contained in:
Torsten Schulz (local)
2026-05-27 23:53:41 +02:00
parent 2e7cf0c28d
commit e57cdc6ad8
25 changed files with 156689 additions and 171 deletions

View File

@@ -237,6 +237,11 @@ class DiaryDateActivityService {
const gpImages = await PredefinedActivityImage.findAll({ where: { predefinedActivityId: gpJson.id }, order: [['createdAt','ASC']] });
gpJson.images = gpImages.map(i => { const ii = i.toJSON(); try { if (ii.drawingData) ii.drawingData = JSON.parse(ii.drawingData); } catch (e){}; return ii; });
const firstImg = gpImages[0];
if (gpJson.drawingData) {
try { if (typeof gpJson.drawingData === 'string') gpJson.drawingData = JSON.parse(gpJson.drawingData); } catch (e) {}
} else if (firstImg && firstImg.drawingData) {
try { gpJson.drawingData = JSON.parse(firstImg.drawingData); } catch (e) {}
}
if (firstImg) {
gpJson.imageUrl = `/api/predefined-activities/${gpJson.id}/image/${firstImg.id}`;
gpJson.imageLink = `/api/predefined-activities/${gpJson.id}/image/${firstImg.id}`;

View File

@@ -4,6 +4,7 @@ import GroupActivity from '../models/GroupActivity.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import sequelize from '../database.js';
import { Op } from 'sequelize';
import { normalizeStoredDrawingData } from '../utils/drawingData.js';
import { devLog } from '../utils/logger.js';
class PredefinedActivityService {
@@ -16,7 +17,7 @@ class PredefinedActivityService {
duration: data.duration,
imageLink: data.imageLink,
excludeFromStats: Boolean(data.excludeFromStats),
drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null,
drawingData: normalizeStoredDrawingData(data.drawingData),
});
}
@@ -33,7 +34,7 @@ class PredefinedActivityService {
duration: data.duration,
imageLink: data.imageLink,
excludeFromStats: Boolean(data.excludeFromStats),
drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null,
drawingData: normalizeStoredDrawingData(data.drawingData),
});
}

View File

@@ -94,6 +94,34 @@ function nextPowerOfTwo(n) {
return p;
}
function knockoutEntrantKey(entrant) {
return `${entrant.isExternal ? 'E' : 'M'}:${entrant.id}`;
}
function compareKnockoutEntrants(a, b) {
const comparisons = [
['place', 1],
['points', -1],
['setDiff', -1],
['setsWon', -1],
['pointsDiff', -1],
['pointsWon', -1],
];
for (const [property, direction] of comparisons) {
const aValue = Number(a[property] ?? 0);
const bValue = Number(b[property] ?? 0);
if (aValue !== bValue) {
return direction * (aValue - bValue);
}
}
if (!!a.isExternal !== !!b.isExternal) {
return a.isExternal ? 1 : -1;
}
return Number(a.id) - Number(b.id);
}
const THIRD_PLACE_ROUND = 'Spiel um Platz 3';
class TournamentService {
// -------- Multi-Stage (Runden) V1 --------
@@ -255,9 +283,15 @@ class TournamentService {
// - Für interne Teilnehmer brauchen wir die ClubMember-ID (Member.id / TournamentMember.clubMemberId),
// nicht die TournamentMember.id.
// - Für externe Teilnehmer ist `id` die ExternalTournamentParticipant.id (bestehende Logik).
participants: (g.participants || []).map(p => ({
participants: (g.participants || []).map((p, index) => ({
id: p.isExternal ? p.id : (p.clubMemberId ?? p.member?.id ?? p.id),
isExternal: !!p.isExternal,
place: Number(p.position ?? index + 1),
points: Number(p.points ?? 0),
setDiff: Number(p.setDiff ?? 0),
setsWon: Number(p.setsWon ?? 0),
pointsDiff: Number(p.pointsDiff ?? 0),
pointsWon: Number(p.pointsWon ?? 0),
})),
}));
@@ -289,7 +323,7 @@ class TournamentService {
if ((grp.classId ?? null) !== classId) continue;
for (const place of fromPlaces) {
const p = getByPlace(grp, Number(place));
if (p) items.push({ ...p, classId, place: Number(place) });
if (p) items.push({ ...p, classId });
}
}
@@ -455,16 +489,21 @@ class TournamentService {
const entrants = classItems.map(p => ({
id: Number(p.id),
isExternal: !!p.isExternal,
place: p.place || 999, // Platz-Information behalten
place: p.place || 999,
points: p.points ?? 0,
setDiff: p.setDiff ?? 0,
setsWon: p.setsWon ?? 0,
pointsDiff: p.pointsDiff ?? 0,
pointsWon: p.pointsWon ?? 0,
}));
// Dedupliziere (falls jemand in mehreren Regeln landet)
// Wenn jemand mehrfach vorkommt, nehme den besten Platz
const seen = new Map();
for (const e of entrants) {
const key = `${e.isExternal ? 'E' : 'M'}:${e.id}`;
const key = knockoutEntrantKey(e);
const existing = seen.get(key);
if (!existing || (e.place < existing.place)) {
if (!existing || compareKnockoutEntrants(e, existing) < 0) {
seen.set(key, e);
}
}
@@ -472,13 +511,22 @@ class TournamentService {
const thirdPlace = wantsThirdPlace;
if (uniqueEntrants.length >= 2) {
// Sortiere nach Platz: beste Plätze zuerst, dann schlechtere
// Wenn mehrere Teilnehmer den gleichen Platz haben, behalte die ursprüngliche Reihenfolge
uniqueEntrants.sort((a, b) => {
const placeA = a.place || 999;
const placeB = b.place || 999;
return placeA - placeB;
});
// Ein KO-Feld wird voll besetzt, sofern weitere Gruppenteilnehmer vorhanden sind.
// Beispiel: 3 Gruppen x 2 Qualifikanten ergeben sechs Starter; die beiden
// besten Drittplatzierten vervollständigen das Viertelfinale.
const targetBracketSize = nextPowerOfTwo(uniqueEntrants.length);
const requiredFillers = targetBracketSize - uniqueEntrants.length;
if (requiredFillers > 0) {
const selectedKeys = new Set(uniqueEntrants.map(knockoutEntrantKey));
const remainingEntrants = perGroupRanked
.filter(group => (group.classId ?? null) === classId)
.flatMap(group => group.participants)
.filter(entrant => !selectedKeys.has(knockoutEntrantKey(entrant)))
.sort(compareKnockoutEntrants);
uniqueEntrants.push(...remainingEntrants.slice(0, requiredFillers));
}
uniqueEntrants.sort(compareKnockoutEntrants);
// Paare: Bester gegen Schlechtesten, Zweiter gegen Vorletzten, etc.
// Reverse die zweite Hälfte, um das gewünschte Pairing zu erreichen