22 Commits

Author SHA1 Message Date
Torsten Schulz (local)
dbede48d4f Entfernt Konsolenausgaben aus der MyTischtennisClient-Klasse, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Methoden getUserProfile und getClubRankings und tragen zur Optimierung der Protokollierung und Performance bei. 2025-10-02 10:40:24 +02:00
Torsten Schulz (local)
6cd3c3a020 Entfernt Konsolenausgaben aus verschiedenen Controllern und Services, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Controller für Clubs, Club-Teams, Mitglieder, Tagebuch-Tags, Saisons und Teams sowie die zugehörigen Services. Ziel ist es, die Protokollierung zu optimieren und die Performance zu steigern. 2025-10-02 10:34:56 +02:00
Torsten Schulz (local)
7ecbef806d Entfernt Konsolenausgaben aus mehreren Komponenten, um den Code zu bereinigen und die Lesbarkeit zu verbessern. Betroffene Dateien sind CourtDrawingRender.vue, CourtDrawingTool.vue, SeasonSelector.vue, PredefinedActivities.vue, ScheduleView.vue, TeamManagementView.vue und TournamentsView.vue. Diese Änderungen tragen zur Optimierung der Performance und zur Reduzierung von unnötigen Protokollierungen bei. 2025-10-02 10:13:03 +02:00
Torsten Schulz (local)
1c70ca97bb Fügt Unterstützung für Team-Dokumente hinzu. Aktualisiert die Backend-Modelle und -Routen, um Team-Dokumente zu verwalten, einschließlich Upload- und Parsing-Funktionen für Code- und Pin-Listen. Ergänzt die Benutzeroberfläche in TeamManagementView.vue zur Anzeige und Verwaltung von Team-Dokumenten sowie zur Integration von PDF-Parsing. Aktualisiert die Match-Modelle, um zusätzliche Felder für Spiel-Codes und PINs zu berücksichtigen. 2025-10-02 09:04:19 +02:00
Torsten Schulz (local)
a6493990d3 Erweitert die Backend- und Frontend-Funktionalität zur Unterstützung von Teams und Saisons. Fügt neue Routen für Team- und Club-Team-Management hinzu, aktualisiert die Match- und Team-Modelle zur Berücksichtigung von Saisons, und implementiert die Saison-Auswahl in der Benutzeroberfläche. Optimiert die Logik zur Abfrage von Ligen und Spielen basierend auf der ausgewählten Saison. 2025-10-01 22:47:13 +02:00
Torsten Schulz (local)
f8f4d23c4e Aktualisiert die Schaltflächen im MyTischtennisAccount.vue, um die Benutzeroberfläche zu verbessern. Ändert den Text der Schaltfläche "Verbindung testen" in "Erneut einloggen" und entfernt die Testausgabe für Login-Tests. Optimiert die Erfolgsmeldung nach erfolgreichem Login und aktualisiert die Account-Daten. Entfernt die nicht mehr benötigte Funktionalität für den Login-Flow-Test. 2025-10-01 13:52:14 +02:00
Torsten Schulz (local)
1ef1711eea Merge branch 'main' into httv 2025-10-01 13:49:26 +02:00
Torsten Schulz (local)
85981a880d Ändert die Navigationsstruktur in App.vue, indem die Klasse der Fußzeile von "nav-menu" zu "sidebar-footer" geändert wird. Fügt eine neue CSS-Klasse "nav-menu-no-flex" hinzu, um das Layout der Navigation zu optimieren und die Flexbox-Eigenschaften anzupassen. 2025-10-01 13:49:13 +02:00
Torsten Schulz (local)
84503b6404 Merge branch 'httv' 2025-10-01 13:31:41 +02:00
Torsten Schulz (local)
bcc3ce036d Ersetzt Konsolenausgaben durch eine bedingte Entwicklungsprotokollierungsfunktion in mehreren Controllern und Services. Dies verbessert die Protokollierung und Fehlerverfolgung im gesamten Code. Aktualisiert die Benutzer-Utils, um die neue Protokollierungsfunktion zu verwenden. 2025-10-01 13:29:49 +02:00
Torsten Schulz (local)
0fe0514660 Verbessert die Protokollierung in den Club-Controller- und Benutzer-Utils-Dateien, indem die Konsolenausgaben durch eine bedingte Entwicklungsprotokollierungsfunktion ersetzt werden. Aktualisiert die Fehlerbehandlung, um detailliertere Fehlermeldungen auszugeben. 2025-10-01 13:24:16 +02:00
Torsten Schulz (local)
431ec861ba Erweitert die Trainingsstatistik-Funktionalität im TrainingStatsController um die Abfrage und Formatierung von Trainingstagen der letzten 12 Monate. Aktualisiert die Benutzeroberfläche in TrainingStatsView.vue zur Anzeige dieser Trainingstage in einer aufklappbaren Tabelle. Fügt Funktionen zum Umschalten der Sichtbarkeit von Trainingstagen und Mitgliedern hinzu. 2025-10-01 13:20:36 +02:00
Torsten Schulz (local)
648b608036 Erweitert die Trainingsstatistik-Funktionalität im TrainingStatsController, um die Anzahl der Trainings in den letzten 12 und 3 Monaten zu berechnen und zurückzugeben. Aktualisiert die Benutzeroberfläche in TrainingStatsView.vue zur Anzeige dieser neuen Daten. Ändert die Navigation in App.vue, um direkt zu den Trainingsstatistiken zu führen. 2025-10-01 13:01:54 +02:00
Torsten Schulz (local)
4ac71d967f Fügt Unterstützung für myTischtennis-Integration hinzu. Aktualisiert die Mitglieder-Controller und -Routen, um die Aktualisierung von TTR/QTTR-Werten zu ermöglichen. Ergänzt die Benutzeroberfläche in MembersView.vue zur Aktualisierung der Bewertungen und fügt neue Routen für die myTischtennis-Daten hinzu. Aktualisiert die Datenmodelle, um die neuen Felder für TTR und QTTR zu integrieren. 2025-10-01 12:09:55 +02:00
Torsten Schulz (local)
75d304ec6d Merge branch 'activitypainter' 2025-10-01 09:42:33 +02:00
Torsten Schulz (local)
afd96f5df1 Optimiert die Berechnung der Startposition im CourtDrawingRender.vue, indem die Offset-Logik für vertikale und horizontale Zeichnungen präzisiert wird. Kommentiert den Code zur besseren Verständlichkeit der Offset-Berechnungen. 2025-10-01 09:42:15 +02:00
Torsten Schulz (local)
4bfa6a5889 Erweitert die Funktionalität in CourtDrawingRender.vue und CourtDrawingTool.vue zur Verbesserung der Zeichnungslogik. Fügt neue Offset-Parameter für Zielkreise hinzu und optimiert die Berechnung der Zielpositionen. Entfernt die Schaltflächen für das manuelle Speichern in CourtDrawingTool.vue zugunsten einer automatischen Speicherung. Aktualisiert die Benutzeroberfläche in PredefinedActivities.vue zur Unterstützung der neuen Zeichnungsdaten-Logik. 2025-10-01 09:41:07 +02:00
Torsten Schulz (local)
f4187512ba Erweitert die Funktionalität zur Erstellung und Aktualisierung von vordefinierten Aktivitäten, indem das Feld für Zeichnungsdaten in den entsprechenden Controllern, Modellen und Services hinzugefügt wird. Aktualisiert die Benutzeroberfläche in CourtDrawingTool.vue und PredefinedActivities.vue, um die Handhabung von Zeichnungsdaten zu verbessern und die Logik für das Laden und Speichern von Zeichnungen zu optimieren. 2025-09-25 19:35:13 +02:00
Torsten Schulz (local)
b557297bf0 Verbessert die Logik zur Erstellung von Aktivitäten im DiaryDateActivityService, um PredefinedActivities robuster zu finden. Fügt Unterstützung für die Suche nach Aktivitäten per ID, Name oder Code hinzu. Aktualisiert die Benutzeroberfläche in DiaryView.vue zur Anzeige von Zeichnungsdaten und integriert ein neues Rendering-Modal für Zeichnungen. Optimiert die Bildanzeige in CourtDrawingTool.vue und implementiert eine verbesserte Fehlerbehandlung beim Laden von Bildern. 2025-09-23 14:40:41 +02:00
Torsten Schulz (local)
eb2273e28c Aktualisiert die Token-Lebensdauer im Authentifizierungsdienst auf 3 Stunden und verbessert die Logik zur Auswahl der Startposition im CourtDrawingTool.vue, um eine Standard-Startposition festzulegen, wenn keine ausgewählt ist. 2025-09-23 09:13:51 +02:00
Torsten Schulz (local)
091599b745 Erweitert die Funktionalität in PredefinedActivityImageController.js, um Zeichnungsdaten aus dem Request zu extrahieren und in der Datenbank zu speichern. Aktualisiert das Datenmodell in PredefinedActivityImage.js, um ein neues Feld für Zeichnungsdaten hinzuzufügen. Passt die Routen in predefinedActivityRoutes.js an, um die neue PUT-Methode für das Hochladen von Bildern zu unterstützen. Integriert die Zeichnungsdaten in die Aktivitätenlogik in diaryDateActivityService.js und aktualisiert die Benutzeroberfläche in CourtDrawingTool.vue zur Unterstützung von Zeichnungsdaten. Verbessert die Handhabung von Bild-Uploads in PredefinedActivities.vue und implementiert die Logik zum Laden von Zeichnungsdaten beim Bearbeiten von Aktivitäten. 2025-09-23 08:39:13 +02:00
Torsten Schulz (local)
d70a5ca63e Erweitert die Funktionalität in PredefinedActivities.vue um die Möglichkeit, eine Übungszeichnung zu erstellen. Fügt ein Zeichen-Tool hinzu, das die Zeichnungsdaten speichert und automatisch als Bild-Link verwendet, wenn kein Bild-Link vorhanden ist. Aktualisiert die Benutzeroberfläche zur Bild- und Zeichnungshinzufügung. 2025-09-22 12:23:39 +02:00
112 changed files with 9175 additions and 800 deletions

View File

@@ -0,0 +1,273 @@
import axios from 'axios';
const BASE_URL = 'https://www.mytischtennis.de';
class MyTischtennisClient {
constructor() {
this.baseURL = BASE_URL;
this.client = axios.create({
baseURL: this.baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*'
},
maxRedirects: 0, // Don't follow redirects automatically
validateStatus: (status) => status >= 200 && status < 400 // Accept 3xx as success
});
}
/**
* Login to myTischtennis API
* @param {string} email - myTischtennis email (not username!)
* @param {string} password - myTischtennis password
* @returns {Promise<Object>} Login response with token and session data
*/
async login(email, password) {
try {
// Create form data
const formData = new URLSearchParams();
formData.append('email', email);
formData.append('password', password);
formData.append('intent', 'login');
const response = await this.client.post(
'/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
formData.toString()
);
// Extract the cookie from response headers
const setCookie = response.headers['set-cookie'];
if (!setCookie || !Array.isArray(setCookie)) {
return {
success: false,
error: 'Keine Session-Cookie erhalten'
};
}
// Find the sb-10-auth-token cookie
const authCookie = setCookie.find(cookie => cookie.startsWith('sb-10-auth-token='));
if (!authCookie) {
return {
success: false,
error: 'Kein Auth-Token in Response gefunden'
};
}
// Extract and decode the token
const tokenMatch = authCookie.match(/sb-10-auth-token=base64-([^;]+)/);
if (!tokenMatch) {
return {
success: false,
error: 'Token-Format ungültig'
};
}
const base64Token = tokenMatch[1];
let tokenData;
try {
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
tokenData = JSON.parse(decodedToken);
} catch (decodeError) {
console.error('Error decoding token:', decodeError);
return {
success: false,
error: 'Token konnte nicht dekodiert werden'
};
}
return {
success: true,
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresAt: tokenData.expires_at,
expiresIn: tokenData.expires_in,
user: tokenData.user,
cookie: authCookie.split(';')[0] // Just the cookie value without attributes
};
} catch (error) {
console.error('MyTischtennis login error:', error.message);
return {
success: false,
error: error.response?.data?.message || 'Login fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Verify login credentials
* @param {string} email - myTischtennis email
* @param {string} password - myTischtennis password
* @returns {Promise<boolean>} True if credentials are valid
*/
async verifyCredentials(email, password) {
const result = await this.login(email, password);
return result.success;
}
/**
* Make an authenticated request
* @param {string} endpoint - API endpoint
* @param {string} cookie - Authentication cookie (sb-10-auth-token)
* @param {Object} options - Additional axios options
* @returns {Promise<Object>} API response
*/
async authenticatedRequest(endpoint, cookie, options = {}) {
try {
const response = await this.client.request({
url: endpoint,
...options,
headers: {
...options.headers,
'Cookie': cookie,
'Accept': '*/*',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
'Referer': 'https://www.mytischtennis.de/',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin'
}
});
return {
success: true,
data: response.data
};
} catch (error) {
console.error('MyTischtennis API error:', error.message);
return {
success: false,
error: error.response?.data?.message || 'API-Anfrage fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Get user profile and club information
* @param {string} cookie - Authentication cookie (sb-10-auth-token)
* @returns {Promise<Object>} User profile with club info
*/
async getUserProfile(cookie) {
const result = await this.authenticatedRequest('/?_data=root', cookie, {
method: 'GET'
});
if (result.success) {
console.log('[getUserProfile] - Response structure:', {
hasUserProfile: !!result.data?.userProfile,
hasClub: !!result.data?.userProfile?.club,
hasOrganization: !!result.data?.userProfile?.organization,
clubnr: result.data?.userProfile?.club?.clubnr,
clubName: result.data?.userProfile?.club?.name,
orgShort: result.data?.userProfile?.organization?.short,
ttr: result.data?.userProfile?.ttr,
qttr: result.data?.userProfile?.qttr
});
return {
success: true,
clubId: result.data?.userProfile?.club?.clubnr || null,
clubName: result.data?.userProfile?.club?.name || null,
fedNickname: result.data?.userProfile?.organization?.short || null,
ttr: result.data?.userProfile?.ttr || null,
qttr: result.data?.userProfile?.qttr || null,
userProfile: result.data?.userProfile || null
};
}
console.error('[getUserProfile] - Failed:', result.error);
return result;
}
/**
* Get club rankings (andro-Rangliste)
* @param {string} cookie - Authentication cookie
* @param {string} clubId - Club number (e.g., "43030")
* @param {string} fedNickname - Federation nickname (e.g., "HeTTV")
* @returns {Promise<Object>} Rankings with player entries (all pages)
*/
async getClubRankings(cookie, clubId, fedNickname) {
const allEntries = [];
let currentPage = 0;
let hasMorePages = true;
while (hasMorePages) {
const endpoint = `/rankings/andro-rangliste?all-players=on&clubnr=${clubId}&fednickname=${fedNickname}&results-per-page=100&page=${currentPage}&_data=routes%2F%24`;
const result = await this.authenticatedRequest(endpoint, cookie, {
method: 'GET'
});
if (!result.success) {
console.error(`[getClubRankings] - Failed to fetch page ${currentPage}:`, result.error);
return result;
}
// Find the dynamic key that contains entries
const blockLoaderData = result.data?.pageContent?.blockLoaderData;
if (!blockLoaderData) {
console.error('[getClubRankings] - No blockLoaderData found');
return {
success: false,
error: 'Keine blockLoaderData gefunden'
};
}
// Finde den Schlüssel, der entries enthält
let entries = null;
let rankingData = null;
for (const key in blockLoaderData) {
if (blockLoaderData[key]?.entries) {
entries = blockLoaderData[key].entries;
rankingData = blockLoaderData[key];
break;
}
}
if (!entries) {
console.error('[getClubRankings] - No entries found in blockLoaderData');
return {
success: false,
error: 'Keine entries in blockLoaderData gefunden'
};
}
// Füge Entries hinzu
allEntries.push(...entries);
// Prüfe ob es weitere Seiten gibt
// Wenn die aktuelle Seite weniger Einträge hat als das Limit, sind wir am Ende
// Oder wenn wir alle erwarteten Einträge haben
if (entries.length === 0) {
hasMorePages = false;
} else if (rankingData.numberOfPages && currentPage >= rankingData.numberOfPages - 1) {
hasMorePages = false;
} else if (allEntries.length >= rankingData.resultLength) {
hasMorePages = false;
} else {
currentPage++;
}
}
return {
success: true,
entries: allEntries,
metadata: {
totalEntries: allEntries.length,
pagesFetched: currentPage + 1
}
};
}
}
export default new MyTischtennisClient();

View File

@@ -0,0 +1,128 @@
import ClubTeamService from '../services/clubTeamService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getClubTeams = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
// Check if user has access to this club
const clubTeams = await ClubTeamService.getAllClubTeamsByClub(clubId, seasonId);
res.status(200).json(clubTeams);
} catch (error) {
console.error('[getClubTeams] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const clubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
if (!clubTeam) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(clubTeam);
} catch (error) {
console.error('[getClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
if (!name) {
return res.status(400).json({ error: "missingname" });
}
const clubTeamData = {
name,
clubId: parseInt(clubId),
leagueId: leagueId ? parseInt(leagueId) : null,
seasonId: seasonId ? parseInt(seasonId) : null
};
const newClubTeam = await ClubTeamService.createClubTeam(clubTeamData);
res.status(201).json(newClubTeam);
} catch (error) {
console.error('[createClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const updateClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
const updateData = {};
if (name !== undefined) updateData.name = name;
if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null;
if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null;
const success = await ClubTeamService.updateClubTeam(clubTeamId, updateData);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
const updatedClubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
res.status(200).json(updatedClubTeam);
} catch (error) {
console.error('[updateClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const success = await ClubTeamService.deleteClubTeam(clubTeamId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "Club team deleted successfully" });
} catch (error) {
console.error('[deleteClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getLeagues = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
const leagues = await ClubTeamService.getLeaguesByClub(clubId, seasonId);
res.status(200).json(leagues);
} catch (error) {
console.error('[getLeagues] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -1,77 +1,57 @@
import ClubService from '../services/clubService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getClubs = async (req, res) => {
try {
console.log('[getClubs] - get clubs');
const clubs = await ClubService.getAllClubs();
console.log('[getClubs] - prepare response');
res.status(200).json(clubs);
console.log('[getClubs] - done');
} catch (error) {
console.log('[getClubs] - error');
console.log(error);
console.error('[getClubs] - error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const addClub = async (req, res) => {
console.log('[addClub] - Read out parameters');
const { authcode: token } = req.headers;
const { name: clubName } = req.body;
try {
console.log('[addClub] - find club by name');
const club = await ClubService.findClubByName(clubName);
console.log('[addClub] - get user');
const user = await getUserByToken(token);
console.log('[addClub] - check if club already exists');
if (club) {
res.status(409).json({ error: "alreadyexists" });
return;
}
console.log('[addClub] - create club');
const newClub = await ClubService.createClub(clubName);
console.log('[addClub] - add user to new club');
await ClubService.addUserToClub(user.id, newClub.id);
console.log('[addClub] - prepare response');
res.status(200).json(newClub);
console.log('[addClub] - done');
} catch (error) {
console.log('[addClub] - error');
console.log(error);
console.error('[addClub] - error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getClub = async (req, res) => {
console.log('[getClub] - start');
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
console.log('[getClub] - get user');
const user = await getUserByToken(token);
console.log('[getClub] - get users club');
const access = await ClubService.getUserClubAccess(user.id, clubId);
console.log('[getClub] - check access');
if (access.length === 0 || !access[0].approved) {
res.status(403).json({ error: "noaccess", status: access.length === 0 ? "notrequested" : "requested" });
return;
}
console.log('[getClub] - get club');
const club = await ClubService.findClubById(clubId);
console.log('[getClub] - check club exists');
if (!club) {
return res.status(404).json({ message: 'Club not found' });
}
console.log('[getClub] - set response');
res.status(200).json(club);
console.log('[getClub] - done');
} catch (error) {
console.log(error);
console.error('[getClub] - error:', error);
res.status(500).json({ message: 'Server error' });
}
};
@@ -82,7 +62,6 @@ export const requestClubAccess = async (req, res) => {
try {
const user = await getUserByToken(token);
console.log(user);
await ClubService.requestAccessToClub(user.id, clubId);
res.status(200).json({});
@@ -92,6 +71,7 @@ export const requestClubAccess = async (req, res) => {
} else if (error.message === 'clubnotfound') {
res.status(404).json({ err: "clubnotfound" });
} else {
console.error('[requestClubAccess] - error:', error);
res.status(500).json({ err: "internalerror" });
}
}

View File

@@ -1,6 +1,7 @@
import diaryService from '../services/diaryService.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
const getDatesForClub = async (req, res) => {
try {
const { clubId } = req.params;
@@ -38,7 +39,7 @@ const updateTrainingTimes = async (req, res) => {
const { authcode: userToken } = req.headers;
const { dateId, trainingStart, trainingEnd } = req.body;
if (!dateId || !trainingStart) {
console.log(dateId, trainingStart, trainingEnd);
devLog(dateId, trainingStart, trainingEnd);
throw new HttpError('notallfieldsfilled', 400);
}
const updatedDate = await diaryService.updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd);

View File

@@ -1,5 +1,6 @@
import diaryDateActivityService from '../services/diaryDateActivityService.js';
import { devLog } from '../utils/logger.js';
export const createDiaryDateActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
@@ -15,7 +16,7 @@ export const createDiaryDateActivity = async (req, res) => {
});
res.status(201).json(activityItem);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error creating activity' });
}
};
@@ -58,7 +59,7 @@ export const updateDiaryDateActivityOrder = async (req, res) => {
const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId);
res.status(200).json(updatedActivity);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error updating activity order' });
}
};
@@ -70,7 +71,7 @@ export const getDiaryDateActivities = async (req, res) => {
const activities = await diaryDateActivityService.getActivities(userToken, clubId, diaryDateId);
res.status(200).json(activities);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error getting activities' });
}
}
@@ -82,7 +83,7 @@ export const addGroupActivity = async(req, res) => {
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity);
res.status(201).json(activityItem);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error adding group activity' });
}
}

View File

@@ -1,7 +1,8 @@
import diaryDateTagService from "../services/diaryDateTagService.js"
import { devLog } from '../utils/logger.js';
export const getDiaryDateMemberTags = async (req, res) => {
console.log("getDiaryDateMemberTags");
devLog("getDiaryDateMemberTags");
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
@@ -14,7 +15,7 @@ export const getDiaryDateMemberTags = async (req, res) => {
}
export const addDiaryDateTag = async (req, res) => {
console.log("addDiaryDateTag");
devLog("addDiaryDateTag");
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;

View File

@@ -1,11 +1,12 @@
import DiaryMemberService from '../services/diaryMemberService.js';
import { devLog } from '../utils/logger.js';
const getMemberTags = async (req, res) => {
try {
const { diaryDateId, memberId } = req.query;
const { clubId } = req.params;
const { authcode: userToken } = req.headers;
console.log(diaryDateId, memberId, clubId);
devLog(diaryDateId, memberId, clubId);
const tags = await DiaryMemberService.getTagsForMemberAndDate(userToken, clubId, diaryDateId, memberId);
res.status(200).json(tags);
} catch (error) {
@@ -19,7 +20,7 @@ const getMemberNotes = async (req, res) => {
const { diaryDateId, memberId } = req.query;
const { clubId } = req.params;
const { authcode: userToken } = req.headers;
console.log('---------->', userToken, clubId);
devLog('---------->', userToken, clubId);
const notes = await DiaryMemberService.getNotesForMember(userToken, clubId, diaryDateId, memberId);
res.status(200).json(notes);
} catch (error) {

View File

@@ -1,5 +1,6 @@
import { DiaryTag, DiaryDateTag } from '../models/index.js';
import { devLog } from '../utils/logger.js';
export const getTags = async (req, res) => {
try {
const tags = await DiaryTag.findAll();
@@ -12,11 +13,10 @@ export const getTags = async (req, res) => {
export const createTag = async (req, res) => {
try {
const { name } = req.body;
console.log(name);
devLog(name);
const newTag = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
res.status(201).json(newTag);
} catch (error) {
console.log('[createTag] - Error:', error);
res.status(500).json({ error: 'Error creating tag' });
}
};

View File

@@ -1,6 +1,7 @@
import HttpError from '../exceptions/HttpError.js';
import groupService from '../services/groupService.js';
import { devLog } from '../utils/logger.js';
const addGroup = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
@@ -9,7 +10,7 @@ const addGroup = async(req, res) => {
res.status(201).json(result);
} catch (error) {
console.error('[addGroup] - Error:', error);
console.log(req.params, req.headers, req.body)
devLog(req.params, req.headers, req.body)
res.status(error.statusCode || 500).json({ error: error.message });
}
}

View File

@@ -1,6 +1,7 @@
import MatchService from '../services/matchService.js';
import fs from 'fs';
import { devLog } from '../utils/logger.js';
export const uploadCSV = async (req, res) => {
try {
const { clubId } = req.body;
@@ -21,10 +22,11 @@ export const uploadCSV = async (req, res) => {
export const getLeaguesForCurrentSeason = async (req, res) => {
try {
console.log(req.headers, req.params);
devLog(req.headers, req.params);
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId);
const { seasonid: seasonId } = req.query;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId, seasonId);
return res.status(200).json(leagues);
} catch (error) {
console.error('Error retrieving leagues:', error);
@@ -36,7 +38,8 @@ export const getMatchesForLeagues = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId);
const { seasonid: seasonId } = req.query;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId, seasonId);
return res.status(200).json(matches);
} catch (error) {
console.error('Error retrieving matches:', error);

View File

@@ -1,5 +1,6 @@
import MemberService from "../services/memberService.js";
import { devLog } from '../utils/logger.js';
const getClubMembers = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
@@ -9,24 +10,17 @@ const getClubMembers = async(req, res) => {
}
res.status(200).json(await MemberService.getClubMembers(userToken, clubId, showAll));
} catch(error) {
console.log('[getClubMembers] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
}
const getWaitingApprovals = async(req, res) => {
try {
console.log('[getWaitingApprovals] - Start');
const { id: clubId } = req.params;
console.log('[getWaitingApprovals] - get token');
const { authcode: userToken } = req.headers;
console.log('[getWaitingApprovals] - load for waiting approvals');
const waitingApprovals = await MemberService.getApprovalRequests(userToken, clubId);
console.log('[getWaitingApprovals] - set response');
res.status(200).json(waitingApprovals);
console.log('[getWaitingApprovals] - done');
} catch(error) {
console.log('[getWaitingApprovals] - Error: ', error);
res.status(403).json({ error: error });
}
}
@@ -34,11 +28,11 @@ const getWaitingApprovals = async(req, res) => {
const setClubMembers = async (req, res) => {
try {
const { id: memberId, firstname: firstName, lastname: lastName, street, city, birthdate, phone, email, active,
testMembership, picsInInternetAllowed, gender } = req.body;
testMembership, picsInInternetAllowed, gender, ttr, qttr } = req.body;
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate,
phone, email, active, testMembership, picsInInternetAllowed, gender);
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr);
res.status(addResult.status || 500).json(addResult.response);
} catch (error) {
console.error('[setClubMembers] - Error:', error);
@@ -59,7 +53,6 @@ const uploadMemberImage = async (req, res) => {
};
const getMemberImage = async (req, res) => {
console.log('[getMemberImage]');
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
@@ -75,4 +68,16 @@ const getMemberImage = async (req, res) => {
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage };
const updateRatingsFromMyTischtennis = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.updateRatingsFromMyTischtennis(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[updateRatingsFromMyTischtennis] - Error:', error);
res.status(500).json({ error: 'Failed to update ratings' });
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis };

View File

@@ -1,15 +1,14 @@
import MemberNoteService from "../services/memberNoteService.js";
import { devLog } from '../utils/logger.js';
const getMemberNotes = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { memberId } = req.params;
const { clubId } = req.query;
console.log('[getMemberNotes]', userToken, memberId, clubId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
console.log('[getMemberNotes] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -18,12 +17,10 @@ const addMemberNote = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { memberId, content, clubId } = req.body;
console.log('[addMemberNote]', userToken, memberId, content, clubId);
await MemberNoteService.addNoteToMember(userToken, clubId, memberId, content);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(201).json(notes);
} catch (error) {
console.log('[addMemberNote] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -33,13 +30,11 @@ const deleteMemberNote = async (req, res) => {
const { authcode: userToken } = req.headers;
const { noteId } = req.params;
const { clubId } = req.body;
console.log('[deleteMemberNote]', userToken, noteId, clubId);
const memberId = await MemberNoteService.getMemberIdForNote(noteId); // Member ID ermitteln
await MemberNoteService.deleteNoteForMember(userToken, clubId, noteId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
console.log('[deleteMemberNote] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};

View File

@@ -0,0 +1,133 @@
import myTischtennisService from '../services/myTischtennisService.js';
import HttpError from '../exceptions/HttpError.js';
class MyTischtennisController {
/**
* GET /api/mytischtennis/account
* Get current user's myTischtennis account
*/
async getAccount(req, res, next) {
try {
const userId = req.user.id;
const account = await myTischtennisService.getAccount(userId);
if (!account) {
return res.status(200).json({ account: null });
}
res.status(200).json({ account });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/status
* Check account configuration status
*/
async getStatus(req, res, next) {
try {
const userId = req.user.id;
const status = await myTischtennisService.checkAccountStatus(userId);
res.status(200).json(status);
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/account
* Create or update myTischtennis account
*/
async upsertAccount(req, res, next) {
try {
const userId = req.user.id;
const { email, password, savePassword, userPassword } = req.body;
if (!email) {
throw new HttpError(400, 'E-Mail-Adresse erforderlich');
}
// Wenn ein Passwort gesetzt wird, muss das App-Passwort angegeben werden
if (password && !userPassword) {
throw new HttpError(400, 'App-Passwort erforderlich zum Setzen des myTischtennis-Passworts');
}
const account = await myTischtennisService.upsertAccount(
userId,
email,
password,
savePassword || false,
userPassword
);
res.status(200).json({
message: 'myTischtennis-Account erfolgreich gespeichert',
account
});
} catch (error) {
next(error);
}
}
/**
* DELETE /api/mytischtennis/account
* Delete myTischtennis account
*/
async deleteAccount(req, res, next) {
try {
const userId = req.user.id;
const deleted = await myTischtennisService.deleteAccount(userId);
if (!deleted) {
throw new HttpError(404, 'Kein myTischtennis-Account gefunden');
}
res.status(200).json({ message: 'myTischtennis-Account gelöscht' });
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/verify
* Verify login credentials
*/
async verifyLogin(req, res, next) {
try {
const userId = req.user.id;
const { password } = req.body;
const result = await myTischtennisService.verifyLogin(userId, password);
res.status(200).json({
message: 'Login erfolgreich',
success: true,
accessToken: result.accessToken,
expiresAt: result.expiresAt,
clubId: result.clubId,
clubName: result.clubName
});
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/session
* Get stored session data for authenticated requests
*/
async getSession(req, res, next) {
try {
const userId = req.user.id;
const session = await myTischtennisService.getSession(userId);
res.status(200).json({ session });
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisController();

View File

@@ -1,12 +1,13 @@
import Participant from '../models/Participant.js';
import { devLog } from '../utils/logger.js';
export const getParticipants = async (req, res) => {
try {
const { dateId } = req.params;
const participants = await Participant.findAll({ where: { diaryDateId: dateId } });
res.status(200).json(participants);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Fehler beim Abrufen der Teilnehmer' });
}
};
@@ -17,7 +18,7 @@ export const addParticipant = async (req, res) => {
const participant = await Participant.create({ diaryDateId, memberId });
res.status(201).json(participant);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Fehler beim Hinzufügen des Teilnehmers' });
}
};
@@ -28,7 +29,7 @@ export const removeParticipant = async (req, res) => {
await Participant.destroy({ where: { diaryDateId, memberId } });
res.status(200).json({ message: 'Teilnehmer entfernt' });
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Fehler beim Entfernen des Teilnehmers' });
}
};

View File

@@ -5,8 +5,8 @@ import fs from 'fs';
export const createPredefinedActivity = async (req, res) => {
try {
const { name, code, description, durationText, duration, imageLink } = req.body;
const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink });
const { name, code, description, durationText, duration, imageLink, drawingData } = req.body;
const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink, drawingData });
res.status(201).json(predefinedActivity);
} catch (error) {
console.error('[createPredefinedActivity] - Error:', error);
@@ -42,8 +42,8 @@ export const getPredefinedActivityById = async (req, res) => {
export const updatePredefinedActivity = async (req, res) => {
try {
const { id } = req.params;
const { name, code, description, durationText, duration, imageLink } = req.body;
const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink });
const { name, code, description, durationText, duration, imageLink, drawingData } = req.body;
const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink, drawingData });
res.status(200).json(updatedActivity);
} catch (error) {
console.error('[updatePredefinedActivity] - Error:', error);

View File

@@ -5,6 +5,7 @@ import path from 'path';
import fs from 'fs';
import sharp from 'sharp';
import { devLog } from '../utils/logger.js';
export const uploadPredefinedActivityImage = async (req, res) => {
try {
const { id } = req.params; // predefinedActivityId
@@ -33,10 +34,14 @@ export const uploadPredefinedActivityImage = async (req, res) => {
.jpeg({ quality: 85 })
.toFile(filePath);
// Extrahiere Zeichnungsdaten aus dem Request
const drawingData = req.body.drawingData ? JSON.parse(req.body.drawingData) : null;
const imageRecord = await PredefinedActivityImage.create({
predefinedActivityId: id,
imagePath: filePath,
mimeType: 'image/jpeg',
drawingData: drawingData ? JSON.stringify(drawingData) : null,
});
// Optional: als imageLink am Activity-Datensatz setzen

View File

@@ -0,0 +1,103 @@
import SeasonService from '../services/seasonService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getSeasons = async (req, res) => {
try {
const { authcode: token } = req.headers;
const user = await getUserByToken(token);
const seasons = await SeasonService.getAllSeasons();
res.status(200).json(seasons);
} catch (error) {
console.error('[getSeasons] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getCurrentSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const user = await getUserByToken(token);
const season = await SeasonService.getOrCreateCurrentSeason();
res.status(200).json(season);
} catch (error) {
console.error('[getCurrentSeason] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { season } = req.body;
const user = await getUserByToken(token);
if (!season) {
return res.status(400).json({ error: "missingseason" });
}
// Validiere Saison-Format (z.B. "2023/2024")
const seasonRegex = /^\d{4}\/\d{4}$/;
if (!seasonRegex.test(season)) {
return res.status(400).json({ error: "invalidseasonformat" });
}
const newSeason = await SeasonService.createSeason(season);
res.status(201).json(newSeason);
} catch (error) {
console.error('[createSeason] - Error:', error);
if (error.message === 'Season already exists') {
res.status(409).json({ error: "alreadyexists" });
} else {
res.status(500).json({ error: "internalerror" });
}
}
};
export const getSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { seasonid: seasonId } = req.params;
const user = await getUserByToken(token);
const season = await SeasonService.getSeasonById(seasonId);
if (!season) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(season);
} catch (error) {
console.error('[getSeason] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { seasonid: seasonId } = req.params;
const user = await getUserByToken(token);
const success = await SeasonService.deleteSeason(seasonId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "deleted" });
} catch (error) {
console.error('[deleteSeason] - Error:', error);
if (error.message === 'Season is used by teams' || error.message === 'Season is used by leagues') {
res.status(409).json({ error: "seasoninuse" });
} else {
res.status(500).json({ error: "internalerror" });
}
}
};

View File

@@ -0,0 +1,130 @@
import TeamService from '../services/teamService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getTeams = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
// Check if user has access to this club
const teams = await TeamService.getAllTeamsByClub(clubId, seasonId);
res.status(200).json(teams);
} catch (error) {
console.error('[getTeams] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const user = await getUserByToken(token);
const team = await TeamService.getTeamById(teamId);
if (!team) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(team);
} catch (error) {
console.error('[getTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
if (!name) {
return res.status(400).json({ error: "missingname" });
}
const teamData = {
name,
clubId: parseInt(clubId),
leagueId: leagueId ? parseInt(leagueId) : null,
seasonId: seasonId ? parseInt(seasonId) : null
};
const newTeam = await TeamService.createTeam(teamData);
res.status(201).json(newTeam);
} catch (error) {
console.error('[createTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const updateTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
const updateData = {};
if (name !== undefined) updateData.name = name;
if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null;
if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null;
const success = await TeamService.updateTeam(teamId, updateData);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
const updatedTeam = await TeamService.getTeamById(teamId);
res.status(200).json(updatedTeam);
} catch (error) {
console.error('[updateTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const user = await getUserByToken(token);
const success = await TeamService.deleteTeam(teamId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "deleted" });
} catch (error) {
console.error('[deleteTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getLeagues = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
const leagues = await TeamService.getLeaguesByClub(clubId, seasonId);
res.status(200).json(leagues);
} catch (error) {
console.error('[getLeagues] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -0,0 +1,215 @@
import multer from 'multer';
import path from 'path';
import TeamDocumentService from '../services/teamDocumentService.js';
import PDFParserService from '../services/pdfParserService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
// Multer-Konfiguration für Datei-Uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/temp/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB Limit
},
fileFilter: (req, file, cb) => {
// Erlaube nur PDF, DOC, DOCX, TXT, CSV Dateien
const allowedTypes = /pdf|doc|docx|txt|csv/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Nur PDF, DOC, DOCX, TXT und CSV Dateien sind erlaubt!'));
}
}
});
export const uploadMiddleware = upload.single('document');
export const uploadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const { documentType } = req.body;
const user = await getUserByToken(token);
if (!req.file) {
return res.status(400).json({ error: "nofile" });
}
if (!documentType || !['code_list', 'pin_list'].includes(documentType)) {
return res.status(400).json({ error: "invaliddocumenttype" });
}
const document = await TeamDocumentService.uploadDocument(req.file, clubTeamId, documentType);
res.status(201).json(document);
} catch (error) {
console.error('[uploadDocument] - Error:', error);
// Lösche temporäre Datei bei Fehler
if (req.file && req.file.path) {
try {
const fs = await import('fs');
fs.unlinkSync(req.file.path);
} catch (cleanupError) {
console.error('Fehler beim Löschen der temporären Datei:', cleanupError);
}
}
if (error.message === 'Club-Team nicht gefunden') {
return res.status(404).json({ error: "clubteamnotfound" });
}
res.status(500).json({ error: "internalerror" });
}
};
export const getDocuments = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const documents = await TeamDocumentService.getDocumentsByClubTeam(clubTeamId);
res.status(200).json(documents);
} catch (error) {
console.error('[getDocuments] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(document);
} catch (error) {
console.error('[getDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const downloadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
const filePath = await TeamDocumentService.getDocumentPath(documentId);
if (!filePath) {
return res.status(404).json({ error: "filenotfound" });
}
// Prüfe ob Datei existiert
const fs = await import('fs');
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: "filenotfound" });
}
// Setze Headers für Inline-Anzeige (PDF-Viewer)
res.setHeader('Content-Disposition', `inline; filename="${document.originalFileName}"`);
res.setHeader('Content-Type', document.mimeType);
// Sende die Datei
res.sendFile(filePath);
} catch (error) {
console.error('[downloadDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const success = await TeamDocumentService.deleteDocument(documentId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "Document deleted successfully" });
} catch (error) {
console.error('[deleteDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const parsePDF = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const { leagueid: leagueId } = req.query;
const user = await getUserByToken(token);
if (!leagueId) {
return res.status(400).json({ error: "missingleagueid" });
}
// Hole Dokument-Informationen
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "documentnotfound" });
}
// Prüfe ob es eine PDF- oder TXT-Datei ist
if (!document.mimeType.includes('pdf') && !document.mimeType.includes('text/plain')) {
return res.status(400).json({ error: "notapdfortxt" });
}
// Parse PDF
const parseResult = await PDFParserService.parsePDF(document.filePath, document.clubTeam.clubId);
// Speichere Matches in Datenbank
const saveResult = await PDFParserService.saveMatchesToDatabase(parseResult.matches, parseInt(leagueId));
res.status(200).json({
parseResult: {
matchesFound: parseResult.matches.length,
debugInfo: parseResult.debugInfo,
allLines: parseResult.allLines,
rawText: parseResult.rawText
},
saveResult: {
created: saveResult.created,
updated: saveResult.updated,
errors: saveResult.errors
}
});
} catch (error) {
console.error('[parsePDF] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -19,6 +19,25 @@ class TrainingStatsController {
}
});
// Anzahl der Trainings im jeweiligen Zeitraum berechnen
const trainingsCount12Months = await DiaryDate.count({
where: {
clubId: parseInt(clubId),
date: {
[Op.gte]: twelveMonthsAgo
}
}
});
const trainingsCount3Months = await DiaryDate.count({
where: {
clubId: parseInt(clubId),
date: {
[Op.gte]: threeMonthsAgo
}
}
});
const stats = [];
for (const member of members) {
@@ -116,7 +135,36 @@ class TrainingStatsController {
// Nach Gesamtteilnahme absteigend sortieren
stats.sort((a, b) => b.participationTotal - a.participationTotal);
res.json(stats);
// Trainingstage mit Teilnehmerzahlen abrufen (letzte 12 Monate, absteigend sortiert)
const trainingDays = await DiaryDate.findAll({
where: {
clubId: parseInt(clubId),
date: {
[Op.gte]: twelveMonthsAgo
}
},
include: [{
model: Participant,
as: 'participantList',
attributes: ['id']
}],
order: [['date', 'DESC']]
});
// Formatiere Trainingstage mit Teilnehmerzahl
const formattedTrainingDays = trainingDays.map(day => ({
id: day.id,
date: day.date,
participantCount: day.participantList ? day.participantList.length : 0
}));
// Zusätzliche Metadaten mit Trainingsanzahl zurückgeben
res.json({
members: stats,
trainingsCount12Months,
trainingsCount3Months,
trainingDays: formattedTrainingDays
});
} catch (error) {
console.error('Fehler beim Laden der Trainings-Statistik:', error);

View File

@@ -0,0 +1,11 @@
-- Migration: Add drawing_data column to predefined_activity_images table
-- Date: 2025-09-22
-- Description: Adds drawing_data column to store Court Drawing Tool metadata
ALTER TABLE `predefined_activity_images`
ADD COLUMN `drawing_data` TEXT NULL
COMMENT 'JSON string containing drawing metadata for Court Drawing Tool'
AFTER `mime_type`;
-- Verify the column was added
DESCRIBE `predefined_activity_images`;

View File

@@ -0,0 +1,44 @@
-- Migration: Add season_id to teams table
-- First, add the column as nullable
ALTER TABLE `team` ADD COLUMN `season_id` INT NULL;
-- Get or create current season
SET @current_season_id = (
SELECT id FROM `season`
WHERE season = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
);
-- If no season exists, create it
INSERT IGNORE INTO `season` (season) VALUES (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
);
-- Get the season ID again (in case we just created it)
SET @current_season_id = (
SELECT id FROM `season`
WHERE season = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
);
-- Update all existing teams to use the current season
UPDATE `team` SET `season_id` = @current_season_id WHERE `season_id` IS NULL;
-- Now make the column NOT NULL and add the foreign key constraint
ALTER TABLE `team` MODIFY COLUMN `season_id` INT NOT NULL;
ALTER TABLE `team` ADD CONSTRAINT `team_season_id_foreign_idx`
FOREIGN KEY (`season_id`) REFERENCES `season` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,54 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Season from './Season.js';
const ClubTeam = sequelize.define('ClubTeam', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
clubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
leagueId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: League,
key: 'id',
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
seasonId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: Season,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'club_team',
timestamps: true,
});
export default ClubTeam;

View File

@@ -3,7 +3,6 @@ import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Team from './Team.js';
import Season from './Season.js';
import Location from './Location.js';
const Match = sequelize.define('Match', {
@@ -21,14 +20,6 @@ const Match = sequelize.define('Match', {
type: DataTypes.TIME,
allowNull: true,
},
seasonId: {
type: DataTypes.INTEGER,
references: {
model: Season,
key: 'id',
},
allowNull: false,
},
locationId: {
type: DataTypes.INTEGER,
references: {
@@ -69,6 +60,21 @@ const Match = sequelize.define('Match', {
},
allowNull: false,
},
code: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Spiel-Code aus PDF-Parsing'
},
homePin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Heimteam aus PDF-Parsing'
},
guestPin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Gastteam aus PDF-Parsing'
},
}, {
underscored: true,
tableName: 'match',

View File

@@ -127,6 +127,16 @@ const Member = sequelize.define('Member', {
type: DataTypes.ENUM('male','female','diverse','unknown'),
allowNull: true,
defaultValue: 'unknown'
},
ttr: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null
},
qttr: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null
}
}, {
underscored: true,

View File

@@ -0,0 +1,122 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const MyTischtennis = sequelize.define('MyTischtennis', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE'
},
email: {
type: DataTypes.STRING,
allowNull: false,
},
encryptedPassword: {
type: DataTypes.TEXT,
allowNull: true,
field: 'encrypted_password'
},
savePassword: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
field: 'save_password'
},
accessToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'access_token'
},
refreshToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'refresh_token'
},
expiresAt: {
type: DataTypes.BIGINT,
allowNull: true,
field: 'expires_at'
},
cookie: {
type: DataTypes.TEXT,
allowNull: true
},
userData: {
type: DataTypes.JSON,
allowNull: true,
field: 'user_data'
},
clubId: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_id'
},
clubName: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_name'
},
fedNickname: {
type: DataTypes.STRING,
allowNull: true,
field: 'fed_nickname'
},
lastLoginAttempt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_attempt'
},
lastLoginSuccess: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_success'
}
}, {
underscored: true,
tableName: 'my_tischtennis',
timestamps: true,
hooks: {
beforeSave: async (instance) => {
// Wenn savePassword false ist, password auf null setzen
if (!instance.savePassword) {
instance.encryptedPassword = null;
}
}
}
});
// Virtuelle Felder für password handling
MyTischtennis.prototype.setPassword = function(password) {
if (password && this.savePassword) {
this.encryptedPassword = encryptData(password);
} else {
this.encryptedPassword = null;
}
};
MyTischtennis.prototype.getPassword = function() {
if (this.encryptedPassword) {
try {
return decryptData(this.encryptedPassword);
} catch (error) {
console.error('Error decrypting myTischtennis password:', error);
return null;
}
}
return null;
};
export default MyTischtennis;

View File

@@ -19,6 +19,11 @@ const PredefinedActivity = sequelize.define('PredefinedActivity', {
type: DataTypes.TEXT,
allowNull: true,
},
drawingData: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'JSON string with metadata for Court Drawing Tool'
},
durationText: {
type: DataTypes.STRING,
allowNull: true,

View File

@@ -19,6 +19,11 @@ const PredefinedActivityImage = sequelize.define('PredefinedActivityImage', {
type: DataTypes.STRING,
allowNull: true,
},
drawingData: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'JSON string containing drawing metadata for Court Drawing Tool'
},
}, {
tableName: 'predefined_activity_images',
timestamps: true,

View File

@@ -1,6 +1,8 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Season from './Season.js';
const Team = sequelize.define('Team', {
id: {
@@ -23,6 +25,26 @@ const Team = sequelize.define('Team', {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
leagueId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: League,
key: 'id',
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
seasonId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: Season,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'team',

View File

@@ -0,0 +1,52 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import ClubTeam from './ClubTeam.js';
const TeamDocument = sequelize.define('TeamDocument', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
fileName: {
type: DataTypes.STRING,
allowNull: false,
},
originalFileName: {
type: DataTypes.STRING,
allowNull: false,
},
filePath: {
type: DataTypes.STRING,
allowNull: false,
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: false,
},
mimeType: {
type: DataTypes.STRING,
allowNull: false,
},
documentType: {
type: DataTypes.ENUM('code_list', 'pin_list'),
allowNull: false,
},
clubTeamId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: ClubTeam,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'team_document',
timestamps: true,
});
export default TeamDocument;

View File

@@ -19,6 +19,8 @@ import DiaryDateActivity from './DiaryDateActivity.js';
import Match from './Match.js';
import League from './League.js';
import Team from './Team.js';
import ClubTeam from './ClubTeam.js';
import TeamDocument from './TeamDocument.js';
import Season from './Season.js';
import Location from './Location.js';
import Group from './Group.js';
@@ -33,6 +35,7 @@ import UserToken from './UserToken.js';
import OfficialTournament from './OfficialTournament.js';
import OfficialCompetition from './OfficialCompetition.js';
import OfficialCompetitionMember from './OfficialCompetitionMember.js';
import MyTischtennis from './MyTischtennis.js';
// Official tournaments relations
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
@@ -117,8 +120,25 @@ Team.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Club.hasMany(League, { foreignKey: 'clubId', as: 'leagues' });
League.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Match.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
Season.hasMany(Match, { foreignKey: 'seasonId', as: 'matches' });
League.hasMany(Team, { foreignKey: 'leagueId', as: 'teams' });
Team.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(Team, { foreignKey: 'seasonId', as: 'teams' });
Team.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// ClubTeam relationships
Club.hasMany(ClubTeam, { foreignKey: 'clubId', as: 'clubTeams' });
ClubTeam.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
League.hasMany(ClubTeam, { foreignKey: 'leagueId', as: 'clubTeams' });
ClubTeam.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(ClubTeam, { foreignKey: 'seasonId', as: 'clubTeams' });
ClubTeam.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// TeamDocument relationships
ClubTeam.hasMany(TeamDocument, { foreignKey: 'clubTeamId', as: 'documents' });
TeamDocument.belongsTo(ClubTeam, { foreignKey: 'clubTeamId', as: 'clubTeam' });
Match.belongsTo(Location, { foreignKey: 'locationId', as: 'location' });
Location.hasMany(Match, { foreignKey: 'locationId', as: 'matches' });
@@ -204,6 +224,9 @@ Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
Accident.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
DiaryDate.hasMany(Accident, { foreignKey: 'diaryDateId', as: 'accidents' });
User.hasOne(MyTischtennis, { foreignKey: 'userId', as: 'myTischtennis' });
MyTischtennis.belongsTo(User, { foreignKey: 'userId', as: 'user' });
export {
User,
Log,
@@ -227,6 +250,8 @@ export {
Match,
League,
Team,
ClubTeam,
TeamDocument,
Group,
GroupActivity,
Tournament,
@@ -239,4 +264,5 @@ export {
OfficialTournament,
OfficialCompetition,
OfficialCompetitionMember,
MyTischtennis,
};

View File

@@ -4,6 +4,15 @@
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
@@ -810,6 +819,12 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz",
@@ -818,6 +833,17 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -955,6 +981,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1084,6 +1123,18 @@
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1227,6 +1278,22 @@
"node": ">= 10"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1259,6 +1326,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -1315,6 +1391,20 @@
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1342,13 +1432,10 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
@@ -1362,6 +1449,33 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1752,6 +1866,42 @@
"dev": true,
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1849,16 +1999,21 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1867,6 +2022,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1913,12 +2081,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1945,10 +2113,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -1957,11 +2125,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
@@ -2405,6 +2576,15 @@
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -2972,6 +3152,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.0.1](https://github.com/ljharb/es-define-property/compare/v1.0.0...v1.0.1) - 2024-12-06
### Commits
- [types] use shared tsconfig [`954a663`](https://github.com/ljharb/es-define-property/commit/954a66360326e508a0e5daa4b07493d58f5e110e)
- [actions] split out node 10-20, and 20+ [`3a8e84b`](https://github.com/ljharb/es-define-property/commit/3a8e84b23883f26ff37b3e82ff283834228e18c6)
- [Dev Deps] update `@ljharb/eslint-config`, `@ljharb/tsconfig`, `@types/get-intrinsic`, `@types/tape`, `auto-changelog`, `gopd`, `tape` [`86ae27b`](https://github.com/ljharb/es-define-property/commit/86ae27bb8cc857b23885136fad9cbe965ae36612)
- [Refactor] avoid using `get-intrinsic` [`02480c0`](https://github.com/ljharb/es-define-property/commit/02480c0353ef6118965282977c3864aff53d98b1)
- [Tests] replace `aud` with `npm audit` [`f6093ff`](https://github.com/ljharb/es-define-property/commit/f6093ff74ab51c98015c2592cd393bd42478e773)
- [Tests] configure testling [`7139e66`](https://github.com/ljharb/es-define-property/commit/7139e66959247a56086d9977359caef27c6849e7)
- [Dev Deps] update `tape` [`b901b51`](https://github.com/ljharb/es-define-property/commit/b901b511a75e001a40ce1a59fef7d9ffcfc87482)
- [Tests] fix types in tests [`469d269`](https://github.com/ljharb/es-define-property/commit/469d269fd141b1e773ec053a9fa35843493583e0)
- [Dev Deps] add missing peer dep [`733acfb`](https://github.com/ljharb/es-define-property/commit/733acfb0c4c96edf337e470b89a25a5b3724c352)
## v1.0.0 - 2024-02-12
### Commits

View File

@@ -1,9 +1,7 @@
'use strict';
var GetIntrinsic = require('get-intrinsic');
/** @type {import('.')} */
var $defineProperty = GetIntrinsic('%Object.defineProperty%', true) || false;
var $defineProperty = Object.defineProperty || false;
if ($defineProperty) {
try {
$defineProperty({}, 'a', { value: 1 });

View File

@@ -1,6 +1,6 @@
{
"name": "es-define-property",
"version": "1.0.0",
"version": "1.0.1",
"description": "`Object.defineProperty`, but not IE 8's broken one.",
"main": "index.js",
"types": "./index.d.ts",
@@ -19,7 +19,7 @@
"pretest": "npm run lint",
"tests-only": "nyc tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"posttest": "npx npm@'>= 10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -42,29 +42,29 @@
"url": "https://github.com/ljharb/es-define-property/issues"
},
"homepage": "https://github.com/ljharb/es-define-property#readme",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.1.0",
"@types/get-intrinsic": "^1.2.2",
"@ljharb/eslint-config": "^21.1.1",
"@ljharb/tsconfig": "^0.2.2",
"@types/gopd": "^1.0.3",
"@types/tape": "^5.6.4",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"@types/tape": "^5.6.5",
"auto-changelog": "^2.5.0",
"encoding": "^0.1.13",
"eslint": "^8.8.0",
"evalmd": "^0.0.19",
"gopd": "^1.0.1",
"gopd": "^1.2.0",
"in-publish": "^2.0.1",
"npmignore": "^0.3.1",
"nyc": "^10.3.2",
"safe-publish-latest": "^2.0.0",
"tape": "^5.7.4",
"tape": "^5.9.0",
"typescript": "next"
},
"engines": {
"node": ">= 0.4"
},
"testling": {
"files": "test/index.js"
},
"auto-changelog": {
"output": "CHANGELOG.md",
"template": "keepachangelog",

View File

@@ -10,6 +10,7 @@ test('defineProperty: supported', { skip: !$defineProperty }, function (t) {
t.equal(typeof $defineProperty, 'function', 'defineProperty is supported');
if ($defineProperty && gOPD) { // this `if` check is just to shut TS up
/** @type {{ a: number, b?: number, c?: number }} */
var o = { a: 1 };
$defineProperty(o, 'b', { enumerable: true, value: 2 });

View File

@@ -1,47 +1,7 @@
{
"extends": "@ljharb/tsconfig",
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
/* Language and Environment */
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
"useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": ["types"], /* Specify multiple folders that act like `./node_modules/@types`. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
"maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
"noEmit": true, /* Disable emitting files from a compilation. */
/* Interop Constraints */
"allowSyntheticDefaultImports": true, /* Allow `import x from y` when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
/* Completeness */
// "skipLibCheck": true /* Skip type checking all .d.ts files. */
"target": "es2022",
},
"exclude": [
"coverage",

View File

@@ -11,6 +11,10 @@
"es2022": true,
},
"globals": {
"Float16Array": false,
},
"rules": {
"array-bracket-newline": 0,
"complexity": 0,

View File

@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.3.0](https://github.com/ljharb/get-intrinsic/compare/v1.2.7...v1.3.0) - 2025-02-22
### Commits
- [Dev Deps] update `es-abstract`, `es-value-fixtures`, `for-each`, `object-inspect` [`9b61553`](https://github.com/ljharb/get-intrinsic/commit/9b61553c587f1c1edbd435597e88c7d387da97dd)
- [Deps] update `call-bind-apply-helpers`, `es-object-atoms`, `get-proto` [`a341fee`](https://github.com/ljharb/get-intrinsic/commit/a341fee0f39a403b0f0069e82c97642d5eb11043)
- [New] add `Float16Array` [`de22116`](https://github.com/ljharb/get-intrinsic/commit/de22116b492fb989a0341bceb6e573abfaed73dc)
## [v1.2.7](https://github.com/ljharb/get-intrinsic/compare/v1.2.6...v1.2.7) - 2025-01-02
### Commits
- [Refactor] use `get-proto` directly [`00ab955`](https://github.com/ljharb/get-intrinsic/commit/00ab95546a0980c8ad42a84253daaa8d2adcedf9)
- [Deps] update `math-intrinsics` [`c716cdd`](https://github.com/ljharb/get-intrinsic/commit/c716cdd6bbe36b438057025561b8bb5a879ac8a0)
- [Dev Deps] update `call-bound`, `es-abstract` [`dc648a6`](https://github.com/ljharb/get-intrinsic/commit/dc648a67eb359037dff8d8619bfa71d86debccb1)
## [v1.2.6](https://github.com/ljharb/get-intrinsic/compare/v1.2.5...v1.2.6) - 2024-12-11
### Commits
- [Refactor] use `math-intrinsics` [`841be86`](https://github.com/ljharb/get-intrinsic/commit/841be8641a9254c4c75483b30c8871b5d5065926)
- [Refactor] use `es-object-atoms` [`42057df`](https://github.com/ljharb/get-intrinsic/commit/42057dfa16f66f64787e66482af381cc6f31d2c1)
- [Deps] update `call-bind-apply-helpers` [`45afa24`](https://github.com/ljharb/get-intrinsic/commit/45afa24a9ee4d6d3c172db1f555b16cb27843ef4)
- [Dev Deps] update `call-bound` [`9cba9c6`](https://github.com/ljharb/get-intrinsic/commit/9cba9c6e70212bc163b7a5529cb25df46071646f)
## [v1.2.5](https://github.com/ljharb/get-intrinsic/compare/v1.2.4...v1.2.5) - 2024-12-06
### Commits
- [actions] split out node 10-20, and 20+ [`6e2b9dd`](https://github.com/ljharb/get-intrinsic/commit/6e2b9dd23902665681ebe453256ccfe21d7966f0)
- [Refactor] use `dunder-proto` and `call-bind-apply-helpers` instead of `has-proto` [`c095d17`](https://github.com/ljharb/get-intrinsic/commit/c095d179ad0f4fbfff20c8a3e0cb4fe668018998)
- [Refactor] use `gopd` [`9841d5b`](https://github.com/ljharb/get-intrinsic/commit/9841d5b35f7ab4fd2d193f0c741a50a077920e90)
- [Dev Deps] update `@ljharb/eslint-config`, `auto-changelog`, `es-abstract`, `es-value-fixtures`, `gopd`, `mock-property`, `object-inspect`, `tape` [`2d07e01`](https://github.com/ljharb/get-intrinsic/commit/2d07e01310cee2cbaedfead6903df128b1f5d425)
- [Deps] update `gopd`, `has-proto`, `has-symbols`, `hasown` [`974d8bf`](https://github.com/ljharb/get-intrinsic/commit/974d8bf5baad7939eef35c25cc1dd88c10a30fa6)
- [Dev Deps] update `call-bind`, `es-abstract`, `tape` [`df9dde1`](https://github.com/ljharb/get-intrinsic/commit/df9dde178186631ab8a3165ede056549918ce4bc)
- [Refactor] cache `es-define-property` as well [`43ef543`](https://github.com/ljharb/get-intrinsic/commit/43ef543cb02194401420e3a914a4ca9168691926)
- [Deps] update `has-proto`, `has-symbols`, `hasown` [`ad4949d`](https://github.com/ljharb/get-intrinsic/commit/ad4949d5467316505aad89bf75f9417ed782f7af)
- [Tests] use `call-bound` directly [`ad5c406`](https://github.com/ljharb/get-intrinsic/commit/ad5c4069774bfe90e520a35eead5fe5ca9d69e80)
- [Deps] update `has-proto`, `hasown` [`45414ca`](https://github.com/ljharb/get-intrinsic/commit/45414caa312333a2798953682c68f85c550627dd)
- [Tests] replace `aud` with `npm audit` [`18d3509`](https://github.com/ljharb/get-intrinsic/commit/18d3509f79460e7924da70409ee81e5053087523)
- [Deps] update `es-define-property` [`aadaa3b`](https://github.com/ljharb/get-intrinsic/commit/aadaa3b2188d77ad9bff394ce5d4249c49eb21f5)
- [Dev Deps] add missing peer dep [`c296a16`](https://github.com/ljharb/get-intrinsic/commit/c296a16246d0c9a5981944f4cc5cf61fbda0cf6a)
## [v1.2.4](https://github.com/ljharb/get-intrinsic/compare/v1.2.3...v1.2.4) - 2024-02-05
### Commits

View File

@@ -2,6 +2,8 @@
var undefined;
var $Object = require('es-object-atoms');
var $Error = require('es-errors');
var $EvalError = require('es-errors/eval');
var $RangeError = require('es-errors/range');
@@ -10,6 +12,14 @@ var $SyntaxError = require('es-errors/syntax');
var $TypeError = require('es-errors/type');
var $URIError = require('es-errors/uri');
var abs = require('math-intrinsics/abs');
var floor = require('math-intrinsics/floor');
var max = require('math-intrinsics/max');
var min = require('math-intrinsics/min');
var pow = require('math-intrinsics/pow');
var round = require('math-intrinsics/round');
var sign = require('math-intrinsics/sign');
var $Function = Function;
// eslint-disable-next-line consistent-return
@@ -19,14 +29,8 @@ var getEvalledConstructor = function (expressionSyntax) {
} catch (e) {}
};
var $gOPD = Object.getOwnPropertyDescriptor;
if ($gOPD) {
try {
$gOPD({}, '');
} catch (e) {
$gOPD = null; // this is IE 8, which has a broken gOPD
}
}
var $gOPD = require('gopd');
var $defineProperty = require('es-define-property');
var throwTypeError = function () {
throw new $TypeError();
@@ -49,13 +53,13 @@ var ThrowTypeError = $gOPD
: throwTypeError;
var hasSymbols = require('has-symbols')();
var hasProto = require('has-proto')();
var getProto = Object.getPrototypeOf || (
hasProto
? function (x) { return x.__proto__; } // eslint-disable-line no-proto
: null
);
var getProto = require('get-proto');
var $ObjectGPO = require('get-proto/Object.getPrototypeOf');
var $ReflectGPO = require('get-proto/Reflect.getPrototypeOf');
var $apply = require('call-bind-apply-helpers/functionApply');
var $call = require('call-bind-apply-helpers/functionCall');
var needsEval = {};
@@ -86,6 +90,7 @@ var INTRINSICS = {
'%Error%': $Error,
'%eval%': eval, // eslint-disable-line no-eval
'%EvalError%': $EvalError,
'%Float16Array%': typeof Float16Array === 'undefined' ? undefined : Float16Array,
'%Float32Array%': typeof Float32Array === 'undefined' ? undefined : Float32Array,
'%Float64Array%': typeof Float64Array === 'undefined' ? undefined : Float64Array,
'%FinalizationRegistry%': typeof FinalizationRegistry === 'undefined' ? undefined : FinalizationRegistry,
@@ -102,7 +107,8 @@ var INTRINSICS = {
'%MapIteratorPrototype%': typeof Map === 'undefined' || !hasSymbols || !getProto ? undefined : getProto(new Map()[Symbol.iterator]()),
'%Math%': Math,
'%Number%': Number,
'%Object%': Object,
'%Object%': $Object,
'%Object.getOwnPropertyDescriptor%': $gOPD,
'%parseFloat%': parseFloat,
'%parseInt%': parseInt,
'%Promise%': typeof Promise === 'undefined' ? undefined : Promise,
@@ -128,7 +134,20 @@ var INTRINSICS = {
'%URIError%': $URIError,
'%WeakMap%': typeof WeakMap === 'undefined' ? undefined : WeakMap,
'%WeakRef%': typeof WeakRef === 'undefined' ? undefined : WeakRef,
'%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet
'%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet,
'%Function.prototype.call%': $call,
'%Function.prototype.apply%': $apply,
'%Object.defineProperty%': $defineProperty,
'%Object.getPrototypeOf%': $ObjectGPO,
'%Math.abs%': abs,
'%Math.floor%': floor,
'%Math.max%': max,
'%Math.min%': min,
'%Math.pow%': pow,
'%Math.round%': round,
'%Math.sign%': sign,
'%Reflect.getPrototypeOf%': $ReflectGPO
};
if (getProto) {
@@ -223,11 +242,11 @@ var LEGACY_ALIASES = {
var bind = require('function-bind');
var hasOwn = require('hasown');
var $concat = bind.call(Function.call, Array.prototype.concat);
var $spliceApply = bind.call(Function.apply, Array.prototype.splice);
var $replace = bind.call(Function.call, String.prototype.replace);
var $strSlice = bind.call(Function.call, String.prototype.slice);
var $exec = bind.call(Function.call, RegExp.prototype.exec);
var $concat = bind.call($call, Array.prototype.concat);
var $spliceApply = bind.call($apply, Array.prototype.splice);
var $replace = bind.call($call, String.prototype.replace);
var $strSlice = bind.call($call, String.prototype.slice);
var $exec = bind.call($call, RegExp.prototype.exec);
/* adapted from https://github.com/lodash/lodash/blob/4.17.15/dist/lodash.js#L6735-L6744 */
var rePropName = /[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g;

View File

@@ -1,6 +1,6 @@
{
"name": "get-intrinsic",
"version": "1.2.4",
"version": "1.3.0",
"description": "Get and robustly cache all JS language-level intrinsics at first require time",
"main": "index.js",
"exports": {
@@ -17,7 +17,7 @@
"pretest": "npm run lint",
"tests-only": "nyc tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"posttest": "npx npm@'>= 10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -43,26 +43,37 @@
"url": "https://github.com/ljharb/get-intrinsic/issues"
},
"homepage": "https://github.com/ljharb/get-intrinsic#readme",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.1.0",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"call-bind": "^1.0.5",
"es-abstract": "^1.22.3",
"es-value-fixtures": "^1.4.2",
"@ljharb/eslint-config": "^21.1.1",
"auto-changelog": "^2.5.0",
"call-bound": "^1.0.3",
"encoding": "^0.1.13",
"es-abstract": "^1.23.9",
"es-value-fixtures": "^1.7.1",
"eslint": "=8.8.0",
"evalmd": "^0.0.19",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
"for-each": "^0.3.5",
"make-async-function": "^1.0.0",
"make-async-generator-function": "^1.0.0",
"make-generator-function": "^2.0.0",
"mock-property": "^1.0.3",
"mock-property": "^1.1.0",
"npmignore": "^0.3.1",
"nyc": "^10.3.2",
"object-inspect": "^1.13.1",
"object-inspect": "^1.13.4",
"safe-publish-latest": "^2.0.0",
"tape": "^5.7.4"
"tape": "^5.9.0"
},
"auto-changelog": {
"output": "CHANGELOG.md",
@@ -72,13 +83,6 @@
"backfillLimit": false,
"hideCredit": true
},
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"testling": {
"files": "test/GetIntrinsic.js"
},

View File

@@ -10,10 +10,10 @@ var asyncFns = require('make-async-function').list();
var asyncGenFns = require('make-async-generator-function')();
var mockProperty = require('mock-property');
var callBound = require('call-bind/callBound');
var callBound = require('call-bound');
var v = require('es-value-fixtures');
var $gOPD = require('gopd');
var DefinePropertyOrThrow = require('es-abstract/2021/DefinePropertyOrThrow');
var DefinePropertyOrThrow = require('es-abstract/2023/DefinePropertyOrThrow');
var $isProto = callBound('%Object.prototype.isPrototypeOf%');

View File

@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.2.0](https://github.com/ljharb/gopd/compare/v1.1.0...v1.2.0) - 2024-12-03
### Commits
- [New] add `gOPD` entry point; remove `get-intrinsic` [`5b61232`](https://github.com/ljharb/gopd/commit/5b61232dedea4591a314bcf16101b1961cee024e)
## [v1.1.0](https://github.com/ljharb/gopd/compare/v1.0.1...v1.1.0) - 2024-11-29
### Commits
- [New] add types [`f585e39`](https://github.com/ljharb/gopd/commit/f585e397886d270e4ba84e53d226e4f9ca2eb0e6)
- [Dev Deps] update `@ljharb/eslint-config`, `auto-changelog`, `tape` [`0b8e4fd`](https://github.com/ljharb/gopd/commit/0b8e4fded64397a7726a9daa144a6cc9a5e2edfa)
- [Dev Deps] update `aud`, `npmignore`, `tape` [`48378b2`](https://github.com/ljharb/gopd/commit/48378b2443f09a4f7efbd0fb6c3ee845a6cabcf3)
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `tape` [`78099ee`](https://github.com/ljharb/gopd/commit/78099eeed41bfdc134c912280483689cc8861c31)
- [Tests] replace `aud` with `npm audit` [`4e0d0ac`](https://github.com/ljharb/gopd/commit/4e0d0ac47619d24a75318a8e1f543ee04b2a2632)
- [meta] add missing `engines.node` [`1443316`](https://github.com/ljharb/gopd/commit/14433165d07835c680155b3dfd62d9217d735eca)
- [Deps] update `get-intrinsic` [`eee5f51`](https://github.com/ljharb/gopd/commit/eee5f51769f3dbaf578b70e2a3199116b01aa670)
- [Deps] update `get-intrinsic` [`550c378`](https://github.com/ljharb/gopd/commit/550c3780e3a9c77b62565712a001b4ed64ea61f5)
- [Dev Deps] add missing peer dep [`8c2ecf8`](https://github.com/ljharb/gopd/commit/8c2ecf848122e4e30abfc5b5086fb48b390dce75)
## [v1.0.1](https://github.com/ljharb/gopd/compare/v1.0.0...v1.0.1) - 2022-11-01
### Commits

5
backend/node_modules/gopd/index.js generated vendored
View File

@@ -1,8 +1,7 @@
'use strict';
var GetIntrinsic = require('get-intrinsic');
var $gOPD = GetIntrinsic('%Object.getOwnPropertyDescriptor%', true);
/** @type {import('.')} */
var $gOPD = require('./gOPD');
if ($gOPD) {
try {

View File

@@ -1,10 +1,11 @@
{
"name": "gopd",
"version": "1.0.1",
"version": "1.2.0",
"description": "`Object.getOwnPropertyDescriptor`, but accounts for IE's broken implementation.",
"main": "index.js",
"exports": {
".": "./index.js",
"./gOPD": "./gOPD.js",
"./package.json": "./package.json"
},
"sideEffects": false,
@@ -12,12 +13,13 @@
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublishOnly": "safe-publish-latest",
"prepublish": "not-in-publish || npm run prepublishOnly",
"prelint": "tsc -p . && attw -P",
"lint": "eslint --ext=js,mjs .",
"postlint": "evalmd README.md",
"pretest": "npm run lint",
"tests-only": "tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"posttest": "npx npm@'>=10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -41,19 +43,20 @@
"url": "https://github.com/ljharb/gopd/issues"
},
"homepage": "https://github.com/ljharb/gopd#readme",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.0.0",
"aud": "^2.0.1",
"auto-changelog": "^2.4.0",
"@arethetypeswrong/cli": "^0.17.0",
"@ljharb/eslint-config": "^21.1.1",
"@ljharb/tsconfig": "^0.2.0",
"@types/tape": "^5.6.5",
"auto-changelog": "^2.5.0",
"encoding": "^0.1.13",
"eslint": "=8.8.0",
"evalmd": "^0.0.19",
"in-publish": "^2.0.1",
"npmignore": "^0.3.0",
"npmignore": "^0.3.1",
"safe-publish-latest": "^2.0.0",
"tape": "^5.6.1"
"tape": "^5.9.0",
"typescript": "next"
},
"auto-changelog": {
"output": "CHANGELOG.md",
@@ -67,5 +70,8 @@
"ignore": [
".github/workflows"
]
},
"engines": {
"node": ">= 0.4"
}
}

View File

@@ -10,6 +10,7 @@ test('gOPD', function (t) {
var obj = { x: 1 };
st.ok('x' in obj, 'property exists');
// @ts-expect-error TS can't figure out narrowing from `skip`
var desc = gOPD(obj, 'x');
st.deepEqual(
desc,
@@ -25,7 +26,7 @@ test('gOPD', function (t) {
st.end();
});
t.test('not supported', { skip: gOPD }, function (st) {
t.test('not supported', { skip: !!gOPD }, function (st) {
st.notOk(gOPD, 'is falsy');
st.end();

View File

@@ -1,5 +0,0 @@
{
"root": true,
"extends": "@ljharb",
}

View File

@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [ljharb]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: npm/has-proto
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,38 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.0.3](https://github.com/inspect-js/has-proto/compare/v1.0.2...v1.0.3) - 2024-02-19
### Commits
- [types] add missing declaration file [`26ecade`](https://github.com/inspect-js/has-proto/commit/26ecade05d253bb5dc376945ee3186d1fbe334f8)
## [v1.0.2](https://github.com/inspect-js/has-proto/compare/v1.0.1...v1.0.2) - 2024-02-19
### Commits
- add types [`6435262`](https://github.com/inspect-js/has-proto/commit/64352626cf511c0276d5f4bb6be770a0bf0f8524)
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `npmignore`, `tape` [`f16a5e4`](https://github.com/inspect-js/has-proto/commit/f16a5e4121651e551271419f9d60fdd3561fd82c)
- [Refactor] tiny cleanup [`d1f1a4b`](https://github.com/inspect-js/has-proto/commit/d1f1a4bdc135f115a10f148ce302676224534702)
- [meta] add `sideEffects` flag [`e7ab1a6`](https://github.com/inspect-js/has-proto/commit/e7ab1a6f153b3e80dee68d1748b71e46767a0531)
## [v1.0.1](https://github.com/inspect-js/has-proto/compare/v1.0.0...v1.0.1) - 2022-12-21
### Commits
- [meta] correct URLs and description [`ef34483`](https://github.com/inspect-js/has-proto/commit/ef34483ca0d35680f271b6b96e35526151b25dfc)
- [patch] add an additional criteria [`e81959e`](https://github.com/inspect-js/has-proto/commit/e81959ed7c7a77fbf459f00cb4ef824f1099497f)
- [Dev Deps] update `aud` [`2bec2c4`](https://github.com/inspect-js/has-proto/commit/2bec2c47b072b122ff5443fba0263f6dc649531f)
## v1.0.0 - 2022-12-12
### Commits
- Initial implementation, tests, readme [`6886fea`](https://github.com/inspect-js/has-proto/commit/6886fea578f67daf69a7920b2eb7637ea6ebb0bc)
- Initial commit [`99129c8`](https://github.com/inspect-js/has-proto/commit/99129c8f42471ac89cb681ba9cb9d52a583eb94f)
- npm init [`2844ad8`](https://github.com/inspect-js/has-proto/commit/2844ad8e75b84d66a46765b3bab9d2e8ea692e10)
- Only apps should have lockfiles [`c65bc5e`](https://github.com/inspect-js/has-proto/commit/c65bc5e40b9004463f7336d47c67245fb139a36a)

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Inspect JS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,38 +0,0 @@
# has-proto <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
[![github actions][actions-image]][actions-url]
[![coverage][codecov-image]][codecov-url]
[![License][license-image]][license-url]
[![Downloads][downloads-image]][downloads-url]
[![npm badge][npm-badge-png]][package-url]
Does this environment have the ability to set the [[Prototype]] of an object on creation with `__proto__`?
## Example
```js
var hasProto = require('has-proto');
var assert = require('assert');
assert.equal(typeof hasProto(), 'boolean');
```
## Tests
Simply clone the repo, `npm install`, and run `npm test`
[package-url]: https://npmjs.org/package/has-proto
[npm-version-svg]: https://versionbadg.es/inspect-js/has-proto.svg
[deps-svg]: https://david-dm.org/inspect-js/has-proto.svg
[deps-url]: https://david-dm.org/inspect-js/has-proto
[dev-deps-svg]: https://david-dm.org/inspect-js/has-proto/dev-status.svg
[dev-deps-url]: https://david-dm.org/inspect-js/has-proto#info=devDependencies
[npm-badge-png]: https://nodei.co/npm/has-proto.png?downloads=true&stars=true
[license-image]: https://img.shields.io/npm/l/has-proto.svg
[license-url]: LICENSE
[downloads-image]: https://img.shields.io/npm/dm/has-proto.svg
[downloads-url]: https://npm-stat.com/charts.html?package=has-proto
[codecov-image]: https://codecov.io/gh/inspect-js/has-proto/branch/main/graphs/badge.svg
[codecov-url]: https://app.codecov.io/gh/inspect-js/has-proto/
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/inspect-js/has-proto
[actions-url]: https://github.com/inspect-js/has-proto/actions

View File

@@ -1,3 +0,0 @@
declare function hasProto(): boolean;
export = hasProto;

View File

@@ -1,15 +0,0 @@
'use strict';
var test = {
__proto__: null,
foo: {}
};
var $Object = Object;
/** @type {import('.')} */
module.exports = function hasProto() {
// @ts-expect-error: TS errors on an inherited property for some reason
return { __proto__: test }.foo === test.foo
&& !(test instanceof $Object);
};

View File

@@ -1,78 +0,0 @@
{
"name": "has-proto",
"version": "1.0.3",
"description": "Does this environment have the ability to get the [[Prototype]] of an object on creation with `__proto__`?",
"main": "index.js",
"exports": {
".": "./index.js",
"./package.json": "./package.json"
},
"sideEffects": false,
"scripts": {
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublishOnly": "safe-publish-latest",
"prepublish": "not-in-publish || npm run prepublishOnly",
"lint": "eslint --ext=js,mjs .",
"postlint": "tsc -p .",
"pretest": "npm run lint",
"tests-only": "tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/inspect-js/has-proto.git"
},
"keywords": [
"prototype",
"proto",
"set",
"get",
"__proto__",
"getPrototypeOf",
"setPrototypeOf",
"has"
],
"author": "Jordan Harband <ljharb@gmail.com>",
"funding": {
"url": "https://github.com/sponsors/ljharb"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/inspect-js/has-proto/issues"
},
"homepage": "https://github.com/inspect-js/has-proto#readme",
"testling": {
"files": "test/index.js"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.1.0",
"@types/tape": "^5.6.4",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"eslint": "=8.8.0",
"in-publish": "^2.0.1",
"npmignore": "^0.3.1",
"safe-publish-latest": "^2.0.0",
"tape": "^5.7.5",
"typescript": "next"
},
"engines": {
"node": ">= 0.4"
},
"auto-changelog": {
"output": "CHANGELOG.md",
"template": "keepachangelog",
"unreleased": false,
"commitLimit": false,
"backfillLimit": false,
"hideCredit": true
},
"publishConfig": {
"ignore": [
".github/workflows"
]
}
}

View File

@@ -1,19 +0,0 @@
'use strict';
var test = require('tape');
var hasProto = require('../');
test('hasProto', function (t) {
var result = hasProto();
t.equal(typeof result, 'boolean', 'returns a boolean (' + result + ')');
var obj = { __proto__: null };
if (result) {
t.notOk('toString' in obj, 'null object lacks toString');
} else {
t.ok('toString' in obj, 'without proto, null object has toString');
t.equal(obj.__proto__, null); // eslint-disable-line no-proto
}
t.end();
});

View File

@@ -1,49 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
/* Language and Environment */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
"useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
"typeRoots": ["types"], /* Specify multiple folders that act like './node_modules/@types'. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
"maxNodeModuleJsDepth": 0, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
"noEmit": true, /* Disable emitting files from a compilation. */
/* Interop Constraints */
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
/* Completeness */
//"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"coverage"
]
}

View File

@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.1.0](https://github.com/inspect-js/has-symbols/compare/v1.0.3...v1.1.0) - 2024-12-02
### Commits
- [actions] update workflows [`548c0bf`](https://github.com/inspect-js/has-symbols/commit/548c0bf8c9b1235458df7a1c0490b0064647a282)
- [actions] further shard; update action deps [`bec56bb`](https://github.com/inspect-js/has-symbols/commit/bec56bb0fb44b43a786686b944875a3175cf3ff3)
- [meta] use `npmignore` to autogenerate an npmignore file [`ac81032`](https://github.com/inspect-js/has-symbols/commit/ac81032809157e0a079e5264e9ce9b6f1275777e)
- [New] add types [`6469cbf`](https://github.com/inspect-js/has-symbols/commit/6469cbff1866cfe367b2b3d181d9296ec14b2a3d)
- [actions] update rebase action to use reusable workflow [`9c9d4d0`](https://github.com/inspect-js/has-symbols/commit/9c9d4d0d8938e4b267acdf8e421f4e92d1716d72)
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `aud`, `tape` [`adb5887`](https://github.com/inspect-js/has-symbols/commit/adb5887ca9444849b08beb5caaa9e1d42320cdfb)
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `tape` [`13ec198`](https://github.com/inspect-js/has-symbols/commit/13ec198ec80f1993a87710af1606a1970b22c7cb)
- [Dev Deps] update `auto-changelog`, `core-js`, `tape` [`941be52`](https://github.com/inspect-js/has-symbols/commit/941be5248387cab1da72509b22acf3fdb223f057)
- [Tests] replace `aud` with `npm audit` [`74f49e9`](https://github.com/inspect-js/has-symbols/commit/74f49e9a9d17a443020784234a1c53ce765b3559)
- [Dev Deps] update `npmignore` [`9c0ac04`](https://github.com/inspect-js/has-symbols/commit/9c0ac0452a834f4c2a4b54044f2d6a89f17e9a70)
- [Dev Deps] add missing peer dep [`52337a5`](https://github.com/inspect-js/has-symbols/commit/52337a5621cced61f846f2afdab7707a8132cc12)
## [v1.0.3](https://github.com/inspect-js/has-symbols/compare/v1.0.2...v1.0.3) - 2022-03-01
### Commits

View File

@@ -3,6 +3,7 @@
var origSymbol = typeof Symbol !== 'undefined' && Symbol;
var hasSymbolSham = require('./shams');
/** @type {import('.')} */
module.exports = function hasNativeSymbols() {
if (typeof origSymbol !== 'function') { return false; }
if (typeof Symbol !== 'function') { return false; }

View File

@@ -1,21 +1,23 @@
{
"name": "has-symbols",
"version": "1.0.3",
"version": "1.1.0",
"description": "Determine if the JS environment has Symbol support. Supports spec, or shams.",
"main": "index.js",
"scripts": {
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublishOnly": "safe-publish-latest",
"prepublish": "not-in-publish || npm run prepublishOnly",
"pretest": "npm run --silent lint",
"test": "npm run tests-only",
"posttest": "aud --production",
"tests-only": "npm run test:stock && npm run test:staging && npm run test:shams",
"posttest": "npx npm@'>=10.2' audit --production",
"tests-only": "npm run test:stock && npm run test:shams",
"test:stock": "nyc node test",
"test:staging": "nyc node --harmony --es-staging test",
"test:shams": "npm run --silent test:shams:getownpropertysymbols && npm run --silent test:shams:corejs",
"test:shams:corejs": "nyc node test/shams/core-js.js",
"test:shams:getownpropertysymbols": "nyc node test/shams/get-own-property-symbols.js",
"lint": "eslint --ext=js,mjs .",
"postlint": "tsc -p . && attw -P",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -54,15 +56,22 @@
},
"homepage": "https://github.com/ljharb/has-symbols#readme",
"devDependencies": {
"@ljharb/eslint-config": "^20.2.3",
"aud": "^2.0.0",
"auto-changelog": "^2.4.0",
"@arethetypeswrong/cli": "^0.17.0",
"@ljharb/eslint-config": "^21.1.1",
"@ljharb/tsconfig": "^0.2.0",
"@types/core-js": "^2.5.8",
"@types/tape": "^5.6.5",
"auto-changelog": "^2.5.0",
"core-js": "^2.6.12",
"encoding": "^0.1.13",
"eslint": "=8.8.0",
"get-own-property-symbols": "^0.9.5",
"in-publish": "^2.0.1",
"npmignore": "^0.3.1",
"nyc": "^10.3.2",
"safe-publish-latest": "^2.0.0",
"tape": "^5.5.2"
"tape": "^5.9.0",
"typescript": "next"
},
"testling": {
"files": "test/index.js",
@@ -93,9 +102,10 @@
"backfillLimit": false,
"hideCredit": true
},
"greenkeeper": {
"publishConfig": {
"ignore": [
"core-js"
".github/workflows",
"types"
]
}
}

View File

@@ -1,10 +1,12 @@
'use strict';
/** @type {import('./shams')} */
/* eslint complexity: [2, 18], max-statements: [2, 33] */
module.exports = function hasSymbols() {
if (typeof Symbol !== 'function' || typeof Object.getOwnPropertySymbols !== 'function') { return false; }
if (typeof Symbol.iterator === 'symbol') { return true; }
/** @type {{ [k in symbol]?: unknown }} */
var obj = {};
var sym = Symbol('test');
var symObj = Object(sym);
@@ -23,7 +25,7 @@ module.exports = function hasSymbols() {
var symVal = 42;
obj[sym] = symVal;
for (sym in obj) { return false; } // eslint-disable-line no-restricted-syntax, no-unreachable-loop
for (var _ in obj) { return false; } // eslint-disable-line no-restricted-syntax, no-unreachable-loop
if (typeof Object.keys === 'function' && Object.keys(obj).length !== 0) { return false; }
if (typeof Object.getOwnPropertyNames === 'function' && Object.getOwnPropertyNames(obj).length !== 0) { return false; }
@@ -34,7 +36,8 @@ module.exports = function hasSymbols() {
if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { return false; }
if (typeof Object.getOwnPropertyDescriptor === 'function') {
var descriptor = Object.getOwnPropertyDescriptor(obj, sym);
// eslint-disable-next-line no-extra-parens
var descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(obj, sym));
if (descriptor.value !== symVal || descriptor.enumerable !== true) { return false; }
}

View File

@@ -8,6 +8,7 @@ if (typeof Symbol === 'function' && typeof Symbol() === 'symbol') {
t.equal(typeof Symbol(), 'symbol');
t.end();
});
// @ts-expect-error TS is stupid and doesn't know about top level return
return;
}

View File

@@ -8,6 +8,7 @@ if (typeof Symbol === 'function' && typeof Symbol() === 'symbol') {
t.equal(typeof Symbol(), 'symbol');
t.end();
});
// @ts-expect-error TS is stupid and doesn't know about top level return
return;
}

View File

@@ -1,5 +1,6 @@
'use strict';
/** @type {(t: import('tape').Test) => false | void} */
// eslint-disable-next-line consistent-return
module.exports = function runSymbolTests(t) {
t.equal(typeof Symbol, 'function', 'global Symbol is a function');
@@ -31,6 +32,7 @@ module.exports = function runSymbolTests(t) {
t.equal(typeof Object.getOwnPropertySymbols, 'function', 'Object.getOwnPropertySymbols is a function');
/** @type {{ [k in symbol]?: unknown }} */
var obj = {};
var sym = Symbol('test');
var symObj = Object(sym);
@@ -40,8 +42,8 @@ module.exports = function runSymbolTests(t) {
var symVal = 42;
obj[sym] = symVal;
// eslint-disable-next-line no-restricted-syntax
for (sym in obj) { t.fail('symbol property key was found in for..in of object'); }
// eslint-disable-next-line no-restricted-syntax, no-unused-vars
for (var _ in obj) { t.fail('symbol property key was found in for..in of object'); }
t.deepEqual(Object.keys(obj), [], 'no enumerable own keys on symbol-valued object');
t.deepEqual(Object.getOwnPropertyNames(obj), [], 'no own names on symbol-valued object');

View File

@@ -10,10 +10,12 @@
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"axios": "^1.12.2",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"csv-parser": "^3.0.0",
"date-fns": "^2.30.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"iconv-lite": "^0.6.3",
@@ -30,6 +32,15 @@
"vue-eslint-parser": "9.4.3"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
@@ -820,6 +831,12 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz",
@@ -828,6 +845,17 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -965,6 +993,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1094,6 +1135,18 @@
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1237,6 +1290,22 @@
"node": ">= 10"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1269,6 +1338,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -1325,6 +1403,20 @@
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1352,13 +1444,10 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
@@ -1372,6 +1461,33 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1762,6 +1878,42 @@
"dev": true,
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1858,16 +2010,21 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1876,6 +2033,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1922,12 +2092,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1954,10 +2124,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -1966,11 +2136,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
@@ -2414,6 +2587,15 @@
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -2981,6 +3163,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@@ -14,6 +14,7 @@
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.12.2",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"crypto": "^1.0.1",

View File

@@ -0,0 +1,32 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getClubTeams,
getClubTeam,
createClubTeam,
updateClubTeam,
deleteClubTeam,
getLeagues
} from '../controllers/clubTeamController.js';
const router = express.Router();
// Get all club teams for a club
router.get('/club/:clubid', authenticate, getClubTeams);
// Create a new club team
router.post('/club/:clubid', authenticate, createClubTeam);
// Get leagues for a club
router.get('/leagues/:clubid', authenticate, getLeagues);
// Get a specific club team
router.get('/:clubteamid', authenticate, getClubTeam);
// Update a club team
router.put('/:clubteamid', authenticate, updateClubTeam);
// Delete a club team
router.delete('/:clubteamid', authenticate, deleteClubTeam);
export default router;

View File

@@ -1,4 +1,4 @@
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage } from '../controllers/memberController.js';
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis } from '../controllers/memberController.js';
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import multer from 'multer';
@@ -13,5 +13,6 @@ router.get('/image/:clubId/:memberId', authenticate, getMemberImage);
router.get('/get/:id/:showAll', authenticate, getClubMembers);
router.post('/set/:id', authenticate, setClubMembers);
router.get('/notapproved/:id', authenticate, getWaitingApprovals);
router.post('/update-ratings/:id', authenticate, updateRatingsFromMyTischtennis);
export default router;

View File

@@ -0,0 +1,29 @@
import express from 'express';
import myTischtennisController from '../controllers/myTischtennisController.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = express.Router();
// All routes require authentication
router.use(authenticate);
// GET /api/mytischtennis/account - Get account
router.get('/account', myTischtennisController.getAccount);
// GET /api/mytischtennis/status - Check status
router.get('/status', myTischtennisController.getStatus);
// POST /api/mytischtennis/account - Create or update account
router.post('/account', myTischtennisController.upsertAccount);
// DELETE /api/mytischtennis/account - Delete account
router.delete('/account', myTischtennisController.deleteAccount);
// POST /api/mytischtennis/verify - Verify login
router.post('/verify', myTischtennisController.verifyLogin);
// GET /api/mytischtennis/session - Get stored session
router.get('/session', myTischtennisController.getSession);
export default router;

View File

@@ -23,6 +23,7 @@ router.get('/', authenticate, getAllPredefinedActivities);
router.get('/:id', authenticate, getPredefinedActivityById);
router.put('/:id', authenticate, updatePredefinedActivity);
router.post('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
router.put('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
router.delete('/:id/image/:imageId', authenticate, deletePredefinedActivityImage);
router.get('/search/query', authenticate, searchPredefinedActivities);
router.post('/merge', authenticate, mergePredefinedActivities);

View File

@@ -0,0 +1,28 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getSeasons,
getCurrentSeason,
createSeason,
getSeason,
deleteSeason
} from '../controllers/seasonController.js';
const router = express.Router();
// Get all seasons
router.get('/', authenticate, getSeasons);
// Get current season (creates if not exists)
router.get('/current', authenticate, getCurrentSeason);
// Get a specific season
router.get('/:seasonid', authenticate, getSeason);
// Create a new season
router.post('/', authenticate, createSeason);
// Delete a season
router.delete('/:seasonid', authenticate, deleteSeason);
export default router;

View File

@@ -0,0 +1,33 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
uploadMiddleware,
uploadDocument,
getDocuments,
getDocument,
downloadDocument,
deleteDocument,
parsePDF
} from '../controllers/teamDocumentController.js';
const router = express.Router();
// Upload eines Dokuments für ein Club-Team
router.post('/club-team/:clubteamid/upload', authenticate, uploadMiddleware, uploadDocument);
// Alle Dokumente für ein Club-Team abrufen
router.get('/club-team/:clubteamid', authenticate, getDocuments);
// Ein spezifisches Dokument abrufen
router.get('/:documentid', authenticate, getDocument);
// Ein Dokument herunterladen
router.get('/:documentid/download', authenticate, downloadDocument);
// Ein Dokument löschen
router.delete('/:documentid', authenticate, deleteDocument);
// PDF parsen und Matches extrahieren
router.post('/:documentid/parse', authenticate, parsePDF);
export default router;

View File

@@ -0,0 +1,32 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getTeams,
getTeam,
createTeam,
updateTeam,
deleteTeam,
getLeagues
} from '../controllers/teamController.js';
const router = express.Router();
// Get all teams for a club
router.get('/club/:clubid', authenticate, getTeams);
// Get leagues for a club
router.get('/leagues/:clubid', authenticate, getLeagues);
// Get a specific team
router.get('/:teamid', authenticate, getTeam);
// Create a new team
router.post('/club/:clubid', authenticate, createTeam);
// Update a team
router.put('/:teamid', authenticate, updateTeam);
// Delete a team
router.delete('/:teamid', authenticate, deleteTeam);
export default router;

View File

@@ -6,9 +6,9 @@ import cors from 'cors';
import {
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, Group,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, TeamDocument, Group,
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis
} from './models/index.js';
import authRoutes from './routes/authRoutes.js';
import clubRoutes from './routes/clubRoutes.js';
@@ -33,6 +33,11 @@ import tournamentRoutes from './routes/tournamentRoutes.js';
import accidentRoutes from './routes/accidentRoutes.js';
import trainingStatsRoutes from './routes/trainingStatsRoutes.js';
import officialTournamentRoutes from './routes/officialTournamentRoutes.js';
import myTischtennisRoutes from './routes/myTischtennisRoutes.js';
import teamRoutes from './routes/teamRoutes.js';
import clubTeamRoutes from './routes/clubTeamRoutes.js';
import teamDocumentRoutes from './routes/teamDocumentRoutes.js';
import seasonRoutes from './routes/seasonRoutes.js';
const app = express();
const port = process.env.PORT || 3000;
@@ -40,7 +45,12 @@ const port = process.env.PORT || 3000;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.use(cors());
app.use(cors({
origin: true,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid']
}));
app.use(express.json());
// Globale Fehlerbehandlung, damit der Server bei unerwarteten Fehlern nicht hart abstürzt
@@ -72,6 +82,11 @@ app.use('/api/tournament', tournamentRoutes);
app.use('/api/accident', accidentRoutes);
app.use('/api/training-stats', trainingStatsRoutes);
app.use('/api/official-tournaments', officialTournamentRoutes);
app.use('/api/mytischtennis', myTischtennisRoutes);
app.use('/api/teams', teamRoutes);
app.use('/api/club-teams', clubTeamRoutes);
app.use('/api/team-documents', teamDocumentRoutes);
app.use('/api/seasons', seasonRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));
@@ -171,6 +186,7 @@ app.get('*', (req, res) => {
await safeSync(TournamentResult);
await safeSync(Accident);
await safeSync(UserToken);
await safeSync(MyTischtennis);
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);

View File

@@ -3,6 +3,7 @@ import DiaryDate from '../models/DiaryDates.js';
import Member from '../models/Member.js';
import { checkAccess, getUserByToken} from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
class AccidentService {
async createAccident(userToken, clubId, memberId, diaryDateId, accident) {
await checkAccess(userToken, clubId);
@@ -14,7 +15,7 @@ class AccidentService {
if (!member || member.clubId != clubId) {
throw new Error('Member not found');
}
console.log(diaryDateId);
devLog(diaryDateId);
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (!diaryDate || diaryDate.clubId != clubId) {
throw new Error('Diary date not found');

View File

@@ -4,6 +4,7 @@ import User from '../models/User.js';
import UserToken from '../models/UserToken.js';
import { sendActivationEmail } from './emailService.js';
import { devLog } from '../utils/logger.js';
const register = async (email, password) => {
try {
const activationCode = Math.random().toString(36).substring(2, 15);
@@ -11,7 +12,7 @@ const register = async (email, password) => {
await sendActivationEmail(email, activationCode);
return user;
} catch (error) {
console.log(error);
devLog(error);
return null;
}
};
@@ -33,11 +34,11 @@ const login = async (email, password) => {
if (!user || !(await bcrypt.compare(password, user.password))) {
throw { status: 401, message: 'Ungültige Anmeldedaten' };
}
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '3h' });
await UserToken.create({
userId: user.id,
token,
expiresAt: new Date(Date.now() + 3600 * 1000),
expiresAt: new Date(Date.now() + 3 * 3600 * 1000),
});
return { token };
};

View File

@@ -0,0 +1,180 @@
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import SeasonService from './seasonService.js';
import { devLog } from '../utils/logger.js';
class ClubTeamService {
/**
* Holt alle ClubTeams für einen Verein, optional gefiltert nach Saison.
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison verwendet.
* @param {number} clubId - Die ID des Vereins.
* @param {number|null} seasonId - Optionale Saison-ID.
* @returns {Promise<Array<ClubTeam>>} Eine Liste von ClubTeams.
*/
static async getAllClubTeamsByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const clubTeams = await ClubTeam.findAll({
where: { clubId, seasonId },
order: [['name', 'ASC']]
});
// Manuelle Datenanreicherung für Liga und Saison
const enrichedClubTeams = [];
for (const clubTeam of clubTeams) {
const enrichedTeam = {
id: clubTeam.id,
name: clubTeam.name,
clubId: clubTeam.clubId,
leagueId: clubTeam.leagueId,
seasonId: clubTeam.seasonId,
createdAt: clubTeam.createdAt,
updatedAt: clubTeam.updatedAt,
league: { name: 'Unbekannt' },
season: { season: 'Unbekannt' }
};
// Lade Liga-Daten
if (clubTeam.leagueId) {
const league = await League.findByPk(clubTeam.leagueId, { attributes: ['name'] });
if (league) enrichedTeam.league = league;
}
// Lade Saison-Daten
if (clubTeam.seasonId) {
const season = await Season.findByPk(clubTeam.seasonId, { attributes: ['season'] });
if (season) enrichedTeam.season = season;
}
enrichedClubTeams.push(enrichedTeam);
}
return enrichedClubTeams;
} catch (error) {
console.error('[ClubTeamService.getAllClubTeamsByClub] - Error:', error);
throw error;
}
}
/**
* Holt ein ClubTeam anhand seiner ID
* @param {number} clubTeamId - Die ID des ClubTeams
* @returns {Promise<ClubTeam|null>} Das ClubTeam oder null, wenn nicht gefunden
*/
static async getClubTeamById(clubTeamId) {
try {
const clubTeam = await ClubTeam.findByPk(clubTeamId, {
include: [
{
model: League,
as: 'league',
attributes: ['id', 'name']
},
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
]
});
return clubTeam;
} catch (error) {
console.error('[ClubTeamService.getClubTeamById] - Error:', error);
throw error;
}
}
/**
* Erstellt ein neues ClubTeam.
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison zugewiesen.
* @param {object} clubTeamData - Die Daten des neuen ClubTeams (name, clubId, optional leagueId, seasonId).
* @returns {Promise<ClubTeam>} Das erstellte ClubTeam.
*/
static async createClubTeam(clubTeamData) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!clubTeamData.seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
clubTeamData.seasonId = currentSeason.id;
}
const clubTeam = await ClubTeam.create(clubTeamData);
return clubTeam;
} catch (error) {
console.error('[ClubTeamService.createClubTeam] - Error:', error);
throw error;
}
}
/**
* Aktualisiert ein bestehendes ClubTeam.
* @param {number} clubTeamId - Die ID des zu aktualisierenden ClubTeams.
* @param {object} updateData - Die zu aktualisierenden Daten.
* @returns {Promise<boolean>} True, wenn das ClubTeam aktualisiert wurde, sonst false.
*/
static async updateClubTeam(clubTeamId, updateData) {
try {
const [updatedRowsCount] = await ClubTeam.update(updateData, {
where: { id: clubTeamId }
});
return updatedRowsCount > 0;
} catch (error) {
console.error('[ClubTeamService.updateClubTeam] - Error:', error);
throw error;
}
}
/**
* Löscht ein ClubTeam.
* @param {number} clubTeamId - Die ID des zu löschenden ClubTeams.
* @returns {Promise<boolean>} True, wenn das ClubTeam gelöscht wurde, sonst false.
*/
static async deleteClubTeam(clubTeamId) {
try {
const deletedRows = await ClubTeam.destroy({
where: { id: clubTeamId }
});
return deletedRows > 0;
} catch (error) {
console.error('[ClubTeamService.deleteClubTeam] - Error:', error);
throw error;
}
}
/**
* Holt alle Ligen für einen Verein, optional gefiltert nach Saison.
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison verwendet.
* @param {number} clubId - Die ID des Vereins.
* @param {number|null} seasonId - Optionale Saison-ID.
* @returns {Promise<Array<League>>} Eine Liste von Ligen.
*/
static async getLeaguesByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const leagues = await League.findAll({
where: { clubId, seasonId },
attributes: ['id', 'name', 'seasonId'],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
console.error('[ClubTeamService.getLeaguesByClub] - Error:', error);
throw error;
}
}
}
export default ClubTeamService;

View File

@@ -2,47 +2,74 @@ import DiaryDateActivity from '../models/DiaryDateActivity.js';
import GroupActivity from '../models/GroupActivity.js';
import Group from '../models/Group.js';
import PredefinedActivity from '../models/PredefinedActivity.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import { checkAccess } from '../utils/userUtils.js';
import { Op } from 'sequelize';
import { devLog } from '../utils/logger.js';
class DiaryDateActivityService {
async createActivity(userToken, clubId, data) {
console.log('[DiaryDateActivityService::createActivity] - check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryDateActivityService::createActivity] - add: ', data);
const { activity, ...restData } = data;
let predefinedActivity = await PredefinedActivity.findOne({ where: { name: data.activity } });
// Versuche, die PredefinedActivity robust zu finden:
// 1) per übergebener ID
// 2) per Name ODER Code (das Feld "activity" kann Kürzel oder Name sein)
// 3) erst dann neu anlegen
let predefinedActivity = null;
if (data.predefinedActivityId) {
predefinedActivity = await PredefinedActivity.findByPk(data.predefinedActivityId);
}
if (!predefinedActivity) {
const normalized = (data.activity || '').trim();
if (normalized) {
predefinedActivity = await PredefinedActivity.findOne({
where: {
[Op.or]: [
{ name: normalized },
{ code: normalized }
]
}
});
}
}
if (!predefinedActivity) {
predefinedActivity = await PredefinedActivity.create({
name: data.activity,
description: '',
duration: data.duration
name: data.name || data.activity || '',
code: data.code || (data.activity || ''),
description: data.description || '',
duration: data.duration && data.duration !== '' ? parseInt(data.duration) : null
});
}
restData.predefinedActivityId = predefinedActivity.id;
// Bereinige duration-Feld für DiaryDateActivity
if (restData.duration === '' || restData.duration === undefined) {
restData.duration = null;
} else if (typeof restData.duration === 'string') {
restData.duration = parseInt(restData.duration);
}
const maxOrderId = await DiaryDateActivity.max('orderId', {
where: { diaryDateId: data.diaryDateId }
});
const newOrderId = maxOrderId !== null ? maxOrderId + 1 : 1;
restData.orderId = newOrderId;
console.log('[DiaryDateActivityService::createActivity] - create diary date activity');
return await DiaryDateActivity.create(restData);
}
async updateActivity(userToken, clubId, id, data) {
console.log('[DiaryDateActivityService::updateActivity] - check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryDateActivityService::updateActivity] - load activity', id);
const activity = await DiaryDateActivity.findByPk(id);
if (!activity) {
console.log('[DiaryDateActivityService::updateActivity] - activity not found');
throw new Error('Activity not found');
}
// Wenn customActivityName gesendet wird, müssen wir die PredefinedActivity behandeln
if (data.customActivityName) {
console.log('[DiaryDateActivityService::updateActivity] - handling customActivityName:', data.customActivityName);
// Suche nach einer existierenden PredefinedActivity mit diesem Namen
let predefinedActivity = await PredefinedActivity.findOne({
@@ -51,11 +78,10 @@ class DiaryDateActivityService {
if (!predefinedActivity) {
// Erstelle eine neue PredefinedActivity
console.log('[DiaryDateActivityService::updateActivity] - creating new PredefinedActivity');
predefinedActivity = await PredefinedActivity.create({
name: data.customActivityName,
description: '',
duration: data.duration || activity.duration
description: data.description || '',
duration: data.duration && data.duration !== '' ? parseInt(data.duration) : (activity.duration || null)
});
}
@@ -66,7 +92,6 @@ class DiaryDateActivityService {
delete data.customActivityName;
}
console.log('[DiaryDateActivityService::updateActivity] - update activity', clubId, id, data, JSON.stringify(data));
return await activity.update(data);
}
@@ -80,22 +105,14 @@ class DiaryDateActivityService {
}
async updateActivityOrder(userToken, clubId, id, newOrderId) {
console.log(`[DiaryDateActivityService::updateActivityOrder] - Start update for activity id: ${id}`);
console.log(`[DiaryDateActivityService::updateActivityOrder] - User token: ${userToken}, Club id: ${clubId}, New order id: ${newOrderId}`);
console.log('[DiaryDateActivityService::updateActivityOrder] - Checking user access');
await checkAccess(userToken, clubId);
console.log('[DiaryDateActivityService::updateActivityOrder] - User access confirmed');
console.log(`[DiaryDateActivityService::updateActivityOrder] - Finding activity with id: ${id}`);
const activity = await DiaryDateActivity.findByPk(id);
if (!activity) {
console.error('[DiaryDateActivityService::updateActivityOrder] - Activity not found, throwing error');
throw new Error('Activity not found');
}
console.log('[DiaryDateActivityService::updateActivityOrder] - Activity found:', activity);
const currentOrderId = activity.orderId;
console.log(`[DiaryDateActivityService::updateActivityOrder] - Current order id: ${currentOrderId}`);
if (newOrderId < currentOrderId) {
console.log(`[DiaryDateActivityService::updateActivityOrder] - Shifting items down. Moving activities with orderId between ${newOrderId} and ${currentOrderId - 1}`);
await DiaryDateActivity.increment(
{ orderId: 1 },
{
@@ -105,9 +122,7 @@ class DiaryDateActivityService {
},
}
);
console.log(`[DiaryDateActivityService::updateActivityOrder] - Items shifted down`);
} else if (newOrderId > currentOrderId) {
console.log(`[DiaryDateActivityService::updateActivityOrder] - Shifting items up. Moving activities with orderId between ${currentOrderId + 1} and ${newOrderId}`);
await DiaryDateActivity.decrement(
{ orderId: 1 },
{
@@ -117,23 +132,15 @@ class DiaryDateActivityService {
},
}
);
console.log(`[DiaryDateActivityService::updateActivityOrder] - Items shifted up`);
} else {
console.log('[DiaryDateActivityService::updateActivityOrder] - New order id is the same as the current order id. No shift required.');
}
console.log(`[DiaryDateActivityService::updateActivityOrder] - Setting new order id for activity id: ${id}`);
activity.orderId = newOrderId;
console.log('[DiaryDateActivityService::updateActivityOrder] - Saving activity with new order id');
const savedActivity = await activity.save();
console.log('[DiaryDateActivityService::updateActivityOrder] - Activity saved:', savedActivity);
console.log(`[DiaryDateActivityService::updateActivityOrder] - Finished update for activity id: ${id}`);
return savedActivity;
}
async getActivities(userToken, clubId, diaryDateId) {
console.log('[DiaryDateActivityService::getActivities] - check user access');
await checkAccess(userToken, clubId);
console.log(`[DiaryDateActivityService::getActivities] - fetch activities for diaryDateId: ${diaryDateId}`);
const activities = await DiaryDateActivity.findAll({
where: { diaryDateId },
order: [['orderId', 'ASC']],
@@ -141,6 +148,12 @@ class DiaryDateActivityService {
{
model: PredefinedActivity,
as: 'predefinedActivity',
include: [
{
model: PredefinedActivityImage,
as: 'images'
}
]
},
{
model: GroupActivity,
@@ -158,14 +171,68 @@ class DiaryDateActivityService {
}
]
});
console.log(`[DiaryDateActivityService::getActivities] - found ${activities.length} activities`);
return activities;
// Füge imageUrl zu jeder PredefinedActivity hinzu
const activitiesWithImages = await Promise.all(activities.map(async activity => {
// Konvertiere zu JSON und zurück, um alle Eigenschaften zu serialisieren
const activityData = activity.toJSON();
if (activityData.predefinedActivity) {
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
const allImages = await PredefinedActivityImage.findAll({
where: { predefinedActivityId: activityData.predefinedActivity.id },
order: [['createdAt', 'ASC']]
});
const firstImage = allImages.length > 0 ? allImages[0] : null;
// Füge Zeichnungsdaten hinzu, falls vorhanden
if (firstImage && firstImage.drawingData) {
try {
activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
} catch (error) {
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error);
}
}
if (firstImage) {
// Füge sowohl imageUrl als auch imageLink mit Image-ID hinzu
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
} else {
// Fallback: Verwende den Basis-Pfad ohne Image-ID
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
}
}
// Auch für GroupActivities
if (activityData.groupActivities && activityData.groupActivities.length > 0) {
for (const groupActivity of activityData.groupActivities) {
if (groupActivity.groupPredefinedActivity) {
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
const firstImage = await PredefinedActivityImage.findOne({
where: { predefinedActivityId: groupActivity.groupPredefinedActivity.id },
order: [['createdAt', 'ASC']]
});
if (firstImage) {
groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
} else {
groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`;
groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`;
}
}
}
}
return activityData;
}));
return activitiesWithImages;
}
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity) {
console.log('[DiaryDateActivityService::addGroupActivity] Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryDateActivityService::addGroupActivity] Check diary date');
const diaryDateActivity = await DiaryDateActivity.findOne({
where: {
diaryDateId,
@@ -178,26 +245,23 @@ class DiaryDateActivityService {
console.error('[DiaryDateActivityService::addGroupActivity] Activity not found');
throw new Error('Activity not found');
}
console.log('[DiaryDateActivityService::addGroupActivity] Check group');
const group = await Group.findByPk(groupId);
if (!group || group.diaryDateId !== diaryDateActivity.diaryDateId) {
console.error('[DiaryDateActivityService::addGroupActivity] Group and date don\'t fit');
throw new Error('Group isn\'t related to date');
}
console.log('[DiaryDateActivityService::addGroupActivity] Get predefined activity');
const [predefinedActivity, created] = await PredefinedActivity.findOrCreate({
where: {
name: activity
}
});
console.log('[DiaryDateActivityService::addGroupActivity] Add group activity');
console.log(predefinedActivity);
devLog(predefinedActivity);
const activityData = {
diaryDateActivity: diaryDateActivity.id,
groupId: groupId,
customActivity: predefinedActivity.id
}
console.log(activityData);
devLog(activityData);
return await GroupActivity.create(activityData);
}
}

View File

@@ -6,6 +6,7 @@ import Member from "../models/Member.js";
import { checkAccess } from '../utils/userUtils.js';
import { Op, literal } from "sequelize";
import { devLog } from '../utils/logger.js';
class DiaryDateTagService {
async getDiaryDateMemberTags(userToken, clubId, memberId) {
await checkAccess(userToken, clubId);
@@ -35,7 +36,7 @@ class DiaryDateTagService {
}
async addDiaryDateTag(userToken, clubId, diaryDateId, memberId, tag) {
console.log(userToken, clubId, diaryDateId, memberId, tag);
devLog(userToken, clubId, diaryDateId, memberId, tag);
await checkAccess(userToken, clubId);
const tagObject = await DiaryTag.findOne({ where: { id: tag.id } });
if (!tagObject) {

View File

@@ -3,6 +3,7 @@ import DiaryMemberTag from '../models/DiaryMemberTag.js';
import { DiaryTag } from '../models/DiaryTag.js';
import { checkAccess } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
class DiaryMemberService {
async addNoteToMember(userToken, clubId, diaryDateId, memberId, content) {
await checkAccess(userToken, clubId);
@@ -14,7 +15,7 @@ class DiaryMemberService {
async getNotesForMember(userToken, clubId, diaryDateId, memberId) {
await checkAccess(userToken, clubId);
console.log(clubId, diaryDateId, memberId);
devLog(clubId, diaryDateId, memberId);
return await DiaryMemberNote.findAll({ where: { diaryDateId, memberId }, order: [['createdAt', 'DESC']] });
}
@@ -50,7 +51,7 @@ class DiaryMemberService {
if (tagLink) {
await tagLink.destroy();
} else {
console.log(diaryDateId, memberId, tagId);
devLog(diaryDateId, memberId, tagId);
throw new Error('Das Tag ist nicht verknüpft.');
}
}

View File

@@ -7,16 +7,14 @@ import DiaryDateTag from '../models/DiaryDateTag.js';
import { checkAccess } from '../utils/userUtils.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class DiaryService {
async getDatesForClub(userToken, clubId) {
console.log('[DiaryService::getDatesForClub] - Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryService::getDatesForClub] - Validate club existence');
const club = await Club.findByPk(clubId);
if (!club) {
throw new HttpError('Club not found', 404);
}
console.log('[DiaryService::getDatesForClub] - Load diary dates');
const dates = await DiaryDate.findAll({
where: { clubId },
include: [
@@ -29,14 +27,11 @@ class DiaryService {
}
async createDateForClub(userToken, clubId, date, trainingStart, trainingEnd) {
console.log('[DiaryService::createDateForClub] - Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryService::createDateForClub] - Validate club existence');
const club = await Club.findByPk(clubId);
if (!club) {
throw new HttpError('Club not found', 404);
}
console.log('[DiaryService::createDateForClub] - Validate date');
const parsedDate = new Date(date);
if (isNaN(parsedDate.getTime())) {
throw new HttpError('Invalid date format', 400);
@@ -44,7 +39,6 @@ class DiaryService {
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
throw new HttpError('Training start time must be before training end time', 400);
}
console.log('[DiaryService::createDateForClub] - Create new diary date');
const newDate = await DiaryDate.create({
date: parsedDate,
clubId,
@@ -56,9 +50,7 @@ class DiaryService {
}
async updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd) {
console.log('[DiaryService::updateTrainingTimes] - Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryService::updateTrainingTimes] - Validate date');
const diaryDate = await DiaryDate.findOne({ where: { clubId, id: dateId } });
if (!diaryDate) {
throw new HttpError('Diary entry not found', 404);
@@ -66,7 +58,6 @@ class DiaryService {
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
throw new HttpError('Training start time must be before training end time', 400);
}
console.log('[DiaryService::updateTrainingTimes] - Update training times');
diaryDate.trainingStart = trainingStart || null;
diaryDate.trainingEnd = trainingEnd || null;
await diaryDate.save();
@@ -74,14 +65,12 @@ class DiaryService {
}
async addNoteToDate(userToken, diaryDateId, content) {
console.log('[DiaryService::addNoteToDate] - Add note');
await checkAccess(userToken, diaryDateId);
await DiaryNote.create({ diaryDateId, content });
return await DiaryNote.findAll({ where: { diaryDateId }, order: [['createdAt', 'DESC']] });
}
async deleteNoteFromDate(userToken, noteId) {
console.log('[DiaryService::deleteNoteFromDate] - Delete note');
const note = await DiaryNote.findByPk(noteId);
if (!note) {
throw new HttpError('Note not found', 404);
@@ -92,7 +81,6 @@ class DiaryService {
}
async addTagToDate(userToken, diaryDateId, tagName) {
console.log('[DiaryService::addTagToDate] - Add tag');
await checkAccess(userToken, diaryDateId);
let tag = await DiaryTag.findOne({ where: { name: tagName } });
if (!tag) {
@@ -105,29 +93,24 @@ class DiaryService {
async addTagToDiaryDate(userToken, clubId, diaryDateId, tagId) {
checkAccess(userToken, clubId);
console.log(`[DiaryService::addTagToDiaryDate] - diaryDateId: ${diaryDateId}, tagId: ${tagId}`);
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (!diaryDate) {
throw new HttpError('DiaryDate not found', 404);
}
console.log('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
const existingEntry = await DiaryDateTag.findOne({
where: { diaryDateId, tagId }
});
if (existingEntry) {
return;
}
console.log('[DiaryService::addTagToDiaryDate] - Tag not found, creating new entry');
const tag = await DiaryTag.findByPk(tagId);
if (!tag) {
throw new HttpError('Tag not found', 404);
}
console.log('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
await DiaryDateTag.create({
diaryDateId,
tagId
});
console.log('[DiaryService::addTagToDiaryDate] - Get tags');
const tags = await DiaryDateTag.findAll({ where: {
diaryDateId: diaryDateId },
include: {
@@ -135,12 +118,11 @@ class DiaryService {
as: 'tag'
}
});
console.log(tags);
devLog(tags);
return tags.map(tag => tag.tag);
}
async getDiaryNotesForDateAndMember(diaryDateId, memberId) {
console.log('[DiaryService::getDiaryNotesForDateAndMember] - Fetching notes');
return await DiaryNote.findAll({
where: { diaryDateId, memberId },
order: [['createdAt', 'DESC']]
@@ -153,19 +135,15 @@ class DiaryService {
}
async removeDateForClub(userToken, clubId, dateId) {
console.log('[DiaryService::removeDateForClub] - Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryService::removeDateForClub] - Validate date');
const diaryDate = await DiaryDate.findOne({ where: { id: dateId, clubId } });
if (!diaryDate) {
throw new HttpError('Diary entry not found', 404);
}
console.log('[DiaryService::removeDateForClub] - Check for activities');
const activityCount = await DiaryDateActivity.count({ where: { diaryDateId: dateId } });
if (activityCount > 0) {
throw new HttpError('Cannot delete date with activities', 409);
}
console.log('[DiaryService::removeDateForClub] - Delete diary date');
await diaryDate.destroy();
return { ok: true };
}

View File

@@ -3,6 +3,7 @@ import DiaryDate from '../models/DiaryDates.js';
import HttpError from '../exceptions/HttpError.js';
import Group from '../models/Group.js';
import { devLog } from '../utils/logger.js';
class GroupService {
async checkDiaryDateToClub(clubId, dateId) {
@@ -40,7 +41,7 @@ class GroupService {
}
async changeGroup(userToken, groupId, clubId, dateId, name, lead) {
console.log("changeGroup: ", groupId, clubId, dateId, name, lead);
devLog("changeGroup: ", groupId, clubId, dateId, name, lead);
await checkAccess(userToken, clubId);
await this.checkDiaryDateToClub(clubId, dateId);
const group = await Group.findOne({

View File

@@ -0,0 +1,94 @@
import League from '../models/League.js';
import Season from '../models/Season.js';
import SeasonService from './seasonService.js';
import { devLog } from '../utils/logger.js';
class LeagueService {
static async getAllLeaguesByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const leagues = await League.findAll({
where: { clubId, seasonId },
include: [
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
console.error('[LeagueService.getAllLeaguesByClub] - Error:', error);
throw error;
}
}
static async getLeagueById(leagueId) {
try {
const league = await League.findByPk(leagueId, {
include: [
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
]
});
return league;
} catch (error) {
console.error('[LeagueService.getLeagueById] - Error:', error);
throw error;
}
}
static async createLeague(leagueData) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!leagueData.seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
leagueData.seasonId = currentSeason.id;
}
const league = await League.create(leagueData);
return league;
} catch (error) {
console.error('[LeagueService.createLeague] - Error:', error);
throw error;
}
}
static async updateLeague(leagueId, updateData) {
try {
const [updatedRowsCount] = await League.update(updateData, {
where: { id: leagueId }
});
return updatedRowsCount > 0;
} catch (error) {
console.error('[LeagueService.updateLeague] - Error:', error);
throw error;
}
}
static async deleteLeague(leagueId) {
try {
const deletedRowsCount = await League.destroy({
where: { id: leagueId }
});
return deletedRowsCount > 0;
} catch (error) {
console.error('[LeagueService.deleteLeague] - Error:', error);
throw error;
}
}
}
export default LeagueService;

View File

@@ -7,9 +7,11 @@ import Season from '../models/Season.js';
import Location from '../models/Location.js';
import League from '../models/League.js';
import Team from '../models/Team.js';
import SeasonService from './seasonService.js';
import { checkAccess } from '../utils/userUtils.js';
import { Op } from 'sequelize';
import { devLog } from '../utils/logger.js';
class MatchService {
generateSeasonString(date = new Date()) {
@@ -21,8 +23,7 @@ class MatchService {
seasonStartYear = currentYear - 1;
}
const seasonEndYear = seasonStartYear + 1;
const seasonEndYearString = seasonEndYear.toString().slice(-2);
return `${seasonStartYear}/${seasonEndYearString}`;
return `${seasonStartYear}/${seasonEndYear}`;
}
async importCSV(userToken, clubId, filePath) {
@@ -57,7 +58,6 @@ class MatchService {
},
});
matches.push({
seasonId: season.id,
date: parsedDate,
time: row['Termin'].split(' ')[1],
homeTeamId: homeTeamId,
@@ -71,14 +71,21 @@ class MatchService {
if (seasonString) {
season = await Season.findOne({ where: { season: seasonString } });
if (season) {
await Match.destroy({ where: { clubId, seasonId: season.id } });
// Lösche alle Matches für Ligen dieser Saison
const leagues = await League.findAll({
where: { seasonId: season.id, clubId }
});
const leagueIds = leagues.map(league => league.id);
if (leagueIds.length > 0) {
await Match.destroy({ where: { clubId, leagueId: leagueIds } });
}
}
}
const result = await Match.bulkCreate(matches);
return result;
} catch (error) {
console.log('Error during CSV import:', error);
devLog('Error during CSV import:', error);
throw error;
}
}
@@ -98,33 +105,28 @@ class MatchService {
}
async getLeaguesForCurrentSeason(userToken, clubId) {
async getLeaguesForCurrentSeason(userToken, clubId, seasonId = null) {
await checkAccess(userToken, clubId);
const seasonString = this.generateSeasonString();
const season = await Season.findOne({
where: {
season: {
[Op.like]: `%${seasonString}%`
}
// Verwende SeasonService für korrekte Saison-Verwaltung
let season;
if (!seasonId) {
season = await SeasonService.getOrCreateCurrentSeason();
} else {
season = await SeasonService.getSeasonById(seasonId);
if (!season) {
throw new Error('Season not found');
}
});
if (!season) {
await Season.create({ season: seasonString });
throw new Error('Season not found');
}
try {
const leagues = await League.findAll({
include: [{
model: Match,
as: 'leagueMatches',
where: {
seasonId: season.id,
clubId: clubId
},
attributes: [],
}],
where: {
clubId: clubId,
seasonId: season.id
},
attributes: ['id', 'name'],
group: ['League.id'],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
@@ -133,48 +135,69 @@ class MatchService {
}
}
async getMatchesForLeagues(userToken, clubId) {
async getMatchesForLeagues(userToken, clubId, seasonId = null) {
await checkAccess(userToken, clubId);
const seasonString = this.generateSeasonString();
const season = await Season.findOne({
where: {
season: {
[Op.like]: `%${seasonString}%`
}
// Wenn keine Saison angegeben, verwende die aktuelle
let season;
if (!seasonId) {
season = await SeasonService.getOrCreateCurrentSeason();
} else {
season = await SeasonService.getSeasonById(seasonId);
if (!season) {
throw new Error('Season not found');
}
});
if (!season) {
throw new Error('Season not found');
}
const matches = await Match.findAll({
where: {
seasonId: season.id,
clubId: clubId,
},
include: [
{
model: League,
as: 'leagueDetails',
attributes: ['name'],
},
{
model: Team,
as: 'homeTeam', // Assuming your associations are set correctly
attributes: ['name'],
},
{
model: Team,
as: 'guestTeam',
attributes: ['name'],
},
{
model: Location,
as: 'location',
attributes: ['name', 'address', 'city', 'zip'],
}
]
}
});
return matches;
// Filtere Matches nach Liga-Saison und lade Daten manuell
const enrichedMatches = [];
for (const match of matches) {
// Lade Liga-Daten
const league = await League.findByPk(match.leagueId, { attributes: ['name', 'seasonId'] });
if (!league || league.seasonId !== season.id) {
continue; // Skip matches from other seasons
}
const enrichedMatch = {
id: match.id,
date: match.date,
time: match.time,
homeTeamId: match.homeTeamId,
guestTeamId: match.guestTeamId,
locationId: match.locationId,
leagueId: match.leagueId,
code: match.code,
homePin: match.homePin,
guestPin: match.guestPin,
homeTeam: { name: 'Unbekannt' },
guestTeam: { name: 'Unbekannt' },
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
leagueDetails: { name: league.name }
};
if (match.homeTeamId) {
const homeTeam = await Team.findByPk(match.homeTeamId, { attributes: ['name'] });
if (homeTeam) enrichedMatch.homeTeam = homeTeam;
}
if (match.guestTeamId) {
const guestTeam = await Team.findByPk(match.guestTeamId, { attributes: ['name'] });
if (guestTeam) enrichedMatch.guestTeam = guestTeam;
}
if (match.locationId) {
const location = await Location.findByPk(match.locationId, {
attributes: ['name', 'address', 'city', 'zip']
});
if (location) enrichedMatch.location = location;
}
enrichedMatches.push(enrichedMatch);
}
return enrichedMatches;
}
async getMatchesForLeague(userToken, clubId, leagueId) {
@@ -192,34 +215,53 @@ class MatchService {
}
const matches = await Match.findAll({
where: {
seasonId: season.id,
clubId: clubId,
leagueId: leagueId
},
include: [
{
model: League,
as: 'leagueDetails',
attributes: ['name'],
},
{
model: Team,
as: 'homeTeam',
attributes: ['name'],
},
{
model: Team,
as: 'guestTeam',
attributes: ['name'],
},
{
model: Location,
as: 'location',
attributes: ['name', 'address', 'city', 'zip'],
}
]
}
});
return matches;
// Lade Team- und Location-Daten manuell
const enrichedMatches = [];
for (const match of matches) {
const enrichedMatch = {
id: match.id,
date: match.date,
time: match.time,
homeTeamId: match.homeTeamId,
guestTeamId: match.guestTeamId,
locationId: match.locationId,
leagueId: match.leagueId,
code: match.code,
homePin: match.homePin,
guestPin: match.guestPin,
homeTeam: { name: 'Unbekannt' },
guestTeam: { name: 'Unbekannt' },
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
leagueDetails: { name: 'Unbekannt' }
};
if (match.homeTeamId) {
const homeTeam = await Team.findByPk(match.homeTeamId, { attributes: ['name'] });
if (homeTeam) enrichedMatch.homeTeam = homeTeam;
}
if (match.guestTeamId) {
const guestTeam = await Team.findByPk(match.guestTeamId, { attributes: ['name'] });
if (guestTeam) enrichedMatch.guestTeam = guestTeam;
}
if (match.locationId) {
const location = await Location.findByPk(match.locationId, {
attributes: ['name', 'address', 'city', 'zip']
});
if (location) enrichedMatch.location = location;
}
if (match.leagueId) {
const league = await League.findByPk(match.leagueId, { attributes: ['name'] });
if (league) enrichedMatch.leagueDetails = league;
}
enrichedMatches.push(enrichedMatch);
}
return enrichedMatches;
}
}

View File

@@ -1,6 +1,7 @@
import MemberNote from '../models/MemberNote.js';
import { checkAccess } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
class MemberNoteService {
async addNoteToMember(userToken, clubId, memberId, content) {
await checkAccess(userToken, clubId);
@@ -8,7 +9,7 @@ class MemberNoteService {
}
async getNotesForMember(userToken, clubId, memberId) {
console.log(userToken, clubId);
devLog(userToken, clubId);
await checkAccess(userToken, clubId);
return await MemberNote.findAll({
where: { memberId },

View File

@@ -6,13 +6,11 @@ import path from 'path';
import fs from 'fs';
import sharp from 'sharp';
import { devLog } from '../utils/logger.js';
class MemberService {
async getApprovalRequests(userToken, clubId) {
console.log('[MemberService::getApprovalRequest] - Check user access');
await checkAccess(userToken, clubId);
console.log('[MemberService::getApprovalRequest] - Load user');
const user = await getUserByToken(userToken);
console.log('[MemberService::getApprovalRequest] - Load userclub');
return await UserClub.findAll({
where: {
clubId: clubId,
@@ -23,9 +21,7 @@ class MemberService {
}
async getClubMembers(userToken, clubId, showAll) {
console.log('[getClubMembers] - Check access');
await checkAccess(userToken, clubId);
console.log('[getClubMembers] - Find members');
const where = {
clubId: clubId
};
@@ -44,7 +40,6 @@ class MemberService {
});
})
.then(membersWithImageStatus => {
console.log('[getClubMembers] - return members');
return membersWithImageStatus;
})
.catch(error => {
@@ -54,17 +49,13 @@ class MemberService {
}
async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate, phone, email, active = true, testMembership = false,
picsInInternetAllowed = false, gender = 'unknown') {
picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null) {
try {
console.log('[setClubMembers] - Check access');
await checkAccess(userToken, clubId);
console.log('[setClubMembers] - set default member');
let member = null;
console.log('[setClubMembers] - load member if possible');
if (memberId) {
member = await Member.findOne({ where: { id: memberId } });
}
console.log('[setClubMembers] - set member');
if (member) {
member.firstName = firstName;
member.lastName = lastName;
@@ -77,6 +68,8 @@ class MemberService {
member.testMembership = testMembership;
member.picsInInternetAllowed = picsInInternetAllowed;
if (gender) member.gender = gender;
if (ttr !== undefined) member.ttr = ttr;
if (qttr !== undefined) member.qttr = qttr;
await member.save();
} else {
await Member.create({
@@ -92,15 +85,16 @@ class MemberService {
testMembership: testMembership,
picsInInternetAllowed: picsInInternetAllowed,
gender: gender || 'unknown',
ttr: ttr,
qttr: qttr,
});
}
console.log('[setClubMembers] - return response');
return {
status: 200,
response: { result: "success" },
}
} catch (error) {
console.log(error);
devLog(error);
return {
status: error.statusCode || 500,
response: { error: "nocreation" }
@@ -110,7 +104,7 @@ class MemberService {
async uploadMemberImage(userToken, clubId, memberId, imageBuffer) {
try {
console.log('------>', userToken, clubId, memberId, imageBuffer);
devLog('------>', userToken, clubId, memberId, imageBuffer);
await checkAccess(userToken, clubId);
const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
if (!member) {
@@ -146,6 +140,156 @@ class MemberService {
return { status: 500, error: 'Failed to retrieve image' };
}
}
async updateRatingsFromMyTischtennis(userToken, clubId) {
await checkAccess(userToken, clubId);
const user = await getUserByToken(userToken);
const myTischtennisService = (await import('./myTischtennisService.js')).default;
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
try {
// 1. myTischtennis-Session abrufen
const session = await myTischtennisService.getSession(user.id);
const account = await myTischtennisService.getAccount(user.id);
if (!account) {
console.error('[updateRatingsFromMyTischtennis] - No account found!');
return {
status: 400,
response: {
message: 'Kein myTischtennis-Account gefunden.',
updated: 0,
errors: [],
debug: { userId: user.id }
}
};
}
if (!account.clubId || !account.fedNickname) {
console.error('[updateRatingsFromMyTischtennis] - Missing clubId or fedNickname:', {
clubId: account.clubId,
fedNickname: account.fedNickname
});
return {
status: 400,
response: {
message: 'Club-ID oder Verbandskürzel nicht verfügbar. Bitte einmal einloggen.',
updated: 0,
errors: [],
debug: {
hasClubId: !!account.clubId,
hasFedNickname: !!account.fedNickname,
clubId: account.clubId,
fedNickname: account.fedNickname
}
}
};
}
// 2. Rangliste vom Verein abrufen
const rankings = await myTischtennisClient.getClubRankings(
session.cookie,
account.clubId,
account.fedNickname
);
if (!rankings.success) {
return {
status: 500,
response: {
message: rankings.error || 'Fehler beim Abrufen der Rangliste',
updated: 0,
errors: [],
debug: {
clubId: account.clubId,
fedNickname: account.fedNickname,
rankingsError: rankings.error
}
}
};
}
// 3. Alle Mitglieder des Clubs laden
const members = await Member.findAll({ where: { clubId } });
let updated = 0;
const errors = [];
const notFound = [];
const matched = [];
// 4. Für jedes Mitglied TTR aktualisieren
for (const member of members) {
const firstName = member.firstName;
const lastName = member.lastName;
// Suche nach Match in rankings entries
const rankingEntry = rankings.entries.find(entry =>
entry.firstname.toLowerCase() === firstName.toLowerCase() &&
entry.lastname.toLowerCase() === lastName.toLowerCase()
);
if (rankingEntry) {
try {
// fedRank ist der TTR-Wert
const oldTtr = member.ttr;
member.ttr = rankingEntry.fedRank;
// TODO: QTTR muss von einem anderen Endpoint geholt werden
await member.save();
updated++;
matched.push({
name: `${firstName} ${lastName}`,
oldTtr: oldTtr,
newTtr: rankingEntry.fedRank
});
} catch (error) {
console.error(`[updateRatingsFromMyTischtennis] - Error updating ${firstName} ${lastName}:`, error);
errors.push({
member: `${firstName} ${lastName}`,
error: error.message
});
}
} else {
notFound.push(`${firstName} ${lastName}`);
}
}
devLog(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`);
let message = `${updated} Mitglied(er) aktualisiert.`;
if (notFound.length > 0) {
message += ` ${notFound.length} nicht in myTischtennis-Rangliste gefunden.`;
}
if (errors.length > 0) {
message += ` ${errors.length} Fehler beim Speichern.`;
}
return {
status: 200,
response: {
message: message,
updated: updated,
matched: matched,
notFound: notFound,
errors: errors,
totalEntries: rankings.entries.length,
totalMembers: members.length
}
};
} catch (error) {
console.error('[updateRatingsFromMyTischtennis] - Error:', error);
return {
status: 500,
response: {
message: error.message || 'Fehler beim Aktualisieren',
updated: 0,
errors: [error.message]
}
};
}
}
}
export default new MemberService();

View File

@@ -0,0 +1,241 @@
import MyTischtennis from '../models/MyTischtennis.js';
import User from '../models/User.js';
import myTischtennisClient from '../clients/myTischtennisClient.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class MyTischtennisService {
/**
* Get myTischtennis account for user
*/
async getAccount(userId) {
const account = await MyTischtennis.findOne({
where: { userId },
attributes: ['id', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
});
return account;
}
/**
* Create or update myTischtennis account
*/
async upsertAccount(userId, email, password, savePassword, userPassword) {
// Verify user's app password
const user = await User.findByPk(userId);
if (!user) {
throw new HttpError(404, 'Benutzer nicht gefunden');
}
let loginResult = null;
// Wenn ein Passwort gesetzt/geändert wird, App-Passwort verifizieren
if (password) {
const isValidPassword = await user.validatePassword(userPassword);
if (!isValidPassword) {
throw new HttpError(401, 'Ungültiges Passwort');
}
// Login-Versuch bei myTischtennis
loginResult = await myTischtennisClient.login(email, password);
if (!loginResult.success) {
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.');
}
}
// Find or create account
let account = await MyTischtennis.findOne({ where: { userId } });
const now = new Date();
if (account) {
// Update existing
account.email = email;
account.savePassword = savePassword;
if (password && savePassword) {
account.setPassword(password);
} else if (!savePassword) {
account.encryptedPassword = null;
}
if (loginResult && loginResult.success) {
account.lastLoginAttempt = now;
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-ID und Federation
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
} else {
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
}
} else if (password) {
account.lastLoginAttempt = now;
}
await account.save();
} else {
// Create new
const accountData = {
userId,
email,
savePassword,
lastLoginAttempt: password ? now : null,
lastLoginSuccess: loginResult?.success ? now : null
};
if (loginResult && loginResult.success) {
accountData.accessToken = loginResult.accessToken;
accountData.refreshToken = loginResult.refreshToken;
accountData.expiresAt = loginResult.expiresAt;
accountData.cookie = loginResult.cookie;
accountData.userData = loginResult.user;
// Hole Club-ID
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
if (profileResult.success) {
accountData.clubId = profileResult.clubId;
accountData.clubName = profileResult.clubName;
}
}
account = await MyTischtennis.create(accountData);
if (password && savePassword) {
account.setPassword(password);
await account.save();
}
}
return {
id: account.id,
email: account.email,
savePassword: account.savePassword,
lastLoginAttempt: account.lastLoginAttempt,
lastLoginSuccess: account.lastLoginSuccess,
expiresAt: account.expiresAt
};
}
/**
* Delete myTischtennis account
*/
async deleteAccount(userId) {
const deleted = await MyTischtennis.destroy({
where: { userId }
});
return deleted > 0;
}
/**
* Verify login with stored or provided credentials
*/
async verifyLogin(userId, providedPassword = null) {
const account = await MyTischtennis.findOne({ where: { userId } });
if (!account) {
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
}
let password = providedPassword;
// Wenn kein Passwort übergeben wurde, versuche gespeichertes Passwort zu verwenden
if (!password) {
if (!account.savePassword || !account.encryptedPassword) {
throw new HttpError(400, 'Kein Passwort gespeichert. Bitte geben Sie Ihr Passwort ein.');
}
password = account.getPassword();
}
// Login-Versuch
const now = new Date();
account.lastLoginAttempt = now;
const loginResult = await myTischtennisClient.login(account.email, password);
if (loginResult.success) {
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-ID und Federation
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
} else {
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
}
await account.save();
return {
success: true,
accessToken: loginResult.accessToken,
refreshToken: loginResult.refreshToken,
expiresAt: loginResult.expiresAt,
user: loginResult.user,
clubId: account.clubId,
clubName: account.clubName
};
} else {
await account.save(); // Save lastLoginAttempt
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen');
}
}
/**
* Check if account is configured and ready
*/
async checkAccountStatus(userId) {
const account = await MyTischtennis.findOne({ where: { userId } });
return {
exists: !!account,
hasEmail: !!account?.email,
hasPassword: !!(account?.savePassword && account?.encryptedPassword),
hasValidSession: !!account?.accessToken && account?.expiresAt > Date.now() / 1000,
needsConfiguration: !account || !account.email,
needsPassword: !!account && (!account.savePassword || !account.encryptedPassword)
};
}
/**
* Get stored session for user (for authenticated API requests)
*/
async getSession(userId) {
const account = await MyTischtennis.findOne({ where: { userId } });
if (!account) {
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
}
// Check if session is valid
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
}
return {
accessToken: account.accessToken,
refreshToken: account.refreshToken,
cookie: account.cookie,
expiresAt: account.expiresAt,
userData: account.userData
};
}
}
export default new MyTischtennisService();

View File

@@ -0,0 +1,638 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pdfParse = require('pdf-parse/lib/pdf-parse.js');
import { Op } from 'sequelize';
import Match from '../models/Match.js';
import Team from '../models/Team.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Location from '../models/Location.js';
import { devLog } from '../utils/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class PDFParserService {
/**
* Parst eine PDF-Datei und extrahiert Spiel-Daten
* @param {string} filePath - Pfad zur PDF-Datei
* @param {number} clubId - ID des Vereins
* @returns {Promise<Object>} Geparste Spiel-Daten
*/
static async parsePDF(filePath, clubId) {
try {
if (!fs.existsSync(filePath)) {
throw new Error('PDF-Datei nicht gefunden');
}
// Bestimme Dateityp basierend auf Dateiendung
const fileExtension = path.extname(filePath).toLowerCase();
let fileContent;
if (fileExtension === '.pdf') {
// Echte PDF-Parsing
const pdfBuffer = fs.readFileSync(filePath);
const pdfData = await pdfParse(pdfBuffer);
fileContent = pdfData.text;
} else {
// Fallback für TXT-Dateien (für Tests)
fileContent = fs.readFileSync(filePath, 'utf8');
}
// Parse den Text nach Spiel-Daten
const parsedData = this.extractMatchData(fileContent, clubId);
return parsedData;
} catch (error) {
console.error('[PDFParserService.parsePDF] - Error:', error);
throw error;
}
}
/**
* Extrahiert Spiel-Daten aus dem PDF-Text
* @param {string} text - Der extrahierte Text aus der PDF
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Daten mit Matches und Metadaten
*/
static extractMatchData(text, clubId) {
const matches = [];
const errors = [];
const metadata = {
totalLines: 0,
parsedMatches: 0,
errors: 0
};
try {
// Teile Text in Zeilen auf
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
metadata.totalLines = lines.length;
// Verschiedene Parsing-Strategien je nach PDF-Format
const strategies = [
{ name: 'Standard Format', fn: this.parseStandardFormat },
{ name: 'Table Format', fn: this.parseTableFormat },
{ name: 'List Format', fn: this.parseListFormat }
];
for (const strategy of strategies) {
try {
const result = strategy.fn(lines, clubId);
if (result.matches.length > 0) {
matches.push(...result.matches);
metadata.parsedMatches += result.matches.length;
break; // Erste erfolgreiche Strategie verwenden
}
} catch (strategyError) {
errors.push(`Strategy ${strategy.name} failed: ${strategyError.message}`);
}
}
metadata.errors = errors.length;
return {
matches,
errors,
metadata,
rawText: text.substring(0, 1000), // Erste 1000 Zeichen für Debugging
allLines: lines, // Alle Zeilen für Debugging
debugInfo: {
totalTextLength: text.length,
totalLines: lines.length,
firstFewLines: lines.slice(0, 10),
lastFewLines: lines.slice(-5)
}
};
} catch (error) {
console.error('[PDFParserService.extractMatchData] - Error:', error);
throw error;
}
}
/**
* Standard-Format Parser (Datum, Zeit, Heimteam, Gastteam, Code, Pins)
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseStandardFormat(lines, clubId) {
const matches = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Suche nach Datum-Pattern (dd.mm.yyyy oder dd/mm/yyyy)
const dateMatch = line.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
// Debug: Zeige die gesamte Zeile mit sichtbaren Whitespaces
const debugLine = line.replace(/\s/g, (match) => {
if (match === ' ') return '·'; // Mittelpunkt für normales Leerzeichen
if (match === '\t') return '→'; // Pfeil für Tab
if (match === '\n') return '↵'; // Enter-Zeichen
if (match === '\r') return '⏎'; // Carriage Return
return `[${match.charCodeAt(0)}]`; // Zeichencode für andere Whitespaces
});
try {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
// Suche nach Zeit-Pattern direkt nach dem Datum (hh:mm) - Format: Wt.dd.mm.yyyyhh:MM
const timeMatch = line.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})(\d{1,2}):(\d{2})/);
let time = null;
if (timeMatch) {
time = `${timeMatch[4].padStart(2, '0')}:${timeMatch[5]}`;
}
// Entferne Datum und Zeit vom Anfang der Zeile
const cleanLine = line.replace(/^[A-Za-z]{2}\.(\d{1,2})[./](\d{1,2})[./](\d{4})(\d{1,2}):(\d{2})\s*/, '');
// Entferne Nummerierung am Anfang (z.B. "(1)")
const cleanLine2 = cleanLine.replace(/^\(\d+\)/, '');
// Entferne alle Inhalte in Klammern (z.B. "(J11)")
const cleanLine3 = cleanLine2.replace(/\([^)]*\)/g, '');
// Suche nach Code (12 Zeichen) oder PIN (4 Ziffern) am Ende
const codeMatch = cleanLine3.match(/([A-Z0-9]{12})$/);
const pinMatch = cleanLine3.match(/(\d{4})$/);
let code = null;
let homePin = null;
let guestPin = null;
let teamsPart = cleanLine3;
if (codeMatch) {
// Code gefunden (12 Zeichen)
code = codeMatch[1];
teamsPart = cleanLine3.substring(0, cleanLine3.length - code.length).trim();
} else if (pinMatch) {
// PIN gefunden (4 Ziffern)
const pin = pinMatch[1];
teamsPart = cleanLine3.substring(0, cleanLine3.length - pin.length).trim();
// PIN gehört zu dem Team, das direkt vor der PIN steht
// Analysiere die Position der PIN in der ursprünglichen Zeile
const pinIndex = cleanLine3.lastIndexOf(pin);
const teamsPartIndex = cleanLine3.indexOf(teamsPart);
// Wenn PIN direkt nach dem Teams-Part steht, gehört sie zur Heimmannschaft
// Wenn PIN zwischen den Teams steht, gehört sie zur Gastmannschaft
if (pinIndex === teamsPartIndex + teamsPart.length) {
// PIN steht direkt nach den Teams -> Heimmannschaft
homePin = pin;
} else {
// PIN steht zwischen den Teams -> Gastmannschaft
guestPin = pin;
}
}
if (code || pinMatch) {
// Debug: Zeige Whitespaces als lesbare Zeichen
const debugTeamsPart = teamsPart.replace(/\s/g, (match) => {
if (match === ' ') return '·'; // Mittelpunkt für normales Leerzeichen
if (match === '\t') return '→'; // Pfeil für Tab
return `[${match.charCodeAt(0)}]`; // Zeichencode für andere Whitespaces
});
// Neue Strategie: Teile die Zeile durch mehrere Leerzeichen (wie in der Tabelle)
// Die Struktur ist: Heimmannschaft Gastmannschaft Code
const parts = teamsPart.split(/\s{2,}/); // Mindestens 2 Leerzeichen als Trenner
let homeTeamName = '';
let guestTeamName = '';
if (parts.length >= 2) {
homeTeamName = parts[0].trim();
guestTeamName = parts[1].trim();
// Entferne noch verbleibende Klammern aus den Team-Namen
homeTeamName = homeTeamName.replace(/\([^)]*\)/g, '').trim();
guestTeamName = guestTeamName.replace(/\([^)]*\)/g, '').trim();
// Erkenne römische Ziffern am Ende der Team-Namen
// Römische Ziffern: I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, etc.
const romanNumeralPattern = /\s+(I{1,3}|IV|V|VI{0,3}|IX|X|XI{0,2})$/;
// Prüfe Heimteam auf römische Ziffern
const homeRomanMatch = homeTeamName.match(romanNumeralPattern);
if (homeRomanMatch) {
const romanNumeral = homeRomanMatch[1];
const baseName = homeTeamName.replace(romanNumeralPattern, '').trim();
homeTeamName = `${baseName} ${romanNumeral}`;
}
// Prüfe Gastteam auf römische Ziffern
const guestRomanMatch = guestTeamName.match(romanNumeralPattern);
if (guestRomanMatch) {
const romanNumeral = guestRomanMatch[1];
const baseName = guestTeamName.replace(romanNumeralPattern, '').trim();
guestTeamName = `${baseName} ${romanNumeral}`;
}
} else {
// Fallback: Versuche mit einzelnen Leerzeichen zu trennen
// Strategie 1: Suche nach "Harheimer TC" als Heimteam
if (teamsPart.includes('Harheimer TC')) {
const harheimerIndex = teamsPart.indexOf('Harheimer TC');
homeTeamName = 'Harheimer TC';
guestTeamName = teamsPart.substring(harheimerIndex + 'Harheimer TC'.length).trim();
// Entferne Klammern aus Gastteam
guestTeamName = guestTeamName.replace(/\([^)]*\)/g, '').trim();
} else {
// Strategie 2: Suche nach Großbuchstaben am Anfang des zweiten Teams
const teamSplitMatch = teamsPart.match(/^([A-Za-z0-9\s\-\.]+?)\s+([A-Z][A-Za-z0-9\s\-\.]+)$/);
if (teamSplitMatch) {
homeTeamName = teamSplitMatch[1].trim();
guestTeamName = teamSplitMatch[2].trim();
} else {
continue;
}
}
}
if (homeTeamName && guestTeamName) {
let debugInfo;
if (code) {
debugInfo = `code: "${code}"`;
} else if (homePin && guestPin) {
debugInfo = `homePin: "${homePin}", guestPin: "${guestPin}"`;
} else if (homePin) {
debugInfo = `homePin: "${homePin}"`;
} else if (guestPin) {
debugInfo = `guestPin: "${guestPin}"`;
}
matches.push({
date: date,
time: time,
homeTeamName: homeTeamName,
guestTeamName: guestTeamName,
code: code,
homePin: homePin,
guestPin: guestPin,
clubId: clubId,
rawLine: line
});
}
} else {
}
} catch (parseError) {
}
}
}
return { matches };
}
/**
* Tabellen-Format Parser
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseTableFormat(lines, clubId) {
const matches = [];
// Suche nach Tabellen-Header
let headerIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes('datum') &&
lines[i].toLowerCase().includes('zeit') &&
lines[i].toLowerCase().includes('heim') &&
lines[i].toLowerCase().includes('gast')) {
headerIndex = i;
break;
}
}
if (headerIndex >= 0) {
// Parse Tabellen-Zeilen
for (let i = headerIndex + 1; i < lines.length; i++) {
const line = lines[i];
const columns = line.split(/\s{2,}|\t/); // Split bei mehreren Leerzeichen oder Tabs
if (columns.length >= 4) {
try {
const dateStr = columns[0];
const timeStr = columns[1];
const homeTeam = columns[2];
const guestTeam = columns[3];
const code = columns[4] || null;
const homePin = columns[5] || null;
const guestPin = columns[6] || null;
// Parse Datum
const dateMatch = dateStr.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
matches.push({
date: date,
time: timeStr || null,
homeTeamName: homeTeam.trim(),
guestTeamName: guestTeam.trim(),
code: code ? code.trim() : null,
homePin: homePin ? homePin.trim() : null,
guestPin: guestPin ? guestPin.trim() : null,
clubId: clubId,
rawLine: line
});
}
} catch (parseError) {
}
}
}
}
return { matches };
}
/**
* Listen-Format Parser
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseListFormat(lines, clubId) {
const matches = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Suche nach Nummerierten Listen (1., 2., etc.)
const listMatch = line.match(/^\d+\.\s*(.+)/);
if (listMatch) {
const content = listMatch[1];
// Versuche verschiedene Formate zu parsen
const patterns = [
/(\d{1,2}[./]\d{1,2}[./]\d{4})\s+(\d{1,2}:\d{2})?\s+(.+?)\s+vs?\s+(.+?)(?:\s+code[:\s]*([A-Za-z0-9]+))?(?:\s+home[:\s]*pin[:\s]*([A-Za-z0-9]+))?(?:\s+guest[:\s]*pin[:\s]*([A-Za-z0-9]+))?/i,
/(\d{1,2}[./]\d{1,2}[./]\d{4})\s+(\d{1,2}:\d{2})?\s+(.+?)\s+-\s+(.+?)(?:\s+code[:\s]*([A-Za-z0-9]+))?(?:\s+heim[:\s]*pin[:\s]*([A-Za-z0-9]+))?(?:\s+gast[:\s]*pin[:\s]*([A-Za-z0-9]+))?/i
];
for (const pattern of patterns) {
const match = content.match(pattern);
if (match) {
try {
const [, dateStr, timeStr, homeTeam, guestTeam, code, homePin, guestPin] = match;
// Parse Datum
const dateMatch = dateStr.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
matches.push({
date: date,
time: timeStr || null,
homeTeamName: homeTeam.trim(),
guestTeamName: guestTeam.trim(),
code: code ? code.trim() : null,
homePin: homePin ? homePin.trim() : null,
guestPin: guestPin ? guestPin.trim() : null,
clubId: clubId,
rawLine: line
});
break; // Erste erfolgreiche Pattern verwenden
}
} catch (parseError) {
}
}
}
}
}
return { matches };
}
/**
* Speichert geparste Matches in der Datenbank
* @param {Array} matches - Array von Match-Objekten
* @param {number} leagueId - ID der Liga
* @returns {Promise<Object>} Ergebnis der Speicherung
*/
static async saveMatchesToDatabase(matches, leagueId) {
try {
const results = {
created: 0,
updated: 0,
errors: []
};
for (const matchData of matches) {
try {
let debugInfo;
if (matchData.code) {
debugInfo = `Code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
debugInfo = `HomePin: ${matchData.homePin}, GuestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
debugInfo = `HomePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
debugInfo = `GuestPin: ${matchData.guestPin}`;
}
// Lade alle Matches für das Datum und die Liga
// Konvertiere das Datum zu einem Datum ohne Zeit für den Vergleich
const dateOnly = new Date(matchData.date.getFullYear(), matchData.date.getMonth(), matchData.date.getDate());
const nextDay = new Date(dateOnly);
nextDay.setDate(nextDay.getDate() + 1);
const existingMatches = await Match.findAll({
where: {
date: {
[Op.gte]: dateOnly, // Größer oder gleich dem Datum
[Op.lt]: nextDay // Kleiner als der nächste Tag
},
leagueId: leagueId,
...(matchData.time && { time: matchData.time }) // Füge Zeit hinzu wenn vorhanden
},
include: [
{
model: Team,
as: 'homeTeam',
attributes: ['id', 'name']
},
{
model: Team,
as: 'guestTeam',
attributes: ['id', 'name']
}
]
});
const timeFilter = matchData.time ? ` and time ${matchData.time}` : '';
// Debug: Zeige alle gefundenen Matches und lade Teams manuell
for (let i = 0; i < existingMatches.length; i++) {
const match = existingMatches[i];
// Lade Teams manuell
const homeTeam = await Team.findByPk(match.homeTeamId);
const guestTeam = await Team.findByPk(match.guestTeamId);
// Füge die Teams zum Match-Objekt hinzu
match.homeTeam = homeTeam;
match.guestTeam = guestTeam;
}
// Suche nach dem passenden Match basierend auf Gastmannschaft
const matchingMatch = existingMatches.find(match => {
if (!match.guestTeam) return false;
const guestTeamName = match.guestTeam.name.toLowerCase();
const searchGuestName = matchData.guestTeamName.toLowerCase();
// Exakte Übereinstimmung oder Teilstring-Match
return guestTeamName === searchGuestName ||
guestTeamName.includes(searchGuestName) ||
searchGuestName.includes(guestTeamName);
});
if (matchingMatch) {
// Update das bestehende Match mit Code und Pins
// Erstelle Update-Objekt nur mit vorhandenen Feldern
const updateData = {};
if (matchData.code) {
updateData.code = matchData.code;
}
if (matchData.homePin) {
updateData.homePin = matchData.homePin;
}
if (matchData.guestPin) {
updateData.guestPin = matchData.guestPin;
}
await matchingMatch.update(updateData);
results.updated++;
let updateInfo;
if (matchData.code) {
updateInfo = `code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
updateInfo = `homePin: ${matchData.homePin}, guestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
updateInfo = `homePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
updateInfo = `guestPin: ${matchData.guestPin}`;
}
// Lade das aktualisierte Match neu, um die aktuellen Werte zu zeigen
await matchingMatch.reload();
const currentValues = [];
if (matchingMatch.code) currentValues.push(`code: ${matchingMatch.code}`);
if (matchingMatch.homePin) currentValues.push(`homePin: ${matchingMatch.homePin}`);
if (matchingMatch.guestPin) currentValues.push(`guestPin: ${matchingMatch.guestPin}`);
} else {
// Fallback: Versuche Teams direkt zu finden
const homeTeam = await Team.findOne({
where: {
name: matchData.homeTeamName,
clubId: matchData.clubId
}
});
const guestTeam = await Team.findOne({
where: {
name: matchData.guestTeamName,
clubId: matchData.clubId
}
});
// Debug: Zeige alle verfügbaren Teams für diesen Club
if (!homeTeam || !guestTeam) {
const allTeams = await Team.findAll({
where: { clubId: matchData.clubId },
attributes: ['id', 'name']
});
// Versuche Fuzzy-Matching für Team-Namen
const homeTeamFuzzy = allTeams.find(t =>
t.name.toLowerCase().includes(matchData.homeTeamName.toLowerCase()) ||
matchData.homeTeamName.toLowerCase().includes(t.name.toLowerCase())
);
const guestTeamFuzzy = allTeams.find(t =>
t.name.toLowerCase().includes(matchData.guestTeamName.toLowerCase()) ||
matchData.guestTeamName.toLowerCase().includes(t.name.toLowerCase())
);
if (homeTeamFuzzy) {
}
if (guestTeamFuzzy) {
}
}
if (!homeTeam || !guestTeam) {
let errorInfo;
if (matchData.code) {
errorInfo = `Code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
errorInfo = `HomePin: ${matchData.homePin}, GuestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
errorInfo = `HomePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
errorInfo = `GuestPin: ${matchData.guestPin}`;
}
results.errors.push(`Teams nicht gefunden: "${matchData.homeTeamName}" oder "${matchData.guestTeamName}" (Datum: ${matchData.date.toISOString().split('T')[0]}, Zeit: ${matchData.time}, ${errorInfo})`);
continue;
}
// Erstelle neues Match (Fallback)
await Match.create({
date: matchData.date,
time: matchData.time,
homeTeamId: homeTeam.id,
guestTeamId: guestTeam.id,
leagueId: leagueId,
clubId: matchData.clubId,
code: matchData.code,
homePin: matchData.homePin,
guestPin: matchData.guestPin,
locationId: 1 // Default Location, kann später angepasst werden
});
results.created++;
}
} catch (matchError) {
console.error('[PDFParserService.saveMatchesToDatabase] - Error:', matchError);
results.errors.push(`Fehler beim Speichern von Match: ${matchData.rawLine} - ${matchError.message}`);
}
}
return results;
} catch (error) {
console.error('[PDFParserService.saveMatchesToDatabase] - Error:', error);
throw error;
}
}
}
export default PDFParserService;

View File

@@ -5,9 +5,9 @@ import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import sequelize from '../database.js';
import { Op } from 'sequelize';
import { devLog } from '../utils/logger.js';
class PredefinedActivityService {
async createPredefinedActivity(data) {
console.log('[PredefinedActivityService::createPredefinedActivity] - Creating predefined activity');
return await PredefinedActivity.create({
name: data.name,
code: data.code,
@@ -15,14 +15,13 @@ class PredefinedActivityService {
durationText: data.durationText,
duration: data.duration,
imageLink: data.imageLink,
drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null,
});
}
async updatePredefinedActivity(id, data) {
console.log(`[PredefinedActivityService::updatePredefinedActivity] - Updating predefined activity with id: ${id}`);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
console.log('[PredefinedActivityService::updatePredefinedActivity] - Activity not found');
throw new Error('Predefined activity not found');
}
return await activity.update({
@@ -32,11 +31,11 @@ class PredefinedActivityService {
durationText: data.durationText,
duration: data.duration,
imageLink: data.imageLink,
drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null,
});
}
async getAllPredefinedActivities() {
console.log('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities');
return await PredefinedActivity.findAll({
order: [
[sequelize.literal('code IS NULL'), 'ASC'], // Non-null codes first
@@ -47,10 +46,8 @@ class PredefinedActivityService {
}
async getPredefinedActivityById(id) {
console.log(`[PredefinedActivityService::getPredefinedActivityById] - Fetching predefined activity with id: ${id}`);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
console.log('[PredefinedActivityService::getPredefinedActivityById] - Activity not found');
throw new Error('Predefined activity not found');
}
return activity;
@@ -78,7 +75,6 @@ class PredefinedActivityService {
}
async mergeActivities(sourceId, targetId) {
console.log(`[PredefinedActivityService::mergeActivities] - Merge ${sourceId} -> ${targetId}`);
if (!sourceId || !targetId) throw new Error('sourceId and targetId are required');
if (Number(sourceId) === Number(targetId)) throw new Error('sourceId and targetId must differ');
@@ -118,7 +114,6 @@ class PredefinedActivityService {
}
async deduplicateActivities() {
console.log('[PredefinedActivityService::deduplicateActivities] - Start');
const all = await PredefinedActivity.findAll();
const nameToActivities = new Map();
for (const activity of all) {
@@ -140,7 +135,6 @@ class PredefinedActivityService {
mergedCount++;
}
}
console.log('[PredefinedActivityService::deduplicateActivities] - Done', { mergedCount, groupCount });
return { mergedCount, groupCount };
}
}

View File

@@ -0,0 +1,149 @@
import Season from '../models/Season.js';
import { devLog } from '../utils/logger.js';
class SeasonService {
/**
* Ermittelt die aktuelle Saison basierend auf dem aktuellen Datum
* @returns {string} Saison im Format "2023/2024"
*/
static getCurrentSeasonString() {
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1; // getMonth() ist 0-basiert
// Ab 1. Juli: neue Saison beginnt
if (currentMonth >= 7) {
return `${currentYear}/${currentYear + 1}`;
} else {
return `${currentYear - 1}/${currentYear}`;
}
}
/**
* Holt oder erstellt die aktuelle Saison
* @returns {Promise<Season>} Die aktuelle Saison
*/
static async getOrCreateCurrentSeason() {
try {
const currentSeasonString = this.getCurrentSeasonString();
// Versuche die aktuelle Saison zu finden
let season = await Season.findOne({
where: { season: currentSeasonString }
});
// Falls nicht vorhanden, erstelle sie
if (!season) {
season = await Season.create({
season: currentSeasonString
});
}
return season;
} catch (error) {
console.error('[SeasonService.getOrCreateCurrentSeason] - Error:', error);
throw error;
}
}
/**
* Holt alle verfügbaren Saisons
* @returns {Promise<Array<Season>>} Alle Saisons sortiert nach Name
*/
static async getAllSeasons() {
try {
const seasons = await Season.findAll({
order: [['season', 'DESC']] // Neueste zuerst
});
return seasons;
} catch (error) {
console.error('[SeasonService.getAllSeasons] - Error:', error);
throw error;
}
}
/**
* Erstellt eine neue Saison
* @param {string} seasonString - Saison im Format "2023/2024"
* @returns {Promise<Season>} Die erstellte Saison
*/
static async createSeason(seasonString) {
try {
// Prüfe ob Saison bereits existiert
const existingSeason = await Season.findOne({
where: { season: seasonString }
});
if (existingSeason) {
throw new Error('Season already exists');
}
const season = await Season.create({
season: seasonString
});
return season;
} catch (error) {
console.error('[SeasonService.createSeason] - Error:', error);
throw error;
}
}
/**
* Holt eine Saison nach ID
* @param {number} seasonId - Die Saison-ID
* @returns {Promise<Season|null>} Die Saison oder null
*/
static async getSeasonById(seasonId) {
try {
const season = await Season.findByPk(seasonId);
return season;
} catch (error) {
console.error('[SeasonService.getSeasonById] - Error:', error);
throw error;
}
}
/**
* Löscht eine Saison (nur wenn keine Teams/Ligen damit verknüpft sind)
* @param {number} seasonId - Die Saison-ID
* @returns {Promise<boolean>} True wenn gelöscht, false wenn nicht möglich
*/
static async deleteSeason(seasonId) {
try {
// Prüfe ob Saison verwendet wird
const season = await Season.findByPk(seasonId, {
include: [
{ association: 'teams' },
{ association: 'leagues' }
]
});
if (!season) {
return false;
}
// Prüfe ob Saison verwendet wird
if (season.teams && season.teams.length > 0) {
throw new Error('Season is used by teams');
}
if (season.leagues && season.leagues.length > 0) {
throw new Error('Season is used by leagues');
}
await Season.destroy({
where: { id: seasonId }
});
return true;
} catch (error) {
console.error('[SeasonService.deleteSeason] - Error:', error);
throw error;
}
}
}
export default SeasonService;

View File

@@ -0,0 +1,188 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import TeamDocument from '../models/TeamDocument.js';
import ClubTeam from '../models/ClubTeam.js';
import { devLog } from '../utils/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class TeamDocumentService {
/**
* Speichert ein hochgeladenes Dokument für ein Club-Team
* @param {Object} file - Das hochgeladene File-Objekt (von multer)
* @param {number} clubTeamId - Die ID des Club-Teams
* @param {string} documentType - Der Typ des Dokuments ('code_list' oder 'pin_list')
* @returns {Promise<TeamDocument>} Das erstellte TeamDocument
*/
static async uploadDocument(file, clubTeamId, documentType) {
try {
// Prüfe ob das Club-Team existiert
const clubTeam = await ClubTeam.findByPk(clubTeamId);
if (!clubTeam) {
throw new Error('Club-Team nicht gefunden');
}
// Generiere einen eindeutigen Dateinamen
const fileExtension = path.extname(file.originalname);
const uniqueFileName = `${clubTeamId}_${documentType}_${Date.now()}${fileExtension}`;
// Zielverzeichnis für Team-Dokumente
const uploadDir = path.join(__dirname, '..', 'uploads', 'team-documents');
// Erstelle Upload-Verzeichnis falls es nicht existiert
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const filePath = path.join(uploadDir, uniqueFileName);
// Verschiebe die Datei vom temporären Verzeichnis zum finalen Speicherort
fs.renameSync(file.path, filePath);
// Lösche alte Dokumente des gleichen Typs für dieses Team
await this.deleteDocumentsByType(clubTeamId, documentType);
// Erstelle Datenbankeintrag
const teamDocument = await TeamDocument.create({
fileName: uniqueFileName,
originalFileName: file.originalname,
filePath: filePath,
fileSize: file.size,
mimeType: file.mimetype,
documentType: documentType,
clubTeamId: clubTeamId
});
return teamDocument;
} catch (error) {
console.error('[TeamDocumentService.uploadDocument] - Error:', error);
throw error;
}
}
/**
* Holt alle Dokumente für ein Club-Team
* @param {number} clubTeamId - Die ID des Club-Teams
* @returns {Promise<Array<TeamDocument>>} Liste der Dokumente
*/
static async getDocumentsByClubTeam(clubTeamId) {
try {
const documents = await TeamDocument.findAll({
where: { clubTeamId },
order: [['createdAt', 'DESC']]
});
return documents;
} catch (error) {
console.error('[TeamDocumentService.getDocumentsByClubTeam] - Error:', error);
throw error;
}
}
/**
* Holt ein spezifisches Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<TeamDocument|null>} Das Dokument oder null
*/
static async getDocumentById(documentId) {
try {
const document = await TeamDocument.findByPk(documentId, {
include: [{
model: ClubTeam,
as: 'clubTeam',
attributes: ['id', 'name', 'clubId']
}]
});
return document;
} catch (error) {
console.error('[TeamDocumentService.getDocumentById] - Error:', error);
throw error;
}
}
/**
* Löscht ein Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<boolean>} True wenn gelöscht, sonst false
*/
static async deleteDocument(documentId) {
try {
const document = await TeamDocument.findByPk(documentId);
if (!document) {
return false;
}
// Lösche die physische Datei
if (fs.existsSync(document.filePath)) {
fs.unlinkSync(document.filePath);
}
// Lösche den Datenbankeintrag
const deletedRows = await TeamDocument.destroy({
where: { id: documentId }
});
return deletedRows > 0;
} catch (error) {
console.error('[TeamDocumentService.deleteDocument] - Error:', error);
throw error;
}
}
/**
* Löscht alle Dokumente eines bestimmten Typs für ein Club-Team
* @param {number} clubTeamId - Die ID des Club-Teams
* @param {string} documentType - Der Typ des Dokuments
* @returns {Promise<number>} Anzahl der gelöschten Dokumente
*/
static async deleteDocumentsByType(clubTeamId, documentType) {
try {
const documents = await TeamDocument.findAll({
where: { clubTeamId, documentType }
});
let deletedCount = 0;
for (const document of documents) {
// Lösche die physische Datei
if (fs.existsSync(document.filePath)) {
fs.unlinkSync(document.filePath);
}
deletedCount++;
}
// Lösche die Datenbankeinträge
const deletedRows = await TeamDocument.destroy({
where: { clubTeamId, documentType }
});
return deletedRows;
} catch (error) {
console.error('[TeamDocumentService.deleteDocumentsByType] - Error:', error);
throw error;
}
}
/**
* Holt den Dateipfad für ein Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<string|null>} Der Dateipfad oder null
*/
static async getDocumentPath(documentId) {
try {
const document = await TeamDocument.findByPk(documentId);
return document ? document.filePath : null;
} catch (error) {
console.error('[TeamDocumentService.getDocumentPath] - Error:', error);
throw error;
}
}
}
export default TeamDocumentService;

View File

@@ -0,0 +1,132 @@
import Team from '../models/Team.js';
import League from '../models/League.js';
import Club from '../models/Club.js';
import Season from '../models/Season.js';
import SeasonService from './seasonService.js';
import { devLog } from '../utils/logger.js';
class TeamService {
static async getAllTeamsByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const teams = await Team.findAll({
where: { clubId, seasonId },
include: [
{
model: League,
as: 'league',
attributes: ['id', 'name']
},
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
],
order: [['name', 'ASC']]
});
return teams;
} catch (error) {
console.error('[TeamService.getAllTeamsByClub] - Error:', error);
throw error;
}
}
static async getTeamById(teamId) {
try {
const team = await Team.findByPk(teamId, {
include: [
{
model: League,
as: 'league',
attributes: ['id', 'name']
},
{
model: Club,
as: 'club',
attributes: ['id', 'name']
},
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
]
});
return team;
} catch (error) {
console.error('[TeamService.getTeamById] - Error:', error);
throw error;
}
}
static async createTeam(teamData) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!teamData.seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
teamData.seasonId = currentSeason.id;
}
const team = await Team.create(teamData);
return team;
} catch (error) {
console.error('[TeamService.createTeam] - Error:', error);
throw error;
}
}
static async updateTeam(teamId, updateData) {
try {
const [updatedRowsCount] = await Team.update(updateData, {
where: { id: teamId }
});
return updatedRowsCount > 0;
} catch (error) {
console.error('[TeamService.updateTeam] - Error:', error);
throw error;
}
}
static async deleteTeam(teamId) {
try {
const deletedRowsCount = await Team.destroy({
where: { id: teamId }
});
return deletedRowsCount > 0;
} catch (error) {
console.error('[TeamService.deleteTeam] - Error:', error);
throw error;
}
}
static async getLeaguesByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const leagues = await League.findAll({
where: { clubId, seasonId },
attributes: ['id', 'name', 'seasonId'],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
console.error('[TeamService.getLeaguesByClub] - Error:', error);
throw error;
}
}
}
export default TeamService;

View File

@@ -9,6 +9,7 @@ import { checkAccess } from '../utils/userUtils.js';
import { Op, literal } from 'sequelize';
import { devLog } from '../utils/logger.js';
function getRoundName(size) {
switch (size) {
case 2: return "Finale";
@@ -88,7 +89,7 @@ class TournamentService {
include: [{
model: Member,
as: 'member',
attributes: ['id', 'firstName', 'lastName'],
attributes: ['id', 'firstName', 'lastName', 'ttr', 'qttr'],
}],
order: [[{ model: Member, as: 'member' }, 'firstName', 'ASC']]
});
@@ -186,7 +187,7 @@ class TournamentService {
if (alreadyAssigned.length > 0) {
// Spieler sind bereits manuell zugeordnet - nicht neu verteilen
console.log(`${alreadyAssigned.length} Spieler bereits zugeordnet, ${unassigned.length} noch nicht zugeordnet`);
devLog(`${alreadyAssigned.length} Spieler bereits zugeordnet, ${unassigned.length} noch nicht zugeordnet`);
} else {
// Keine manuellen Zuordnungen - zufällig verteilen
const shuffled = members.slice();
@@ -202,11 +203,8 @@ class TournamentService {
}
// 4) RoundRobin anlegen wie gehabt - NUR innerhalb jeder Gruppe
console.log(`[fillGroups] Erstelle Matches für ${groups.length} Gruppen`);
for (const g of groups) {
console.log(`[fillGroups] Verarbeite Gruppe ${g.id}`);
const gm = await TournamentMember.findAll({ where: { groupId: g.id } });
console.log(`[fillGroups] Gruppe ${g.id} hat ${gm.length} Teilnehmer:`, gm.map(m => ({ id: m.id, name: m.member?.firstName + ' ' + m.member?.lastName })));
if (gm.length < 2) {
console.warn(`Gruppe ${g.id} hat nur ${gm.length} Teilnehmer - keine Matches erstellt`);
@@ -214,10 +212,8 @@ class TournamentService {
}
const rounds = this.generateRoundRobinSchedule(gm);
console.log(`[fillGroups] Gruppe ${g.id} hat ${rounds.length} Runden`);
for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
console.log(`[fillGroups] Runde ${roundIndex + 1} für Gruppe ${g.id}:`, rounds[roundIndex]);
for (const [p1Id, p2Id] of rounds[roundIndex]) {
// Prüfe, ob beide Spieler zur gleichen Gruppe gehören
const p1 = gm.find(p => p.id === p1Id);
@@ -231,7 +227,6 @@ class TournamentService {
player2Id: p2Id,
groupRound: roundIndex + 1
});
console.log(`[fillGroups] Match erstellt: ${match.id} - Spieler ${p1Id} vs ${p2Id} in Gruppe ${g.id}`);
} else {
console.warn(`Spieler gehören nicht zur gleichen Gruppe: ${p1Id} (${p1?.groupId}) vs ${p2Id} (${p2?.groupId}) in Gruppe ${g.id}`);
}

View File

@@ -0,0 +1,7 @@
Spielplan 2025/2026 - Test Liga
1. 15.01.2025 19:00 Team Alpha vs Team Beta code: ABC123 home pin: PIN001 guest pin: PIN002
2. 22.01.2025 20:00 Team Gamma gegen Team Delta code: DEF456 heim pin: PIN003 gast pin: PIN004
3. 29.01.2025 18:30 Team Epsilon - Team Zeta code: GHI789 home pin: PIN005 guest pin: PIN006
4. 05.02.2025 19:30 Team Alpha vs Team Gamma code: JKL012 home pin: PIN007 guest pin: PIN008
5. 12.02.2025 20:00 Team Beta gegen Team Delta code: MNO345 heim pin: PIN009 gast pin: PIN010

19
backend/utils/logger.js Normal file
View File

@@ -0,0 +1,19 @@
// Zentrale Logger-Utility für dev/prod Umgebungen
const isDev = process.env.STAGE === 'dev';
// Debug-Logs nur im dev-Modus
export const devLog = (...args) => isDev && console.log(...args);
// Fehler-Logs immer ausgeben
export const errorLog = (...args) => console.error(...args);
// Info-Logs immer ausgeben (für wichtige Produktions-Events)
export const infoLog = (...args) => console.log(...args);
export default {
devLog,
errorLog,
infoLog,
isDev
};

View File

@@ -2,9 +2,10 @@ import jwt from 'jsonwebtoken';
import { Op } from 'sequelize';
import User from '../models/User.js';
import UserToken from '../models/UserToken.js';
import UserClub from '../models/UserClub.js'; // <-- hier hinzufügen
import UserClub from '../models/UserClub.js';
import HttpError from '../exceptions/HttpError.js';
import { config } from 'dotenv';
import { devLog } from './logger.js';
config(); // sorgt dafür, dass process.env.JWT_SECRET geladen wird
export const getUserByToken = async (token) => {
@@ -41,7 +42,6 @@ export const getUserByToken = async (token) => {
export const hasUserClubAccess = async (userId, clubId) => {
try {
console.log('[hasUserClubAccess]');
const userClub = await UserClub.findOne({
where: {
user_id: userId,
@@ -51,10 +51,9 @@ export const hasUserClubAccess = async (userId, clubId) => {
});
return userClub !== null;
} catch (error) {
console.log(error);
console.error('[hasUserClubAccess] - error:', error);
throw new HttpError('notfound', 500);
}
console.log('---- no user found');
}
export const checkAccess = async (userToken, clubId) => {
@@ -62,11 +61,9 @@ export const checkAccess = async (userToken, clubId) => {
const user = await getUserByToken(userToken);
const hasAccess = await hasUserClubAccess(user.id, clubId);
if (!hasAccess) {
console.log('no club access');
throw new HttpError('noaccess', 403);
}
} catch (error) {
console.log(error);
throw error;
}
};
@@ -76,7 +73,6 @@ export const checkGlobalAccess = async (userToken) => {
const user = await getUserByToken(userToken);
return user; // Einfach den User zurückgeben, da globale Zugriffe nur Authentifizierung benötigen
} catch (error) {
console.log(error);
throw error;
}
};

View File

@@ -65,6 +65,20 @@
<span class="nav-icon"></span>
Vordefinierte Aktivitäten
</a>
<a href="/team-management" class="nav-link">
<span class="nav-icon">👥</span>
Team-Verwaltung
</a>
</div>
</nav>
<nav class="sidebar-footer">
<div class="nav-section">
<h4 class="nav-title">Einstellungen</h4>
<a href="/mytischtennis-account" class="nav-link">
<span class="nav-icon">🔗</span>
myTischtennis-Account
</a>
</div>
</nav>
@@ -123,7 +137,7 @@ export default {
if (newVal === 'new') {
this.$router.push('/createclub');
} else if (newVal) {
this.$router.push(`/showclub/${newVal}`);
this.$router.push('/training-stats');
}
},
isAuthenticated(newVal) {
@@ -160,8 +174,8 @@ export default {
},
loadClub() {
this.setCurrentClub(this.currentClub);
this.$router.push(`/showclub/${this.currentClub}`);
this.setCurrentClub(this.selectedClub);
this.$router.push('/training-stats');
},
async checkSession() {
@@ -331,6 +345,15 @@ export default {
overflow-y: auto;
}
.nav-menu-no-flex {
display: flex;
flex-direction: column;
gap: 0.75rem;
flex: 0;
min-height: 0;
overflow-y: auto;
}
.nav-section {
display: flex;
flex-direction: column;

View File

@@ -0,0 +1,439 @@
<template>
<div class="render-container">
<canvas
ref="canvas"
:width="config.canvas.width"
:height="config.canvas.height"
style="margin:0 auto;"
></canvas>
</div>
</template>
<script>
export default {
name: 'CourtDrawingRender',
props: {
drawingData: {
type: Object,
required: true
},
width: {
type: Number,
default: 600
},
height: {
type: Number,
default: 400
}
},
data() {
return {
canvas: null,
ctx: null,
config: {
// Mehr Platz links für die drei Startkreise (Rand außerhalb des Tisches)
canvas: { width: 640, height: 400 },
table: {
// Tabelle etwas schmaler und weiter rechts platzieren
width: 500,
height: 300,
color: '#2d5a2d',
borderWidth: 2,
borderColor: '#000000',
outerFrameWidth: 5,
outerFrameColor: '#ffffff',
outerFrameBorderWidth: 0.5,
outerFrameBorderColor: '#000000'
},
horizontalLine: { color: '#cfd8dc', width: 2, gap: 10, edgeMargin: 10 },
net: { color: '#ffffff', width: 4, overhang: 14, borderColor: '#000000', borderWidth: 0.5 },
serviceLine: { color: 'rgba(255,255,255,0.0)', width: 0 },
startCircles: {
radius: 10,
// Abstand vom linken Canvas-Rand (größer = weiter links, da wir von tableX abziehen)
x: 20,
topYOffset: 35,
bottomYOffset: 35,
selectedColor: '#ff4444',
unselectedColor: '#9e9e9e',
selectedBorderColor: '#ffffff',
unselectedBorderColor: '#555555',
selectedBorderWidth: 2,
unselectedBorderWidth: 1
},
arrows: {
primaryColor: '#d32f2f', // rechts -> target (rot)
secondaryColor: '#1565c0', // zurück (blau)
width: 6,
headLength: 24,
vhOffsetX: 5,
vhOffsetY: 8,
rhOffsetX: 10,
rhOffsetY: 0
},
targetCircles: {
radius: 13,
rightXOffset: 20,
middleXOffset: 40,
topYOffset: 45,
bottomYOffset: 45,
transparency: 0.25
},
leftTargetCircles: {
radius: 10,
leftXOffset: 20,
rightXOffset: 40,
topYOffset: 40,
bottomYOffset: 40
},
hitMarker: {
radius: 10.5,
fill: '#ffffff',
stroke: '#000000',
lineWidth: 1
}
}
};
},
watch: {
drawingData: {
handler() {
this.redraw();
},
deep: true,
immediate: true
}
},
mounted() {
// apply custom dimensions if provided
this.config.canvas.width = this.width || 600;
this.config.canvas.height = this.height || 400;
this.$nextTick(() => {
this.init();
this.redraw();
});
},
methods: {
init() {
this.canvas = this.$refs.canvas;
if (this.canvas) {
this.ctx = this.canvas.getContext('2d');
}
},
redraw() {
if (!this.ctx) return;
const { width, height } = this.config.canvas;
// clear and background
this.ctx.clearRect(0, 0, width, height);
this.ctx.fillStyle = '#f0f0f0';
this.ctx.fillRect(0, 0, width, height);
this.drawTable();
this.drawStartCircles();
this.drawArrowsAndLabels();
},
drawTable() {
const ctx = this.ctx;
const { table, horizontalLine, net, outerFrameWidth } = {
...this.config,
outerFrameWidth: this.config.table.outerFrameWidth
};
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
// table fill
ctx.fillStyle = table.color;
ctx.fillRect(tableX, tableY, tableWidth, tableHeight);
// table stroke
ctx.strokeStyle = table.borderColor;
ctx.lineWidth = table.borderWidth;
ctx.strokeRect(tableX, tableY, tableWidth, tableHeight);
// outer white frame
ctx.strokeStyle = table.outerFrameColor;
ctx.lineWidth = table.outerFrameWidth;
ctx.strokeRect(
tableX - table.outerFrameWidth,
tableY - table.outerFrameWidth,
tableWidth + table.outerFrameWidth * 2,
tableHeight + table.outerFrameWidth * 2
);
// black thin frame around white
ctx.strokeStyle = table.outerFrameBorderColor;
ctx.lineWidth = table.outerFrameBorderWidth;
ctx.strokeRect(
tableX - table.outerFrameWidth - table.outerFrameBorderWidth,
tableY - table.outerFrameWidth - table.outerFrameBorderWidth,
tableWidth + (table.outerFrameWidth + table.outerFrameBorderWidth) * 2,
tableHeight + (table.outerFrameWidth + table.outerFrameBorderWidth) * 2
);
// horizontal split line (two segments)
ctx.strokeStyle = horizontalLine.color;
ctx.lineWidth = horizontalLine.width;
const centerY = tableY + tableHeight / 2;
const centerX = tableX + tableWidth / 2;
const gap = horizontalLine.gap;
const edge = horizontalLine.edgeMargin;
ctx.beginPath();
ctx.moveTo(tableX + edge, centerY);
ctx.lineTo(centerX - gap, centerY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX + gap, centerY);
ctx.lineTo(tableX + tableWidth - edge, centerY);
ctx.stroke();
// net vertical
ctx.strokeStyle = net.color;
ctx.lineWidth = net.width;
ctx.beginPath();
ctx.moveTo(centerX, tableY - net.overhang);
ctx.lineTo(centerX, tableY + tableHeight + net.overhang);
ctx.stroke();
ctx.strokeStyle = net.borderColor;
ctx.lineWidth = net.borderWidth;
ctx.beginPath();
ctx.moveTo(centerX, tableY - net.overhang);
ctx.lineTo(centerX, tableY + tableHeight + net.overhang);
ctx.stroke();
},
drawStartCircles() {
const ctx = this.ctx;
const cfg = this.config.startCircles;
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
// Startkreis links VOR dem Tisch positionieren nur den Kreis zeichnen,
// von dem auch der rote Pfeil startet
const circleX = tableX - cfg.x; // Links vom Tisch
const topY = tableY + cfg.topYOffset;
const midY = tableY + tableHeight / 2;
const botY = tableY + tableHeight - cfg.bottomYOffset;
// Mapping und Fallback: bei "AS" auf 'middle'
const map = { AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' };
const selKey = this.drawingData?.selectedStartPosition || 'AS';
const selectedPos = map[selKey] || 'middle';
const y = selectedPos === 'top' ? topY : selectedPos === 'bottom' ? botY : midY;
ctx.fillStyle = cfg.selectedColor;
ctx.beginPath();
ctx.arc(circleX, y, cfg.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = cfg.selectedBorderColor;
ctx.lineWidth = cfg.selectedBorderWidth;
ctx.stroke();
},
computeRightTargetPosition(number) {
// Geometrie wie im Tool: nutzt rightXOffset/middleXOffset
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
const cfg = this.config.targetCircles;
const x1 = tableX + tableWidth - cfg.rightXOffset; // (1,2,3)
const x3 = tableX + tableWidth / 2 + cfg.middleXOffset; // (7,8,9)
const xdiff = x3 - x1;
const x2 = x3 - xdiff / 2; // (4,5,6)
const positions = {
1: { x: x1, y: tableY + cfg.topYOffset },
2: { x: x1, y: tableY + tableHeight / 2 },
3: { x: x1, y: tableY + tableHeight - cfg.bottomYOffset },
4: { x: x2, y: tableY + cfg.topYOffset },
5: { x: x2, y: tableY + tableHeight / 2 },
6: { x: x2, y: tableY + tableHeight - cfg.bottomYOffset },
7: { x: x3, y: tableY + cfg.topYOffset },
8: { x: x3, y: tableY + tableHeight / 2 },
9: { x: x3, y: tableY + tableHeight - cfg.bottomYOffset }
};
return positions[number] || null;
},
getStartPoint() {
// Startpunkt wie im Tool abhängig von Schlagseite (VH/RH)
const sc = this.config.startCircles;
const ar = this.config.arrows;
const tblW = this.config.table.width;
const tblH = this.config.table.height;
const tableX = (this.config.canvas.width - tblW) / 2;
const tableY = (this.config.canvas.height - tblH) / 2;
const circleX = tableX - sc.x; // Kreis links vor dem Tisch
const map = { AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' };
const pos = map[this.drawingData?.selectedStartPosition] || 'middle';
const y = pos === 'top' ? tableY + sc.topYOffset : pos === 'bottom' ? tableY + tblH - sc.bottomYOffset : tableY + tblH / 2;
const isVH = (this.drawingData?.strokeType || 'VH') === 'VH';
const startX = isVH
? circleX + sc.radius + ar.vhOffsetX
: circleX + sc.radius + ar.rhOffsetX;
// VH: unterhalb seitlich (circleRadius + vhOffsetY), RH: rechts (rhOffsetY = 0)
const startYOffset = isVH ? (sc.radius + ar.vhOffsetY) : ar.rhOffsetY;
return { x: startX, y: y + startYOffset };
},
drawArrow(ctx, from, to, color, label) {
const { width, headLength } = this.config.arrows;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = width;
// line
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
// arrow head
const angle = Math.atan2(to.y - from.y, to.x - from.x);
ctx.beginPath();
ctx.moveTo(to.x, to.y);
ctx.lineTo(to.x - headLength * Math.cos(angle - Math.PI / 6), to.y - headLength * Math.sin(angle - Math.PI / 6));
ctx.lineTo(to.x - headLength * Math.cos(angle + Math.PI / 6), to.y - headLength * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fill();
},
drawLabelBelow(ctx, text, anchor) {
if (!text) return;
ctx.font = 'bold 13px Helvetica';
const padding = 6;
const yOffset = 23; // 5px tiefer
const metrics = ctx.measureText(text);
const textWidth = metrics.width;
let x = anchor.x - textWidth / 2; // zentriert unter dem Punkt
let y = anchor.y + yOffset;
// clamp innerhalb Canvas
x = Math.max(4, Math.min(this.config.canvas.width - textWidth - 4, x));
y = Math.max(14, Math.min(this.config.canvas.height - 4, y));
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
ctx.strokeText(text, x, y);
ctx.fillText(text, x, y);
},
drawArrowsAndLabels() {
if (!this.drawingData) return;
const ctx = this.ctx;
const from = this.getStartPoint();
// First arrow: to right target
const tp = Number(this.drawingData.targetPosition);
if (tp) {
const to = this.computeRightTargetPosition(tp);
// Zielmarkierung (unter dem Pfeilkopf)
this.drawHitMarker(ctx, to);
const strokeSide = this.drawingData.strokeType || '';
const spinAbbrev = this.abbrevSpin(this.drawingData.spinType);
// Text gehört an die Quelle (ohne "target")
const sourceLabel = `${strokeSide} ${spinAbbrev}`.trim();
const toEnd = { x: to.x - this.config.targetCircles.radius, y: to.y };
this.drawArrow(ctx, from, toEnd, this.config.arrows.primaryColor);
// Unter dem Startkreis beschriften
const startCenter = this.getStartCircleCenter();
this.drawLabelBelow(ctx, sourceLabel, startCenter);
}
// Second arrow (optional): from right source to left target
const leftTarget = this.drawingData.nextStrokeTargetPosition ? Number(this.drawingData.nextStrokeTargetPosition) : null;
if (tp && leftTarget) {
// source near previous right target
const sourceRightCenter = this.computeRightTargetPosition(tp);
// left target mapping: mirror scheme to left half
const toLeftCenter = this.computeLeftTargetPosition(leftTarget);
// Zielmarkierung links
this.drawHitMarker(ctx, toLeftCenter);
const side = this.drawingData.nextStrokeSide || '';
const type = this.drawingData.nextStrokeType || '';
// Text gehört ans Ziel (ohne "extra target")
const targetLabel = `${side} ${type}`.trim();
const sourceRight = { x: sourceRightCenter.x - this.config.targetCircles.radius, y: sourceRightCenter.y };
const toLeft = { x: toLeftCenter.x + this.config.leftTargetCircles.radius, y: toLeftCenter.y };
this.drawArrow(ctx, sourceRight, toLeft, this.config.arrows.secondaryColor);
// Unter dem rechten Ziel (target der ersten Linie) beschriften
this.drawLabelBelow(ctx, targetLabel, sourceRightCenter);
}
},
getStartCircleCenter() {
const cfg = this.config.startCircles;
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
const circleX = tableX - cfg.x;
const map = { AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' };
const selKey = this.drawingData?.selectedStartPosition || 'AS';
const selectedPos = map[selKey] || 'middle';
const topY = tableY + cfg.topYOffset;
const midY = tableY + tableHeight / 2;
const botY = tableY + tableHeight - cfg.bottomYOffset;
const y = selectedPos === 'top' ? topY : selectedPos === 'bottom' ? botY : midY;
return { x: circleX, y };
},
drawHitMarker(ctx, pos) {
if (!pos) return;
const mk = this.config.hitMarker;
ctx.beginPath();
ctx.arc(pos.x, pos.y, mk.radius, 0, Math.PI * 2);
ctx.fillStyle = mk.fill;
ctx.fill();
ctx.lineWidth = mk.lineWidth;
ctx.strokeStyle = mk.stroke;
ctx.stroke();
},
computeLeftTargetPosition(number) {
// Spiegelung wie im Tool: nutzt leftTargetCircles Offsets
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
const cfg = this.config.leftTargetCircles;
const x1 = tableX + cfg.leftXOffset; // linke Spalte (Lang)
const x3 = tableX + tableWidth / 2 - cfg.rightXOffset; // nah am Netz (Kurz)
const xdiff = x3 - x1;
const x2 = x3 - xdiff / 2;
// Gespiegelte Y-Zuordnung wie im Tool:
// 1,4,7 = unten (VH gespiegelt)
// 2,5,8 = mitte
// 3,6,9 = oben (RH gespiegelt)
const positions = {
1: { x: x1, y: tableY + tableHeight - cfg.bottomYOffset },
2: { x: x1, y: tableY + tableHeight / 2 },
3: { x: x1, y: tableY + cfg.topYOffset },
4: { x: x2, y: tableY + tableHeight - cfg.bottomYOffset },
5: { x: x2, y: tableY + tableHeight / 2 },
6: { x: x2, y: tableY + cfg.topYOffset },
7: { x: x3, y: tableY + tableHeight - cfg.bottomYOffset },
8: { x: x3, y: tableY + tableHeight / 2 },
9: { x: x3, y: tableY + cfg.topYOffset }
};
return positions[number] || null;
},
abbrevSpin(spin) {
if (!spin) return '';
const map = {
Unterschnitt: 'US',
Überschnitt: 'OS',
Seitschnitt: 'SR',
Seitunterschnitt: 'SU',
Gegenläufer: 'GL'
};
return map[spin] || spin;
}
}
};
</script>
<style scoped>
.render-container {
width: 100%;
}
canvas { display: block; max-width: 100%; height: auto; }
</style>

Some files were not shown because too many files have changed in this diff Show More