262 Commits
httv ... main

Author SHA1 Message Date
438029a3a4 Merge pull request 'chore(deps): update dependency eslint to v9.39.2' (#3) from renovate/eslint-monorepo into main
Reviewed-on: #3
2025-12-19 16:15:43 +01:00
c58491c97a Merge pull request 'chore(deps): update dependency @vitejs/plugin-vue to v6.0.3' (#2) from renovate/vitejs-plugin-vue-6.x-lockfile into main
Reviewed-on: #2
2025-12-19 16:15:34 +01:00
1d9b9dbc45 chore(deps): update dependency eslint to v9.39.2 2025-12-19 16:13:52 +01:00
dc791dc33d chore(deps): update dependency @vitejs/plugin-vue to v6.0.3 2025-12-19 16:13:48 +01:00
57fbbff353 Merge pull request 'chore: Configure Renovate' (#1) from renovate/configure into main
Reviewed-on: #1
2025-12-19 16:08:23 +01:00
b00a35af30 Add renovate.json 2025-12-19 15:59:45 +01:00
Torsten Schulz (local)
dd0f29124c feat(tournament): add player details dialog and enhance player name interactions
- Implemented clickable player names in the TournamentPlacementsTab for improved user experience.
- Added a PlayerDetailsDialog component to display detailed player information when names are clicked.
- Updated localization files to include new strings for player details.
- Enhanced data handling for internal and external participants in player dialog logic.
2025-12-17 14:31:36 +01:00
Torsten Schulz (local)
dc084806ab feat(tournament): add group match creation and enhance match handling
- Implemented createGroupMatches function to generate matches for existing groups without altering group assignments.
- Updated resetMatches function to support optional class filtering when resetting group matches.
- Enhanced frontend components to filter and display group matches based on selected class, improving user experience.
- Adjusted tournament results display to reflect accurate match statistics, including wins and losses.
2025-12-17 13:38:40 +01:00
Torsten Schulz (local)
4b4c48a50f feat(tournament): refine external participant handling in match assignments
- Updated logic to replace null player1 and player2 with external participants, ensuring that the assigned objects do not include the member field.
- Created clean player objects for external participants, maintaining essential attributes while omitting sensitive information.
2025-12-17 08:52:38 +01:00
Torsten Schulz (local)
65acc9e0d5 feat(tournament): enhance validation of tournament members and external participants
- Implemented logic to load and validate TournamentMember and ExternalTournamentParticipant IDs for matches.
- Updated checks to set player1 and player2 to null if they belong to external participants or do not match valid tournament members.
2025-12-17 08:50:05 +01:00
Torsten Schulz (local)
13cd55c051 feat(tournament): validate tournament members and load external participants
- Added logic to check if loaded TournamentMembers belong to the current tournament, setting them to null if not.
- Updated filtering for external participant IDs to ensure only valid IDs are processed for matches with null players.
2025-12-17 08:45:52 +01:00
Torsten Schulz (local)
9bf37399d5 feat(tournament): improve result handling and display for matches and participants 2025-12-15 21:08:38 +01:00
Torsten Schulz (local)
047b1801b3 feat(tournament): enhance tournament configuration and results handling
- Updated TournamentConfigTab.vue to conditionally disable target type selection based on final stage type.
- Improved logic for determining target type and group count based on stage configuration.
- Refactored TournamentPlacementsTab.vue to streamline class and group placements display, including better handling of class visibility and player names.
- Enhanced TournamentResultsTab.vue to handle 'BYE' results and limit displayed entries to top three.
- Modified TournamentTab.vue to robustly determine match winners and losers, including handling 'BYE' scenarios and ensuring accurate knockout progression.
- Added logic to reset knockout matches with optional class filtering.
2025-12-15 15:36:18 +01:00
Torsten Schulz (local)
945ec0d48c feat(tournament): implement multi-stage tournament support with intermediate and final stages
- Added backend controller for tournament stages with endpoints to get, upsert, and advance stages.
- Created database migration for new tables: tournament_stage and tournament_stage_advancement.
- Updated models for TournamentStage and TournamentStageAdvancement.
- Enhanced frontend components to manage tournament stages, including configuration for intermediate and final rounds.
- Implemented logic for saving and advancing tournament stages, including handling of pool rules and third place matches.
- Added error handling and loading states in the frontend for better user experience.
2025-12-14 06:46:00 +01:00
Torsten Schulz (local)
e83bc250a8 Erlaube das Hinzufügen von Teilnehmern ohne Klasse und normalisiere die Anzahl der Gruppen auf mindestens 1 in der Turnierverwaltung 2025-12-13 12:25:17 +01:00
Torsten Schulz (local)
0c28b12978 Enhance HTTPS server setup and logging for Socket.IO
- Added detailed logging for SSL certificate loading and server status checks.
- Implemented error handling for port conflicts, providing guidance on resolving issues.
- Introduced a verification step to confirm server activation after startup.
- Improved fallback behavior for Socket.IO when SSL certificates are not found.
2025-12-01 08:43:31 +01:00
Torsten Schulz (local)
5aa11151cf Improve Socket.IO logging and connection handling
- Enhanced logging during WebSocket upgrade attempts by including specific headers for better debugging.
- Updated frontend socket service to allow forced upgrade attempts after successful polling connections, improving connection reliability.
2025-12-01 08:14:26 +01:00
Torsten Schulz (local)
a651113dee Enhance Socket.IO integration and improve error handling
- Updated CORS configuration for Socket.IO to allow all origins and added specific allowed headers.
- Improved error handling for Socket.IO connections, including detailed logging for connection errors and upgrade attempts.
- Implemented cleanup logic for socket connections during page reloads to prevent stale connections.
- Enhanced reconnection logic with unlimited attempts and improved logging for connection status.
- Updated frontend socket service to manage club room joining and leaving more effectively, with better state handling.
- Configured Vite for improved hot module replacement (HMR) settings to support local development.
2025-11-29 00:52:29 +01:00
Torsten Schulz (local)
bf0d5b0935 Refactor TournamentPlacementsTab to use groupedRankingList and improve final placements display
- Updated the final placements logic to utilize groupedRankingList for better performance and accuracy.
- Enhanced the display of class placements, including handling cases for entries without a class.
- Improved the no placements message condition to reflect the new data structure.
- Added a new computed method to retrieve player names from entry objects, enhancing clarity in the UI.
2025-11-29 00:28:47 +01:00
Torsten Schulz (local)
6acdcfa5c3 Add placements tab and localization support in TournamentTab
- Introduced a new tab for displaying tournament placements in the TournamentTab component.
- Added localization entries for placements in the German language JSON file, enhancing the user interface for German-speaking users.
- Updated the component structure to include the new TournamentPlacementsTab and ensure proper rendering based on the active tab.
2025-11-29 00:23:34 +01:00
Torsten Schulz (local)
dc2c60cefe Implement tournament pairing functionality and enhance participant management
- Introduced new endpoints for managing tournament pairings, including creating, updating, and deleting pairings.
- Updated the tournament service to handle pairing logic, ensuring validation for participants and preventing duplicate pairings.
- Enhanced participant management by adding class-based checks for gender and age restrictions when adding participants.
- Updated the tournament controller and routes to support the new pairing features and improved participant handling.
- Added localization support for new UI elements related to pairings in the frontend, enhancing user experience.
2025-11-29 00:15:01 +01:00
Torsten Schulz (local)
bdbbb88be9 Enhance MemberGalleryDialog to manage initial load state for gallery members. Introduce isInitialLoad flag to control gallery member loading behavior, ensuring size updates only occur on the first load. Reset isInitialLoad on dialog close for consistent state management. 2025-11-26 17:15:42 +01:00
Torsten Schulz (local)
e6146b8f5a Add participant assignment to groups functionality
Implement a new endpoint to assign participants to specific groups within tournaments. This includes the addition of the `assignParticipantToGroup` function in the tournament controller, which handles the assignment logic and emits relevant events. Update the tournament routes to include this new functionality. Enhance the tournament service to manage group assignments and ensure proper statistics are calculated for participants. Additionally, update the frontend to support adding participants, including external ones, and reflect changes in the UI for group assignments.
2025-11-23 17:09:41 +01:00
Torsten Schulz (local)
f7a799ea7f Implement login page proxy and CAPTCHA handling in MyTischtennisClient and Controller. Enhance login process with CAPTCHA token extraction and error handling. Update frontend to support iframe-based login and improve user experience with loading indicators. 2025-11-23 15:18:53 +01:00
Torsten Schulz (local)
b74cb30cf6 Enhance error handling in MyTischtennisClient and MyTischtennisService. Include status codes in error messages for better debugging and consistency across login failure responses. 2025-11-21 17:02:25 +01:00
Torsten Schulz (local)
0d2dfd9a07 Revert: Restore Vite 7.2.4 after Node.js upgrade to 20.19+ 2025-11-21 14:23:31 +01:00
Torsten Schulz (local)
61e5efadb8 Downgrade Vite to 6.0.0 for Node.js 20.17 compatibility on server 2025-11-21 14:20:12 +01:00
Torsten Schulz (local)
88d050392f Refactor localization handling in MemberTransferSettingsView to align with updated i18n methods. Update translation calls and placeholders for improved consistency and clarity in rendering across the component. 2025-11-21 14:17:20 +01:00
Torsten Schulz (local)
08b0be78ad Add server check script for i18n fixes 2025-11-21 14:15:21 +01:00
Torsten Schulz (local)
b0e610f3ab Fix: Replace all $t() calls with t() in PermissionsView and LogsView templates and add t to return statements 2025-11-21 14:12:42 +01:00
Torsten Schulz (local)
0285c05fa6 Fix: Replace all $t() calls with t() in TeamManagementView template to match Composition API setup 2025-11-21 14:10:01 +01:00
Torsten Schulz (local)
5d4f2ebd4b Update localization handling in TeamManagementView to use the new i18n method. Replace all instances of the translation function with the updated syntax for improved consistency and maintainability across the component. 2025-11-21 13:56:08 +01:00
Torsten Schulz (local)
bfa908ac9a Refactor localization handling in SeasonSelector and TeamManagementView to utilize the new i18n import method. Update German localization by removing escaped characters from placeholders for improved clarity and consistency in rendering. Ensure all translation calls are updated to use the new method for better maintainability. 2025-11-21 13:51:14 +01:00
Torsten Schulz (local)
9592459348 Enhance German localization for member transfer template by updating import template placeholder to include explicit quotes. Modify MemberTransferSettingsView to replace placeholders with the new format, ensuring accurate rendering in the UI. 2025-11-21 13:20:17 +01:00
Torsten Schulz (local)
47f53ee3fd Update German localization in member transfer templates to use explicit placeholders. Modify MemberTransferSettingsView to replace placeholder text with new format, ensuring accurate rendering in the UI. 2025-11-21 13:15:42 +01:00
Torsten Schulz (local)
c22f4016cc Refactor placeholders method in MemberTransferSettingsView to improve localization handling. Implement fallback German text when translation function is unavailable, ensuring consistent user experience across different scenarios. 2025-11-21 12:14:37 +01:00
Torsten Schulz (local)
2458ba2d37 Upgrade '@vitejs/plugin-vue' to version 6.0.2 and 'vite' to version 7.2.4 in package.json for improved compatibility. Add '@rolldown/pluginutils' as a new dependency in package-lock.json. 2025-11-21 12:02:07 +01:00
Torsten Schulz (local)
6eb42812fd Downgrade 'vite' package version to 6.0.0 in package.json to maintain compatibility with existing project dependencies. 2025-11-21 12:00:29 +01:00
Torsten Schulz (local)
938ce4d991 Improve error handling in MemberTransferSettingsView for translation availability, ensuring fallback text is provided when localization is unavailable. Update comments for clarity and maintain consistent localization handling across bulk wrapper, template, and import template placeholder texts. 2025-11-21 11:58:47 +01:00
Torsten Schulz (local)
cb6e84945b Enhance German localization for member transfer template placeholder by replacing escaped curly braces with placeholders. Update MemberTransferSettingsView to reflect these changes, ensuring proper rendering of the template in the UI. 2025-11-21 11:50:52 +01:00
Torsten Schulz (local)
8c6be234c6 Update package dependencies and enhance German localization for member transfer template placeholder
This commit updates the 'vite' package version to 7.2.4 and upgrades various esbuild dependencies to version 0.25.12. Additionally, it improves the German localization for the member transfer template placeholder in the MemberTransferSettingsView, ensuring proper rendering of escaped curly braces in the translation text.
2025-11-21 11:48:51 +01:00
Torsten Schulz (local)
fe160420c1 Update German localization for member transfer template placeholder and enhance MemberTransferSettingsView to improve error handling for translation availability 2025-11-21 11:47:02 +01:00
Torsten Schulz (local)
167e3ba3ec Refactor MemberTransferSettingsView to use a computed property for the import template placeholder text, improving localization handling and avoiding issues with vue-i18n placeholders. 2025-11-21 11:40:34 +01:00
Torsten Schulz (local)
9455b5d65a Update German localization for member transfer template to reflect new structure and adjust MemberTransferSettingsView for placeholder rendering 2025-11-21 11:38:13 +01:00
Torsten Schulz (local)
e6627a897e Refactor MemberTransferSettingsView to utilize computed properties for bulk wrapper and template descriptions, enhancing localization handling with placeholders. 2025-11-21 11:30:24 +01:00
Torsten Schulz (local)
71fc85427b Update German localization for bulk wrapper and template descriptions to use placeholders, and adjust MemberTransferSettingsView to reflect these changes. 2025-11-21 09:53:15 +01:00
Torsten Schulz (local)
76597a4360 Update German localization for bulk wrapper placeholder and refactor MemberTransferSettingsView to use a computed property for placeholder text 2025-11-21 09:35:56 +01:00
Torsten Schulz (local)
4f9761efb0 Update MyTischtennis model to use LONGTEXT for encrypted fields and enhance TeamManagementView with season change handling and async loading 2025-11-21 09:31:43 +01:00
Torsten Schulz (local)
51e47cf9f9 Refactor global error handling in server and improve logging in myTischtennisService
This commit moves the global error handling middleware in server.js to ensure it is applied after all routes. It also enhances the error handling in myTischtennisService by adding detailed logging for login attempts and failures, improving the visibility of issues during the login process. Additionally, the decryptData function in encrypt.js is updated to check if data is already decrypted, enhancing its robustness. Frontend changes include a minor adjustment to the button type in MyTischtennisAccount.vue for better accessibility.
2025-11-21 09:17:48 +01:00
Torsten Schulz (local)
0525f7908d Refactor localization handling in frontend components to use centralized i18n instance
This commit updates the localization implementation in App.vue, LogsView.vue, and PermissionsView.vue by replacing the use of `useI18n` with a direct import of the i18n instance. This change simplifies the translation function calls and ensures consistency across the application.
2025-11-21 09:03:53 +01:00
Torsten Schulz (local)
a4d89374b7 Remove unused localization keys from German translation files and update PermissionsView to include currentClub property 2025-11-21 09:00:46 +01:00
Torsten Schulz (local)
de907df092 Enhance API client configuration with timeout and error handling improvements
This commit updates the API client configuration to include a 60-second timeout for requests, allows for a maximum of 5 redirects, and modifies the status validation to handle all status codes. Additionally, it improves error handling by checking for network errors and preventing automatic logout on timeouts or network issues, enhancing the robustness of the API interactions.
2025-11-17 11:54:38 +01:00
Torsten Schulz (local)
b906ac64b3 Add updateGroupActivity method and corresponding route for editing group activities
This commit introduces the updateGroupActivity method in the diaryDateActivityController, allowing users to update existing group activities by linking them to predefined activities. The method includes error handling and emits a socket event upon successful updates. Additionally, the diaryDateActivityRoutes file is updated to include a new PUT route for updating group activities. Frontend changes in DiaryView enhance the user experience by enabling inline editing of group activities, including search functionality for predefined activities.
2025-11-17 10:12:21 +01:00
Torsten Schulz (local)
b7bbb92f86 Add initial fields to CourtDrawingDialog and enhance DiaryView for editing group activities
This commit introduces new props in CourtDrawingDialog for initialCode, initialName, and initialDescription, allowing for better handling of drawing data. Additionally, it updates the DiaryView to include an edit button for group activities, enabling users to modify existing activities and reset fields upon dialog closure. This enhances the user experience by providing more intuitive editing capabilities.
2025-11-16 22:25:34 +01:00
Torsten Schulz (local)
6896484e9e Enhance addGroupActivity method to support predefined activities in diary date activities
This commit updates the addGroupActivity method in the DiaryDateActivityService to accept a predefinedActivityId, allowing for the retrieval or creation of predefined activities based on the provided ID or activity name. Additionally, the frontend DiaryView is modified to include predefinedActivityId in the new plan item, improving the handling of group activities.
2025-11-16 22:12:11 +01:00
Torsten Schulz (local)
9cc9db3a5a Refine image validation logic in DiaryView to ensure accurate image handling
This commit enhances the image validation process in DiaryView by adding checks to confirm the presence of valid image IDs in the imageLink and verifying the existence of images in the associated array. This improvement ensures that only actual images are processed, leading to better data integrity and user experience.
2025-11-16 22:06:02 +01:00
Torsten Schulz (local)
1c99fb30a1 Enhance diary date activity service to include predefined activity images and improve image validation in DiaryView
This commit updates the DiaryDateActivityService to include associated images for predefined activities, enhancing the data structure. Additionally, it refines the image validation logic in DiaryView to check for both drawing data and standard images, ensuring a more robust handling of image data.
2025-11-16 21:57:27 +01:00
Torsten Schulz (local)
2782661206 Enhance member gallery generation to support JSON format without image creation
This commit updates the member gallery generation functionality to allow returning a list of members in JSON format without creating images. The `generateMemberGallery` method in the MemberService is modified to accept a new parameter, `createImage`, which determines whether images should be generated. This change improves the flexibility of the API for different use cases.
2025-11-16 21:26:15 +01:00
Torsten Schulz (local)
d10b663dc1 Refactor error handling and localization in frontend components
This commit enhances the error handling and user interface of various frontend components by integrating localization support. It updates error messages and titles across multiple views and dialogs to utilize the translation function, ensuring a consistent user experience in different languages. Additionally, it refines the handling of error messages in the MyTischtennis account and member transfer settings, improving clarity and user feedback during operations.
2025-11-16 20:48:31 +01:00
Torsten Schulz (local)
9baa6bae01 Update sitemap.xml to include lastmod dates and adjust priority values for SEO optimization 2025-11-16 12:18:05 +01:00
Torsten Schulz (local)
945fd85e39 Update Apache configuration example to clarify structure and enhance redirect handling
This commit revises the apache.conf.example file to provide clearer documentation on the configuration structure, emphasizing the separation of HTTP and HTTPS settings. It adds detailed comments regarding the use of separate configuration files for HTTP and HTTPS, and enhances the redirect rules for both www and non-www domains, ensuring proper traffic management and SEO practices.
2025-11-16 12:14:17 +01:00
Torsten Schulz (local)
5b04ed7904 Implement 301 redirects for www to non-www and enhance canonical tag handling
This commit adds 301 redirects in the Apache configuration to redirect traffic from www.tt-tagebuch.de to tt-tagebuch.de for both HTTP and HTTPS. Additionally, it introduces middleware in the backend to dynamically set canonical tags based on the request URL, ensuring proper SEO practices. The request logging middleware has been disabled, and sensitive data handling has been improved in the MyTischtennis model and API logging service, ensuring compliance with data protection regulations. Frontend updates include enhanced descriptions and features in the application, improving user experience and clarity.
2025-11-16 12:08:56 +01:00
Torsten Schulz (local)
de36a8ce2b Enhance fixCertPermissions.sh to set directory permissions for SSL certificate paths
This commit updates the fixCertPermissions.sh script to include specific permission settings for the archive and live directories of SSL certificates. It ensures that the group can navigate these directories by setting appropriate execute permissions, improving security and access management for SSL certificate handling.
2025-11-16 11:26:57 +01:00
Torsten Schulz (local)
903b036a63 Enhance fixCertPermissions.sh to improve SSL certificate permission handling
This commit updates the fixCertPermissions.sh script to include more specific permission settings for SSL certificate files by using the `-type f` option with the `find` command. It also adds a new feature to display the found private key files, improving visibility and debugging capabilities during the permission setting process.
2025-11-16 11:24:23 +01:00
Torsten Schulz (local)
5f3b6200ec Refactor fixCertPermissions.sh to improve permission handling for SSL certificates
This commit updates the fixCertPermissions.sh script to utilize the `find` command for setting permissions on SSL certificate files, ensuring that symlinks are properly handled. It also enhances the check for the archive directory's existence by using `sudo`, and reorganizes the output messages for clarity, emphasizing the need to restart the service after changes are made.
2025-11-16 11:19:15 +01:00
Torsten Schulz (local)
eff211856f Refactor fixCertPermissions.sh to improve SSL certificate handling and user configuration
This commit refines the fixCertPermissions.sh script to enhance its functionality for managing SSL certificate permissions. It introduces checks for the existence of the service user and defaults to `www-data` if not defined, ensuring proper access to SSL certificates. Additionally, the script is updated to handle scenarios where the service user is set to `nobody`, improving overall security and usability in the deployment process.
2025-11-16 11:17:30 +01:00
Torsten Schulz (local)
a81c3453b5 Update Socket.IO deployment documentation and fixCertPermissions.sh script for improved service user configuration
This commit enhances the Socket.IO deployment documentation by adding a new section on configuring the systemd service to run as `www-data`, ensuring proper permissions for SSL certificate access. It also updates the fixCertPermissions.sh script to handle cases where the service user is not defined or is set to `nobody`, defaulting to `www-data` and verifying its existence. These changes improve the overall security and functionality of the deployment process.
2025-11-16 09:50:26 +01:00
Torsten Schulz (local)
56c708d3a0 Update fixCertPermissions.sh to set executable permissions
This commit changes the permissions of the fixCertPermissions.sh script to make it executable. This adjustment ensures that the script can be run directly, facilitating its use in managing SSL certificate permissions.
2025-11-16 09:44:23 +01:00
Torsten Schulz (local)
062bddcf52 Update fixCertPermissions.sh to use sudo for certificate checks and listings
This commit modifies the fixCertPermissions.sh script to ensure that checks for the SSL certificate directory and the ssl-cert group are performed with sudo, allowing proper access for non-privileged users. Additionally, the script now lists the permissions of the private key and full chain certificate using sudo, enhancing its functionality for managing SSL certificate permissions.
2025-11-16 09:43:51 +01:00
Torsten Schulz (local)
4f98c782f3 Update Socket.IO deployment documentation to reflect new testing and SSL certificate error handling
This commit revises the deployment documentation for Socket.IO, updating the section numbers for clarity and adding detailed instructions for resolving SSL certificate permission errors. A new troubleshooting script is introduced to manage certificate access, ensuring the HTTPS server operates correctly on port 3051. These enhancements improve the overall guidance for deploying and testing the Socket.IO service.
2025-11-16 09:41:48 +01:00
Torsten Schulz (local)
3ea2907d08 Update Socket.IO deployment documentation to include SSL certificate permissions setup
This commit revises the deployment documentation for the Socket.IO backend, adding a new section on setting SSL certificate permissions. It introduces a script to manage certificate access for the Node.js process, ensuring proper functionality of the HTTPS server on port 3051. The order of sections has also been adjusted for clarity, enhancing the overall deployment guidance.
2025-11-16 09:41:33 +01:00
Torsten Schulz (local)
ba5d6b14a8 Enhance Socket.IO backend server configuration for improved HTTPS support
This commit updates the backend server configuration to ensure it properly handles HTTPS connections on port 3051. It includes adjustments to error handling for SSL certificate loading and improves the server's accessibility by listening on all interfaces (0.0.0.0). These changes aim to streamline the deployment process and enhance the overall reliability of the Socket.IO service over HTTPS.
2025-11-16 09:37:03 +01:00
Torsten Schulz (local)
004a94404a Update Socket.IO deployment documentation and backend server configuration
This commit revises the deployment documentation for Socket.IO, emphasizing the need to restart the backend server for HTTPS support on port 3051. It introduces a new diagnostic script to check SSL certificate existence, server accessibility, and port status. Additionally, the backend server configuration is updated to ensure it listens on all interfaces (0.0.0.0), enhancing accessibility. These changes improve clarity and troubleshooting guidance for deploying Socket.IO over HTTPS.
2025-11-16 09:35:57 +01:00
Torsten Schulz (local)
5ddf998672 Update Apache and backend configuration for direct Socket.IO HTTPS support
This commit modifies the Apache configuration to reflect that Socket.IO now runs directly on HTTPS port 3051, eliminating the need for Apache proxying. Additionally, the backend server setup is updated to create an HTTPS server for Socket.IO, including error handling for SSL certificate loading. The frontend service is also adjusted to connect to the new HTTPS endpoint, ensuring compatibility with the updated architecture.
2025-11-16 09:31:16 +01:00
Torsten Schulz (local)
baf5bda6f2 Update Apache configuration to improve WebSocket upgrade handling and enhance logging
This commit modifies the Apache configuration to refine the handling of WebSocket upgrades, ensuring better compatibility and reliability. Additionally, it enhances logging for the WebSocket upgrade process, providing more detailed insights into requests and responses, which aids in diagnostics and troubleshooting.
2025-11-16 00:13:30 +01:00
Torsten Schulz (local)
572de5f7d4 Update Apache configuration to include alternative WebSocket handling for Apache 2.4.47
This commit adds comments to the Apache configuration file, highlighting that mod_proxy_http can directly handle WebSockets starting from Apache 2.4.47. It provides an alternative ProxyPass configuration for WebSocket connections, ensuring better compatibility and guidance for users facing issues with the existing RewriteRule setup.
2025-11-16 00:12:44 +01:00
Torsten Schulz (local)
37893474b1 Refine Apache configuration for WebSocket upgrade handling
This commit updates the Apache configuration to improve the handling of WebSocket upgrades by clarifying the RewriteCond conditions for the Upgrade and Connection headers. The changes enhance the readability of the configuration and ensure more reliable WebSocket connections, aligning with previous enhancements for real-time communication.
2025-11-16 00:08:49 +01:00
Torsten Schulz (local)
f437747664 Enhance socketService configuration for WebSocket upgrades
This commit updates the `socketService.js` file to improve WebSocket upgrade handling by adding two new configuration options: `allowEIO3`, which permits WebSocket upgrades without an Origin header, and `serveClient`, which disables serving the client bundle. These changes enhance compatibility with reverse proxies and optimize the WebSocket connection process.
2025-11-16 00:05:45 +01:00
Torsten Schulz (local)
22e9750e5d Enhance WebSocket upgrade logging for improved diagnostics
This commit updates the `testWebSocket.js` script to include additional logging for the WebSocket upgrade process. It now logs the request path, Sec-WebSocket-Key, response status, and response headers, providing better visibility into the upgrade process. Additionally, error handling has been improved by logging the error code and adding a message for potential server connection issues during timeouts.
2025-11-16 00:04:43 +01:00
Torsten Schulz (local)
bd95f77131 Refactor WebSocket upgrade key generation for improved security
This commit updates the `testWebSocket.js` and `testWebSocketApache.js` scripts to enhance the generation of the Sec-WebSocket-Key. The key is now generated using a secure method that allocates 16 bytes of random data, ensuring compliance with WebSocket protocol requirements. This change improves the robustness of WebSocket upgrade requests in both scripts.
2025-11-16 00:02:55 +01:00
Torsten Schulz (local)
bbdc923950 Enhance WebSocket testing script to include session ID handling
This commit updates the `testWebSocket.js` script to extract and utilize the session ID from the HTTP polling response. The WebSocket upgrade request is modified to include the session ID when available, improving the accuracy of the upgrade tests. Additionally, detailed logging for response bodies is added to aid in diagnosing potential issues during the WebSocket upgrade process.
2025-11-16 00:00:42 +01:00
Torsten Schulz (local)
3e5ddd8a05 Refine Apache configuration for WebSocket upgrade handling and clarify conditions
This commit updates the Apache configuration to enhance the handling of WebSocket upgrades by specifying exact conditions for the Upgrade and Connection headers. It also clarifies the fallback mechanism for HTTP polling, ensuring better compatibility and reliability in real-time communication scenarios.
2025-11-15 23:59:00 +01:00
Torsten Schulz (local)
f4e5cf2edb Enhance Apache configuration for WebSocket handling by preserving query strings
This commit updates the Apache configuration to ensure that the query string is retained during WebSocket connections. The RewriteRule for the WebSocket upgrade has been modified to include the QSA flag, improving compatibility for applications relying on query parameters. These changes further optimize the server's handling of real-time communication.
2025-11-15 23:54:12 +01:00
Torsten Schulz (local)
44dba70aac Refactor Apache configuration for enhanced WebSocket support and security improvements
This commit further refines the Apache configuration to optimize WebSocket handling by adjusting the <LocationMatch> directive and ensuring proper upgrade handling. It also updates SSL settings and certificate paths to bolster security, while improving the organization of DocumentRoot and logging paths. These changes enhance the server's capability to manage real-time communication effectively and securely.
2025-11-15 23:45:56 +01:00
Torsten Schulz (local)
7698d87ba0 Update frontend dependencies to align with compatibility requirements
This commit downgrades the `@vitejs/plugin-vue` dependency from version 6.0.0 to 5.2.1 and the `vite` dependency from version 7.2.2 to 5.4.21 in the frontend package.json. These changes ensure compatibility with the current project setup and address potential issues arising from newer versions.
2025-11-15 23:42:50 +01:00
Torsten Schulz (local)
201d5e9214 Update eslint-plugin-vue dependency to version 9.0.0 for improved linting capabilities 2025-11-15 23:41:47 +01:00
Torsten Schulz (local)
c21544d9b6 Refine Apache configuration for WebSocket handling and improve request processing
This commit updates the Apache configuration to enhance WebSocket support by clarifying the <LocationMatch> directives. It ensures proper handling of WebSocket upgrade requests and introduces a fallback mechanism for HTTP polling. Additionally, it encapsulates header settings for better compatibility and emphasizes the order of LocationMatch blocks. These changes improve the server's capability to manage real-time communication effectively.
2025-11-15 23:40:43 +01:00
Torsten Schulz (local)
6167116630 Update frontend dependencies to improve compatibility and performance
This commit upgrades the `@vitejs/plugin-vue` dependency from version 5.2.1 to 6.0.0 in the frontend package.json. This change ensures better compatibility with the latest features and improvements in the Vue ecosystem, contributing to a more efficient development environment.
2025-11-15 23:36:26 +01:00
Torsten Schulz (local)
1bb5f61b57 Update Apache configuration and frontend dependencies for improved performance and compatibility
This commit adds a ProxyTimeout setting for all proxy connections in the Apache configuration, enhancing the server's ability to manage long-lived connections. Additionally, it updates the frontend dependencies, specifically upgrading `eslint` to version 9.39.1 and `vite` to version 7.2.2, ensuring better compatibility and performance in the development environment. These changes contribute to a more robust and efficient application.
2025-11-15 23:35:15 +01:00
Torsten Schulz (local)
1535c8795b Refactor WebSocket testing script to utilize HTTP polling and WebSocket upgrade checks
This commit updates the `testWebSocket.js` script to replace the Socket.IO client connection with direct HTTP polling for the initial handshake. It introduces structured tests for both HTTP polling and WebSocket upgrades, providing detailed logging for success and error scenarios. The changes enhance the script's ability to verify server connectivity and support for WebSocket protocols, improving the overall testing process for real-time communication.
2025-11-15 23:27:42 +01:00
Torsten Schulz (local)
cb2d7d3936 Update Apache configuration to optimize WebSocket handling and enhance security measures
This commit refines the Apache configuration for improved WebSocket support by adjusting the <LocationMatch> directive and ensuring proper upgrade handling. It also updates SSL settings and certificate paths for better security, while enhancing the organization of DocumentRoot and logging paths. These changes bolster the server's capability to manage real-time communication effectively and securely.
2025-11-15 23:24:55 +01:00
Torsten Schulz (local)
5b4a5ba501 Refine Apache configuration for WebSocket handling and improve fallback mechanisms
This commit updates the Apache configuration to enhance WebSocket support by clarifying the <LocationMatch> directive and ensuring proper handling of WebSocket upgrades. It introduces a fallback mechanism for HTTP polling and emphasizes the need for mod_rewrite and mod_proxy_wstunnel. These changes improve the server's capability to manage real-time communication effectively.
2025-11-15 23:21:59 +01:00
Torsten Schulz (local)
90b5f8d63d Refine Apache configuration for WebSocket handling and improve documentation
This commit updates the Apache configuration to enhance WebSocket support by refining the <LocationMatch> directive. It clarifies the need for multiple LocationMatch blocks and ensures proper handling of WebSocket upgrades and fallbacks to HTTP. Additionally, it encapsulates header settings within a conditional block for better compatibility. These changes improve the server's ability to manage real-time communication effectively.
2025-11-15 23:19:27 +01:00
Torsten Schulz (local)
1ff3d9d1a6 Update Apache configuration and socket service for WebSocket support
This commit adds a ProxyTimeout setting for WebSocket connections in the Apache configuration, enhancing the server's ability to manage long-lived connections. Additionally, the socket service documentation is updated to clarify compatibility with both Apache and Nginx as reverse proxies. These changes improve the handling of real-time communication in the application.
2025-11-15 23:10:32 +01:00
Torsten Schulz (local)
df6fb23132 Update Apache configuration for tt-tagebuch.de to support WebSocket and SSL
This commit modifies the Apache configuration for tt-tagebuch.de by adding WebSocket support and updating SSL settings. The configuration now includes a dedicated <LocationMatch> for WebSocket connections, ensuring proper handling of upgrade requests. Additionally, the SSL certificate paths have been updated to reflect the use of Let's Encrypt. The DocumentRoot and logging paths have also been adjusted for better organization and clarity. These changes enhance the server's capability to handle real-time communication and improve security.
2025-11-15 23:06:04 +01:00
Torsten Schulz (local)
1e86b821e8 Enhance socket service configuration for improved connection handling
This commit updates the socket service in both the backend and frontend to include explicit path and transport settings for Socket.IO. The backend configuration now allows for upgrades from polling to WebSocket, with defined timeouts for upgrades and pings. The frontend configuration adjusts the transport order and adds a timeout for reconnections, ensuring a more robust and efficient socket connection experience. These changes improve the reliability and performance of real-time communication in the application.
2025-11-15 23:05:27 +01:00
Torsten Schulz (local)
5923ef8bba Add training day PDF generation and summary functionality
This commit introduces a new method, `addTrainingDaySummary`, in the `PDFGenerator` class to create detailed summaries for training days, including member activities and their respective times. Additionally, the `DiaryView` component is updated with a new button to generate training day PDFs, enhancing the user experience by allowing easy access to training summaries. Debugging outputs are included for better tracking of data during PDF generation.
2025-11-15 22:58:47 +01:00
Torsten Schulz (local)
cd8f40aa9d Enhance member activity retrieval by tracking unique activity dates
This commit updates the `getMemberActivities` function to track unique date-activity combinations using a Set, ensuring accurate counting of distinct participation dates. It also refines the logic for handling dates, skipping entries without a date and sorting the dates in descending order. These changes improve the accuracy and clarity of member activity statistics presented to the user.
2025-11-15 22:41:45 +01:00
Torsten Schulz (local)
d392ccddd5 Update navigation titles and enhance tournament group validation logic
This commit modifies the navigation titles in `App.vue`, changing "Verwaltung" to "Tagesgeschäft" and "Organisation" to "Wettbewerbe", while also introducing a new section for "Einstellungen". Additionally, it refines the group count validation logic in `TournamentTab.vue` by correcting the method calls for `getTotalNumberOfGroups`, ensuring proper functionality for starting and resetting knockout rounds based on the number of groups present. These changes improve the user interface and enhance the tournament management experience.
2025-11-15 22:35:02 +01:00
Torsten Schulz (local)
4a83e5c159 Enhance knockout round functionality by adding group count validation
This commit updates the `TournamentTab.vue` component to ensure that knockout rounds can only be started or reset if there is more than one group present in the tournament. A new method, `getTotalNumberOfGroups`, is introduced to calculate the total number of groups, improving the logic for determining the availability of knockout rounds. These changes enhance the tournament management experience by preventing invalid operations in single-group scenarios.
2025-11-15 22:26:17 +01:00
Torsten Schulz (local)
911c07e522 Refactor tournament management by removing external and official tournament views
This commit simplifies the tournament management system by removing the `ExternalTournamentsView` and `OfficialTournaments` components, consolidating tournament functionality into the `TournamentsView`. The routing has been updated accordingly, and the UI has been adjusted to reflect these changes. Additionally, the `App.vue` navigation has been streamlined to enhance user experience by focusing on internal tournaments only.
2025-11-15 22:20:59 +01:00
Torsten Schulz (local)
cd89c68a69 Add participation overview to OfficialTournaments.vue 2025-11-15 21:31:09 +01:00
Torsten Schulz (local)
f1321b18bb Enhance official tournament listing and upload functionality
This commit updates the `listOfficialTournaments` function to ensure it returns an empty array if no tournaments are found, improving data handling. Additionally, the frontend `OfficialTournaments.vue` is enhanced with a file upload feature for PDF documents, along with improved error handling in the tournament list loading process. These changes enhance user experience by providing clearer feedback and functionality for managing official tournaments.
2025-11-15 21:25:03 +01:00
Torsten Schulz (local)
54ce09e9a9 Add training times management and enhance diary view with group selection dialog
This commit introduces the `TrainingTime` model and related functionality, allowing for the management of training times associated with training groups. The backend is updated to include new routes for training times, while the frontend is enhanced with a new dialog in the `DiaryView` for selecting training groups and suggesting available training times. This improves user experience by streamlining the process of scheduling training sessions and managing associated data.
2025-11-15 20:51:08 +01:00
Torsten Schulz (local)
7a9e856961 Update training group management and enhance UI components
This commit introduces the `TrainingGroup` model and related functionality, allowing for the management of training groups within the application. The `ClubService` is updated to automatically create preset groups upon club creation. The frontend is enhanced with new views and components, including `TrainingGroupsView` and `TrainingGroupsTab`, to facilitate the display and management of training groups. Additionally, the `MembersView` is updated to allow adding and removing members from training groups, improving the overall user experience and interactivity in managing club members and their associated training groups.
2025-11-15 20:38:53 +01:00
Torsten Schulz (local)
fd4b47327f Enhance diary date activity service and diary view with improved drawing data handling
This commit updates the `DiaryDateActivityService` to retrieve all images associated with predefined activities and parse their drawing data, ensuring robust error handling. The `DiaryView` component is also enhanced to support both string and object representations of drawing data, improving the logic for determining activity visuals. These changes streamline the handling of drawing data across the application, enhancing user experience and data integrity.
2025-11-14 23:48:30 +01:00
Torsten Schulz (local)
3a26f10110 Update member activity handling to support activity codes and improve display logic
This commit enhances the `getMemberActivities` and `getMemberLastParticipations` functions to utilize activity codes when available, improving the uniqueness of activity identification. The `MemberActivityStatsDialog` component is updated to handle both string and object representations of activities, ensuring a consistent display and tooltip functionality. These changes streamline the activity data structure and enhance the user experience when viewing participation statistics.
2025-11-14 23:38:36 +01:00
Torsten Schulz (local)
ce2bda37ac Refactor member activity display to group participations by date
This commit updates the `MemberActivityStatsDialog` component to group member participations by date, enhancing the presentation of activity data. The logic is introduced to aggregate activities under their respective dates, ensuring a clearer and more organized display. Additionally, CSS styles are added to improve the visual hierarchy and user experience when viewing recent participations.
2025-11-14 23:27:46 +01:00
Torsten Schulz (local)
5dda346fd7 Refactor member activity retrieval to support optional predefined activities for group activities
This commit updates the `getMemberActivities` and `getMemberLastParticipations` functions to allow for optional inclusion of predefined activities in group activities. The logic is enhanced to check for predefined activities associated with group activities, providing a fallback mechanism to ensure accurate activity representation. This change improves the clarity and efficiency of the activity retrieval process, ensuring that the correct data is returned to the frontend.
2025-11-14 23:24:51 +01:00
Torsten Schulz (local)
28c92b66af Refactor member activity retrieval to include group activities and eliminate duplicates
This commit enhances the `getMemberActivities` and `getMemberLastParticipations` functions by introducing logic to gather group activities for members based on their group associations. It ensures that both explicitly assigned member activities and group activities are combined while filtering out duplicates. The overall structure of the activity retrieval process is improved for better clarity and efficiency, enhancing the accuracy of the data returned to the frontend.
2025-11-14 23:07:02 +01:00
Torsten Schulz (local)
d08835e206 Implement external participant management and tournament class features
This commit enhances the tournament management system by introducing functionality for handling external participants and tournament classes. New methods are added to the `tournamentController` and `tournamentService` for adding, retrieving, updating, and removing external participants, as well as managing tournament classes. The backend models are updated to support these features, including new relationships and attributes. The frontend is also updated to allow users to manage external participants and classes, improving the overall user experience and interactivity in tournament management.
2025-11-14 22:36:51 +01:00
Torsten Schulz (local)
3334d76688 Enhance tournament management with new features and UI improvements
This commit introduces several enhancements to the tournament management system, including the addition of winning sets to tournament creation and updates. The `updateTournament` and `addTournament` methods in the backend now accept winning sets as a parameter, ensuring proper validation and handling. New functionality for updating participant seeded status and setting match activity is also implemented, along with corresponding routes and controller methods. The frontend is updated to reflect these changes, featuring new input fields for winning sets and improved participant management UI, enhancing overall user experience and interactivity.
2025-11-14 14:36:21 +01:00
Torsten Schulz (local)
d48cc4385f Add tournament update functionality and enhance UI for tournament management
This commit introduces the ability to update tournament details, including name and date, in the backend and frontend. The new `updateTournament` method is added to the `tournamentController` and `tournamentService`, allowing for validation and error handling. The frontend `TournamentsView` is updated to include input fields for editing tournament details, with real-time updates reflected in the UI. Additionally, new CSS styles are introduced for improved layout and user interaction, enhancing the overall experience in tournament management.
2025-11-14 10:44:18 +01:00
Torsten Schulz (local)
9b8dcd8561 Add group deletion functionality and socket event emissions for real-time updates
This commit introduces the ability to delete groups in the groupController, along with the necessary backend service updates. It also adds socket event emissions for group and activity changes, ensuring real-time updates are sent to clients when groups are deleted. The frontend is updated to include a delete button in the DiaryView, allowing users to remove groups easily. Additionally, the groupRoutes and socketService are modified to support these new features, enhancing the overall interactivity of the application.
2025-11-13 18:48:51 +01:00
Torsten Schulz (local)
2b06a8dd10 Enhance participant update handling and UI responsiveness in DiaryView
This commit improves the participant update process by ensuring the latest participant data is fetched from the database before emitting socket events. It also refines the DiaryView component's UI, adding better handling for dropdowns and member group selections, enhancing user experience. Additionally, new CSS styles are introduced for member group select elements to ensure consistent appearance across browsers.
2025-11-13 18:18:31 +01:00
Torsten Schulz (local)
58e773e51e Enhance DiaryView with mobile and desktop tab navigation improvements
This commit refines the DiaryView component by implementing a responsive tab navigation system for both mobile and desktop views. It introduces new CSS styles for better layout management and user interaction, ensuring a seamless experience when switching between 'Teilnehmer' and 'Aktivitäten' sections. The active tab state is now visually indicated, improving usability across devices.
2025-11-13 17:39:38 +01:00
Torsten Schulz (local)
8d17cad299 Add mobile tab navigation to DiaryView for improved user experience
This commit introduces a mobile-friendly tab navigation system in the DiaryView component, allowing users to switch between 'Trainingsplan', 'Teilnehmer', and 'Aktivitäten' seamlessly. The active tab state is managed with a new reactive property, enhancing the overall usability of the application on mobile devices.
2025-11-13 17:34:10 +01:00
Torsten Schulz (local)
156f4d6921 Add member change event handling for real-time updates
This commit introduces a new socket event for member changes, allowing real-time updates when members are created or updated. The backend now emits a 'member:changed' event upon successful member modifications, while the frontend listens for this event to refresh the member list in the DiaryView component. This enhances the interactivity and responsiveness of the application, ensuring users receive immediate feedback on member changes.
2025-11-13 17:32:29 +01:00
Torsten Schulz (local)
e27a4d960d Update dependencies in package.json and package-lock.json for improved functionality
This commit updates the versions of several dependencies in the frontend, including upgrading `jspdf` to version 3.0.3, `vite` to version 7.2.2, and `@vitejs/plugin-vue` to version 5.2.1. Additionally, it updates `axios` to version 1.13.2 and `dompurify` to version 3.3.0, among others. These changes enhance the overall performance and security of the application by incorporating the latest features and fixes from the respective libraries.
2025-11-13 17:00:28 +01:00
Torsten Schulz (local)
c589c11607 Add Socket.IO integration for real-time updates in diary features
This commit introduces Socket.IO to the backend and frontend, enabling real-time communication for diary-related events. Key updates include the addition of socket event emissions for diary date updates, tag additions/removals, and activity member changes in the backend controllers. The frontend DiaryView component has been enhanced to connect to the socket server and handle incoming events, ensuring that users receive immediate feedback on changes. Additionally, new dependencies for Socket.IO have been added to both the backend and frontend package files, improving the overall interactivity and responsiveness of the application.
2025-11-13 16:54:31 +01:00
Torsten Schulz (local)
0caa31e3eb Refactor file input sections in ImageViewerDialog and MembersView to enhance user experience. The updates include separate file input options for selecting files and capturing images via the device camera, improving accessibility and usability for image uploads. 2025-11-13 15:55:33 +01:00
Torsten Schulz (local)
fff5d404f5 Refactor file input fields in ImageViewerDialog and MembersView to remove camera capture attribute. This change simplifies the file selection process for users while maintaining functionality for image uploads. 2025-11-13 15:52:12 +01:00
Torsten Schulz (local)
7aff827711 Update file input fields to capture images using the device camera and set default match start date from scheduleDate if not already set. This enhances the user experience for image uploads and ensures match data is initialized correctly. 2025-11-13 15:47:58 +01:00
Torsten Schulz (local)
cf3bd3cd6d Enhance member training participation tracking and UI feedback
This commit adds functionality to track training participations for members in the backend, updating the MemberService to include a count of training participations. The frontend components, DiaryView and MembersView, have been updated to display warning icons based on the number of training participations, providing visual feedback for members with significant participation. CSS styles for warning icons have also been refined to improve visibility and user experience.
2025-11-13 12:12:49 +01:00
Torsten Schulz (local)
4c1a919d17 Add club settings link for admin users and remove test PIN references in MatchReportApiDialog
This commit introduces a new link to the club settings in the App.vue component, visible only to admin users. Additionally, it removes references to a test PIN ('1234') in the MatchReportApiDialog.vue, enhancing security and streamlining the user experience by eliminating unnecessary hints and validation logic.
2025-11-13 09:27:08 +01:00
Torsten Schulz (local)
eb9748dd89 Enhance group assignment functionality in DiaryView with improved reactivity
This commit updates the DiaryView component to enhance the group assignment feature for participants. It adds an @input event listener to the group selection dropdown for immediate UI updates and refines the logic for updating the memberGroupsMap to ensure reactivity in Vue 3. Additionally, it includes error handling improvements to reload participant data upon failure, ensuring data consistency and a better user experience.
2025-11-12 16:21:08 +01:00
Torsten Schulz (local)
e295657621 Add validation endpoint for meeting reports and enhance frontend functionality
This commit introduces a new API endpoint in the backend for validating meeting reports, allowing for improved error handling and cookie management. The frontend has been updated to include a validation function that checks the report data before submission, providing users with feedback on validation warnings and errors. These changes enhance the overall user experience by ensuring data integrity and improving interaction with the meeting report feature.
2025-11-12 16:13:04 +01:00
Torsten Schulz (local)
da351b40b2 Add node-fetch dependency and implement meeting report submission endpoint
This commit introduces the node-fetch package to facilitate HTTP requests in the backend. Additionally, a new API endpoint for submitting meeting reports has been implemented in the nuscoreApiRoutes. The endpoint handles report data submission, cookie management, and error handling, enhancing the functionality of the meeting report feature. Frontend components have been updated to support this new functionality, improving the overall user experience.
2025-11-12 15:29:04 +01:00
Torsten Schulz (local)
3c64e2e92d Remove Playwright dependency and related proxy service files from the backend. This commit deletes the node-fetch package and its associated files, as well as the nuscoreProxyRoutes and nuscoreProxyService, streamlining the backend by eliminating unused components. 2025-11-12 13:41:09 +01:00
Torsten Schulz (local)
45381707ea Erweitert den MatchReportApiDialog um neue Funktionen zur Verwaltung von Spielberichten. Implementiert eine verbesserte Logik zur Berechnung der Gesamtpunkte und Sätze sowie zur Validierung von Eingaben. Fügt visuelle Hinweise für den Abschlussstatus und Warnungen bei fehlerhaften Eingaben hinzu. Optimiert die Benutzeroberfläche mit neuen CSS-Stilen für eine bessere Benutzererfahrung. 2025-11-12 13:40:55 +01:00
Torsten Schulz (local)
b166f7c7d5 Refactor member gallery dialog in DiaryView for improved functionality and user experience
This commit replaces the existing BaseDialog for the member gallery with a new MemberGalleryDialog component, streamlining the dialog's functionality. The new component integrates props for current club, date, and participant status, enhancing interactivity. Additionally, redundant gallery loading logic and state management have been removed, simplifying the codebase and improving maintainability.
2025-11-12 12:53:29 +01:00
Torsten Schulz (local)
1f20737721 Enhance member gallery functionality with latest image retrieval and JSON format support
This commit improves the member gallery feature by allowing users to request the latest member images and receive member information in JSON format. The backend has been updated to handle "latest" as a valid imageId, ensuring the most recent image is fetched. Additionally, the frontend has been modified to support displaying member details in an interactive gallery format, enhancing user experience and providing more flexibility in how member information is presented.
2025-11-12 11:58:37 +01:00
Torsten Schulz (local)
8ef4e1dc9d Enhance member gallery generation with customizable image size selection
This commit updates the member gallery generation feature by allowing users to specify the image size through the frontend. The DiaryView component now includes a dropdown for selecting image dimensions (100x100, 150x150, 200x200), which is passed to the backend for processing. The memberService has been modified to validate the size parameter, ensuring only allowed dimensions are used. These changes improve user experience by providing flexibility in gallery display options.
2025-11-12 10:05:08 +01:00
Torsten Schulz (local)
98c50bc03a Refactor gallery layout in memberService and update DiaryView styles for improved responsiveness
This commit refines the gallery layout logic in memberService by adjusting the calculations for canvas dimensions and positions, ensuring a more accurate representation of images. Additionally, the DiaryView component's styles have been updated to enhance the alignment and responsiveness of the gallery images, contributing to a better user experience across devices.
2025-11-12 08:46:58 +01:00
Torsten Schulz (local)
7b28eb04ac Refactor memberService and update DiaryView styles for improved functionality and layout
This commit includes minor refactoring in the memberService to enhance error logging and improve image handling logic. Additionally, the DiaryView component has been updated to adjust the gallery dialog layout, ensuring better alignment and responsiveness of images. These changes contribute to a more robust backend and a better user experience in the frontend.
2025-11-12 07:57:55 +01:00
Torsten Schulz (local)
ed15137003 Add member gallery generation feature in backend and frontend
This commit introduces a new API endpoint for generating a member gallery, allowing users to retrieve a composite image of active members' latest images. The backend has been updated with a new method in MemberService to handle gallery creation, while the frontend has been enhanced with a dialog for displaying the generated gallery. This feature improves the user experience by providing a visual representation of club members.
2025-11-11 16:22:47 +01:00
Torsten Schulz (local)
2bf5c0137b Adjust image height settings in ImageViewerDialog component for improved responsiveness. Updated max-height for desktop and mobile views to enhance user experience when viewing images. 2025-11-11 15:56:21 +01:00
Torsten Schulz (local)
22e6913005 Implement member image management features in backend and frontend
This commit introduces new functionalities for managing member images, including uploading, deleting, and setting primary images. The memberController and memberService have been updated to handle these operations, while new routes have been added to facilitate image management. The frontend has been enhanced with an ImageViewerDialog component that supports image rotation, deletion, and setting primary images. Additionally, improvements to the member view allow for better image handling and display. These changes enhance the overall user experience and functionality of the member management system.
2025-11-11 15:53:21 +01:00
Torsten Schulz (local)
f7eff0bcb7 Enhance error handling and logging in backend controllers and services
This commit improves error handling in various controllers, including diaryNoteController, memberNoteController, and permissionController, by adding console error logging for better debugging. Additionally, it updates the diaryService and teamDocumentService to enhance functionality and maintainability. The config.js file is also updated to ensure proper configuration for the development environment. These changes contribute to a more robust and user-friendly application.
2025-11-11 11:36:47 +01:00
Torsten Schulz (local)
45c90280f8 Update package dependencies and types for improved compatibility and functionality
This commit updates the package-lock.json and package.json files to reflect the latest versions of dependencies, including @types/node and validator. Additionally, it removes unused type definitions and enhances the overall structure of the project, ensuring better compatibility and performance across the application.
2025-11-11 09:33:24 +01:00
Torsten Schulz (local)
a4890f241b Add new API routes for season, session, team, and tournament management
Integrated additional routes into the Express application for managing seasons, sessions, teams, team documents, and tournaments. This enhancement improves the API's structure and expands its capabilities, allowing for better organization and access to related resources.
2025-11-11 09:30:40 +01:00
Torsten Schulz (local)
684409491f Add permission management tests for role and custom permissions updates
Implemented new tests in permissionRoutes.test.js to validate role changes and custom permissions for users within clubs. Added checks for successful updates and error handling for invalid club IDs. This enhancement improves test coverage for permission management features in the application.
2025-11-11 09:00:18 +01:00
Torsten Schulz (local)
15b88f8177 Add member transfer validation and contact management tests
Implemented a new test for validating transfer configuration via the API in memberRoutes.test.js, ensuring proper error handling for invalid transfer requests. Additionally, enhanced memberService.test.js with tests for creating and updating members along with their contacts, including filtering inactive members. These additions improve the test coverage and reliability of member management features in the application.
2025-11-11 08:35:55 +01:00
Torsten Schulz (local)
20f204e70b Enhance diary note and tag management in backend controllers
Updated the diaryNoteController to require diaryDateId in note creation and improved error handling for missing fields. Enhanced the createTag function in diaryTagController to validate tag names and return appropriate responses. Additionally, refined the deleteTag function to ensure proper error handling when a tag is not found. These changes improve the robustness and usability of the diary management features.
2025-11-11 08:29:18 +01:00
Torsten Schulz (local)
b8191e41ee Add club team and diary routes to the Express application
Integrated new routes for club team management and diary functionality into the backend. This enhancement improves the API's structure and accessibility, allowing for better organization of related resources and expanding the application's capabilities.
2025-11-10 18:29:55 +01:00
Torsten Schulz (local)
b906a218a5 Add API logging and club routes to the Express application
Integrated new routes for API logging and club management into the backend, enhancing the API's functionality and organization. This addition allows for better access to logging and club-related resources, improving overall application structure.
2025-11-10 17:09:57 +01:00
Torsten Schulz (local)
6c3b46c037 Add accident and activity routes to the Express application
Enhanced the backend by integrating new routes for accident and activity management. This addition improves the API's functionality, allowing for better organization and access to related resources.
2025-11-10 17:01:43 +01:00
Torsten Schulz (local)
3f1018ef93 Refactor database configuration and enhance error handling in authentication services
Updated the database configuration to centralize settings and improve maintainability. Enhanced error handling in the authentication service to provide clearer and more specific error messages for various failure scenarios, including registration, activation, and login processes. Additionally, added new dependencies for testing and SQLite support in the package.json file.
2025-11-10 16:54:49 +01:00
Torsten Schulz (local)
620b065ac8 Refactor dialog handling to utilize utility functions for improved consistency
Updated various components to replace direct dialog configurations with utility functions for building dialog configurations. This change enhances the maintainability and readability of the code by centralizing dialog setup logic, ensuring a consistent approach across the application. Additionally, improved error handling and user feedback mechanisms were implemented to provide clearer messages during interactions.
2025-11-10 16:07:24 +01:00
Torsten Schulz (local)
4cfa03834e Refactor alert handling to use dialog components for improved user feedback
Updated various components to replace alert calls with showInfo and showConfirm dialog methods, enhancing user experience by providing more informative and styled feedback. This change standardizes the way messages are displayed across the application, improving consistency and clarity in user interactions.
2025-11-10 14:20:34 +01:00
Torsten Schulz (local)
d94238f6df Refactor logging in cleanup scripts to use report array for improved output management
Updated various cleanup scripts to replace console.log statements with a report array, enhancing the output handling and allowing for better formatting of messages. This change improves the readability of logs and ensures consistent reporting across different cleanup operations, including database connection status, index management, and summary reports.
2025-11-10 13:25:11 +01:00
Torsten Schulz (local)
eb37532de2 Refactor group ID handling in DiaryView and related services for consistency
Updated the handling of group IDs in DiaryView.vue to ensure they are consistently treated as strings. Enhanced the DiaryDateActivityService to filter out duplicate group activities and ensure proper handling of group IDs during updates. This improves data integrity and consistency across the application.
2025-11-08 22:31:38 +01:00
Torsten Schulz (local)
d79e71d6d7 Update PDF parsing and document upload handling in team management
Enhanced the PDFParserService to support layout-based text extraction from PDFs using pdfjs-dist, improving parsing accuracy. Updated the team management view to streamline document uploads and parsing processes, removing unnecessary UI elements and consolidating upload logic. Improved error handling and user feedback during document processing, ensuring better user experience and clarity in case of issues.
2025-11-08 10:15:25 +01:00
Torsten Schulz (local)
f0e3c6a717 Enhance MyTischtennisService and MyTischtennisAccount.vue for improved login handling and account status display
Updated MyTischtennisService to include federation nickname in account data and refined login logic to utilize stored passwords or valid sessions more effectively. Enhanced error handling for login failures. In MyTischtennisAccount.vue, added account status management to display password storage status and improved account loading logic to fetch both account and status data concurrently.
2025-11-07 14:54:15 +01:00
Torsten Schulz (local)
b2d47c7a37 Update error message in MyTischtennisUrlController for clarity
Modified the error handling in MyTischtennisUrlController to specify that only the URL is required, enhancing the clarity of the error response. This change aligns with recent efforts to standardize error handling across the application.
2025-11-07 13:50:18 +01:00
Torsten Schulz (local)
498742e6ae Refactor error handling in controllers and services to standardize HttpError instantiation
Updated multiple controllers and services to ensure consistent error handling by modifying the HttpError instantiation format. This change enhances clarity and maintains uniformity across error responses, improving overall code quality and maintainability.
2025-11-07 13:45:58 +01:00
Torsten Schulz (local)
94aab93f7d Add global error handling middleware for API routes and enhance rating update logic
Implemented a global error handling middleware in the server to standardize error responses for API routes. Additionally, updated the AutoUpdateRatingsService to retrieve the approved user club before updating ratings, ensuring proper access control. Modified the error handling in MyTischtennisAccount.vue to provide more informative login failure messages.
2025-11-07 13:22:22 +01:00
Torsten Schulz (local)
eba8ba30aa Add notes placeholder in member transfer templates for improved data handling
Updated the MemberTransferService to include a new '{{notes}}' placeholder in the member transfer templates. This addition ensures that notes are accounted for during member data transfers, enhancing the completeness of the information being processed.
2025-11-07 10:05:03 +01:00
Torsten Schulz (local)
9d24c6ae7b Filter active members in phone list generation to exclude test memberships for improved accuracy 2025-11-06 17:15:19 +01:00
Torsten Schulz (local)
02b8ba3d0a Enhance PDF generation for member phone lists by improving formatting and fallback handling
Updated the PDFGenerator component to ensure consistent font sizes for phone list entries and added fallback logic for handling cases where no phone contacts are available. Improved the display of parent contact names with appropriate font sizes, enhancing the readability and organization of member information in generated PDFs.
2025-11-06 17:12:58 +01:00
Torsten Schulz (local)
fc7b70b307 Standardize phone number formatting in member service and MembersView
Integrated phone number standardization in the member service to ensure consistent formatting during member updates and contact creation. Added a new utility function in MembersView for phone number standardization, enhancing data integrity and user experience when adding or updating member information.
2025-11-06 16:45:44 +01:00
Torsten Schulz (local)
9cdbd60a23 Enhance member management by adding postal code and contact handling
Introduced a postal code field to the member model and implemented a new MemberContact model to manage multiple phone numbers and email addresses. Updated the member service and controller to handle contact data during member creation and updates. Enhanced the MembersView component to support input for multiple contacts, ensuring better organization and accessibility of member information.
2025-11-06 16:12:34 +01:00
Torsten Schulz (local)
5a4553a8a0 Add member_contact table and postal_code column to member table
Created a new SQL migration to add a postal_code column to the member table and introduced a member_contact table to store multiple phone numbers and email addresses. Implemented data migration for existing phone numbers and email addresses from the member table to the new member_contact table, ensuring data integrity and improved contact management.
2025-11-06 16:03:42 +01:00
Torsten Schulz (local)
98637eec00 Update MembersView styling for action icons to improve layout consistency
Modified the action-icons-row CSS to prevent wrapping and ensure icons remain in a single line, enhancing the visual layout and usability of the MembersView component.
2025-11-06 15:01:40 +01:00
Torsten Schulz (local)
106c63890e Enhance training participation loading in MembersView with improved ID handling and logging
Updated the loadTrainingParticipations method to store participation totals using both string and number keys in the participation map, addressing potential type issues. Enhanced logging to provide detailed insights into the loading process, including responses and member counts, improving debugging capabilities and user experience.
2025-11-06 14:58:19 +01:00
Torsten Schulz (local)
f6b8388819 Add quick deactivate member functionality and update routes and UI
Implemented a new quickDeactivateMember function in MemberService to handle member deactivation. Updated member routes to include a new endpoint for quick deactivation. Enhanced the MembersView component to support quick deactivation actions with updated UI elements, improving user experience for managing member statuses.
2025-11-06 14:46:16 +01:00
Torsten Schulz (local)
f1a29e4111 Add last scheduler executions endpoint and member quick update functions
Implemented a new endpoint in ApiLogController to retrieve the last execution information for scheduler jobs. Added quick update functions in MemberService and corresponding routes for updating test membership status and marking member forms as handed over. Enhanced the MembersView to support quick actions for managing test memberships and form statuses, improving user experience and operational efficiency.
2025-11-06 14:25:15 +01:00
Torsten Schulz (local)
c9d82827ff Enhance member validation and error reporting in MemberTransferService and MemberTransferDialog
Updated the MemberTransferService to include validation for required member fields, categorizing members into valid and invalid lists. Improved error handling to provide detailed feedback on invalid members during transfers. Enhanced the MemberTransferDialog to display information about excluded members and their errors, ensuring users are informed of any issues during the transfer process.
2025-11-06 08:28:27 +01:00
Torsten Schulz (local)
75242f63fc Enhance error handling and logging in MemberTransferService and MemberTransferDialog
Updated the MemberTransferService to provide detailed error messages during bulk transfers, including identification of problematic members. Improved logging for error responses to facilitate debugging. In the MemberTransferDialog, implemented $nextTick to ensure login credentials are cleared properly upon loading and closing, enhancing user experience and preventing autofill issues.
2025-11-06 08:24:39 +01:00
Torsten Schulz (local)
2f161d1eb5 Refactor logging in MemberTransferService to use info and error logs
Updated the MemberTransferService to replace debug logging with info and error logging for better visibility in production. This change enhances the logging of login credentials, transfer details, and error handling, ensuring critical information is captured appropriately during member transfers.
2025-11-06 08:16:14 +01:00
Torsten Schulz (local)
cad76edaad Enhance MemberTransferService and MemberTransferDialog with detailed debug logging
Added extensive debug logging to the MemberTransferService to track member transfer details, including logging the first few members and bulk data structure. Updated the MemberTransferDialog to clear login credentials upon loading and closing, improving user experience and ensuring a clean state for configuration loading. These enhancements aim to facilitate troubleshooting and provide better visibility into the member transfer process.
2025-11-06 08:13:43 +01:00
Torsten Schulz (local)
d0a8ef5ff2 Enhance MemberTransferService with detailed debug logging for login credentials
Added comprehensive debug logging to the MemberTransferService to track login credentials and endpoint details during member transfers. Implemented checks for the presence of login credentials, including mapping username to email when necessary, and improved error handling for missing credentials. This enhancement aims to facilitate troubleshooting and improve the overall robustness of the login process.
2025-11-06 07:29:49 +01:00
Torsten Schulz (local)
a0d12a895e Enhance permission validation and error handling in permissionController
Updated the getUserPermissions function to include validation for clubId, ensuring it is a valid positive integer. Added error handling to return a 400 status with a descriptive message for invalid club IDs, improving the robustness of the API response.
2025-11-05 16:33:52 +01:00
Torsten Schulz (local)
ad99787f75 Enhance member transfer service and dialog for improved credential handling
Updated the MemberTransferService to better manage login credentials, ensuring that saved credentials are utilized when none are provided. Improved error handling during login attempts by extracting detailed error messages and adjusting status codes to prevent user logout. Refined the MemberTransferDialog to clear login credentials when no club is selected and to only include non-empty values in the login credentials object, enhancing user experience and validation logic.
2025-11-05 16:22:40 +01:00
Torsten Schulz (local)
c05cfbbe38 Refactor MemberTransferDialog to improve configuration handling and UI
Updated the MemberTransferDialog component to enhance user experience by displaying configuration summaries and handling missing configurations more effectively. Removed redundant form sections and added conditional rendering for login credentials. Improved validation logic for transfer settings and introduced new styles for better visual clarity.
2025-11-05 15:39:34 +01:00
Torsten Schulz (local)
1f47a11091 Add member transfer configuration and UI enhancements
Introduced MemberTransferConfig model and integrated it into the backend, allowing for the storage and retrieval of member transfer settings. Updated server routes to include member transfer configuration endpoints. Enhanced the frontend with a new MemberTransferDialog component for user interaction, added a dedicated route for member transfer settings, and updated the App.vue to include a link for accessing these settings. Improved the loading state and configuration handling in the dialog for better user experience.
2025-11-05 15:30:12 +01:00
Torsten Schulz (local)
5bba9522b3 Add member transfer functionality to memberController and update routes and UI
Implemented a new transferMembers function in memberController to handle member transfers, including validation for transfer endpoint and template. Updated memberRoutes to include a new route for member transfers. Enhanced MembersView with a button to open a transfer dialog and integrated a MemberTransferDialog component for user interaction during the transfer process.
2025-11-05 14:33:09 +01:00
Torsten Schulz (local)
5bdcd946cf Update diagonal cell styling in TournamentsView for improved visibility
Modified the CSS for diagonal cells in TournamentsView to change the background and text color from black to a lighter gray. This enhancement improves the visual clarity of the tournament display, making it easier for users to identify active matches.
2025-11-05 08:55:29 +01:00
Torsten Schulz (local)
6500493314 Enhance TournamentsView with diagonal cell styling for match rankings
Updated the TournamentsView to include a new CSS class for diagonal cells, improving the visual representation of match rankings. The diagonal cells now have distinct styling to indicate active matches, enhancing user interaction and clarity in tournament displays.
2025-11-05 08:49:27 +01:00
Torsten Schulz (local)
d0e3ae3610 Implement ranking logic for players with identical scores in TournamentsView
Updated the ranking assignment logic in TournamentsView to ensure players with identical points, set differences, and sets won receive the same position. This change enhances the accuracy of player rankings and improves the user experience by reflecting true standings in tournament scenarios. Additionally, adjusted the position retrieval for players in live stats to accommodate the new ranking logic.
2025-11-05 08:45:23 +01:00
Torsten Schulz (local)
8db827adeb Refactor participant list in DiaryView for improved layout and interaction
Updated the participant list in DiaryView to enhance the layout and user interaction. Introduced new CSS classes for better styling of participant rows, including flexbox for alignment and spacing. Improved the checkbox label structure and added a dedicated actions section for each participant, streamlining the user experience when managing participants. Additionally, modified the selectMember method to automatically open the notes modal for the selected participant.
2025-11-04 16:37:09 +01:00
Torsten Schulz (local)
d40eea5e46 Enhance DiaryView with member form handover functionality and styling improvements
Added a new feature to mark the member form as handed over, updating the member's status accordingly. Enhanced the participant list with conditional styling based on member activity and membership status. Introduced new CSS classes for better visual representation of inactive members and those with test memberships. Improved user interaction by adding an icon for marking the form handover.
2025-11-04 16:22:21 +01:00
Torsten Schulz (local)
a3ed130211 Add animation controls and enhance arrow drawing in CourtDrawingRender component
Implemented animation controls for arrow rendering in the CourtDrawingRender component, allowing users to start and stop animations. Enhanced the drawArrow method to support progressive rendering based on animation state. Added computed properties to determine the presence of arrows and updated the component's lifecycle methods for proper animation management. Improved styling for animation buttons to enhance user experience.
2025-11-04 15:54:18 +01:00
Torsten Schulz (local)
e8766b919a --- 2025-11-03 14:15:00 +01:00
Torsten Schulz (local)
76ee9ee742 Refactor memberController and memberService to include memberFormHandedOver field and improve parameter handling
Updated memberController to handle the showAll parameter more effectively and added memberFormHandedOver to the setClubMember method in memberService. Enhanced Member model to include memberFormHandedOver field with appropriate defaults. Updated MembersView to reflect changes in the UI, allowing for better member data management and visibility.
2025-11-03 12:36:13 +01:00
Torsten Schulz (local)
84ff4e126e Enhance MyTischtennisUrlController with ratings update and improve apiLogService truncation limits
Added functionality in MyTischtennisUrlController to update (Q)TTR ratings for clubs based on user authentication. Enhanced error handling for ratings updates to provide clearer feedback. Updated apiLogService to increase truncation limits for request and response bodies, accommodating larger API JSON payloads, ensuring better logging accuracy.
2025-11-03 12:03:34 +01:00
Torsten Schulz (local)
23708b99b5 Refactor error handling in MyTischtennisUrlController and improve memberService indexing
Refactored error handling in MyTischtennisUrlController to standardize error messages and ensure consistent status codes. Enhanced memberService by implementing a more efficient indexing system for member data retrieval, improving performance and accuracy in TTR and QTTR updates. Updated TeamManagementView to handle timeout errors and provide detailed user feedback, enhancing overall user experience.
2025-11-03 10:45:04 +01:00
Torsten Schulz (local)
acf2cf00bd Refactor MyTischtennisUrlController and enhance error handling in TeamManagementView
Refactored MyTischtennisUrlController to define variables outside of try/catch for better error handling. Improved error messaging in TeamManagementView by providing more detailed debug information and ensuring fallback messages are available. This enhances the robustness of data retrieval and user feedback.
2025-11-03 10:07:24 +01:00
Torsten Schulz (local)
bb3f0f3a03 Enhance error handling in MyTischtennisUrlController and update TrainingStatsView with TTR and QTTR columns
Improved error handling in MyTischtennisUrlController by adding detailed debug information in the response, including status codes and relevant data. Updated TrainingStatsView to display TTR and QTTR values for members, enhancing data visibility and user experience.
2025-11-03 09:46:26 +01:00
Torsten Schulz (local)
f4411a4ee5 Enhance club rankings retrieval and member TTR updates
Updated the getClubRankings method in myTischtennisClient to include an optional parameter for current rankings. Modified memberService to fetch both current and quarterly TTR values, improving member data accuracy. Enhanced the TeamManagementView to display (Q)TTR values for better user visibility. Added error handling for QTTR retrieval, ensuring robustness in member updates.
2025-11-03 09:35:04 +01:00
Torsten Schulz (local)
e32871a005 Implement deleteGroupActivity endpoint and enhance addGroupActivity functionality
Added a new endpoint to delete group activities in the diaryDateActivityController and corresponding route in diaryDateActivityRoutes. Enhanced the addGroupActivity function to accept an optional timeblockId parameter, allowing for more precise activity management. Updated the DiaryView component to support the removal of group activities, improving user experience and functionality in activity management.
2025-11-03 08:43:24 +01:00
Torsten Schulz (local)
a8318c74cf Implement last participations endpoint and enhance member activity retrieval
Added a new endpoint to fetch the last participations of a member, including group assignment checks for activities. Updated the member activity controller to include logic for filtering activities based on participant group IDs. Enhanced the DiaryView component to display activity statistics and last participations in a modal, improving user experience and data accessibility.
2025-10-31 16:33:20 +01:00
Torsten Schulz (local)
7e85926aa1 Refactor CourtDrawing components by removing overlay text and debug logs
Removed overlay text rendering and debug logging from CourtDrawingRender.vue and CourtDrawingTool.vue to streamline the drawing process. Updated DiaryView.vue to suppress debug messages and handle parsing errors more gracefully, enhancing overall code clarity and user experience.
2025-10-31 15:14:11 +01:00
Torsten Schulz (local)
91fc3e9d13 Implement Court Drawing Dialog and enhance activity management in DiaryView and PredefinedActivities
Added a Court Drawing Dialog component to facilitate the creation and editing of drawing data for activities. Updated DiaryView.vue to include a button for opening the drawing dialog and handling the resulting data. Enhanced PredefinedActivities.vue to allow users to create or edit drawing data directly from the activity form, improving the overall user experience. Refactored related styles and logic for better integration and usability.
2025-10-31 15:05:40 +01:00
Torsten Schulz (local)
6a333f198d Enhance CourtDrawing components with new rendering logic and overlay features
Updated CourtDrawingRender.vue to include an overlay displaying the rendered code string and raw values for debugging. Refactored position resolution logic to streamline the determination of start positions. Added methods for building and applying render codes, improving the overall functionality of the drawing tool. Enhanced CourtDrawingTool.vue with updated button styles and layout adjustments for better user experience. Updated DiaryView.vue to ensure proper handling of drawing data and start positions from render codes.
2025-10-30 15:24:57 +01:00
Torsten Schulz (local)
7ea719c178 Enhance CourtDrawing components with additional target selection and arrow drawing features
Added new target selection buttons for main and additional strokes in CourtDrawingTool.vue, allowing users to explicitly choose target positions. Updated CourtDrawingRender.vue to support drawing up to three additional arrows, alternating between left and right sides, with distinct colors for each stroke. Improved the logic for determining the next stroke side and updated related methods for better clarity and functionality.
2025-10-30 11:09:55 +01:00
Torsten Schulz (local)
e23d9fbc44 Update testMembership flag in DiaryView to enable testing mode 2025-10-30 08:32:04 +01:00
Torsten Schulz (local)
3f2b92d886 Refactor backend configuration and enhance logging in services
Updated config.js to ensure .env is loaded correctly from the backend directory. Enhanced MyTischtennisUrlController by removing unnecessary console logs and improving error handling. Updated autoFetchMatchResultsService and autoUpdateRatingsService to return detailed summaries, including counts of fetched or updated items. Improved logging in schedulerService to capture execution details, enhancing monitoring capabilities across scheduled tasks.
2025-10-30 08:14:17 +01:00
Torsten Schulz (local)
89329607dc Enhance myTischtennis URL controller with improved error handling and logging
Updated MyTischtennisUrlController to include detailed error messages for missing user IDs and team configurations. Added logging for session details and automatic login attempts, improving debugging capabilities. Enhanced request logging for API calls to myTischtennis, ensuring all requests are logged, even in case of errors. Updated requestLoggingMiddleware to only log myTischtennis-related requests, streamlining log management. Improved validation checks in autoFetchMatchResultsService for team and league data integrity.
2025-10-29 18:07:43 +01:00
Torsten Schulz (local)
c2b8656783 Refactor request logging middleware to simplify endpoint exclusion logic
Updated the requestLoggingMiddleware to streamline the logic for excluding specific endpoints from logging. The new implementation checks for 'status' in the path and handles root paths more efficiently, improving code readability and maintainability.
2025-10-29 13:41:31 +01:00
Torsten Schulz (local)
0b1e745f03 Add API logging functionality and enhance scheduler service
Introduced ApiLog model and integrated logging for scheduled tasks in the SchedulerService. Updated server.js to include request logging middleware and new API log routes. Enhanced frontend navigation by adding a link to system logs for admin users. Adjusted session check interval in App.vue for improved performance. This update improves monitoring and debugging capabilities across the application.
2025-10-29 13:35:25 +01:00
Torsten Schulz (local)
7a35a0a1d3 Update server port and enhance participant management features
Changed the server port from 3000 to 3005 for local development. Enhanced the participant management functionality by adding a new endpoint to update participant group assignments, including error handling for non-existent participants. Updated the participant model to include a groupId reference, and modified the participant retrieval logic to include group information. Additionally, improved the frontend API client to accommodate the new backend structure and added filtering options in the MembersView for better user experience.
2025-10-29 11:48:24 +01:00
Torsten Schulz (local)
bb2164f666 Add league configuration endpoint and frontend integration for myTischtennis
Implemented a new POST endpoint in MyTischtennisUrlController to configure leagues from table URLs, including season creation logic. Updated myTischtennisRoutes to include the new route for league configuration. Enhanced the myTischtennisUrlParserService to support parsing of table URLs and added a method for decoding group names. Updated TeamManagementView.vue to prompt users for league configuration when a table URL is detected, providing feedback upon successful configuration and reloading relevant data.
2025-10-24 17:06:10 +02:00
Torsten Schulz (local)
d16f250f80 Refactor match and predefined activity services for improved functionality and user experience
Removed unnecessary logging from the MatchService to streamline performance. Enhanced the PredefinedActivityService by implementing a more intelligent search feature that splits queries into individual terms, allowing for more precise filtering of activities. Updated the frontend PredefinedActivities.vue to include a search input with real-time results and a clear search button, improving user interaction and accessibility.
2025-10-24 16:35:03 +02:00
Torsten Schulz (local)
c18b70c6f6 Update styles and enhance clipboard functionality in ScheduleView
Modified main.scss to adjust table header styles, changing font weight to 600 and text transformation to uppercase for improved readability. In ScheduleView.vue, updated clipboard copy functionality to provide user feedback with a visual confirmation when copying codes and PINs, enhancing user experience. The feedback mechanism now includes style changes for better visibility and a reset after a brief display period.
2025-10-24 15:56:59 +02:00
Torsten Schulz (local)
67f4f728fe Refactor PDF table generation to support manual column exclusion
Updated the addTable method in PDFGenerator.js to enhance the functionality for excluding specified columns when generating PDF tables. This change allows for more flexible table rendering by manually parsing the HTML table if columns are to be excluded, improving the overall usability of the PDF generation feature.
2025-10-20 23:43:14 +02:00
Torsten Schulz (local)
b69684ad03 Enhance PDF generation by adding column exclusion functionality in PDFGenerator
Updated PDFGenerator.js to allow exclusion of specified columns when generating tables in PDFs. Modified ScheduleView.vue to determine which columns to exclude based on the selected league, improving the flexibility and usability of the PDF generation feature.
2025-10-20 23:40:05 +02:00
Torsten Schulz (local)
4ff021a85c Refactor schedule view to improve PDF generation and enhance element referencing
Updated ScheduleView.vue to replace direct DOM querying with Vue's ref system for better maintainability. The 'flex-item' div now uses a reference for PDF generation, improving code clarity and performance. This change aligns with best practices in Vue component design.
2025-10-20 23:36:31 +02:00
Torsten Schulz (local)
f1b37d131f Refactor predefined activity routes to simplify permission checks and enhance manual navigation in the frontend
Updated backend predefined activity routes to remove explicit permission checks, allowing for streamlined authentication. Modified frontend App.vue to eliminate automatic redirection to the training-stats page, enabling users to navigate manually. This change improves user experience by providing more control over navigation.
2025-10-17 12:48:58 +02:00
Torsten Schulz (local)
48bbc8015b Enhance permission management by adding caching control and improving permission parsing
Implement middleware to disable caching for permission routes, ensuring up-to-date responses. Update permission parsing logic in the backend to handle JSON strings more robustly, preventing errors during permission retrieval. Enhance the frontend PermissionsView with improved UI elements for managing permissions, including reset functionality and better state representation for actions. Ensure that only explicitly set permissions are saved, optimizing data handling.
2025-10-17 11:55:43 +02:00
Torsten Schulz (local)
56f0ce2f27 Implement permission management and enhance user interface for permissions in the application
Add new permission routes and integrate permission checks across various existing routes to ensure proper access control. Update the UserClub model to include role and permissions fields, allowing for more granular user access management. Enhance the frontend by introducing a user dropdown menu for managing permissions and displaying relevant options based on user roles. Improve the overall user experience by implementing permission-based visibility for navigation links and actions throughout the application.
2025-10-17 09:44:10 +02:00
Torsten Schulz (local)
2dd5e28cbc Add manual trigger endpoints for scheduler service in sessionRoutes
Introduce new POST endpoints for triggering rating updates and fetching match results, along with a GET endpoint for retrieving scheduler status. Enhance error handling and response formatting for better API usability.
2025-10-17 08:10:26 +02:00
Torsten Schulz (local)
c74217f6d8 Add member activity routes and UI enhancements in MembersView
Integrate member activity management by adding new routes in the backend for member activities. Update MembersView.vue to include a button for opening the activities modal and implement the MemberActivitiesDialog component for displaying member activities. Enhance the UI with new button styles for better user interaction.
2025-10-16 22:36:49 +02:00
Torsten Schulz (local)
01bbb85485 Enhance diary member activity management by adding validation and logging in addMembersToActivity function. Implement checks for participantIds to ensure they are an array, and log relevant information for better debugging. Update DiaryDateActivityService to improve error handling and logging for group activity associations. Modify frontend DiaryView to support group activity member assignment, including new methods for toggling and assigning members to group activities, enhancing user experience and functionality. 2025-10-16 22:20:51 +02:00
Torsten Schulz (local)
24aaa9c150 Update favicon links and Open Graph/Twitter image metadata in index.html for improved branding and social sharing. Replace existing favicon with multiple formats and update image references for better compatibility across platforms. 2025-10-16 21:36:59 +02:00
Torsten Schulz (local)
ea3cca563b Enhance match management functionality by adding player selection capabilities. Introduce new endpoints for updating match players and retrieving player match statistics in matchController and matchService. Update Match model to include fields for players ready, planned, and played. Modify frontend components to support player selection dialog, allowing users to manage player statuses effectively. Improve UI for better user experience and data visibility. 2025-10-16 21:09:13 +02:00
Torsten Schulz (local)
e0d56ddadd Enhance MyTischtennis fetch logging in AutoFetchMatchResultsService and AutoUpdateRatingsService. Integrate logging for match results and league table fetch attempts, including success status and execution details. Update updateRatings method to utilize memberService for fetching ratings, improving error handling and logging consistency. Update .gitignore to exclude backend log files. 2025-10-16 18:53:28 +02:00
Torsten Schulz (local)
32f06d7399 Refactor MyTischtennis URL controller to streamline match results and league table fetching. Remove redundant logging and execution time tracking for match results, while ensuring successful fetch counts are accurately reported in the response. Simplify error handling for league table updates without failing the entire request. 2025-10-14 23:31:12 +02:00
Torsten Schulz (local)
36bf99c013 Add MyTischtennis fetch log functionality and new endpoints
Enhance MyTischtennis integration by introducing fetch log capabilities. Implement new controller methods to retrieve fetch logs and latest successful fetches for users. Update routes to include these new endpoints. Modify the MyTischtennis model to support fetch logs and ensure proper logging of fetch operations in various services. Update frontend components to display fetch statistics, improving user experience and data visibility.
2025-10-14 23:07:57 +02:00
Torsten Schulz (local)
7549fb5730 Implement league table functionality and MyTischtennis integration. Add new endpoints for retrieving and updating league tables in matchController and matchRoutes. Enhance Team model with additional fields for match statistics. Update frontend components to display league tables and allow fetching data from MyTischtennis, improving user experience and data accuracy. 2025-10-14 22:55:39 +02:00
Torsten Schulz (local)
1517d83f6c Refactor backend to enhance MyTischtennis integration. Update package.json to change main entry point to server.js. Modify server.js to improve scheduler service logging. Add new fields to ClubTeam, League, Match, and Member models for MyTischtennis data. Update routes to include new MyTischtennis URL parsing and configuration endpoints. Enhance services for fetching team data and scheduling match results. Improve frontend components for MyTischtennis URL configuration and display match results with scores. 2025-10-14 21:58:21 +02:00
Torsten Schulz (local)
993e12d4a5 Update MyTischtennis functionality to support automatic rating updates. Introduce new autoUpdateRatings field in MyTischtennis model and enhance MyTischtennisController to handle update history retrieval. Integrate node-cron for scheduling daily updates at 6:00 AM. Update frontend components to allow users to enable/disable automatic updates and display last update timestamps. 2025-10-09 00:18:41 +02:00
Torsten Schulz (local)
806cb527d4 Refactor group creation and assignment logic in DiaryView. Update group creation to allow specifying the number of groups, enhance participant assignment with group selection, and improve UI elements for better user experience. 2025-10-08 19:21:15 +02:00
Torsten Schulz (local)
7e9d2d2c4f Update member count calculations in MembersView to exclude inactive test memberships. Adjust activeMembersCount and testMembersCount computed properties for improved accuracy in member statistics. 2025-10-08 18:15:18 +02:00
Torsten Schulz (local)
ec9b92000e Update Member model to allow optional birthDate and enhance QuickAddMemberDialog for better input handling. Refactor DiaryView to remove default birthDate logic and improve member creation process. Adjust MembersView to handle empty birthDate gracefully in formatting. 2025-10-08 18:06:22 +02:00
Torsten Schulz (local)
d110900e85 Implement member statistics dropdown in MembersView. Add computed properties for active, test, and inactive member counts, and introduce a toggle for displaying member info. Enhance styling for the dropdown and member stats for improved user experience. 2025-10-08 17:47:43 +02:00
Torsten Schulz (local)
cd3c3502f6 Add tournament name column to OfficialTournaments view. Update data structure to include tournament names for better clarity in participation details. 2025-10-08 15:44:37 +02:00
Torsten Schulz (local)
ccce9bffac Enhance MembersView by adding a new row style for test memberships. Update the member row class to include 'row-test' for better visual distinction of test members, improving user experience in member management. 2025-10-08 15:00:38 +02:00
Torsten Schulz (local)
f1ba25f9f5 Implement logic to determine the quarter with the highest training data in TrainingStatsView. Update average participation calculation to reflect the best quarter's total participants, enhancing accuracy in participation metrics. 2025-10-08 14:54:57 +02:00
Torsten Schulz (local)
548f51ac54 Refactor participation metrics in TrainingStatsView to use total participant counts instead of member participation estimates. Update methods for calculating average participation across different time periods to enhance accuracy and maintainability. 2025-10-08 14:47:30 +02:00
Torsten Schulz (local)
946e4fce1e Enhance SeasonSelector and TeamManagementView with dialog components for improved user interaction. Introduce new dialog states and helper methods for consistent handling of information and confirmations. Update styles in TrainingStatsView to reflect new participation metrics and improve layout. Refactor document display in TeamManagementView to a table format for better readability. 2025-10-08 14:43:53 +02:00
Torsten Schulz (local)
40dcd0e54c Refactor modals in DiaryView, MembersView, OfficialTournaments, ScheduleView, and TrainingStatsView to use dedicated dialog components for improved maintainability and user experience. Update styles and structure for consistency across the application. 2025-10-08 12:49:42 +02:00
Torsten Schulz (local)
bd338b86df Implementiert InfoDialog und ConfirmDialog in mehreren Komponenten, um die Benutzerinteraktion zu verbessern. Fügt Dialogzustände und Hilfsmethoden hinzu, die eine konsistente Handhabung von Informationen und Bestätigungen ermöglichen. Diese Änderungen erhöhen die Benutzerfreundlichkeit und verbessern die visuelle Rückmeldung in der Anwendung. 2025-10-08 11:46:07 +02:00
Torsten Schulz (local)
1d4aa43b02 Aktualisiert das Styling in App.vue durch Hinzufügen von Padding am unteren Rand für die Statusleiste. Ändert den Hintergrund und die Polsterung in DialogManager.vue, um das visuelle Design zu verbessern und die Benutzeroberfläche zu optimieren. 2025-10-08 11:22:45 +02:00
Torsten Schulz (local)
cc08f4ba43 Verbessert die Lesbarkeit und Struktur des Codes in DiaryView.vue durch Anpassungen der Einrückungen und Formatierungen. Optimiert die Anzeige von Aktivitätsvisualisierungen und aktualisiert die Logik für die Eingabefelder, um die Benutzerfreundlichkeit zu erhöhen. Diese Änderungen tragen zur allgemeinen Verbesserung der Benutzeroberfläche und der Codequalität bei. 2025-10-08 11:14:20 +02:00
Torsten Schulz (local)
d0ccaa9e54 Fügt eine neue Methode hasActivityVisual in DiaryView.vue hinzu, um die Sichtbarkeit von Aktivitätsvisualisierungen zu überprüfen. Aktualisiert die Bedingungen für die Anzeige von Icons, die Bilder oder Zeichnungen darstellen, um die Benutzeroberfläche zu verbessern und die Logik zu optimieren. 2025-10-08 11:00:20 +02:00
Torsten Schulz (local)
dc0eff4e4c Entfernt die PDF-Datei 9_code_list_1759357969975.pdf und implementiert eine Sidebar-Toggle-Funktionalität in App.vue. Die Sidebar kann nun auf mobilen Geräten ein- und ausgeklappt werden, um die Benutzeroberfläche zu optimieren. Zudem wurden Titelattribute zu Navigationslinks hinzugefügt, um die Benutzerfreundlichkeit zu verbessern. Der Vuex-Store wurde aktualisiert, um den Zustand der Sidebar zu speichern und zu verwalten. 2025-10-08 10:52:07 +02:00
Torsten Schulz (local)
db9e404372 Aktualisiert die Logik zum Löschen eines Datums in DiaryView.vue. Die Schaltfläche zum Löschen wird nun nur angezeigt, wenn keine Inhalte (Trainingplan, Teilnehmer, Aktivitäten, Unfälle oder Notizen) vorhanden sind. Fügt eine neue Methode canDeleteCurrentDate hinzu, die diese Überprüfung durchführt, um die Benutzerfreundlichkeit zu verbessern und versehentliche Löschungen zu verhindern. 2025-10-04 02:30:23 +02:00
Torsten Schulz (local)
60ac89636e Ändert das Eingabefeld für den Nachnamen in der Mitgliederregistrierung auf optional und aktualisiert die Validierungslogik entsprechend. Setzt ein Standard-Geburtsdatum für neue Mitglieder auf den 01.01. des aktuellen Jahres minus 10 Jahre, wenn kein Geburtsdatum eingegeben wird. Diese Änderungen verbessern die Benutzerfreundlichkeit und Flexibilität bei der Registrierung neuer Mitglieder. 2025-10-04 02:27:49 +02:00
Torsten Schulz (local)
2b1365339e Fügt die Funktion zum Drehen von Mitgliedsbildern hinzu. Implementiert die Logik zur Bildrotation in MemberService und aktualisiert die entsprechenden Routen und Frontend-Komponenten, um die Benutzeroberfläche für die Bildbearbeitung zu verbessern. Ermöglicht das Drehen von Bildern über die Mitgliederansicht und aktualisiert die Anzeige nach der Bearbeitung. 2025-10-04 01:59:21 +02:00
Torsten Schulz (local)
0cf2351c79 Erweitert die Funktionalität von updateRatingsFromMyTischtennis, um eine automatische Anmeldung bei abgelaufener Session zu ermöglichen. Fügt Fehlerbehandlung für den Login-Prozess hinzu und entfernt die Bestätigungsabfrage vor der Aktualisierung der TTR/QTTR-Werte, um den Benutzerfluss zu verbessern. 2025-10-04 01:49:30 +02:00
Torsten Schulz (local)
5c32fad34e Fügt eine bedingte Navigation in App.vue hinzu, die eine leere Navigationsleiste anzeigt, wenn keine Authentifizierung vorliegt. Diese Änderung verbessert die Benutzeroberfläche und sorgt für eine klarere Struktur der Navigation. 2025-10-04 01:43:37 +02:00
Torsten Schulz (local)
7f0b681e88 Ermöglicht die Bearbeitung von Spielergebnissen in TournamentsView.vue durch klickbare Labels und editierbare Eingabefelder. Fügt Logik zum Speichern und Abbrechen von Änderungen hinzu. Aktualisiert das Styling für Eingabefelder und klickbare Texte, um die Benutzererfahrung zu verbessern. 2025-10-04 01:38:27 +02:00
Torsten Schulz (local)
e823af064e Erweitert den MatchReportApiDialog um neue Funktionen zur Anzeige und Verwaltung von Start- und Endzeiten. Fügt Buttons zum Setzen der aktuellen Zeit für beide Zeitpunkte hinzu und implementiert eine Formatierungsfunktion für die Zeitdarstellung. Optimiert die Benutzeroberfläche mit neuen CSS-Stilen für die Zeitanzeigen und die Auto-Fill-Funktion. 2025-10-04 01:30:10 +02:00
Torsten Schulz (local)
3bc6a465a2 Erweitert den MatchReportApiDialog um eine neue Funktion zur Verwaltung des Abschlussstatus der Begrüßung. Fügt einen Toggle-Button hinzu, um den Status als abgeschlossen oder nicht abgeschlossen zu kennzeichnen, und aktualisiert die Benutzeroberfläche mit entsprechenden visuellen Hinweisen. Implementiert Logik zum automatischen Wechsel zur Ergebniserfassung nach Abschluss der Begrüßung. 2025-10-04 01:06:10 +02:00
Torsten Schulz (local)
e8b6578bd4 Erweitert den MatchReportApiDialog um eine neue Validierungslogik zur Überprüfung, ob beide Mannschaften genügend Spieler für den ALLGAMES-Modus haben. Implementiert die Methode canBothTeamsPlayAllGames, die die Spieleranzahl basierend auf verschiedenen Spielsystemen überprüft. Aktualisiert die Benutzeroberfläche mit neuen Warnungen und Rückmeldungen für unvollständige Mannschaften und ermöglicht das Absenden des Berichts trotz unvollständiger Spieleranzahl. 2025-10-04 00:54:41 +02:00
Torsten Schulz (local)
280c1432b7 Fügt eine Auto-Fill-Funktion für leere Matches im MatchReportApiDialog hinzu. Implementiert einen neuen Header mit einem Button zum automatischen Ausfüllen, der die Spielergebnisse für unvollständige Matches basierend auf vorhandenen Spielern automatisch ausfüllt. Verbessert die Benutzeroberfläche mit neuen CSS-Stilen für den Header und den Button. 2025-10-04 00:51:25 +02:00
Torsten Schulz (local)
fd82efdcee Optimiert die Spieleranzahl-Berechnung im MatchReportApiDialog, um die Braunschweiger Regel zu berücksichtigen. Implementiert die Methode getEffectivePlayerCount, die die Spieleranzahl für 2er-Mannschaften als 3 zählt. Aktualisiert die Logik zur Überprüfung der verfügbaren Spieler für Positionen und verbessert die Benutzeroberfläche durch präzisere Rückmeldungen. 2025-10-04 00:45:45 +02:00
Torsten Schulz (local)
e354d82969 Erweitert den MatchReportApiDialog um umfassende Validierungslogik für die Eingabe von Spielberichten. Implementiert neue Warnungen für unvollständige oder fehlerhafte Match-Ergebnisse sowie für ungültige Start- und Endzeiten. Aktualisiert die Logik zur Aktivierung und Deaktivierung des Absende-Buttons basierend auf den Validierungsbedingungen. Verbessert die Benutzeroberfläche mit neuen CSS-Stilen für Validierungsbenachrichtigungen und optimiert die Benutzererfahrung durch dynamische Rückmeldungen. 2025-10-03 23:42:52 +02:00
Torsten Schulz (local)
049ee56571 Erweitert den MatchReportApiDialog um neue Validierungsfunktionen zur Überprüfung von Satz-Inputs und zur Handhabung von Lücken in der Eingabe. Implementiert Methoden zur Berechnung gewonnener Sätze und zur Deaktivierung von Eingabefeldern, wenn ein Spieler bereits 3 Sätze gewonnen hat. Fügt visuelle Warnungen für problematische Eingaben hinzu und verbessert die Benutzeroberfläche mit neuen CSS-Stilen für Eingabefelder. 2025-10-03 23:11:18 +02:00
Torsten Schulz (local)
c6bb534a0d Erweitert den MatchReportApiDialog um Validierungslogik für Mindestspielerzahlen und Doppel. Fügt Methoden zur Überprüfung der Spieleranzahl und Doppelformationen basierend auf dem Spielmodus hinzu. Implementiert Fehlermeldungen zur Anzeige von Validierungsfehlern für Heim- und Gastteams. Verbessert die Benutzeroberfläche mit neuen CSS-Stilen für Fehlermeldungen. 2025-10-03 23:01:13 +02:00
Torsten Schulz (local)
a0fdf256e7 Fügt neue Funktionen zur Bearbeitung von Spielberichten im MatchReportApiDialog hinzu. Implementiert eine Anzeige für den Abschlussstatus des Spiels, einschließlich Warnungen und Deaktivierungen von Eingabefeldern, wenn das Match bereits abgeschlossen ist. Aktualisiert die Logik zur Berechnung der Gesamtpunkte und Sätze sowie zur Validierung von PINs. Verbessert die Benutzeroberfläche mit neuen CSS-Stilen für abgeschlossene Matches und Dialoge zur Anzeige von Match-Daten. 2025-10-03 22:49:05 +02:00
Torsten Schulz (local)
d23a9f086c Erweitert den MatchReportApiDialog mit neuen Funktionen zur Abschlussbearbeitung. Fügt die Anzeige von Aufstellungen, Endergebnis, Zeitangaben, Protest-Eingabe und PIN-Eingaben hinzu. Implementiert Methoden zur Extraktion von Einzel- und Doppelspielern sowie zur Berechnung des Gesamtergebnisses. Optimiert die Benutzeroberfläche mit neuen CSS-Stilen für eine verbesserte Darstellung. 2025-10-03 20:13:49 +02:00
Torsten Schulz (local)
ac727c6c5b Fügt die Funktionalität zur Aktualisierung der Vereinseinstellungen hinzu. Implementiert die Methode updateClubSettings im clubsController, um Begrüßungstexte und Mitgliedsnummern zu aktualisieren. Aktualisiert das Club-Modell, um neue Felder für greetingText und associationMemberNumber zu unterstützen. Ergänzt die Routen in clubRoutes, um die neuen Einstellungen zu verarbeiten. Fügt eine neue Ansicht für die Vereins-Einstellungen im Frontend hinzu und aktualisiert die Navigation entsprechend. 2025-10-03 19:49:19 +02:00
Torsten Schulz (local)
4b1a046149 Fügt Unterstützung für die neue nuscore API hinzu. Aktualisiert die Backend-Routen zur Verarbeitung von Anfragen an die nuscore API und integriert die neuen Dialogkomponenten im Frontend. Ermöglicht das Erstellen lokaler Kopien von nuscore-Daten und verbessert die Benutzeroberfläche durch neue Schaltflächen und Dialoge. Entfernt veraltete Konsolenausgaben und optimiert die Logik zur PIN-Verwaltung. 2025-10-03 15:57:57 +02:00
Torsten Schulz (local)
cc964da9cf Fügt einen Dialog-Manager hinzu, um die Verwaltung von Dialogen zu ermöglichen. Aktualisiert den Vuex-Store mit neuen Mutationen und Aktionen zur Handhabung von Dialogen. Integriert den MatchReportDialog in ScheduleView.vue und ermöglicht das Öffnen von Spielberichten über die Benutzeroberfläche. Verbessert die Benutzererfahrung durch neue Schaltflächen und CSS-Stile für die Dialoge. 2025-10-02 11:44:27 +02:00
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
677 changed files with 95693 additions and 26225 deletions

View File

View File

3
.gitignore vendored
View File

@@ -6,3 +6,6 @@ frontend/.env
backend/.env
backend/images/*
backend/backend-debug.log
backend/*.log
backend/.env.local

86
CHECK_SERVER.md Normal file
View File

@@ -0,0 +1,86 @@
# Server-Prüfung: i18n-Fixes
## Lokale Prüfung (bereits durchgeführt)
✅ Alle Dateien sind lokal korrekt:
- `TeamManagementView.vue` - Alle `$t()` durch `t()` ersetzt, `t` im return Statement
- `PermissionsView.vue` - Alle `$t()` durch `t()` ersetzt, `t` im return Statement
- `LogsView.vue` - Alle `$t()` durch `t()` ersetzt, `t` im return Statement
- `SeasonSelector.vue` - Bereits korrekt
## Server-Prüfung
### 1. Prüfskript auf den Server kopieren
```bash
# Vom lokalen Rechner aus:
scp check-i18n-fixes.sh rv2756:/var/www/tt-tagebuch.de/
```
### 2. Auf dem Server ausführen
```bash
# Auf dem Server:
cd /var/www/tt-tagebuch.de
chmod +x check-i18n-fixes.sh
./check-i18n-fixes.sh
```
### 3. Falls Dateien nicht aktualisiert sind
```bash
# Auf dem Server:
cd /var/www/tt-tagebuch.de
git pull origin main
cd backend
npm install # Erstellt automatisch den Frontend-Build (via postinstall script)
```
### 4. Backend neu starten (falls nötig)
```bash
# Falls als systemd-Service:
sudo systemctl restart tt-tagebuch
# Oder falls als PM2-Prozess:
pm2 restart tt-tagebuch-backend
```
## Erwartete Ergebnisse
Das Prüfskript sollte folgende Ausgabe zeigen:
```
1. TeamManagementView.vue:
✓ Enthält 'const t = (key, params) => i18n.global.t'
✓ Enthält keine $t() Aufrufe mehr
✓ 't' ist im return Statement enthalten
2. PermissionsView.vue:
✓ Enthält 'const t = (key, params) => i18n.global.t'
✓ Enthält keine $t() Aufrufe mehr
✓ 't' ist im return Statement enthalten
3. LogsView.vue:
✓ Enthält 'const t = (key, params) => i18n.global.t'
✓ Enthält keine $t() Aufrufe mehr
✓ 't' ist im return Statement enthalten
4. SeasonSelector.vue:
✓ Enthält 'const t = (key, params) => i18n.global.t'
✓ Enthält keine $t() Aufrufe mehr
```
## Commits, die auf den Server müssen
Die folgenden Commits müssen auf dem Server sein:
- `b0e610f` - Fix: Replace all $t() calls with t() in PermissionsView and LogsView templates
- `0285c05` - Fix: Replace all $t() calls with t() in TeamManagementView template
- `5d4f2eb` - Update localization handling in TeamManagementView
Prüfe mit:
```bash
git log --oneline -5
```

191
DEPLOYMENT_SOCKET_IO.md Normal file
View File

@@ -0,0 +1,191 @@
# Deployment-Anleitung: Socket.IO mit SSL
Socket.IO läuft jetzt direkt auf HTTPS-Port 3051 (nicht über Apache-Proxy).
## Schritte nach dem Deployment
### 1. Firewall-Port öffnen
```bash
# UFW (Ubuntu Firewall)
sudo ufw allow 3051/tcp
```
### 2. Apache-Konfiguration aktualisieren
```bash
sudo cp /var/www/tt-tagebuch.de/apache.conf.example /etc/apache2/sites-available/tt-tagebuch.de-le-ssl.conf
sudo systemctl restart apache2
```
### 3. systemd-Service konfigurieren (als www-data)
**WICHTIG:** Der Service sollte als `www-data` laufen, nicht als `nobody`!
```bash
# Service-Datei installieren
sudo cp /var/www/tt-tagebuch.de/tt-tagebuch.service /etc/systemd/system/
sudo systemctl daemon-reload
```
Die Service-Datei konfiguriert:
- User: `www-data` (Standard-Webserver-Benutzer)
- Group: `www-data`
- Port: 3050 (HTTP) und 3051 (HTTPS)
### 4. SSL-Zertifikat-Berechtigungen setzen
**WICHTIG:** Der Node.js-Prozess muss Zugriff auf die SSL-Zertifikate haben!
```bash
cd /var/www/tt-tagebuch.de/backend
chmod +x scripts/fixCertPermissions.sh
sudo ./scripts/fixCertPermissions.sh
```
Dieses Skript:
- Erstellt die Gruppe `ssl-cert` (falls nicht vorhanden)
- Fügt den Service-Benutzer (`www-data`) zur Gruppe hinzu
- Setzt die Berechtigungen für die Zertifikate
### 5. Backend neu starten
**WICHTIG:** Der Backend-Server muss neu gestartet werden, damit der HTTPS-Server auf Port 3051 läuft!
```bash
# Falls als systemd-Service:
sudo systemctl restart tt-tagebuch
# Oder falls als PM2-Prozess:
pm2 restart tt-tagebuch-backend
```
### 6. Prüfen, ob HTTPS-Server läuft
```bash
# Prüfe, ob Port 3051 geöffnet ist
sudo netstat -tlnp | grep 3051
# Oder:
sudo ss -tlnp | grep 3051
# Prüfe Backend-Logs
sudo journalctl -u tt-tagebuch -f
# Oder bei PM2:
pm2 logs tt-tagebuch-backend
```
Du solltest folgende Meldung sehen:
```
🚀 HTTPS-Server für Socket.IO läuft auf Port 3051
```
### 7. Diagnose-Skript ausführen
```bash
cd /var/www/tt-tagebuch.de/backend
node scripts/checkSocketIOServer.js
```
Dieses Skript prüft:
- Ob SSL-Zertifikate existieren
- Ob Port 3051 geöffnet ist
- Ob der Server erreichbar ist
### 8. Testen
Im Browser sollte Socket.IO jetzt direkt zu `wss://tt-tagebuch.de:3051` verbinden.
## Troubleshooting
### Port 3051 ist nicht erreichbar
1. **Prüfe Firewall:**
```bash
sudo ufw status
sudo ufw allow 3051/tcp
```
2. **Prüfe, ob der Server läuft:**
```bash
sudo netstat -tlnp | grep 3051
sudo ss -tlnp | grep 3051
```
3. **Prüfe Backend-Logs auf Fehler:**
```bash
sudo journalctl -u tt-tagebuch -n 50
# Oder:
pm2 logs tt-tagebuch-backend --lines 50
```
4. **Prüfe, ob HTTPS-Server gestartet wurde:**
- Suche in den Logs nach: `🚀 HTTPS-Server für Socket.IO läuft auf Port 3051`
- Falls nicht vorhanden, prüfe auf Fehler: `⚠️ HTTPS-Server konnte nicht gestartet werden`
### SSL-Zertifikat-Fehler / Berechtigungsfehler
**Fehler:** `EACCES: permission denied, open '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem'`
**Lösung:**
```bash
cd /var/www/tt-tagebuch.de/backend
chmod +x scripts/fixCertPermissions.sh
sudo ./scripts/fixCertPermissions.sh
sudo systemctl restart tt-tagebuch
```
Stelle sicher, dass die Zertifikate existieren:
```bash
ls -la /etc/letsencrypt/live/tt-tagebuch.de/
```
Falls die Zertifikate nicht existieren:
```bash
sudo certbot certonly --standalone -d tt-tagebuch.de
```
### Service läuft als "nobody"
**Problem:** Der Service läuft als `nobody`, was zu eingeschränkt ist.
**Lösung:**
1. Installiere die Service-Datei (siehe Schritt 3)
2. Führe das Berechtigungs-Skript aus (siehe Schritt 4)
3. Starte den Service neu
```bash
# Prüfe aktuellen Service-User
sudo systemctl show -p User tt-tagebuch.service
# Installiere Service-Datei
sudo cp /var/www/tt-tagebuch.de/tt-tagebuch.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl restart tt-tagebuch
# Prüfe, ob jetzt als www-data läuft
sudo systemctl show -p User tt-tagebuch.service
```
### Frontend verbindet nicht
1. **Prüfe Browser-Konsole auf Fehler**
2. **Prüfe, ob `import.meta.env.PROD` korrekt gesetzt ist:**
- In Produktion sollte die Socket.IO-URL `https://tt-tagebuch.de:3051` sein
- In Entwicklung sollte sie `http://localhost:3005` sein
3. **Prüfe, ob die Socket.IO-URL korrekt ist:**
- Öffne Browser-Entwicklertools → Network
- Suche nach WebSocket-Verbindungen
- Die URL sollte `wss://tt-tagebuch.de:3051/socket.io/...` sein
### Server lauscht nur auf localhost
Der Server sollte auf `0.0.0.0` lauschen (nicht nur auf `localhost`).
Dies ist bereits in der Konfiguration eingestellt:
```javascript
httpsServer.listen(httpsPort, '0.0.0.0', () => {
console.log(`🚀 HTTPS-Server für Socket.IO läuft auf Port ${httpsPort}`);
});
```
Falls der Server trotzdem nicht erreichbar ist, prüfe die Backend-Logs.

342
DSGVO_CHECKLIST.md Normal file
View File

@@ -0,0 +1,342 @@
# DSGVO-Konformitäts-Checkliste für Trainingstagebuch
## Status: ⚠️ PRÜFUNG ERFORDERLICH
Diese Checkliste dokumentiert den aktuellen Stand der DSGVO-Konformität der Anwendung.
---
## 1. Datenschutzerklärung ✅ / ⚠️
### Status: ⚠️ Teilweise vorhanden, muss aktualisiert werden
**Vorhanden:**
- ✅ Datenschutzerklärung vorhanden (`/datenschutz`)
- ✅ Impressum vorhanden (`/impressum`)
- ✅ Verlinkung im Footer
**Fehlend/Verbesserungsbedarf:**
- ⚠️ MyTischtennis-Integration nicht erwähnt (Drittlandübermittlung?)
- ⚠️ Logging von API-Requests nicht erwähnt
- ⚠️ Verschlüsselung von Mitgliederdaten nicht erwähnt
- ⚠️ Speicherdauer für Logs nicht konkretisiert
- ⚠️ Keine Informationen zu automatischer Löschung
---
## 2. Einwilligungen ⚠️
### Status: ⚠️ Teilweise vorhanden
**Vorhanden:**
-`picsInInternetAllowed` bei Mitgliedern (Einwilligung für Fotos im Internet)
- ✅ MyTischtennis: `savePassword` und `autoUpdateRatings` (Einwilligungen)
**Fehlend/Verbesserungsbedarf:**
- ⚠️ Keine explizite Einwilligung bei Registrierung zur Datenschutzerklärung
- ⚠️ Keine Einwilligung für Logging von API-Requests
- ⚠️ Keine Einwilligung für Datenübertragung an MyTischtennis.de
- ⚠️ Keine Möglichkeit, Einwilligungen zu widerrufen (außer manuell)
---
## 3. Löschrechte (Art. 17 DSGVO) ⚠️
### Status: ⚠️ Teilweise implementiert
**Vorhanden:**
- ✅ DELETE-Endpunkte für viele Ressourcen (Member, Tournament, etc.)
- ✅ MyTischtennis-Account kann gelöscht werden
**Fehlend/Verbesserungsbedarf:**
-**KRITISCH:** Kein Endpunkt zum vollständigen Löschen eines User-Accounts
-**KRITISCH:** Keine automatische Löschung aller zugehörigen Daten (Cascade-Delete)
- ❌ Keine Löschung von Logs nach Ablauf der Speicherdauer
- ⚠️ Keine Anonymisierung statt Löschung (falls gesetzliche Aufbewahrungspflichten bestehen)
- ⚠️ Keine Bestätigung vor Löschung kritischer Daten
**Empfehlung:**
- Implementiere `/api/user/delete` Endpunkt
- Implementiere automatische Löschung aller zugehörigen Daten:
- UserClub-Einträge
- MyTischtennis-Account
- Alle Logs (nach Anonymisierung)
- Alle Mitglieder, die nur diesem User zugeordnet sind
- Implementiere automatische Löschung von Logs nach 90 Tagen
---
## 4. Auskunftsrechte (Art. 15 DSGVO) ❌
### Status: ❌ Nicht implementiert
**Fehlend:**
-**KRITISCH:** Kein Endpunkt zur Auskunft über gespeicherte Daten
- ❌ Keine Übersicht über alle personenbezogenen Daten eines Users
- ❌ Keine Übersicht über alle Mitgliederdaten
- ❌ Keine Übersicht über Logs, die einen User betreffen
**Empfehlung:**
- Implementiere `/api/user/data-export` Endpunkt
- Exportiere alle Daten in strukturiertem Format (JSON)
- Inkludiere:
- User-Daten
- Vereinszugehörigkeiten
- Mitgliederdaten (falls User Zugriff hat)
- Logs
- MyTischtennis-Daten
---
## 5. Datenportabilität (Art. 20 DSGVO) ❌
### Status: ❌ Nicht implementiert
**Fehlend:**
-**KRITISCH:** Kein Export in maschinenlesbarem Format
- ❌ Keine JSON/XML-Export-Funktion
- ⚠️ PDF-Export für Trainingstage vorhanden, aber nicht für alle Daten
**Empfehlung:**
- Implementiere `/api/user/data-export` mit JSON-Format
- Implementiere Export für:
- Alle eigenen Daten
- Alle Mitgliederdaten (falls berechtigt)
- Alle Trainingsdaten
- Alle Turnierdaten
---
## 6. Verschlüsselung ✅ / ⚠️
### Status: ✅ Gut implementiert
**Vorhanden:**
- ✅ AES-256-CBC Verschlüsselung für Mitgliederdaten:
- firstName, lastName
- birthDate
- phone, street, city, postalCode
- email
- notes (Participant)
- ✅ Passwörter werden mit bcrypt gehasht
- ✅ HTTPS für alle Verbindungen
**Verbesserungsbedarf:**
- ⚠️ Verschlüsselungsschlüssel sollte in separater, sicherer Konfiguration sein
-**BEHOBEN:** MyTischtennis-Daten werden jetzt vollständig verschlüsselt (E-Mail, Zugriffstoken, Refresh-Token, Cookie, Benutzerdaten, Vereinsinformationen)
- ⚠️ Keine Verschlüsselung für Logs (können personenbezogene Daten enthalten)
---
## 7. Logging ⚠️
### Status: ⚠️ Verbesserungsbedarf
**Vorhanden:**
- ✅ Aktivitäts-Logging (`log` Tabelle) - protokolliert wichtige Aktionen
- ✅ Server-Logs - Standard-Server-Logs für Fehlerbehebung
-**ENTFERNT:** API-Logging für MyTischtennis-Requests wurde deaktiviert
**Probleme:**
-**BEHOBEN:** API-Logging für MyTischtennis-Requests wurde komplett entfernt (keine personenbezogenen Daten mehr in API-Logs)
- ⚠️ Keine automatische Löschung von Aktivitätslogs (noch zu implementieren)
-**BEHOBEN:** In Datenschutzerklärung dokumentiert, was geloggt wird
**Empfehlung:**
- ⚠️ Implementiere automatische Löschung von Aktivitätslogs nach angemessener Frist (noch ausstehend)
---
## 8. MyTischtennis-Integration ⚠️
### Status: ⚠️ Verbesserungsbedarf
**Vorhanden:**
- ✅ Verschlüsselung von Passwörtern
- ✅ Einwilligungen (`savePassword`, `autoUpdateRatings`)
- ✅ DELETE-Endpunkt für Account
**Probleme:**
-**BEHOBEN:** Drittlandübermittlung in Datenschutzerklärung erwähnt
- ⚠️ Keine explizite Einwilligung für Datenübertragung an MyTischtennis.de
-**BEHOBEN:** Informationen über Datenschutz bei MyTischtennis.de in Datenschutzerklärung
-**BEHOBEN:** Alle MyTischtennis-Daten werden jetzt verschlüsselt gespeichert
**Empfehlung:**
- Aktualisiere Datenschutzerklärung:
- Erwähne MyTischtennis-Integration
- Erkläre, welche Daten übertragen werden
- Verweise auf Datenschutzerklärung von MyTischtennis.de
- Erkläre Rechtsgrundlage (Einwilligung)
- Implementiere explizite Einwilligung bei Einrichtung der Integration
- Verschlüssele auch Zugriffstoken
---
## 9. Cookies & Local Storage ✅
### Status: ✅ Konform
**Vorhanden:**
- ✅ Nur technisch notwendige Cookies/Storage:
- Session-Token (Session Storage)
- Username, Clubs, Permissions (Local Storage)
- ✅ Keine Tracking-Cookies
- ✅ Keine Werbe-Cookies
- ✅ Dokumentiert in Datenschutzerklärung
**Hinweis:**
- Local Storage wird für persistente Daten verwendet (Clubs, Permissions)
- Dies ist technisch notwendig und DSGVO-konform
---
## 10. Berechtigungssystem ✅
### Status: ✅ Gut implementiert
**Vorhanden:**
- ✅ Rollenbasierte Zugriffe (Admin, Trainer, Mannschaftsführer, Mitglied)
- ✅ Individuelle Berechtigungen pro Ressource
- ✅ Transparente Zugriffskontrolle
- ✅ Logging von Aktivitäten
**Hinweis:**
- Berechtigungssystem ist DSGVO-konform
- Ermöglicht Datenminimierung (Zugriff nur auf notwendige Daten)
---
## 11. Datenminimierung ⚠️
### Status: ⚠️ Teilweise konform
**Vorhanden:**
- ✅ Nur notwendige Daten werden gespeichert
- ✅ Berechtigungssystem ermöglicht minimale Datenzugriffe
**Verbesserungsbedarf:**
- ⚠️ Logs enthalten möglicherweise zu viele Daten (Request/Response-Bodies)
- ⚠️ Keine automatische Löschung alter Daten
- ⚠️ Keine Option, Daten zu anonymisieren statt zu löschen
---
## 12. Technische und organisatorische Maßnahmen (TOM) ✅ / ⚠️
### Status: ✅ Gut, aber verbesserungsbedürftig
**Vorhanden:**
- ✅ Verschlüsselung sensibler Daten
- ✅ HTTPS für alle Verbindungen
- ✅ Passwort-Hashing (bcrypt)
- ✅ Authentifizierung und Autorisierung
- ✅ Berechtigungssystem
**Verbesserungsbedarf:**
- ⚠️ Keine Dokumentation der TOM
- ⚠️ Keine regelmäßigen Sicherheitsupdates dokumentiert
- ⚠️ Keine Backup-Strategie dokumentiert
- ⚠️ Keine Notfallpläne dokumentiert
---
## 13. Auftragsverarbeitung ⚠️
### Status: ⚠️ Nicht dokumentiert
**Fehlend:**
- ⚠️ Keine Informationen über Hosting-Provider
- ⚠️ Keine Informationen über Auftragsverarbeitungsverträge (AVV)
- ⚠️ Keine Informationen über Subunternehmer
**Empfehlung:**
- Dokumentiere alle Auftragsverarbeiter (Hosting, etc.)
- Erwähne in Datenschutzerklärung, dass AVV abgeschlossen wurden
---
## 14. Betroffenenrechte - Umsetzung ❌
### Status: ❌ Nicht vollständig implementiert
**Fehlend:**
-**KRITISCH:** Kein Endpunkt für Auskunft (Art. 15)
-**KRITISCH:** Kein Endpunkt für Löschung (Art. 17)
-**KRITISCH:** Kein Endpunkt für Datenexport (Art. 20)
- ❌ Kein Endpunkt für Berichtigung (Art. 16) - teilweise vorhanden über normale Edit-Endpunkte
- ❌ Kein Endpunkt für Einschränkung (Art. 18)
- ❌ Kein Endpunkt für Widerspruch (Art. 21)
**Empfehlung:**
- Implementiere zentrale Endpunkte für alle Betroffenenrechte:
- `GET /api/user/rights/information` - Auskunft
- `DELETE /api/user/rights/deletion` - Löschung
- `GET /api/user/rights/export` - Datenexport
- `PUT /api/user/rights/restriction` - Einschränkung
- `POST /api/user/rights/objection` - Widerspruch
---
## 15. Kontakt für Datenschutz ✅
### Status: ✅ Vorhanden
**Vorhanden:**
- ✅ E-Mail-Adresse in Datenschutzerklärung: tsschulz@tsschulz.de
- ✅ Vollständige Anschrift im Impressum
---
## Zusammenfassung
### ✅ Gut implementiert:
1. Verschlüsselung sensibler Daten
2. HTTPS
3. Berechtigungssystem
4. Cookies/Local Storage (nur technisch notwendig)
5. Datenschutzerklärung vorhanden
### ⚠️ Verbesserungsbedarf:
1. Datenschutzerklärung aktualisieren (MyTischtennis, Logging)
2. Logging von personenbezogenen Daten reduzieren/anonymisieren
3. Automatische Löschung von Logs implementieren
4. MyTischtennis-Integration in Datenschutzerklärung erwähnen
### ❌ Kritisch - Muss implementiert werden:
1. **Löschrechte-API** (Art. 17 DSGVO)
2. **Auskunftsrechte-API** (Art. 15 DSGVO)
3. **Datenexport-API** (Art. 20 DSGVO)
4. **Automatische Löschung von Logs** nach Retention-Periode
---
## Prioritäten
### Sofort (vor Live-Betrieb):
1. Datenschutzerklärung aktualisieren
2. Löschrechte-API implementieren
3. Auskunftsrechte-API implementieren
4. Datenexport-API implementieren
### Kurzfristig (innerhalb 1 Monat):
1. Automatische Löschung von Logs implementieren
2. Logging von personenbezogenen Daten reduzieren/anonymisieren
3. MyTischtennis-Integration in Datenschutzerklärung dokumentieren
### Mittelfristig (innerhalb 3 Monate):
1. Einwilligungsmanagement implementieren
2. TOM dokumentieren
3. Auftragsverarbeitung dokumentieren
---
## Nächste Schritte
1. ✅ Diese Checkliste erstellen
2. ⏳ Datenschutzerklärung aktualisieren
3. ⏳ Löschrechte-API implementieren
4. ⏳ Auskunftsrechte-API implementieren
5. ⏳ Datenexport-API implementieren
6. ⏳ Logging verbessern

210
PERMISSIONS_GUIDE.md Normal file
View File

@@ -0,0 +1,210 @@
# Berechtigungssystem - Dokumentation
## Übersicht
Das Trainingstagebuch verfügt nun über ein vollständiges rollenbasiertes Berechtigungssystem (RBAC - Role-Based Access Control). Der Club-Ersteller hat automatisch Admin-Rechte und kann anderen Mitgliedern Rollen und spezifische Berechtigungen zuweisen.
## Rollen
### 1. Administrator (admin)
- **Vollzugriff** auf alle Funktionen
- Kann Berechtigungen anderer Benutzer verwalten
- Der Club-Ersteller ist automatisch Administrator und kann nicht degradiert werden
### 2. Trainer (trainer)
- Kann Trainingseinheiten planen und verwalten
- Kann Mitglieder anlegen und bearbeiten
- Kann Spielpläne einsehen und bearbeiten
- Kann Turniere organisieren
- **Kann nicht**: Einstellungen ändern, Berechtigungen verwalten
### 3. Mannschaftsführer (team_manager)
- Kann Teams und Spielpläne verwalten
- Kann Spieler für Matches einteilen
- Kann Spielergebnisse eintragen
- **Kann nicht**: Trainingseinheiten planen, Mitglieder verwalten
### 4. Mitglied (member)
- Nur Lesezugriff auf alle Bereiche
- Kann eigene Daten einsehen
- **Kann nicht**: Daten ändern oder löschen
## Berechtigungsbereiche
- **diary**: Trainingstagebuch
- **members**: Mitgliederverwaltung
- **teams**: Team-Management
- **schedule**: Spielpläne
- **tournaments**: Turniere
- **statistics**: Statistiken
- **settings**: Einstellungen
- **permissions**: Berechtigungsverwaltung
- **mytischtennis**: MyTischtennis-Integration (für alle zugänglich)
## Backend-Integration
### Migration ausführen
```sql
mysql -u username -p database_name < backend/migrations/add_permissions_to_user_club.sql
```
### Authorization Middleware verwenden
```javascript
import { authorize, requireAdmin, requireOwner } from '../middleware/authorizationMiddleware.js';
// Beispiel: Nur Lesezugriff erforderlich
router.get('/diary/:clubId', authenticate, authorize('diary', 'read'), getDiary);
// Beispiel: Schreibzugriff erforderlich
router.post('/diary/:clubId', authenticate, authorize('diary', 'write'), createDiary);
// Beispiel: Admin-Rechte erforderlich
router.put('/settings/:clubId', authenticate, requireAdmin(), updateSettings);
// Beispiel: Nur Owner
router.delete('/club/:clubId', authenticate, requireOwner(), deleteClub);
```
### Permission Service verwenden
```javascript
import permissionService from '../services/permissionService.js';
// Berechtigungen prüfen
const hasPermission = await permissionService.hasPermission(userId, clubId, 'diary', 'write');
// Rolle setzen
await permissionService.setUserRole(userId, clubId, 'trainer', adminUserId);
// Custom Permissions setzen
await permissionService.setCustomPermissions(
userId,
clubId,
{ diary: { write: false }, members: { write: true } },
adminUserId
);
```
## Frontend-Integration
### Composable verwenden
```vue
<script setup>
import { usePermissions } from '@/composables/usePermissions.js';
const { can, canWrite, canDelete, isAdmin, isOwner, userRole } = usePermissions();
// Beispiel
if (can('diary', 'write')) {
// Zeige Bearbeitungsbutton
}
</script>
```
### Direktiven verwenden
```vue
<template>
<!-- Nur anzeigen, wenn Schreibrechte für diary vorhanden -->
<button v-can:diary.write>Bearbeiten</button>
<!-- Nur anzeigen, wenn Löschrechte für members vorhanden -->
<button v-can:members.delete>Löschen</button>
<!-- Alternative Syntax -->
<div v-can="'diary.write'">Inhalt nur für Berechtigte</div>
<!-- Nur für Admins -->
<div v-admin>Admin-Bereich</div>
<!-- Nur für Owner -->
<div v-owner>Owner-Bereich</div>
</template>
```
### Store verwenden
```javascript
import { useStore } from 'vuex';
const store = useStore();
// Berechtigungen abrufen
const permissions = store.getters.currentPermissions;
const hasPermission = store.getters.hasPermission('diary', 'write');
const isOwner = store.getters.isClubOwner;
const userRole = store.getters.userRole;
// Berechtigungen laden (wird automatisch beim Club-Wechsel gemacht)
await store.dispatch('loadPermissions', clubId);
```
## Admin-UI
Die Berechtigungsverwaltung ist unter `/permissions` verfügbar und nur für Administratoren sichtbar.
**Funktionen:**
- Übersicht aller Clubmitglieder mit ihren Rollen
- Rollen zuweisen/ändern
- Custom Permissions für einzelne Benutzer definieren
- Erklärung der verfügbaren Rollen
## MyTischtennis-Integration
Die MyTischtennis-Einstellungen und -Funktionen sind für **alle Club-Mitglieder** zugänglich, unabhängig von ihrer Rolle. Dies ermöglicht es jedem, die Anbindung einzurichten und Daten abzurufen.
## Sicherheitshinweise
1. **Der Club-Ersteller** (Owner) kann nicht degradiert oder gelöscht werden
2. **Owner-Rechte** können nicht übertragen werden
3. **Backend-Validierung** wird immer durchgeführt, auch wenn das Frontend Elemente ausblendet
4. **Alle API-Routen** sind durch Middleware geschützt
5. **Permissions werden gecacht** im localStorage für bessere Performance
## Beispiel-Szenarien
### Szenario 1: Trainer hinzufügen
1. Admin öffnet `/permissions`
2. Wählt Benutzer aus
3. Ändert Rolle zu "Trainer"
4. Benutzer kann jetzt Trainingseinheiten planen
### Szenario 2: Custom Permissions
1. Admin öffnet `/permissions`
2. Wählt Benutzer aus
3. Klickt auf "Anpassen"
4. Setzt individuelle Berechtigungen (z.B. nur Diary-Schreibrecht)
5. Speichert
### Szenario 3: Neues Mitglied
1. Mitglied registriert sich und fordert Zugang an
2. Admin genehmigt Anfrage (Standardrolle: "member")
3. Mitglied hat Lesezugriff
4. Bei Bedarf kann Admin die Rolle später ändern
## Troubleshooting
**Problem**: Berechtigungen werden nicht aktualisiert
- **Lösung**: Seite neu laden oder Club neu auswählen
**Problem**: "Keine Berechtigung" trotz korrekter Rolle
- **Lösung**: Prüfen, ob Custom Permissions die Rolle überschreiben
**Problem**: Owner kann keine Änderungen vornehmen
- **Lösung**: Owner sollte automatisch alle Rechte haben. Prüfen Sie die `isOwner`-Flag in der Datenbank
## API-Endpunkte
```
GET /api/permissions/:clubId - Eigene Berechtigungen abrufen
GET /api/permissions/:clubId/members - Alle Mitglieder mit Berechtigungen (Admin)
PUT /api/permissions/:clubId/user/:userId/role - Rolle ändern (Admin)
PUT /api/permissions/:clubId/user/:userId/permissions - Custom Permissions setzen (Admin)
GET /api/permissions/roles/available - Verfügbare Rollen abrufen
GET /api/permissions/structure/all - Berechtigungsstruktur abrufen
```

235
PERMISSIONS_MIGRATION.md Normal file
View File

@@ -0,0 +1,235 @@
# Berechtigungssystem - Migrations-Anleitung
## Übersicht
Diese Anleitung hilft Ihnen, das neue Berechtigungssystem für bestehende Clubs einzurichten.
## Schritt 1: Datenbank-Schema erweitern
Führen Sie zuerst die SQL-Migration aus, um die neuen Spalten hinzuzufügen:
```bash
mysql -u username -p database_name < backend/migrations/add_permissions_to_user_club.sql
```
Dies fügt folgende Spalten zur `user_club` Tabelle hinzu:
- `role` (VARCHAR) - Benutzerrolle (admin, trainer, team_manager, member)
- `permissions` (JSON) - Custom Permissions
- `is_owner` (BOOLEAN) - Markiert den Club-Ersteller
## Schritt 2: Bestehende Daten migrieren
Sie haben zwei Optionen:
### Option A: Node.js Script (Empfohlen)
Das Script identifiziert automatisch den ersten Benutzer jedes Clubs (nach `createdAt`) und setzt ihn als Owner.
```bash
cd /home/torsten/Programs/trainingstagebuch/backend
node scripts/migratePermissions.js
```
**Ausgabe:**
```
Starting permissions migration...
Found 3 club(s)
--- Club: TTC Beispiel (ID: 1) ---
Members found: 5
First member (will be owner): admin@example.com
✓ Updated admin@example.com: role=admin, isOwner=true
✓ Updated user1@example.com: role=member, isOwner=false
✓ Updated user2@example.com: role=member, isOwner=false
...
✅ Migration completed successfully!
Summary:
Club Owners (3):
- TTC Beispiel: admin@example.com
- SV Teststadt: owner@test.de
- TSC Demo: demo@example.com
Role Distribution:
- Admins: 3
- Members: 12
```
### Option B: SQL Script
Wenn Sie lieber SQL verwenden möchten:
```bash
mysql -u username -p database_name < backend/migrations/update_existing_user_club_permissions.sql
```
Dieses Script:
1. Setzt `role = 'member'` für alle genehmigten Benutzer ohne Rolle
2. Markiert den Benutzer mit der niedrigsten `user_id` pro Club als Owner
## Schritt 3: Manuelle Anpassungen (Optional)
### Falscher Owner?
Falls das Script den falschen Benutzer als Owner markiert hat, können Sie dies manuell korrigieren:
```sql
-- Alten Owner zurücksetzen
UPDATE user_club
SET is_owner = 0, role = 'member'
WHERE club_id = 1 AND user_id = 123;
-- Neuen Owner setzen
UPDATE user_club
SET is_owner = 1, role = 'admin'
WHERE club_id = 1 AND user_id = 456;
```
### Weitere Admins ernennen
```sql
UPDATE user_club
SET role = 'admin'
WHERE club_id = 1 AND user_id = 789;
```
### Trainer ernennen
```sql
UPDATE user_club
SET role = 'trainer'
WHERE club_id = 1 AND user_id = 101;
```
## Schritt 4: Verifizierung
### Backend neu starten
```bash
# Server neu starten (wenn er läuft)
sudo systemctl restart tt-tagebuch
```
### Im Browser testen
1. Loggen Sie sich ein
2. Wählen Sie einen Club aus
3. Navigieren Sie zu "Berechtigungen" (nur für Admins sichtbar)
4. Überprüfen Sie, dass alle Mitglieder korrekt angezeigt werden
### SQL Verifizierung
```sql
-- Alle Club-Mitglieder mit ihren Berechtigungen anzeigen
SELECT
c.name as club_name,
u.email as user_email,
uc.role,
uc.is_owner,
uc.approved
FROM user_club uc
JOIN club c ON c.id = uc.club_id
JOIN user u ON u.id = uc.user_id
WHERE uc.approved = 1
ORDER BY c.name, uc.is_owner DESC, uc.role, u.email;
```
## Troubleshooting
### Problem: "Keine Berechtigung" trotz Owner-Status
**Lösung:** Überprüfen Sie in der Datenbank:
```sql
SELECT role, is_owner, approved
FROM user_club
WHERE user_id = YOUR_USER_ID AND club_id = YOUR_CLUB_ID;
```
Sollte sein: `role='admin'`, `is_owner=1`, `approved=1`
### Problem: Owner kann nicht geändert werden
Das ist korrekt! Der Owner (Club-Ersteller) kann seine eigenen Rechte nicht verlieren. Dies ist eine Sicherheitsmaßnahme.
### Problem: Berechtigungen werden nicht geladen
**Lösung:**
1. Browser-Cache leeren
2. LocalStorage leeren: `localStorage.clear()` in der Browser-Console
3. Neu einloggen
### Problem: "Lade Mitglieder..." bleibt hängen
**Mögliche Ursachen:**
1. Migration noch nicht ausgeführt
2. Backend nicht neu gestartet
3. Frontend nicht neu gebaut
**Lösung:**
```bash
# Backend
cd /home/torsten/Programs/trainingstagebuch/backend
node scripts/migratePermissions.js
# Frontend
cd /home/torsten/Programs/trainingstagebuch/frontend
npm run build
# Server neu starten
sudo systemctl restart tt-tagebuch
```
## Nach der Migration
### Neue Clubs
Bei neuen Clubs wird der Ersteller automatisch als Owner mit Admin-Rechten eingerichtet. Keine manuelle Aktion erforderlich.
### Neue Mitglieder
Neue Mitglieder erhalten automatisch die Rolle "member" (Lesezugriff). Admins können die Rolle später ändern.
### Berechtigungen verwalten
Admins können über die Web-UI unter `/permissions` Berechtigungen verwalten:
1. Rollen zuweisen (Admin, Trainer, Mannschaftsführer, Mitglied)
2. Custom Permissions definieren (für spezielle Anwendungsfälle)
## Wichtige Hinweise
⚠️ **Sicherung erstellen:**
```bash
mysqldump -u username -p database_name > backup_before_permissions_$(date +%Y%m%d).sql
```
⚠️ **Owner-Rechte:**
- Der Owner (is_owner=1) kann nicht degradiert oder gelöscht werden
- Jeder Club hat genau einen Owner
- Owner-Rechte können nicht übertragen werden (nur durch direkte DB-Änderung)
⚠️ **MyTischtennis:**
- MyTischtennis-Funktionen sind für ALLE Mitglieder zugänglich
- Keine Berechtigungsprüfung für MyTischtennis-Endpunkte
## Rollback (falls nötig)
Falls Sie das Berechtigungssystem zurücknehmen müssen:
```sql
-- Spalten entfernen (Achtung: Datenverlust!)
ALTER TABLE user_club
DROP COLUMN role,
DROP COLUMN permissions,
DROP COLUMN is_owner;
-- Indizes entfernen
DROP INDEX idx_user_club_role ON user_club;
DROP INDEX idx_user_club_owner ON user_club;
```
Dann Backend-Code auf vorherige Version zurücksetzen.

69
SERVER_NODE_UPGRADE.md Normal file
View File

@@ -0,0 +1,69 @@
# Server Node.js Upgrade-Anleitung
## Problem
Der Server verwendet Node.js 20.17.0, aber Vite 7.2.4 benötigt Node.js 20.19+ oder 22.12+.
## Lösung 1: Node.js auf dem Server upgraden (Empfohlen)
### Option A: Node.js 20.19+ installieren
```bash
# Auf dem Server:
# Mit nvm (falls installiert):
nvm install 20.19.0
nvm use 20.19.0
nvm alias default 20.19.0
# Oder mit NodeSource Repository:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs=20.19.0-1nodesource1
# Prüfe Version:
node --version # Sollte 20.19.0 oder höher sein
```
### Option B: Node.js 22.12+ installieren (LTS)
```bash
# Auf dem Server:
# Mit nvm:
nvm install 22.12.0
nvm use 22.12.0
nvm alias default 22.12.0
# Oder mit NodeSource Repository:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# Prüfe Version:
node --version # Sollte 22.12.0 oder höher sein
```
### Nach dem Upgrade
```bash
cd /var/www/tt-tagebuch.de/backend
npm install # Erstellt automatisch den Frontend-Build
sudo systemctl restart tt-tagebuch
```
## Lösung 2: Vite auf Version 6 downgraden (Temporär)
Falls Node.js nicht upgradet werden kann, wurde Vite bereits auf Version 6.0.0 downgraded.
```bash
cd /var/www/tt-tagebuch.de/backend
npm install # Erstellt automatisch den Frontend-Build
sudo systemctl restart tt-tagebuch
```
**Hinweis:** Vite 6 funktioniert mit Node.js 20.17.0, aber Vite 7 bietet bessere Performance und Features.
## Empfehlung
**Node.js upgraden** ist die bessere Lösung, da:
- Vite 7 bessere Performance bietet
- Zukünftige Updates einfacher sind
- Node.js 20.19+ oder 22.12+ LTS-Versionen sind

109
SITEMAP_ANLEITUNG.md Normal file
View File

@@ -0,0 +1,109 @@
# Sitemap für Google Search Console einreichen
## Aktuelle Sitemap
Die Sitemap ist verfügbar unter: `https://tt-tagebuch.de/sitemap.xml`
Sie enthält folgende öffentliche Seiten:
- `/` (Home) - Priorität: 1.0
- `/register` (Registrierung) - Priorität: 0.8
- `/login` (Anmeldung) - Priorität: 0.7
- `/impressum` (Impressum) - Priorität: 0.3
- `/datenschutz` (Datenschutz) - Priorität: 0.3
## Sitemap aktualisieren
### Automatisch (empfohlen)
```bash
./update-sitemap.sh
```
Das Skript aktualisiert automatisch das `lastmod`-Datum auf das heutige Datum.
### Manuell
Die Sitemap-Datei befindet sich in: `frontend/public/sitemap.xml`
Nach Änderungen:
1. Frontend neu bauen: `cd frontend && npm run build`
2. Backend neu starten (falls nötig)
## Sitemap in Google Search Console einreichen
### Schritt 1: Google Search Console öffnen
1. Gehe zu: https://search.google.com/search-console
2. Wähle die Property für `tt-tagebuch.de` aus
### Schritt 2: Sitemap hinzufügen
1. Klicke im linken Menü auf **"Sitemaps"**
2. Im Feld **"Neue Sitemap hinzufügen"** eingeben:
```
sitemap.xml
```
Oder die vollständige URL:
```
https://tt-tagebuch.de/sitemap.xml
```
3. Klicke auf **"Senden"**
### Schritt 3: Status prüfen
- Google wird die Sitemap innerhalb weniger Minuten verarbeiten
- Der Status wird angezeigt:
- ✅ **Erfolgreich**: Sitemap wurde erfolgreich verarbeitet
- ⚠️ **Warnung**: Sitemap wurde verarbeitet, aber es gibt Warnungen
- ❌ **Fehler**: Sitemap konnte nicht verarbeitet werden
### Schritt 4: Indexierung anfordern
Nach dem Einreichen der Sitemap kannst du auch einzelne URLs zur Indexierung anfordern:
1. Gehe zu **"URL-Prüfung"**
2. Gib die URL ein: `https://tt-tagebuch.de/`
3. Klicke auf **"Indexierung anfordern"**
## Sitemap testen
### Online-Tools
- Google Sitemap Tester: https://www.xml-sitemaps.com/validate-xml-sitemap.html
- Sitemap Validator: https://validator.w3.org/
### Per Kommandozeile
```bash
# Sitemap abrufen
curl https://tt-tagebuch.de/sitemap.xml
# XML-Validierung (falls xmllint installiert ist)
curl -s https://tt-tagebuch.de/sitemap.xml | xmllint --noout -
```
## Wichtige Hinweise
1. **robots.txt**: Die Sitemap ist bereits in der `robots.txt` referenziert:
```
Sitemap: https://tt-tagebuch.de/sitemap.xml
```
2. **lastmod-Datum**: Wird automatisch beim Ausführen von `update-sitemap.sh` aktualisiert
3. **Nur öffentliche Seiten**: Die Sitemap enthält nur öffentlich zugängliche Seiten. Geschützte Seiten (die eine Anmeldung erfordern) sind nicht enthalten.
4. **Prioritäten**:
- Homepage: 1.0 (höchste Priorität)
- Registrierung/Login: 0.7-0.8 (wichtig für neue Nutzer)
- Rechtliche Seiten: 0.3 (niedrige Priorität, ändern sich selten)
## Troubleshooting
### Sitemap wird nicht gefunden
- Prüfe, ob die Sitemap unter `https://tt-tagebuch.de/sitemap.xml` erreichbar ist
- Stelle sicher, dass das Frontend gebaut wurde: `cd frontend && npm run build`
- Prüfe die Apache-Konfiguration (sollte statische Dateien aus `/var/www/tt-tagebuch.de` servieren)
### Sitemap wird nicht indexiert
- Warte einige Stunden/Tage - Google braucht Zeit zum Crawlen
- Prüfe in der Search Console, ob es Fehler gibt
- Stelle sicher, dass die URLs in der Sitemap erreichbar sind
- Prüfe, ob die `robots.txt` die Seiten nicht blockiert
### Sitemap enthält Fehler
- Validiere die XML-Struktur mit einem XML-Validator
- Prüfe, ob alle URLs korrekt sind (keine 404-Fehler)
- Stelle sicher, dass alle URLs HTTPS verwenden (nicht HTTP)

22
apache-http.conf.example Normal file
View File

@@ -0,0 +1,22 @@
# Apache-Konfiguration für tt-tagebuch.de - HTTP (Port 80)
#
# Diese Datei kopieren nach: /etc/apache2/sites-available/tt-tagebuch.de.conf
# Dann aktivieren mit: sudo a2ensite tt-tagebuch.de.conf
# Und neu starten: sudo systemctl restart apache2
#
# WICHTIG: Folgende Module müssen aktiviert sein:
# sudo a2enmod rewrite
# sudo systemctl restart apache2
# HTTP: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de
<VirtualHost *:80>
ServerName www.tt-tagebuch.de
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# HTTP: tt-tagebuch.de -> HTTPS: tt-tagebuch.de
<VirtualHost *:80>
ServerName tt-tagebuch.de
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>

60
apache-https.conf.example Normal file
View File

@@ -0,0 +1,60 @@
# Apache-Konfiguration für tt-tagebuch.de - HTTPS (Port 443)
#
# Diese Datei kopieren nach: /etc/apache2/sites-available/tt-tagebuch.de-le-ssl.conf
# Dann aktivieren mit: sudo a2ensite tt-tagebuch.de-le-ssl.conf
# Und neu starten: sudo systemctl restart apache2
#
# WICHTIG: Folgende Module müssen aktiviert sein:
# sudo a2enmod proxy
# sudo a2enmod proxy_http
# sudo a2enmod proxy_wstunnel
# sudo a2enmod rewrite
# sudo a2enmod headers
# sudo systemctl restart apache2
# HTTPS: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de (301-Weiterleitung)
<VirtualHost *:443>
ServerName www.tt-tagebuch.de
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# HTTPS: tt-tagebuch.de - Hauptkonfiguration (non-www)
<VirtualHost *:443>
ServerName tt-tagebuch.de
DocumentRoot /var/www/tt-tagebuch.de
<Directory /var/www/tt-tagebuch.de>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/tt-tagebuch.de_error.log
CustomLog ${APACHE_LOG_DIR}/tt-tagebuch.de_access.log combined
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
ProxyRequests Off
# HINWEIS: Socket.IO läuft jetzt direkt auf HTTPS-Port 3051 (nicht über Apache-Proxy)
# Siehe backend/SOCKET_IO_SSL_SETUP.md für Details
# API-Routen
ProxyPass /api http://localhost:3050/api
ProxyPassReverse /api http://localhost:3050/api
# Alle anderen Anfragen an den Backend-Server (für Frontend)
ProxyPass / http://localhost:3050/
ProxyPassReverse / http://localhost:3050/
</VirtualHost>

89
apache.conf.example Normal file
View File

@@ -0,0 +1,89 @@
# Apache-Konfiguration für tt-tagebuch.de
#
# HINWEIS: Diese Datei ist eine kombinierte Referenz.
# Für die tatsächliche Konfiguration werden zwei separate Dateien verwendet:
#
# 1. apache-http.conf.example -> /etc/apache2/sites-available/tt-tagebuch.de.conf
# (HTTP, Port 80 - Weiterleitung zu HTTPS)
#
# 2. apache-https.conf.example -> /etc/apache2/sites-available/tt-tagebuch.de-le-ssl.conf
# (HTTPS, Port 443 - Hauptkonfiguration)
#
# Oder verwende das Update-Skript: ./update-apache-config.sh
#
# WICHTIG: Folgende Module müssen aktiviert sein:
# sudo a2enmod proxy
# sudo a2enmod proxy_http
# sudo a2enmod proxy_wstunnel
# sudo a2enmod rewrite
# sudo a2enmod headers
# sudo systemctl restart apache2
# ============================================
# HTTP (Port 80) - Weiterleitung zu HTTPS
# ============================================
# HTTP: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de
<VirtualHost *:80>
ServerName www.tt-tagebuch.de
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# HTTP: tt-tagebuch.de -> HTTPS: tt-tagebuch.de
<VirtualHost *:80>
ServerName tt-tagebuch.de
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# ============================================
# HTTPS (Port 443) - Weiterleitung www -> non-www
# ============================================
# HTTPS: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de (301-Weiterleitung)
<VirtualHost *:443>
ServerName www.tt-tagebuch.de
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# ============================================
# HTTPS (Port 443) - Hauptkonfiguration (non-www)
# ============================================
<VirtualHost *:443>
ServerName tt-tagebuch.de
DocumentRoot /var/www/tt-tagebuch.de
<Directory /var/www/tt-tagebuch.de>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/tt-tagebuch.de_error.log
CustomLog ${APACHE_LOG_DIR}/tt-tagebuch.de_access.log combined
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
ProxyRequests Off
# HINWEIS: Socket.IO läuft jetzt direkt auf HTTPS-Port 3051 (nicht über Apache-Proxy)
# Siehe backend/SOCKET_IO_SSL_SETUP.md für Details
# API-Routen
ProxyPass /api http://localhost:3050/api
ProxyPassReverse /api http://localhost:3050/api
# Alle anderen Anfragen an den Backend-Server (für Frontend)
ProxyPass / http://localhost:3050/
ProxyPassReverse / http://localhost:3050/
</VirtualHost>

View File

@@ -0,0 +1,212 @@
# MyTischtennis Automatischer Datenabruf
## Übersicht
Dieses System ermöglicht den automatischen Abruf von Spielergebnissen und Statistiken von myTischtennis.de.
## Scheduler
### 6:00 Uhr - Rating Updates
- **Service:** `autoUpdateRatingsService.js`
- **Funktion:** Aktualisiert TTR/QTTR-Werte für Spieler
- **TODO:** Implementierung der eigentlichen Rating-Update-Logik
### 6:30 Uhr - Spielergebnisse
- **Service:** `autoFetchMatchResultsService.js`
- **Funktion:** Ruft Spielerbilanzen für konfigurierte Teams ab
- **Status:** ✅ Grundlegende Implementierung fertig
## Benötigte Konfiguration
### 1. MyTischtennis-Account
- Account muss in den MyTischtennis-Settings verknüpft sein
- Checkbox "Automatische Updates" aktivieren
- Passwort speichern (erforderlich für automatische Re-Authentifizierung)
### 2. League-Konfiguration
Für jede Liga müssen folgende Felder ausgefüllt werden:
```sql
UPDATE league SET
my_tischtennis_group_id = '504417', -- Group ID von myTischtennis
association = 'HeTTV', -- Verband (z.B. HeTTV, DTTB)
groupname = '1.Kreisklasse' -- Gruppenname für URL
WHERE id = 1;
```
**Beispiel-URL:**
```
https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/...
^^^^^ ^^^^^^^^^^^^^^ ^^^^^^
association groupname group_id
```
### 3. Team-Konfiguration
Für jedes Team muss die myTischtennis Team-ID gesetzt werden:
```sql
UPDATE club_team SET
my_tischtennis_team_id = '2995094' -- Team ID von myTischtennis
WHERE id = 1;
```
**Beispiel-URL:**
```
.../mannschaft/2995094/Harheimer_TC_(J11)/spielerbilanzen/gesamt
^^^^^^^
team_id
```
### 4. Spieler-Zuordnung (Optional)
Spieler werden automatisch anhand des Namens zugeordnet. Für genauere Zuordnung kann die myTischtennis Player-ID gesetzt werden:
```sql
UPDATE member SET
my_tischtennis_player_id = 'NU2705037' -- Player ID von myTischtennis
WHERE id = 1;
```
## Migrationen
Folgende Migrationen müssen ausgeführt werden:
```bash
# 1. MyTischtennis Auto-Update-Felder
mysql -u root -p trainingstagebuch < backend/migrations/add_auto_update_ratings_to_my_tischtennis.sql
# 2. MyTischtennis Update-History-Tabelle
mysql -u root -p trainingstagebuch < backend/migrations/create_my_tischtennis_update_history.sql
# 3. League MyTischtennis-Felder
mysql -u root -p trainingstagebuch < backend/migrations/add_mytischtennis_fields_to_league.sql
# 4. Team MyTischtennis-ID
mysql -u root -p trainingstagebuch < backend/migrations/add_mytischtennis_team_id_to_club_team.sql
# 5. Member MyTischtennis Player-ID
mysql -u root -p trainingstagebuch < backend/migrations/add_mytischtennis_player_id_to_member.sql
# 6. Match Result-Felder
mysql -u root -p trainingstagebuch < backend/migrations/add_match_result_fields.sql
```
## Abgerufene Daten
Von der myTischtennis API werden folgende Daten abgerufen:
### Einzelstatistiken
- Player ID, Vorname, Nachname
- Gewonnene/Verlorene Punkte
- Anzahl Spiele
- Detaillierte Statistiken nach Gegner-Position
### Doppelstatistiken
- Player IDs, Namen der beiden Spieler
- Gewonnene/Verlorene Punkte
- Anzahl Spiele
### Team-Informationen
- Teamname, Liga, Saison
- Gesamtpunkte (gewonnen/verloren)
- Doppel- und Einzelpunkte
## Implementierungsdetails
### Datenfluss
1. **Scheduler** (6:30 Uhr):
- `schedulerService.js` triggert `autoFetchMatchResultsService.executeAutomaticFetch()`
2. **Account-Verarbeitung**:
- Lädt alle MyTischtennis-Accounts mit `autoUpdateRatings = true`
- Prüft Session-Gültigkeit
- Re-Authentifizierung bei abgelaufener Session
3. **Team-Abfrage**:
- Lädt alle Teams mit konfigurierten myTischtennis-IDs
- Baut API-URL dynamisch zusammen
- Führt authentifizierten GET-Request durch
4. **Datenverarbeitung**:
- Parst JSON-Response
- Matched Spieler anhand von ID oder Name
- Speichert myTischtennis Player-ID bei Mitgliedern
- Loggt Statistiken
### Player-Matching-Algorithmus
```javascript
1. Suche nach myTischtennis Player-ID (exakte Übereinstimmung)
2. Falls nicht gefunden: Suche nach Name (case-insensitive)
3. Falls gefunden: Speichere myTischtennis Player-ID für zukünftige Abfragen
```
**Hinweis:** Da Namen verschlüsselt gespeichert werden, müssen für den Namens-Abgleich alle Members geladen und entschlüsselt werden. Dies ist bei großen Datenbanken ineffizient.
## TODO / Offene Punkte
### Noch zu implementieren:
1. **TTR/QTTR Updates** (6:00 Uhr Job):
- Endpoint für TTR/QTTR-Daten identifizieren
- Daten abrufen und in Member-Tabelle speichern
2. **Spielergebnis-Details**:
- Einzelne Matches mit Satzständen speichern
- Tabelle für Match-Historie erstellen
3. **History-Tabelle für Spielergebnis-Abrufe** (optional):
- Ähnlich zu `my_tischtennis_update_history`
- Speichert Erfolg/Fehler der Abrufe
4. **Benachrichtigungen** (optional):
- Email/Push bei neuen Ergebnissen
- Highlights für besondere Siege
5. **Performance-Optimierung**:
- Caching für Player-Matches
- Incremental Updates (nur neue Daten)
## Manueller Test
```javascript
// Im Node-Backend-Code oder über API-Endpoint:
import schedulerService from './services/schedulerService.js';
// Rating Updates manuell triggern
await schedulerService.triggerRatingUpdates();
// Spielergebnisse manuell abrufen
await schedulerService.triggerMatchResultsFetch();
```
## API-Dokumentation
### MyTischtennis Spielerbilanzen-Endpoint
**URL-Format:**
```
https://www.mytischtennis.de/click-tt/{association}/{season}/ligen/{groupname}/gruppe/{groupId}/mannschaft/{teamId}/{teamname}/spielerbilanzen/gesamt?_data=routes%2Fclick-tt%2B%2F%24association%2B%2F%24season%2B%2F%24type%2B%2F%28%24groupname%29.gruppe.%24urlid_.mannschaft.%24teamid.%24teamname%2B%2Fspielerbilanzen.%24filter
```
**Parameter:**
- `{association}`: Verband (z.B. "HeTTV")
- `{season}`: Saison im Format "25--26"
- `{groupname}`: Gruppenname URL-encoded (z.B. "1.Kreisklasse")
- `{groupId}`: Gruppen-ID (numerisch, z.B. "504417")
- `{teamId}`: Team-ID (numerisch, z.B. "2995094")
- `{teamname}`: Teamname URL-encoded mit Underscores (z.B. "Harheimer_TC_(J11)")
**Response:** JSON mit `data.balancesheet` Array
## Sicherheit
- ✅ Automatische Session-Verwaltung
- ✅ Re-Authentifizierung bei abgelaufenen Sessions
- ✅ Passwörter verschlüsselt gespeichert
- ✅ Fehlerbehandlung und Logging
- ✅ Graceful Degradation (einzelne Team-Fehler stoppen nicht den gesamten Prozess)

View File

@@ -0,0 +1,332 @@
# MyTischtennis URL Parser
## Übersicht
Der URL-Parser ermöglicht es, myTischtennis-Team-URLs automatisch zu parsen und die Konfiguration für automatische Datenabrufe vorzunehmen.
## Verwendung
### 1. URL Parsen
**Endpoint:** `POST /api/mytischtennis/parse-url`
**Request:**
```json
{
"url": "https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/spielerbilanzen/gesamt"
}
```
**Response:**
```json
{
"success": true,
"data": {
"association": "HeTTV",
"season": "25/26",
"type": "ligen",
"groupname": "1.Kreisklasse",
"groupId": "504417",
"teamId": "2995094",
"teamname": "Harheimer TC (J11)",
"originalUrl": "https://www.mytischtennis.de/click-tt/...",
"clubId": "43030",
"clubName": "Harheimer TC",
"teamName": "Jugend 11",
"leagueName": "Jugend 13 1. Kreisklasse",
"region": "Frankfurt",
"tableRank": 8,
"matchesWon": 0,
"matchesLost": 3
}
}
```
### 2. Team Automatisch Konfigurieren
**Endpoint:** `POST /api/mytischtennis/configure-team`
**Request:**
```json
{
"url": "https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/spielerbilanzen/gesamt",
"clubTeamId": 1,
"createLeague": false,
"createSeason": false
}
```
**Parameter:**
- `url` (required): Die myTischtennis-URL
- `clubTeamId` (required): Die ID des lokalen Club-Teams
- `createLeague` (optional): Wenn `true`, wird eine neue League erstellt
- `createSeason` (optional): Wenn `true`, wird eine neue Season erstellt
**Response:**
```json
{
"success": true,
"message": "Team configured successfully",
"data": {
"team": {
"id": 1,
"name": "Jugend 11",
"myTischtennisTeamId": "2995094"
},
"league": {
"id": 5,
"name": "Jugend 13 1. Kreisklasse",
"myTischtennisGroupId": "504417",
"association": "HeTTV",
"groupname": "1.Kreisklasse"
},
"season": {
"id": 2,
"name": "25/26"
},
"parsedData": { ... }
}
}
```
### 3. URL für Team Abrufen
**Endpoint:** `GET /api/mytischtennis/team-url/:teamId`
**Response:**
```json
{
"success": true,
"url": "https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer%20TC%20%28J11%29/spielerbilanzen/gesamt"
}
```
## URL-Format
### Unterstützte URL-Muster
```
https://www.mytischtennis.de/click-tt/{association}/{season}/{type}/{groupname}/gruppe/{groupId}/mannschaft/{teamId}/{teamname}/...
```
**Komponenten:**
- `{association}`: Verband (z.B. "HeTTV", "DTTB", "WestD")
- `{season}`: Saison im Format "YY--YY" (z.B. "25--26" für 2025/2026)
- `{type}`: Typ (meist "ligen")
- `{groupname}`: Gruppenname URL-encoded (z.B. "1.Kreisklasse", "Kreisliga")
- `{groupId}`: Numerische Gruppen-ID (z.B. "504417")
- `{teamId}`: Numerische Team-ID (z.B. "2995094")
- `{teamname}`: Teamname URL-encoded mit Underscores (z.B. "Harheimer_TC_(J11)")
### Beispiel-URLs
**Spielerbilanzen:**
```
https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/spielerbilanzen/gesamt
```
**Spielplan:**
```
https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/spielplan
```
**Tabelle:**
```
https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/tabelle
```
## Datenfluss
### Ohne MyTischtennis-Login
1. URL wird geparst
2. Nur URL-Komponenten werden extrahiert
3. Zusätzliche Daten (clubName, leagueName, etc.) sind nicht verfügbar
### Mit MyTischtennis-Login
1. URL wird geparst
2. API-Request an myTischtennis mit Authentication
3. Vollständige Team-Daten werden abgerufen
4. Alle Felder sind verfügbar
## Frontend-Integration
### Vue.js Beispiel
```javascript
<template>
<div>
<input
v-model="myTischtennisUrl"
placeholder="MyTischtennis URL einfügen..."
@blur="parseUrl"
/>
<div v-if="parsedData">
<h3>{{ parsedData.teamname }}</h3>
<p>Liga: {{ parsedData.leagueName }}</p>
<p>Verband: {{ parsedData.association }}</p>
<p>Tabelle: Platz {{ parsedData.tableRank }}</p>
<button @click="configureTeam">Team konfigurieren</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
myTischtennisUrl: '',
parsedData: null
};
},
methods: {
async parseUrl() {
if (!this.myTischtennisUrl) return;
try {
const response = await fetch('/api/mytischtennis/parse-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'userid': this.userId,
'authcode': this.authCode
},
body: JSON.stringify({
url: this.myTischtennisUrl
})
});
const result = await response.json();
this.parsedData = result.data;
} catch (error) {
console.error('Fehler beim Parsen:', error);
// Hinweis: Im Frontend stattdessen InfoDialog/ConfirmDialog verwenden
// alert('URL konnte nicht geparst werden');
}
},
async configureTeam() {
try {
const response = await fetch('/api/mytischtennis/configure-team', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'userid': this.userId,
'authcode': this.authCode
},
body: JSON.stringify({
url: this.myTischtennisUrl,
clubTeamId: this.selectedTeamId,
createLeague: false,
createSeason: true
})
});
const result = await response.json();
if (result.success) {
// In der Anwendung bitte InfoDialog nutzen
// alert('Team erfolgreich konfiguriert!');
} else {
// alert('Team konnte nicht konfiguriert werden');
}
} catch (error) {
console.error('Fehler bei Konfiguration:', error);
// alert('Team konnte nicht konfiguriert werden');
}
}
}
};
</script>
```
## Workflow
### Empfohlener Workflow für Benutzer
1. **MyTischtennis-URL kopieren:**
- Auf myTischtennis.de zum Team navigieren
- URL aus Adresszeile kopieren
2. **URL in Trainingstagebuch einfügen:**
- Zu Team-Verwaltung navigieren
- URL einfügen
- Automatisches Parsen
3. **Konfiguration überprüfen:**
- Geparste Daten werden angezeigt
- Benutzer kann Daten überprüfen und bei Bedarf anpassen
4. **Team konfigurieren:**
- Auf "Konfigurieren" klicken
- System speichert alle benötigten IDs
- Automatischer Datenabruf ist ab sofort aktiv
## Fehlerbehandlung
### Häufige Fehler
**"Invalid myTischtennis URL format"**
- URL entspricht nicht dem erwarteten Format
- Lösung: Vollständige URL von der Spielerbilanzen-Seite kopieren
**"Season not found"**
- Saison existiert noch nicht in der Datenbank
- Lösung: `createSeason: true` setzen
**"Team has no league assigned"**
- Team hat keine verknüpfte Liga
- Lösung: `createLeague: true` setzen oder Liga manuell zuweisen
**"HTTP 401: Unauthorized"**
- MyTischtennis-Login abgelaufen oder nicht vorhanden
- Lösung: In MyTischtennis-Settings erneut anmelden
## Sicherheit
- ✅ Alle Endpoints erfordern Authentifizierung
- ✅ UserID wird aus Header-Parameter gelesen
- ✅ MyTischtennis-Credentials werden sicher gespeichert
- ✅ Keine sensiblen Daten in URLs
## Technische Details
### Service: `myTischtennisUrlParserService`
**Methoden:**
- `parseUrl(url)` - Parst URL und extrahiert Komponenten
- `fetchTeamData(parsedUrl, cookie, accessToken)` - Ruft zusätzliche Daten ab
- `getCompleteConfig(url, cookie, accessToken)` - Kombination aus Parsen + Abrufen
- `isValidTeamUrl(url)` - Validiert URL-Format
- `buildUrl(config)` - Baut URL aus Komponenten
### Controller: `myTischtennisUrlController`
**Endpoints:**
- `POST /api/mytischtennis/parse-url` - URL parsen
- `POST /api/mytischtennis/configure-team` - Team konfigurieren
- `GET /api/mytischtennis/team-url/:teamId` - URL abrufen
## Zukünftige Erweiterungen
### Geplante Features
1. **Bulk-Import:**
- Mehrere URLs gleichzeitig importieren
- Alle Teams einer Liga auf einmal konfigurieren
2. **Auto-Discovery:**
- Automatisches Finden aller Teams eines Vereins
- Vorschläge für ähnliche Teams
3. **Validierung:**
- Prüfung, ob Team bereits konfiguriert ist
- Warnung bei Duplikaten
4. **History:**
- Speichern der URL-Konfigurationen
- Versionierung bei Änderungen

View File

@@ -0,0 +1,140 @@
# Socket.IO mit SSL direkt betreiben (Alternative zu Apache-Proxy)
Falls die Apache-WebSocket-Proxy-Konfiguration nicht funktioniert, kann Socket.IO direkt mit SSL betrieben werden.
## Voraussetzungen
1. SSL-Zertifikat (z.B. von Let's Encrypt)
2. Port in der Firewall öffnen (z.B. 3051)
3. Socket.IO-Server auf HTTPS konfigurieren
## Backend-Konfiguration
### 1. Socket.IO auf HTTPS umstellen
Ändere `backend/server.js`:
```javascript
import https from 'https';
import fs from 'fs';
// SSL-Zertifikat laden
const httpsOptions = {
key: fs.readFileSync('/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem'),
cert: fs.readFileSync('/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem')
};
// HTTPS-Server erstellen
const httpsServer = https.createServer(httpsOptions, app);
// Socket.IO initialisieren
initializeSocketIO(httpsServer);
// HTTPS-Server starten
const httpsPort = process.env.HTTPS_PORT || 3051;
httpsServer.listen(httpsPort, () => {
console.log(`🚀 HTTPS-Server läuft auf Port ${httpsPort}`);
});
// HTTP-Server für API (optional, falls API weiterhin über HTTP laufen soll)
const httpServer = createServer(app);
const httpPort = process.env.PORT || 3005;
httpServer.listen(httpPort, () => {
console.log(`🚀 HTTP-Server läuft auf Port ${httpPort}`);
});
```
### 2. Frontend-Konfiguration
Ändere `frontend/src/services/socketService.js`:
```javascript
import { io } from 'socket.io-client';
import { backendBaseUrl } from '../apiClient.js';
let socket = null;
export const connectSocket = (clubId) => {
// Verwende HTTPS-URL für Socket.IO
const socketUrl = backendBaseUrl.replace('http://', 'https://').replace(':3005', ':3051');
if (socket && socket.connected) {
// Wenn bereits verbunden, verlasse den alten Club-Raum und trete dem neuen bei
if (socket.currentClubId) {
socket.emit('leave-club', socket.currentClubId);
}
} else {
// Neue Verbindung erstellen
socket = io(socketUrl, {
path: '/socket.io/',
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
timeout: 20000,
upgrade: true,
forceNew: false,
secure: true // Wichtig für HTTPS
});
socket.on('connect', () => {
console.log('Socket.IO verbunden');
if (socket.currentClubId) {
socket.emit('join-club', socket.currentClubId);
}
});
socket.on('disconnect', () => {
console.log('Socket.IO getrennt');
});
socket.on('connect_error', (error) => {
console.error('Socket.IO Verbindungsfehler:', error);
});
}
// Club-Raum beitreten
if (clubId) {
socket.emit('join-club', clubId);
socket.currentClubId = clubId;
}
return socket;
};
export const disconnectSocket = () => {
if (socket) {
socket.disconnect();
socket = null;
}
};
export const getSocket = () => socket;
```
### 3. Firewall-Port öffnen
```bash
# UFW (Ubuntu Firewall)
sudo ufw allow 3051/tcp
# Oder iptables
sudo iptables -A INPUT -p tcp --dport 3051 -j ACCEPT
```
### 4. Apache-Konfiguration anpassen
Entferne die Socket.IO-Proxy-Konfiguration aus Apache, da Socket.IO jetzt direkt erreichbar ist.
## Vorteile
- Einfacher zu konfigurieren
- Keine Apache-Proxy-Probleme
- Direkte WebSocket-Verbindung
## Nachteile
- Separater Port muss geöffnet sein
- Zwei Ports (HTTP für API, HTTPS für Socket.IO)
- CORS-Konfiguration muss angepasst werden

View File

@@ -9,15 +9,18 @@ const dbConfig = {
database: process.env.DB_NAME || 'trainingsdiary'
};
const report = [];
async function cleanupKeys() {
let connection;
try {
console.log('🔌 Verbinde mit der Datenbank...');
report.push('🔌 Verbinde mit der Datenbank...');
connection = await mysql.createConnection(dbConfig);
// 1. Status vor dem Cleanup
console.log('\n📊 STATUS VOR DEM CLEANUP:');
report.push('');
report.push('📊 STATUS VOR DEM CLEANUP:');
const [tablesBefore] = await connection.execute(`
SELECT
TABLE_NAME,
@@ -29,57 +32,60 @@ async function cleanupKeys() {
`, [dbConfig.database]);
tablesBefore.forEach(table => {
console.log(` ${table.TABLE_NAME}: ${table.key_count} Keys`);
report.push(` ${table.TABLE_NAME}: ${table.key_count} Keys`);
});
// 2. Alle INDEX der Problem-Tabellen anzeigen
const problemTables = ['member', 'diary_tags', 'season'];
for (const tableName of problemTables) {
console.log(`\n🔍 INDEX für Tabelle '${tableName}':`);
report.push('');
report.push(`🔍 INDEX für Tabelle '${tableName}':`);
try {
const [indexes] = await connection.execute(`SHOW INDEX FROM \`${tableName}\``);
if (indexes.length === 0) {
console.log(` Keine INDEX gefunden für Tabelle '${tableName}'`);
report.push(` Keine INDEX gefunden für Tabelle '${tableName}'`);
continue;
}
indexes.forEach(index => {
console.log(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`);
report.push(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`);
});
// 3. Überflüssige INDEX entfernen (alle außer PRIMARY und UNIQUE)
console.log(`\n🗑️ Entferne überflüssige INDEX aus '${tableName}':`);
report.push('');
report.push(`🗑️ Entferne überflüssige INDEX aus '${tableName}':`);
for (const index of indexes) {
// Behalte PRIMARY KEY und UNIQUE constraints
if (index.Key_name === 'PRIMARY' || index.Non_unique === 0) {
console.log(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`);
report.push(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`);
continue;
}
// Entferne alle anderen INDEX
try {
await connection.execute(`DROP INDEX \`${index.Key_name}\` ON \`${tableName}\``);
console.log(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`);
report.push(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`);
} catch (error) {
if (error.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
console.log(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`);
report.push(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`);
} else {
console.log(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`);
report.push(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`);
}
}
}
} catch (error) {
console.log(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`);
report.push(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`);
}
}
// 4. Status nach dem Cleanup
console.log('\n📊 STATUS NACH DEM CLEANUP:');
report.push('');
report.push('📊 STATUS NACH DEM CLEANUP:');
const [tablesAfter] = await connection.execute(`
SELECT
TABLE_NAME,
@@ -96,7 +102,7 @@ async function cleanupKeys() {
const diff = beforeCount - table.key_count;
const status = table.key_count <= 5 ? '✅' : table.key_count <= 10 ? '⚠️' : '❌';
console.log(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`);
report.push(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`);
});
// 5. Gesamtanzahl der Keys
@@ -106,18 +112,20 @@ async function cleanupKeys() {
WHERE TABLE_SCHEMA = ?
`, [dbConfig.database]);
console.log(`\n📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`);
report.push('');
report.push(`📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`);
// 6. Zusammenfassung
console.log('\n🎯 ZUSAMMENFASSUNG:');
report.push('');
report.push('🎯 ZUSAMMENFASSUNG:');
const problemTablesAfter = tablesAfter.filter(t => t.key_count > 10);
if (problemTablesAfter.length === 0) {
console.log(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!');
report.push(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!');
} else {
console.log(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:');
report.push(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:');
problemTablesAfter.forEach(table => {
console.log(` - ${table.TABLE_NAME}: ${table.key_count} Keys`);
report.push(` - ${table.TABLE_NAME}: ${table.key_count} Keys`);
});
}
@@ -126,15 +134,18 @@ async function cleanupKeys() {
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 Datenbankverbindung geschlossen.');
report.push('');
report.push('🔌 Datenbankverbindung geschlossen.');
}
}
}
// Script ausführen
console.log('🚀 Starte intelligentes INDEX-Cleanup...\n');
report.push('🚀 Starte intelligentes INDEX-Cleanup...');
cleanupKeys().then(() => {
console.log('\n✨ Cleanup abgeschlossen!');
report.push('');
report.push('✨ Cleanup abgeschlossen!');
process.stdout.write(`${report.join('\n')}\n`);
process.exit(0);
}).catch(error => {
console.error('\n💥 Fehler beim Cleanup:', error);

View File

@@ -7,41 +7,23 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const report = [];
// Umgebungsvariablen aus dem Root-Verzeichnis laden
//const envPath = path.join(__dirname, '..', '.env');
//console.log('🔍 Lade .env-Datei von:', envPath);
const envPath = path.join(__dirname, '..', '.env');
dotenv.config();
// Debug: Zeige geladene Umgebungsvariablen
console.log('🔍 Geladene Umgebungsvariablen:');
console.log(' DB_HOST:', process.env.DB_HOST);
console.log(' DB_USER:', process.env.DB_USER);
console.log(' DB_NAME:', process.env.DB_NAME);
console.log(' DB_PASSWORD:', process.env.DB_PASSWORD ? '***gesetzt***' : 'nicht gesetzt');
// Datenbankverbindung
const dbConfig = {
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'trainingsdiary'
};
console.log('🔍 Datenbankverbindung:');
console.log(' Host:', dbConfig.host);
console.log(' User:', dbConfig.user);
console.log(' Database:', dbConfig.database);
console.log(' Password:', dbConfig.password ? '***gesetzt***' : 'nicht gesetzt');
report.push('Environment variables loaded');
async function cleanupKeys() {
let connection;
try {
console.log('🔌 Verbinde mit der Datenbank...');
report.push('🔌 Verbinde mit der Datenbank...');
connection = await mysql.createConnection(dbConfig);
// 1. Status vor dem Cleanup
console.log('\n📊 STATUS VOR DEM CLEANUP:');
report.push('');
report.push('📊 STATUS VOR DEM CLEANUP:');
const [tablesBefore] = await connection.execute(`
SELECT
TABLE_NAME,
@@ -53,57 +35,60 @@ async function cleanupKeys() {
`, [dbConfig.database]);
tablesBefore.forEach(table => {
console.log(` ${table.TABLE_NAME}: ${table.key_count} Keys`);
report.push(` ${table.TABLE_NAME}: ${table.key_count} Keys`);
});
// 2. Alle INDEX der Problem-Tabellen anzeigen
const problemTables = ['member', 'diary_tags', 'season'];
for (const tableName of problemTables) {
console.log(`\n🔍 INDEX für Tabelle '${tableName}':`);
report.push('');
report.push(`🔍 INDEX für Tabelle '${tableName}':`);
try {
const [indexes] = await connection.execute(`SHOW INDEX FROM \`${tableName}\``);
if (indexes.length === 0) {
console.log(` Keine INDEX gefunden für Tabelle '${tableName}'`);
report.push(` Keine INDEX gefunden für Tabelle '${tableName}'`);
continue;
}
indexes.forEach(index => {
console.log(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`);
report.push(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`);
});
// 3. Überflüssige INDEX entfernen (alle außer PRIMARY und UNIQUE)
console.log(`\n🗑️ Entferne überflüssige INDEX aus '${tableName}':`);
report.push('');
report.push(`🗑️ Entferne überflüssige INDEX aus '${tableName}':`);
for (const index of indexes) {
// Behalte PRIMARY KEY und UNIQUE constraints
if (index.Key_name === 'PRIMARY' || index.Non_unique === 0) {
console.log(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`);
report.push(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`);
continue;
}
// Entferne alle anderen INDEX
try {
await connection.execute(`DROP INDEX \`${index.Key_name}\` ON \`${tableName}\``);
console.log(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`);
report.push(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`);
} catch (error) {
if (error.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
console.log(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`);
report.push(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`);
} else {
console.log(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`);
report.push(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`);
}
}
}
} catch (error) {
console.log(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`);
report.push(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`);
}
}
// 4. Status nach dem Cleanup
console.log('\n📊 STATUS NACH DEM CLEANUP:');
report.push('');
report.push('📊 STATUS NACH DEM CLEANUP:');
const [tablesAfter] = await connection.execute(`
SELECT
TABLE_NAME,
@@ -120,7 +105,7 @@ async function cleanupKeys() {
const diff = beforeCount - table.key_count;
const status = table.key_count <= 5 ? '✅' : table.key_count <= 10 ? '⚠️' : '❌';
console.log(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`);
report.push(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`);
});
// 5. Gesamtanzahl der Keys
@@ -130,18 +115,20 @@ async function cleanupKeys() {
WHERE TABLE_SCHEMA = ?
`, [dbConfig.database]);
console.log(`\n📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`);
report.push('');
report.push(`📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`);
// 6. Zusammenfassung
console.log('\n🎯 ZUSAMMENFASSUNG:');
report.push('');
report.push('🎯 ZUSAMMENFASSUNG:');
const problemTablesAfter = tablesAfter.filter(t => t.key_count > 10);
if (problemTablesAfter.length === 0) {
console.log(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!');
report.push(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!');
} else {
console.log(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:');
report.push(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:');
problemTablesAfter.forEach(table => {
console.log(` - ${table.TABLE_NAME}: ${table.key_count} Keys`);
report.push(` - ${table.TABLE_NAME}: ${table.key_count} Keys`);
});
}
@@ -150,15 +137,18 @@ async function cleanupKeys() {
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 Datenbankverbindung geschlossen.');
report.push('');
report.push('🔌 Datenbankverbindung geschlossen.');
}
}
}
// Script ausführen
console.log('🚀 Starte intelligentes INDEX-Cleanup...\n');
report.push('🚀 Starte intelligentes INDEX-Cleanup...');
cleanupKeys().then(() => {
console.log('\n✨ Cleanup abgeschlossen!');
report.push('');
report.push('✨ Cleanup abgeschlossen!');
process.stdout.write(`${report.join('\n')}\n`);
process.exit(0);
}).catch(error => {
console.error('\n💥 Fehler beim Cleanup:', error);

View File

@@ -17,19 +17,146 @@ class MyTischtennisClient {
});
}
/**
* Get login page to extract XSRF token and CAPTCHA token
* @returns {Promise<Object>} Object with xsrfToken, captchaToken, and captchaClicked flag
*/
async getLoginPage() {
try {
const response = await this.client.get('/login?next=%2F');
const html = response.data;
// Extract XSRF token from hidden input
const xsrfMatch = html.match(/<input[^>]*name="xsrf"[^>]*value="([^"]+)"/);
const xsrfToken = xsrfMatch ? xsrfMatch[1] : null;
// Extract CAPTCHA token from hidden input (if present)
const captchaMatch = html.match(/<input[^>]*name="captcha"[^>]*value="([^"]+)"/);
const captchaToken = captchaMatch ? captchaMatch[1] : null;
// Check if captcha_clicked is true or false
const captchaClickedMatch = html.match(/<input[^>]*name="captcha_clicked"[^>]*value="([^"]+)"/);
const captchaClicked = captchaClickedMatch ? captchaClickedMatch[1] === 'true' : false;
// Check if CAPTCHA is required (look for private-captcha element or captcha input)
const requiresCaptcha = html.includes('private-captcha') || html.includes('name="captcha"');
console.log('[myTischtennisClient.getLoginPage]', {
hasXsrfToken: !!xsrfToken,
hasCaptchaToken: !!captchaToken,
captchaClicked,
requiresCaptcha
});
return {
success: true,
xsrfToken,
captchaToken,
captchaClicked,
requiresCaptcha
};
} catch (error) {
console.error('Error fetching login page:', error.message);
return {
success: false,
error: error.message
};
}
}
/**
* Login to myTischtennis API
* @param {string} email - myTischtennis email (not username!)
* @param {string} password - myTischtennis password
* @param {string} captchaToken - Optional CAPTCHA token if required
* @param {string} xsrfToken - Optional XSRF token (will be fetched if not provided)
* @returns {Promise<Object>} Login response with token and session data
*/
async login(email, password) {
async login(email, password, captchaToken = null, xsrfToken = null) {
try {
let loginPage = null;
let captchaClicked = false;
// If XSRF token not provided, fetch login page to get it
if (!xsrfToken) {
loginPage = await this.getLoginPage();
if (!loginPage.success) {
return {
success: false,
error: 'Konnte Login-Seite nicht abrufen: ' + loginPage.error
};
}
xsrfToken = loginPage.xsrfToken;
// If CAPTCHA token not provided but found in HTML, use it
if (!captchaToken && loginPage.captchaToken) {
captchaToken = loginPage.captchaToken;
captchaClicked = loginPage.captchaClicked;
console.log('[myTischtennisClient.login] CAPTCHA-Token aus HTML extrahiert, captcha_clicked:', captchaClicked);
}
// If CAPTCHA is required but no token found yet, wait and try to get it again
// Das CAPTCHA-System löst das Puzzle im Hintergrund via JavaScript, daher kann es einen Moment dauern
// Wir müssen mehrmals versuchen, da das Token erst generiert wird, nachdem das JavaScript gelaufen ist
if (loginPage.requiresCaptcha && !captchaToken) {
console.log('[myTischtennisClient.login] CAPTCHA erforderlich, aber noch kein Token gefunden. Warte und versuche erneut...');
// Versuche bis zu 5 Mal, das CAPTCHA-Token zu erhalten
let maxRetries = 5;
let retryCount = 0;
let foundToken = false;
while (retryCount < maxRetries && !foundToken) {
// Warte 2-4 Sekunden zwischen den Versuchen
const waitMs = Math.floor(Math.random() * 2000) + 2000; // 2000-4000ms
console.log(`[myTischtennisClient.login] Versuch ${retryCount + 1}/${maxRetries}: Warte ${waitMs}ms...`);
await new Promise(resolve => setTimeout(resolve, waitMs));
// Versuche erneut, die Login-Seite abzurufen, um das gelöste CAPTCHA-Token zu erhalten
const retryLoginPage = await this.getLoginPage();
if (retryLoginPage.success && retryLoginPage.captchaToken) {
captchaToken = retryLoginPage.captchaToken;
captchaClicked = retryLoginPage.captchaClicked;
xsrfToken = retryLoginPage.xsrfToken || xsrfToken; // Aktualisiere XSRF-Token falls nötig
foundToken = true;
console.log(`[myTischtennisClient.login] CAPTCHA-Token nach ${retryCount + 1} Versuchen gefunden, captcha_clicked:`, captchaClicked);
} else {
retryCount++;
}
}
if (!foundToken) {
// Wenn nach allen Versuchen kein Token gefunden wurde, Fehler zurückgeben
console.log('[myTischtennisClient.login] CAPTCHA-Token konnte nach mehreren Versuchen nicht gefunden werden');
return {
success: false,
error: 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.',
requiresCaptcha: true
};
}
}
// Zufällige Verzögerung von 2-5 Sekunden zwischen Laden des Forms und Absenden
// Simuliert menschliches Verhalten und gibt dem CAPTCHA-System Zeit
const delayMs = Math.floor(Math.random() * 3000) + 2000; // 2000-5000ms
console.log(`[myTischtennisClient] Warte ${delayMs}ms vor Login-Request (simuliert menschliches Verhalten)`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
// Create form data
const formData = new URLSearchParams();
formData.append('email', email);
formData.append('password', password);
formData.append('intent', 'login');
if (xsrfToken) {
formData.append('xsrf', xsrfToken);
}
if (captchaToken) {
formData.append('captcha', captchaToken);
formData.append('captcha_clicked', captchaClicked ? 'true' : 'false');
}
const response = await this.client.post(
'/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
@@ -86,11 +213,36 @@ class MyTischtennisClient {
cookie: authCookie.split(';')[0] // Just the cookie value without attributes
};
} catch (error) {
console.error('MyTischtennis login error:', error.message);
const statusCode = error.response?.status || 500;
const responseData = error.response?.data;
// Check if response contains CAPTCHA error
let errorMessage = error.response?.data?.message || error.message || 'Login fehlgeschlagen';
let requiresCaptcha = false;
// Check for CAPTCHA-related errors in response
if (typeof responseData === 'string') {
if (responseData.includes('Captcha') || responseData.includes('CAPTCHA') ||
responseData.includes('captcha') || responseData.includes('Captcha-Bestätigung')) {
requiresCaptcha = true;
errorMessage = 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.';
}
} else if (responseData && typeof responseData === 'object') {
// Check for CAPTCHA errors in JSON response or HTML
const dataString = JSON.stringify(responseData);
if (dataString.includes('Captcha') || dataString.includes('CAPTCHA') ||
dataString.includes('captcha') || dataString.includes('Captcha-Bestätigung')) {
requiresCaptcha = true;
errorMessage = 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.';
}
}
console.error('MyTischtennis login error:', errorMessage, `(Status: ${statusCode})`, requiresCaptcha ? '(CAPTCHA erforderlich)' : '');
return {
success: false,
error: error.response?.data?.message || 'Login fehlgeschlagen',
status: error.response?.status || 500
error: errorMessage,
status: statusCode,
requiresCaptcha
};
}
}
@@ -149,28 +301,13 @@ class MyTischtennisClient {
* @returns {Promise<Object>} User profile with club info
*/
async getUserProfile(cookie) {
console.log('[getUserProfile] - Calling /?_data=root with cookie:', cookie?.substring(0, 50) + '...');
const result = await this.authenticatedRequest('/?_data=root', cookie, {
method: 'GET'
});
console.log('[getUserProfile] - Result success:', result.success);
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
});
console.log('[getUserProfile] - Full userProfile.club:', result.data?.userProfile?.club);
console.log('[getUserProfile] - Full userProfile.organization:', result.data?.userProfile?.organization);
return {
success: true,
@@ -194,17 +331,15 @@ class MyTischtennisClient {
* @param {string} fedNickname - Federation nickname (e.g., "HeTTV")
* @returns {Promise<Object>} Rankings with player entries (all pages)
*/
async getClubRankings(cookie, clubId, fedNickname) {
async getClubRankings(cookie, clubId, fedNickname, currentRanking = 'yes') {
const allEntries = [];
let currentPage = 0;
let hasMorePages = true;
console.log('[getClubRankings] - Starting to fetch rankings for club', clubId);
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 endpoint = `/rankings/andro-rangliste?all-players=on&clubnr=${clubId}&fednickname=${fedNickname}&current-ranking=${currentRanking}&results-per-page=100&page=${currentPage}&_data=routes%2F%24`;
console.log(`[getClubRankings] - Fetching page ${currentPage}...`);
const result = await this.authenticatedRequest(endpoint, cookie, {
method: 'GET'
@@ -245,7 +380,6 @@ class MyTischtennisClient {
};
}
console.log(`[getClubRankings] - Page ${currentPage}: Found ${entries.length} entries`);
// Füge Entries hinzu
allEntries.push(...entries);
@@ -255,19 +389,15 @@ class MyTischtennisClient {
// Oder wenn wir alle erwarteten Einträge haben
if (entries.length === 0) {
hasMorePages = false;
console.log('[getClubRankings] - No more entries, stopping');
} else if (rankingData.numberOfPages && currentPage >= rankingData.numberOfPages - 1) {
hasMorePages = false;
console.log(`[getClubRankings] - Reached last page (${rankingData.numberOfPages})`);
} else if (allEntries.length >= rankingData.resultLength) {
hasMorePages = false;
console.log(`[getClubRankings] - Got all entries (${allEntries.length}/${rankingData.resultLength})`);
} else {
currentPage++;
}
}
console.log(`[getClubRankings] - Total entries fetched: ${allEntries.length}`);
return {
success: true,

View File

@@ -1,17 +1,41 @@
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
dotenv.config();
// Ensure .env is loaded from the backend folder (not dependent on process.cwd())
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
dotenv.config({ path: path.resolve(__dirname, '.env') });
export const development = {
const isTestEnv = process.env.NODE_ENV === 'test';
const baseConfig = {
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'hitomisan',
database: process.env.DB_NAME || 'trainingdiary',
host: process.env.DB_HOST,
dialect: process.env.DB_DIALECT,
host: process.env.DB_HOST || 'localhost',
dialect: process.env.DB_DIALECT || 'mysql',
define: {
freezeTableName: true,
underscored: true,
underscoredAll: true,
},
logging: false,
storage: process.env.DB_STORAGE,
};
if (isTestEnv) {
baseConfig.username = 'sqlite';
baseConfig.password = '';
baseConfig.database = 'sqlite';
baseConfig.host = 'localhost';
baseConfig.dialect = 'sqlite';
baseConfig.storage = process.env.DB_STORAGE || ':memory:';
}
if (baseConfig.dialect === 'sqlite' && !baseConfig.storage) {
baseConfig.storage = ':memory:';
}
export const development = baseConfig;

View File

@@ -0,0 +1,125 @@
# Fehlercode-System - Verwendungsanleitung
## Übersicht
Das Fehlercode-System ersetzt hardcodierte deutsche Fehlermeldungen durch strukturierte Fehlercodes, die im Frontend übersetzt werden.
## Backend-Verwendung
### 1. Fehlercode verwenden
```javascript
import HttpError from '../exceptions/HttpError.js';
import { ERROR_CODES, createError } from '../constants/errorCodes.js';
// Einfacher Fehlercode ohne Parameter
throw new HttpError(createError(ERROR_CODES.USER_NOT_FOUND), 404);
// Fehlercode mit Parametern
throw new HttpError(
createError(ERROR_CODES.MEMBER_NOT_FOUND, { memberId: 123 }),
404
);
// Oder direkt:
throw new HttpError(
{ code: ERROR_CODES.MEMBER_NOT_FOUND, params: { memberId: 123 } },
404
);
```
### 2. Legacy-Format (wird weiterhin unterstützt)
```javascript
// Alte Variante funktioniert noch:
throw new HttpError('Benutzer nicht gefunden', 404);
```
## Frontend-Verwendung
### 1. Fehlermeldungen automatisch übersetzen
Die `getSafeErrorMessage`-Funktion erkennt automatisch Fehlercodes:
```javascript
import { getSafeErrorMessage } from '../utils/errorMessages.js';
// In einer Vue-Komponente (Options API)
try {
await apiClient.post('/api/endpoint', data);
} catch (error) {
const message = getSafeErrorMessage(error, this.$t('errors.ERROR_UNKNOWN_ERROR'), this.$t);
await this.showInfo(this.$t('messages.error'), message, '', 'error');
}
// In einer Vue-Komponente (Composition API)
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
try {
await apiClient.post('/api/endpoint', data);
} catch (error) {
const message = getSafeErrorMessage(error, t('errors.ERROR_UNKNOWN_ERROR'), t);
await showInfo(t('messages.error'), message, '', 'error');
}
```
### 2. Dialog-Utils mit Übersetzung
```javascript
import { buildInfoConfig, safeErrorMessage } from '../utils/dialogUtils.js';
// Mit Übersetzungsfunktion
this.infoDialog = buildInfoConfig({
title: this.$t('messages.error'),
message: safeErrorMessage(error, this.$t('errors.ERROR_UNKNOWN_ERROR'), this.$t),
type: 'error'
}, this.$t);
```
## API-Response-Format
### Neues Format (mit Fehlercode):
```json
{
"success": false,
"code": "ERROR_MEMBER_NOT_FOUND",
"params": {
"memberId": 123
},
"error": "ERROR_MEMBER_NOT_FOUND" // Für Rückwärtskompatibilität
}
```
### Legacy-Format (wird weiterhin unterstützt):
```json
{
"success": false,
"message": "Mitglied nicht gefunden",
"error": "Mitglied nicht gefunden"
}
```
## Übersetzungen hinzufügen
1. **Backend**: Fehlercode in `backend/constants/errorCodes.js` definieren
2. **Frontend**: Übersetzung in `frontend/src/i18n/locales/de.json` unter `errors` hinzufügen
Beispiel:
```json
{
"errors": {
"ERROR_MEMBER_NOT_FOUND": "Mitglied nicht gefunden.",
"ERROR_MEMBER_NOT_FOUND_WITH_ID": "Mitglied mit ID {memberId} nicht gefunden."
}
}
```
## Migration bestehender Fehler
1. Hardcodierte Fehlermeldung identifizieren
2. Passenden Fehlercode in `errorCodes.js` finden oder erstellen
3. Backend-Code anpassen: `throw new HttpError(createError(ERROR_CODES.XXX), status)`
4. Übersetzung in `de.json` hinzufügen
5. Frontend-Code muss nicht geändert werden (automatische Erkennung)

View File

@@ -0,0 +1,121 @@
/**
* Fehlercodes für die API
* Diese Codes werden an das Frontend gesendet und dort übersetzt
*
* Format: { code: string, params?: object }
*
* Beispiel:
* - { code: 'ERROR_USER_NOT_FOUND' }
* - { code: 'ERROR_MEMBER_NOT_FOUND', params: { memberId: 123 } }
* - { code: 'ERROR_VALIDATION_FAILED', params: { field: 'email', value: 'invalid' } }
*/
export const ERROR_CODES = {
// Allgemeine Fehler
INTERNAL_SERVER_ERROR: 'ERROR_INTERNAL_SERVER_ERROR',
UNKNOWN_ERROR: 'ERROR_UNKNOWN_ERROR',
VALIDATION_FAILED: 'ERROR_VALIDATION_FAILED',
NOT_FOUND: 'ERROR_NOT_FOUND',
UNAUTHORIZED: 'ERROR_UNAUTHORIZED',
FORBIDDEN: 'ERROR_FORBIDDEN',
BAD_REQUEST: 'ERROR_BAD_REQUEST',
// Authentifizierung
USER_NOT_FOUND: 'ERROR_USER_NOT_FOUND',
INVALID_PASSWORD: 'ERROR_INVALID_PASSWORD',
LOGIN_FAILED: 'ERROR_LOGIN_FAILED',
SESSION_EXPIRED: 'ERROR_SESSION_EXPIRED',
// MyTischtennis
MYTISCHTENNIS_USER_NOT_FOUND: 'ERROR_MYTISCHTENNIS_USER_NOT_FOUND',
MYTISCHTENNIS_INVALID_PASSWORD: 'ERROR_MYTISCHTENNIS_INVALID_PASSWORD',
MYTISCHTENNIS_LOGIN_FAILED: 'ERROR_MYTISCHTENNIS_LOGIN_FAILED',
MYTISCHTENNIS_ACCOUNT_NOT_LINKED: 'ERROR_MYTISCHTENNIS_ACCOUNT_NOT_LINKED',
MYTISCHTENNIS_PASSWORD_NOT_SAVED: 'ERROR_MYTISCHTENNIS_PASSWORD_NOT_SAVED',
MYTISCHTENNIS_SESSION_EXPIRED: 'ERROR_MYTISCHTENNIS_SESSION_EXPIRED',
MYTISCHTENNIS_NO_PASSWORD_SAVED: 'ERROR_MYTISCHTENNIS_NO_PASSWORD_SAVED',
MYTISCHTENNIS_CAPTCHA_REQUIRED: 'ERROR_MYTISCHTENNIS_CAPTCHA_REQUIRED',
// Mitglieder
MEMBER_NOT_FOUND: 'ERROR_MEMBER_NOT_FOUND',
MEMBER_ALREADY_EXISTS: 'ERROR_MEMBER_ALREADY_EXISTS',
MEMBER_FIRSTNAME_REQUIRED: 'ERROR_MEMBER_FIRSTNAME_REQUIRED',
MEMBER_LASTNAME_REQUIRED: 'ERROR_MEMBER_LASTNAME_REQUIRED',
// Gruppen
GROUP_NOT_FOUND: 'ERROR_GROUP_NOT_FOUND',
GROUP_NAME_REQUIRED: 'ERROR_GROUP_NAME_REQUIRED',
GROUP_ALREADY_EXISTS: 'ERROR_GROUP_ALREADY_EXISTS',
GROUP_INVALID_PRESET_TYPE: 'ERROR_GROUP_INVALID_PRESET_TYPE',
GROUP_CANNOT_RENAME_PRESET: 'ERROR_GROUP_CANNOT_RENAME_PRESET',
// Turniere
TOURNAMENT_NOT_FOUND: 'ERROR_TOURNAMENT_NOT_FOUND',
TOURNAMENT_NO_DATE: 'ERROR_TOURNAMENT_NO_DATE',
TOURNAMENT_CLASS_NAME_REQUIRED: 'ERROR_TOURNAMENT_CLASS_NAME_REQUIRED',
TOURNAMENT_NO_PARTICIPANTS: 'ERROR_TOURNAMENT_NO_PARTICIPANTS',
TOURNAMENT_NO_VALID_PARTICIPANTS: 'ERROR_TOURNAMENT_NO_VALID_PARTICIPANTS',
TOURNAMENT_NO_TRAINING_DAY: 'ERROR_TOURNAMENT_NO_TRAINING_DAY',
TOURNAMENT_PDF_GENERATION_FAILED: 'ERROR_TOURNAMENT_PDF_GENERATION_FAILED',
TOURNAMENT_SELECT_FIRST: 'ERROR_TOURNAMENT_SELECT_FIRST',
// Trainingstagebuch
DIARY_DATE_NOT_FOUND: 'ERROR_DIARY_DATE_NOT_FOUND',
DIARY_DATE_UPDATED: 'ERROR_DIARY_DATE_UPDATED',
DIARY_NO_PARTICIPANTS: 'ERROR_DIARY_NO_PARTICIPANTS',
DIARY_PDF_GENERATION_FAILED: 'ERROR_DIARY_PDF_GENERATION_FAILED',
DIARY_IMAGE_LOAD_FAILED: 'ERROR_DIARY_IMAGE_LOAD_FAILED',
DIARY_STATS_LOAD_FAILED: 'ERROR_DIARY_STATS_LOAD_FAILED',
DIARY_NO_EXERCISE_DATA: 'ERROR_DIARY_NO_EXERCISE_DATA',
DIARY_ACTIVITY_PARTICIPANTS_UPDATE_FAILED: 'ERROR_DIARY_ACTIVITY_PARTICIPANTS_UPDATE_FAILED',
DIARY_GROUP_ASSIGNMENT_UPDATED: 'SUCCESS_DIARY_GROUP_ASSIGNMENT_UPDATED',
DIARY_GROUP_ASSIGNMENT_UPDATE_FAILED: 'ERROR_DIARY_GROUP_ASSIGNMENT_UPDATE_FAILED',
DIARY_ASSIGN_ALL_PARTICIPANTS_FAILED: 'ERROR_DIARY_ASSIGN_ALL_PARTICIPANTS_FAILED',
DIARY_ASSIGN_GROUP_FAILED: 'ERROR_DIARY_ASSIGN_GROUP_FAILED',
DIARY_PARTICIPANT_ASSIGN_FAILED: 'ERROR_DIARY_PARTICIPANT_ASSIGN_FAILED',
DIARY_PARTICIPANT_GROUP_ASSIGNMENT_UPDATE_FAILED: 'ERROR_DIARY_PARTICIPANT_GROUP_ASSIGNMENT_UPDATE_FAILED',
DIARY_MEMBER_CREATED: 'SUCCESS_DIARY_MEMBER_CREATED',
DIARY_MEMBER_CREATE_FAILED: 'ERROR_DIARY_MEMBER_CREATE_FAILED',
// Team Management
TEAM_NOT_LINKED_TO_LEAGUE: 'ERROR_TEAM_NOT_LINKED_TO_LEAGUE',
TEAM_LINK_TO_LEAGUE_REQUIRED: 'ERROR_TEAM_LINK_TO_LEAGUE_REQUIRED',
TEAM_PDF_LOAD_FAILED: 'ERROR_TEAM_PDF_LOAD_FAILED',
TEAM_STATS_LOAD_FAILED: 'ERROR_TEAM_STATS_LOAD_FAILED',
// Aktivitäten
ACTIVITY_IMAGE_DELETE_FAILED: 'ERROR_ACTIVITY_IMAGE_DELETE_FAILED',
// Offizielle Turniere
OFFICIAL_TOURNAMENT_PDF_UPLOAD_SUCCESS: 'SUCCESS_OFFICIAL_TOURNAMENT_PDF_UPLOAD',
OFFICIAL_TOURNAMENT_PDF_UPLOAD_FAILED: 'ERROR_OFFICIAL_TOURNAMENT_PDF_UPLOAD',
// Vereine
CLUB_NOT_FOUND: 'ERROR_CLUB_NOT_FOUND',
CLUB_ALREADY_EXISTS: 'ERROR_CLUB_ALREADY_EXISTS',
CLUB_NAME_REQUIRED: 'ERROR_CLUB_NAME_REQUIRED',
CLUB_NAME_TOO_SHORT: 'ERROR_CLUB_NAME_TOO_SHORT',
// Mitglieder-Übertragung
MEMBER_TRANSFER_BULK_FAILED: 'ERROR_MEMBER_TRANSFER_BULK_FAILED',
// Training
TRAINING_STATS_LOAD_FAILED: 'ERROR_TRAINING_STATS_LOAD_FAILED',
// Logs
LOG_NOT_FOUND: 'ERROR_LOG_NOT_FOUND',
};
/**
* Erstellt ein Fehler-Objekt mit Code und optionalen Parametern
* @param {string} code - Fehlercode aus ERROR_CODES
* @param {object} params - Optionale Parameter für die Fehlermeldung
* @returns {object} Fehler-Objekt mit code und params
*/
export function createError(code, params = null) {
return {
code,
...(params && { params })
};
}

View File

@@ -0,0 +1,85 @@
import apiLogService from '../services/apiLogService.js';
import HttpError from '../exceptions/HttpError.js';
class ApiLogController {
/**
* GET /api/logs
* Get API logs with optional filters
*/
async getLogs(req, res, next) {
try {
const {
userId,
logType,
method,
path,
statusCode,
startDate,
endDate,
limit = 100,
offset = 0
} = req.query;
const result = await apiLogService.getLogs({
userId: userId ? parseInt(userId) : null,
logType,
method,
path,
statusCode: statusCode ? parseInt(statusCode) : null,
startDate,
endDate,
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({
success: true,
data: result
});
} catch (error) {
next(error);
}
}
/**
* GET /api/logs/:id
* Get a single log entry by ID
*/
async getLogById(req, res, next) {
try {
const { id } = req.params;
const log = await apiLogService.getLogById(parseInt(id));
if (!log) {
throw new HttpError('Log entry not found', 404);
}
res.json({
success: true,
data: log
});
} catch (error) {
next(error);
}
}
/**
* GET /api/logs/scheduler/last-executions
* Get last execution info for scheduler jobs
*/
async getLastSchedulerExecutions(req, res, next) {
try {
const { clubId } = req.query;
const results = await apiLogService.getLastSchedulerExecutions(clubId ? parseInt(clubId) : null);
res.json({
success: true,
data: results
});
} catch (error) {
next(error);
}
}
}
export default new ApiLogController();

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

@@ -4,11 +4,8 @@ import { devLog } from '../utils/logger.js';
export const getClubs = async (req, res) => {
try {
devLog('[getClubs] - get clubs');
const clubs = await ClubService.getAllClubs();
devLog('[getClubs] - prepare response');
res.status(200).json(clubs);
devLog('[getClubs] - done');
} catch (error) {
console.error('[getClubs] - error:', error);
res.status(500).json({ error: "internalerror" });
@@ -16,28 +13,20 @@ export const getClubs = async (req, res) => {
};
export const addClub = async (req, res) => {
devLog('[addClub] - Read out parameters');
const { authcode: token } = req.headers;
const { name: clubName } = req.body;
try {
devLog('[addClub] - find club by name');
const club = await ClubService.findClubByName(clubName);
devLog('[addClub] - get user');
const user = await getUserByToken(token);
devLog('[addClub] - check if club already exists');
if (club) {
res.status(409).json({ error: "alreadyexists" });
return;
}
devLog('[addClub] - create club');
const newClub = await ClubService.createClub(clubName);
devLog('[addClub] - add user to new club');
await ClubService.addUserToClub(user.id, newClub.id);
devLog('[addClub] - prepare response');
await ClubService.addUserToClub(user.id, newClub.id, true); // true = isOwner
res.status(200).json(newClub);
devLog('[addClub] - done');
} catch (error) {
console.error('[addClub] - error:', error);
res.status(500).json({ error: "internalerror" });
@@ -45,43 +34,53 @@ export const addClub = async (req, res) => {
};
export const getClub = async (req, res) => {
devLog('[getClub] - start');
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
devLog('[getClub] - get user');
const user = await getUserByToken(token);
devLog('[getClub] - get users club');
const access = await ClubService.getUserClubAccess(user.id, clubId);
devLog('[getClub] - check access');
if (access.length === 0 || !access[0].approved) {
res.status(403).json({ error: "noaccess", status: access.length === 0 ? "notrequested" : "requested" });
return;
}
devLog('[getClub] - get club');
const club = await ClubService.findClubById(clubId);
devLog('[getClub] - check club exists');
if (!club) {
return res.status(404).json({ message: 'Club not found' });
}
devLog('[getClub] - set response');
res.status(200).json(club);
devLog('[getClub] - done');
} catch (error) {
console.error('[getClub] - error:', error);
res.status(500).json({ message: 'Server error' });
}
};
export const updateClubSettings = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid } = req.params;
const { greetingText, associationMemberNumber } = req.body;
const updated = await ClubService.updateClubSettings(token, clubid, { greetingText, associationMemberNumber });
res.status(200).json(updated);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'noaccess' });
} else if (error.message === 'clubnotfound') {
res.status(404).json({ error: 'clubnotfound' });
} else {
console.error('[updateClubSettings] - error:', error);
res.status(500).json({ error: 'internalerror' });
}
}
};
export const requestClubAccess = async (req, res) => {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
try {
const user = await getUserByToken(token);
devLog('[requestClubAccess] - user:', user);
await ClubService.requestAccessToClub(user.id, clubId);
res.status(200).json({});

View File

@@ -1,7 +1,7 @@
import diaryService from '../services/diaryService.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
import { emitDiaryDateUpdated, emitDiaryTagAdded, emitDiaryTagRemoved } from '../services/socketService.js';
const getDatesForClub = async (req, res) => {
try {
const { clubId } = req.params;
@@ -43,6 +43,10 @@ const updateTrainingTimes = async (req, res) => {
throw new HttpError('notallfieldsfilled', 400);
}
const updatedDate = await diaryService.updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd);
// Emit Socket-Event
emitDiaryDateUpdated(clubId, dateId, { trainingStart, trainingEnd });
res.status(200).json(updatedDate);
} catch (error) {
console.error('[updateTrainingTimes] - Error:', error);
@@ -79,6 +83,14 @@ const addDiaryTag = async (req, res) => {
const { authcode: userToken } = req.headers;
const { diaryDateId, tagName } = req.body;
const tags = await diaryService.addTagToDate(userToken, diaryDateId, tagName);
// Hole clubId für Event
const { DiaryDate } = await import('../models/index.js');
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId && tags && tags.length > 0) {
emitDiaryTagAdded(diaryDate.clubId, diaryDateId, tags[tags.length - 1]);
}
res.status(201).json(tags);
} catch (error) {
console.error('[addDiaryTag] - Error:', error);
@@ -95,6 +107,12 @@ const addTagToDiaryDate = async (req, res) => {
return res.status(400).json({ message: 'diaryDateId and tagId are required.' });
}
const result = await diaryService.addTagToDiaryDate(userToken, clubId, diaryDateId, tagId);
// Emit Socket-Event
if (result && result.tag) {
emitDiaryTagAdded(clubId, diaryDateId, result.tag);
}
res.status(200).json(result);
} catch (error) {
console.error('[addTagToDiaryDate] - Error:', error);
@@ -106,8 +124,20 @@ const deleteTagFromDiaryDate = async (req, res) => {
try {
const { tagId } = req.query;
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const { clubId } = req.params;
// Hole diaryDateId vor dem Löschen
const { DiaryDateTag } = await import('../models/index.js');
const diaryDateTag = await DiaryDateTag.findByPk(tagId);
const diaryDateId = diaryDateTag?.diaryDateId;
await diaryService.removeTagFromDiaryDate(userToken, clubId, tagId);
// Emit Socket-Event
if (diaryDateId) {
emitDiaryTagRemoved(clubId, diaryDateId, tagId);
}
res.status(200).json({ message: 'Tag deleted' });
} catch (error) {
console.error('[deleteTag] - Error:', error);

View File

@@ -1,4 +1,6 @@
import diaryDateActivityService from '../services/diaryDateActivityService.js';
import { emitActivityChanged } from '../services/socketService.js';
import DiaryDate from '../models/DiaryDates.js';
import { devLog } from '../utils/logger.js';
export const createDiaryDateActivity = async (req, res) => {
@@ -14,6 +16,13 @@ export const createDiaryDateActivity = async (req, res) => {
orderId,
isTimeblock,
});
// Emit Socket-Event
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
res.status(201).json(activityItem);
} catch (error) {
devLog(error);
@@ -34,6 +43,15 @@ export const updateDiaryDateActivity = async (req, res) => {
orderId,
groupId, // Pass groupId to the service
});
// Emit Socket-Event
if (updatedActivity?.diaryDateId) {
const diaryDate = await DiaryDate.findByPk(updatedActivity.diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, updatedActivity.diaryDateId);
}
}
res.status(200).json(updatedActivity);
} catch (error) {
res.status(500).json({ error: 'Error updating activity' });
@@ -44,7 +62,22 @@ export const deleteDiaryDateActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, id } = req.params;
// Hole diaryDateId vor dem Löschen
const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default;
const activity = await DiaryDateActivity.findByPk(id);
const diaryDateId = activity?.diaryDateId;
await diaryDateActivityService.deleteActivity(userToken, clubId, id);
// Emit Socket-Event
if (diaryDateId) {
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
}
res.status(200).json({ message: 'Activity deleted' });
} catch (error) {
res.status(500).json({ error: 'Error deleting activity' });
@@ -57,6 +90,15 @@ export const updateDiaryDateActivityOrder = async (req, res) => {
const { clubId, id } = req.params;
const { orderId } = req.body;
const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId);
// Emit Socket-Event
if (updatedActivity?.diaryDateId) {
const diaryDate = await DiaryDate.findByPk(updatedActivity.diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, updatedActivity.diaryDateId);
}
}
res.status(200).json(updatedActivity);
} catch (error) {
devLog(error);
@@ -79,11 +121,80 @@ export const getDiaryDateActivities = async (req, res) => {
export const addGroupActivity = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateId, groupId, activity } = req.body;
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity);
const { clubId, diaryDateId, groupId, activity, predefinedActivityId, timeblockId } = req.body;
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, predefinedActivityId, timeblockId);
// Emit Socket-Event
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
res.status(201).json(activityItem);
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Error adding group activity' });
}
}
export const updateGroupActivity = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupActivityId } = req.params;
const { predefinedActivityId } = req.body;
const activityItem = await diaryDateActivityService.updateGroupActivity(userToken, clubId, groupActivityId, predefinedActivityId);
// Emit Socket-Event
const GroupActivity = (await import('../models/GroupActivity.js')).default;
const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default;
const groupActivity = await GroupActivity.findByPk(groupActivityId);
let diaryDateId = null;
if (groupActivity?.diaryDateActivity) {
const activity = await DiaryDateActivity.findByPk(groupActivity.diaryDateActivity);
diaryDateId = activity?.diaryDateId;
}
if (diaryDateId) {
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
}
res.status(200).json(activityItem);
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Error updating group activity' });
}
}
export const deleteGroupActivity = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupActivityId } = req.params;
// Hole diaryDateId vor dem Löschen
const GroupActivity = (await import('../models/GroupActivity.js')).default;
const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default;
const groupActivity = await GroupActivity.findByPk(groupActivityId);
let diaryDateId = null;
if (groupActivity?.diaryDateActivity) {
const activity = await DiaryDateActivity.findByPk(groupActivity.diaryDateActivity);
diaryDateId = activity?.diaryDateId;
}
await diaryDateActivityService.deleteGroupActivity(userToken, clubId, groupActivityId);
// Emit Socket-Event
if (diaryDateId) {
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
}
res.status(200).json({ message: 'Group activity deleted' });
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Error deleting group activity' });
}
}

View File

@@ -1,6 +1,9 @@
import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import DiaryDates from '../models/DiaryDates.js';
import Participant from '../models/Participant.js';
import { checkAccess } from '../utils/userUtils.js';
import { emitActivityMemberAdded, emitActivityMemberRemoved } from '../services/socketService.js';
export const getMembersForActivity = async (req, res) => {
try {
@@ -19,20 +22,44 @@ export const addMembersToActivity = async (req, res) => {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId } = req.params;
const { participantIds } = req.body; // array of participant ids
await checkAccess(userToken, clubId);
if (!participantIds || !Array.isArray(participantIds)) {
console.error('[addMembersToActivity] Invalid participantIds:', participantIds);
return res.status(400).json({ error: 'participantIds must be an array' });
}
const validParticipants = await Participant.findAll({ where: { id: participantIds } });
const validIds = new Set(validParticipants.map(p => p.id));
const created = [];
// Hole clubId und dateId für Events (falls nicht aus params verfügbar)
const activity = await DiaryDateActivity.findByPk(diaryDateActivityId);
const diaryDate = activity ? await DiaryDates.findByPk(activity.diaryDateId) : null;
const eventClubId = diaryDate?.clubId || clubId;
const dateId = diaryDate?.id || null;
for (const pid of participantIds) {
if (!validIds.has(pid)) continue;
if (!validIds.has(pid)) {
continue;
}
const existing = await DiaryMemberActivity.findOne({ where: { diaryDateActivityId, participantId: pid } });
if (!existing) {
const rec = await DiaryMemberActivity.create({ diaryDateActivityId, participantId: pid });
created.push(rec);
// Emit Socket-Event
if (eventClubId && dateId) {
emitActivityMemberAdded(eventClubId, diaryDateActivityId, pid, dateId);
}
} else {
}
}
res.status(201).json(created);
} catch (e) {
console.error('[addMembersToActivity] Error:', e);
res.status(500).json({ error: 'Error adding members to activity' });
}
};
@@ -42,7 +69,19 @@ export const removeMemberFromActivity = async (req, res) => {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId, participantId } = req.params;
await checkAccess(userToken, clubId);
// Hole dateId für Event
const activity = await DiaryDateActivity.findByPk(diaryDateActivityId);
const diaryDate = activity ? await DiaryDates.findByPk(activity.diaryDateId) : null;
const dateId = diaryDate?.id || null;
await DiaryMemberActivity.destroy({ where: { diaryDateActivityId, participantId } });
// Emit Socket-Event
if (dateId) {
emitActivityMemberRemoved(clubId, diaryDateActivityId, participantId, dateId);
}
res.status(200).json({ ok: true });
} catch (e) {
res.status(500).json({ error: 'Error removing member from activity' });

View File

@@ -1,5 +1,6 @@
import { DiaryNote, DiaryTag } from '../models/index.js';
import { DiaryNote, DiaryTag, DiaryDate } from '../models/index.js';
import diaryService from '../services/diaryService.js';
import { emitDiaryNoteAdded, emitDiaryNoteDeleted } from '../services/socketService.js';
export const getNotes = async (req, res) => {
try {
@@ -18,17 +19,41 @@ export const getNotes = async (req, res) => {
export const createNote = async (req, res) => {
try {
const { memberId, content, tags } = req.body;
const newNote = await DiaryNote.create({ memberId, content });
if (tags && tags.length > 0) {
const { memberId, diaryDateId, content, tags } = req.body;
if (!memberId || !diaryDateId || !content) {
return res.status(400).json({ error: 'memberId, diaryDateId und content sind erforderlich.' });
}
const newNote = await DiaryNote.create({ memberId, diaryDateId, content });
// Hole DiaryDate für clubId
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (Array.isArray(tags) && tags.length > 0 && typeof newNote.addTags === 'function') {
const tagInstances = await DiaryTag.findAll({ where: { id: tags } });
await newNote.addTags(tagInstances);
const noteWithTags = await DiaryNote.findByPk(newNote.id, {
include: [{ model: DiaryTag, as: 'tags', required: false }],
});
// Emit Socket-Event
if (diaryDate?.clubId) {
emitDiaryNoteAdded(diaryDate.clubId, diaryDateId, noteWithTags ?? newNote);
}
return res.status(201).json(noteWithTags ?? newNote);
}
const noteWithTags = await DiaryNote.findByPk(newNote.id, {
include: [{ model: DiaryTag, as: 'tags' }],
});
res.status(201).json(noteWithTags);
// Emit Socket-Event
if (diaryDate?.clubId) {
emitDiaryNoteAdded(diaryDate.clubId, diaryDateId, newNote);
}
res.status(201).json(newNote);
} catch (error) {
console.error('[createNote] - Error:', error);
res.status(500).json({ error: 'Error creating note' });
}
};
@@ -36,7 +61,25 @@ export const createNote = async (req, res) => {
export const deleteNote = async (req, res) => {
try {
const { noteId } = req.params;
// Hole Note für diaryDateId vor dem Löschen
const note = await DiaryNote.findByPk(noteId);
const diaryDateId = note?.diaryDateId;
// Hole DiaryDate für clubId
let clubId = null;
if (diaryDateId) {
const diaryDate = await DiaryDate.findByPk(diaryDateId);
clubId = diaryDate?.clubId;
}
await DiaryNote.destroy({ where: { id: noteId } });
// Emit Socket-Event
if (clubId && diaryDateId) {
emitDiaryNoteDeleted(clubId, diaryDateId, noteId);
}
res.status(200).json({ message: 'Note deleted' });
} catch (error) {
res.status(500).json({ error: 'Error deleting note' });

View File

@@ -1,6 +1,5 @@
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();
@@ -13,11 +12,13 @@ export const getTags = async (req, res) => {
export const createTag = async (req, res) => {
try {
const { name } = req.body;
devLog(name);
const newTag = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
res.status(201).json(newTag);
if (!name) {
return res.status(400).json({ error: 'Der Name des Tags ist erforderlich.' });
}
const [tag, created] = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
res.status(created ? 201 : 200).json(tag);
} catch (error) {
devLog('[createTag] - Error:', error);
res.status(500).json({ error: 'Error creating tag' });
}
};
@@ -25,9 +26,14 @@ export const createTag = async (req, res) => {
export const deleteTag = async (req, res) => {
try {
const { tagId } = req.params;
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
await diaryService.removeTagFromDiaryDate(userToken, clubId, tagId);
await DiaryDateTag.destroy({ where: { tagId } });
const deleted = await DiaryTag.destroy({ where: { id: tagId } });
if (!deleted) {
return res.status(404).json({ error: 'Tag nicht gefunden' });
}
res.status(200).json({ message: 'Tag deleted' });
} catch (error) {
console.error('[deleteTag] - Error:', error);

View File

@@ -1,5 +1,7 @@
import HttpError from '../exceptions/HttpError.js';
import groupService from '../services/groupService.js';
import { emitActivityChanged, emitGroupChanged } from '../services/socketService.js';
import DiaryDate from '../models/DiaryDates.js';
import { devLog } from '../utils/logger.js';
const addGroup = async(req, res) => {
@@ -7,6 +9,15 @@ const addGroup = async(req, res) => {
const { authcode: userToken } = req.headers;
const { clubid: clubId, dateid: dateId, name, lead } = req.body;
const result = await groupService.addGroup(userToken, clubId, dateId, name, lead);
// Emit Socket-Event für Gruppen-Änderungen
if (dateId) {
const diaryDate = await DiaryDate.findByPk(dateId);
if (diaryDate?.clubId) {
emitGroupChanged(diaryDate.clubId, dateId);
}
}
res.status(201).json(result);
} catch (error) {
console.error('[addGroup] - Error:', error);
@@ -33,6 +44,15 @@ const changeGroup = async(req, res) => {
const { groupId } = req.params;
const { clubid: clubId, dateid: dateId, name, lead } = req.body;
const result = await groupService.changeGroup(userToken, groupId, clubId, dateId, name, lead);
// Emit Socket-Event für Gruppen-Änderungen
if (dateId) {
const diaryDate = await DiaryDate.findByPk(dateId);
if (diaryDate?.clubId) {
emitGroupChanged(diaryDate.clubId, dateId);
}
}
res.status(200).json(result);
} catch (error) {
console.error('[changeGroup] - Error:', error);
@@ -40,4 +60,27 @@ const changeGroup = async(req, res) => {
}
}
export { addGroup, getGroups, changeGroup};
const deleteGroup = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { groupId } = req.params;
const { clubid: clubId, dateid: dateId } = req.body;
const result = await groupService.deleteGroup(userToken, groupId, clubId, dateId);
// Emit Socket-Events für Gruppen- und Aktivitäts-Änderungen (Gruppen werden in Aktivitäten verwendet)
if (dateId) {
const diaryDate = await DiaryDate.findByPk(dateId);
if (diaryDate?.clubId) {
emitGroupChanged(diaryDate.clubId, dateId);
emitActivityChanged(diaryDate.clubId, dateId);
}
}
res.status(200).json(result);
} catch (error) {
console.error('[deleteGroup] - Error:', error);
res.status(error.statusCode || 500).json({ error: error.message });
}
}
export { addGroup, getGroups, changeGroup, deleteGroup};

View File

@@ -25,7 +25,8 @@ export const getLeaguesForCurrentSeason = async (req, res) => {
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);
@@ -37,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);
@@ -56,3 +58,90 @@ export const getMatchesForLeague = async (req, res) => {
return res.status(500).json({ error: 'Failed to retrieve matches' });
}
};
export const getLeagueTable = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
return res.status(200).json(table);
} catch (error) {
console.error('Error retrieving league table:', error);
return res.status(500).json({ error: 'Failed to retrieve league table' });
}
};
export const fetchLeagueTableFromMyTischtennis = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const autoFetchService = (await import('../services/autoFetchMatchResultsService.js')).default;
await autoFetchService.fetchAndUpdateLeagueTable(userId, leagueId);
// Return updated table data
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
return res.status(200).json({
message: 'League table updated from MyTischtennis',
data: table
});
} catch (error) {
console.error('Error fetching league table from MyTischtennis:', error);
return res.status(500).json({ error: 'Failed to fetch league table from MyTischtennis' });
}
};
export const updateMatchPlayers = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { matchId } = req.params;
const { playersReady, playersPlanned, playersPlayed } = req.body;
const result = await MatchService.updateMatchPlayers(
userToken,
matchId,
playersReady,
playersPlanned,
playersPlayed
);
return res.status(200).json({
message: 'Match players updated successfully',
data: result
});
} catch (error) {
console.error('Error updating match players:', error);
return res.status(error.statusCode || 500).json({
error: error.message || 'Failed to update match players'
});
}
};
export const getPlayerMatchStats = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const { seasonid: seasonId } = req.query;
const stats = await MatchService.getPlayerMatchStats(userToken, clubId, leagueId, seasonId);
return res.status(200).json(stats);
} catch (error) {
console.error('Error retrieving player match stats:', error);
return res.status(error.statusCode || 500).json({
error: error.message || 'Failed to retrieve player match stats'
});
}
};

View File

@@ -0,0 +1,531 @@
import { checkAccess } from '../utils/userUtils.js';
import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import DiaryDates from '../models/DiaryDates.js';
import Participant from '../models/Participant.js';
import PredefinedActivity from '../models/PredefinedActivity.js';
import GroupActivity from '../models/GroupActivity.js';
import { Op } from 'sequelize';
export const getMemberActivities = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
const { period } = req.query; // 'month', '3months', '6months', 'year', 'all'
await checkAccess(userToken, clubId);
// Calculate date range based on period
const now = new Date();
let startDate = null;
switch (period) {
case 'month':
startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
break;
case '3months':
startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
break;
case '6months':
startDate = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate());
break;
case 'year':
startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
break;
case 'all':
default:
startDate = null;
break;
}
// Get participant ID for this member
const participants = await Participant.findAll({
where: { memberId: memberId }
});
if (participants.length === 0) {
return res.status(200).json([]);
}
const participantIds = participants.map(p => p.id);
// Sammle alle Gruppen-IDs, zu denen der Member gehört
const memberGroupIds = new Set();
participants.forEach(p => {
if (p.groupId !== null && p.groupId !== undefined) {
memberGroupIds.add(p.groupId);
}
});
// 1. Get all diary member activities explicitly assigned to this member
const memberActivities = await DiaryMemberActivity.findAll({
where: {
participantId: participantIds
},
include: [
{
model: Participant,
as: 'participant',
attributes: ['id', 'groupId', 'diaryDateId']
},
{
model: DiaryDateActivity,
as: 'activity',
include: [
{
model: DiaryDates,
as: 'diaryDate',
where: startDate ? {
date: {
[Op.gte]: startDate
}
} : {}
},
{
model: PredefinedActivity,
as: 'predefinedActivity'
},
{
model: GroupActivity,
as: 'groupActivities',
required: false
}
]
}
],
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']]
});
// 2. Get all group activities for groups the member belongs to
const groupActivities = [];
if (memberGroupIds.size > 0) {
// Suche direkt nach GroupActivity-Einträgen für die Gruppen des Members
const groupActivitiesData = await GroupActivity.findAll({
where: {
groupId: {
[Op.in]: Array.from(memberGroupIds)
}
},
include: [
{
model: DiaryDateActivity,
as: 'activityGroupActivity',
include: [
{
model: DiaryDates,
as: 'diaryDate',
where: startDate ? {
date: {
[Op.gte]: startDate
}
} : {}
},
{
model: PredefinedActivity,
as: 'predefinedActivity',
required: false
}
]
},
{
model: PredefinedActivity,
as: 'groupPredefinedActivity',
required: false
}
]
});
// Erstelle virtuelle DiaryMemberActivity-Objekte für Gruppen-Aktivitäten
for (const groupActivity of groupActivitiesData) {
if (!groupActivity.activityGroupActivity || !groupActivity.activityGroupActivity.diaryDate) {
continue; // Überspringe, wenn keine DiaryDateActivity oder kein DiaryDate vorhanden
}
const activity = groupActivity.activityGroupActivity;
const diaryDateId = activity.diaryDateId;
// Finde alle relevanten Participants für dieses DiaryDate
const relevantParticipants = participants.filter(p =>
p.diaryDateId === diaryDateId &&
p.groupId === groupActivity.groupId
);
for (const participant of relevantParticipants) {
// Verwende die PredefinedActivity aus GroupActivity, falls vorhanden
// Sonst die aus DiaryDateActivity
const predefinedActivity = groupActivity.groupPredefinedActivity || activity.predefinedActivity;
if (predefinedActivity) {
// Erstelle ein modifiziertes Activity-Objekt
const modifiedActivity = {
...activity.toJSON(),
predefinedActivity: predefinedActivity
};
groupActivities.push({
activity: modifiedActivity,
participant: participant,
id: null // Virtuell, nicht in DB
});
}
}
}
}
// Filter: explizite Zuordnungen sollen nur dann zählen, wenn
// - der Participant keine Gruppe hat UND die Aktivität KEINE Gruppenbindung hat, oder
// - die Aktivität keine Gruppenbindung hat, oder
// - es eine Gruppenbindung gibt, die zur Gruppe des Participants passt.
const filteredMemberActivities = memberActivities.filter((ma) => {
if (!ma?.participant || !ma?.activity) {
return false;
}
const participantGroupId = ma.participant.groupId;
const groupActivitiesForActivity = ma.activity.groupActivities || [];
// Participant ohne Gruppe -> nur Aktivitäten ohne Gruppenbindung zählen
if (participantGroupId === null || participantGroupId === undefined) {
return !groupActivitiesForActivity.length;
}
// Keine Gruppenbindung -> immer zählen
if (!groupActivitiesForActivity.length) {
return true;
}
// Gruppenbindung vorhanden -> nur zählen, wenn die Gruppe passt
return groupActivitiesForActivity.some((ga) => Number(ga.groupId) === Number(participantGroupId));
});
// 3. Kombiniere beide Listen und entferne Duplikate
// Ein Duplikat liegt vor, wenn dieselbe Aktivität für denselben Participant bereits explizit zugeordnet ist
const explicitActivityKeys = new Set();
filteredMemberActivities.forEach(ma => {
if (ma.activity && ma.activity.id && ma.participant && ma.participant.id) {
// Erstelle einen eindeutigen Schlüssel: activityId-participantId
const key = `${ma.activity.id}-${ma.participant.id}`;
explicitActivityKeys.add(key);
}
});
// Filtere Gruppen-Aktivitäten, die bereits explizit zugeordnet sind
const uniqueGroupActivities = groupActivities.filter(ga => {
if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) {
return false;
}
const key = `${ga.activity.id}-${ga.participant.id}`;
return !explicitActivityKeys.has(key);
});
// Kombiniere beide Listen
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
// Group activities by name and count occurrences
// Verwende einen Set pro Aktivität, um eindeutige Datum-Aktivität-Kombinationen zu tracken
const activityMap = new Map();
for (const ma of allActivities) {
if (!ma.activity || !ma.activity.predefinedActivity || !ma.participant) {
continue;
}
const activity = ma.activity.predefinedActivity;
const activityName = activity.name;
const activityCode = activity.code || activity.name; // Verwende Code falls vorhanden, sonst Name
const date = ma.activity.diaryDate?.date;
if (!date) {
continue; // Überspringe Einträge ohne Datum
}
// Verwende Code als Key, falls vorhanden, sonst Name
const key = activityCode;
if (!activityMap.has(key)) {
activityMap.set(key, {
name: activityName, // Vollständiger Name für Tooltip
code: activityCode, // Code/Kürzel für Anzeige
uniqueDates: new Set(), // Set für eindeutige Daten
dates: []
});
}
const activityData = activityMap.get(key);
// Konvertiere Datum zu String für Set-Vergleich (nur Datum, keine Zeit)
const dateString = date instanceof Date
? date.toISOString().split('T')[0]
: new Date(date).toISOString().split('T')[0];
// Füge Datum nur hinzu, wenn es noch nicht vorhanden ist
if (!activityData.uniqueDates.has(dateString)) {
activityData.uniqueDates.add(dateString);
activityData.dates.push(date);
}
}
// Konvertiere Sets zu Arrays und setze count basierend auf eindeutigen Daten
activityMap.forEach((activityData, key) => {
activityData.count = activityData.uniqueDates.size;
// Sortiere Daten (neueste zuerst)
activityData.dates.sort((a, b) => {
const dateA = new Date(a);
const dateB = new Date(b);
return dateB - dateA;
});
// Entferne uniqueDates, da es nicht an Frontend gesendet werden muss
delete activityData.uniqueDates;
});
// Convert map to array and sort by count
const activities = Array.from(activityMap.values())
.sort((a, b) => b.count - a.count);
return res.status(200).json(activities);
} catch (error) {
console.error('Error fetching member activities:', error);
return res.status(500).json({ error: 'Failed to fetch member activities' });
}
};
export const getMemberLastParticipations = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
const { limit = 3 } = req.query;
await checkAccess(userToken, clubId);
// Get participant ID for this member
const participants = await Participant.findAll({
where: { memberId: memberId }
});
if (participants.length === 0) {
return res.status(200).json([]);
}
const participantIds = participants.map(p => p.id);
// Sammle alle Gruppen-IDs, zu denen der Member gehört
const memberGroupIds = new Set();
participants.forEach(p => {
if (p.groupId !== null && p.groupId !== undefined) {
memberGroupIds.add(p.groupId);
}
});
// 1. Get last participations explicitly assigned to this member
const memberActivities = await DiaryMemberActivity.findAll({
where: {
participantId: participantIds
},
include: [
{
model: Participant,
as: 'participant',
attributes: ['id', 'groupId', 'diaryDateId']
},
{
model: DiaryDateActivity,
as: 'activity',
include: [
{
model: DiaryDates,
as: 'diaryDate'
},
{
model: PredefinedActivity,
as: 'predefinedActivity'
},
{
model: GroupActivity,
as: 'groupActivities',
required: false
}
]
}
],
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
limit: parseInt(limit) * 10 // Get more to filter by group
});
// Siehe getMemberActivities(): nur zählen, wenn Gruppenbindung passt (oder keine existiert)
const filteredMemberActivities = memberActivities.filter((ma) => {
if (!ma?.participant || !ma?.activity) {
return false;
}
const participantGroupId = ma.participant.groupId;
const groupActivitiesForActivity = ma.activity.groupActivities || [];
if (!groupActivitiesForActivity.length) {
return true;
}
return groupActivitiesForActivity.some((ga) => Number(ga.groupId) === Number(participantGroupId));
});
// 2. Get all group activities for groups the member belongs to
const groupActivities = [];
if (memberGroupIds.size > 0) {
// Suche direkt nach GroupActivity-Einträgen für die Gruppen des Members
const groupActivitiesData = await GroupActivity.findAll({
where: {
groupId: {
[Op.in]: Array.from(memberGroupIds)
}
},
include: [
{
model: DiaryDateActivity,
as: 'activityGroupActivity',
include: [
{
model: DiaryDates,
as: 'diaryDate'
},
{
model: PredefinedActivity,
as: 'predefinedActivity',
required: false
}
]
},
{
model: PredefinedActivity,
as: 'groupPredefinedActivity',
required: false
}
],
order: [[{ model: DiaryDateActivity, as: 'activityGroupActivity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
limit: parseInt(limit) * 10 // Get more to filter
});
// Erstelle virtuelle DiaryMemberActivity-Objekte für Gruppen-Aktivitäten
for (const groupActivity of groupActivitiesData) {
if (!groupActivity.activityGroupActivity || !groupActivity.activityGroupActivity.diaryDate) {
continue; // Überspringe, wenn keine DiaryDateActivity oder kein DiaryDate vorhanden
}
const activity = groupActivity.activityGroupActivity;
const diaryDateId = activity.diaryDateId;
// Finde alle relevanten Participants für dieses DiaryDate
const relevantParticipants = participants.filter(p =>
p.diaryDateId === diaryDateId &&
p.groupId === groupActivity.groupId
);
for (const participant of relevantParticipants) {
// Verwende die PredefinedActivity aus GroupActivity, falls vorhanden
// Sonst die aus DiaryDateActivity
const predefinedActivity = groupActivity.groupPredefinedActivity || activity.predefinedActivity;
if (predefinedActivity) {
// Erstelle ein modifiziertes Activity-Objekt
const modifiedActivity = {
...activity.toJSON(),
predefinedActivity: predefinedActivity
};
groupActivities.push({
activity: modifiedActivity,
participant: participant,
id: null // Virtuell, nicht in DB
});
}
}
}
}
// 3. Kombiniere beide Listen und entferne Duplikate
// Ein Duplikat liegt vor, wenn dieselbe Aktivität für denselben Participant bereits explizit zugeordnet ist
const explicitActivityKeys = new Set();
filteredMemberActivities.forEach(ma => {
if (ma.activity && ma.activity.id && ma.participant && ma.participant.id) {
// Erstelle einen eindeutigen Schlüssel: activityId-participantId
const key = `${ma.activity.id}-${ma.participant.id}`;
explicitActivityKeys.add(key);
}
});
// Filtere Gruppen-Aktivitäten, die bereits explizit zugeordnet sind
const uniqueGroupActivities = groupActivities.filter(ga => {
if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) {
return false;
}
const key = `${ga.activity.id}-${ga.participant.id}`;
return !explicitActivityKeys.has(key);
});
// Kombiniere beide Listen
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
// Gruppiere nach Datum
const participationsByDate = new Map();
allActivities
.filter(ma => {
if (!ma.activity || !ma.activity.predefinedActivity || !ma.activity.diaryDate || !ma.participant) {
return false;
}
return true;
})
.forEach(ma => {
const date = ma.activity.diaryDate.date;
const diaryDateId = ma.activity.diaryDate.id;
const activity = ma.activity.predefinedActivity;
const activityName = activity.name;
const activityCode = activity.code || activity.name;
if (!participationsByDate.has(date)) {
participationsByDate.set(date, {
date: date,
diaryDateId: diaryDateId,
activities: []
});
}
const dateEntry = participationsByDate.get(date);
// Füge Aktivität nur hinzu, wenn sie noch nicht vorhanden ist (vermeide Duplikate)
// Speichere sowohl code als auch name
const activityEntry = {
code: activityCode,
name: activityName
};
if (!dateEntry.activities.find(a => (a.code || a.name) === activityCode)) {
dateEntry.activities.push(activityEntry);
}
});
// Sortiere nach Datum (neueste zuerst) und nehme die letzten N Daten
const sortedDates = Array.from(participationsByDate.values())
.sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB - dateA;
})
.slice(0, parseInt(limit));
// Formatiere für das Frontend: Flache Liste mit Datum und Aktivität
const participations = [];
sortedDates.forEach(dateEntry => {
dateEntry.activities.forEach(activity => {
participations.push({
id: null, // Virtuell
activityName: activity.code || activity.name, // Code für Anzeige
activityFullName: activity.name, // Vollständiger Name für Tooltip
date: dateEntry.date,
diaryDateId: dateEntry.diaryDateId
});
});
});
return res.status(200).json(participations);
} catch (error) {
console.error('Error fetching member last participations:', error);
return res.status(500).json({ error: 'Failed to fetch member last participations' });
}
};

View File

@@ -1,49 +1,48 @@
import MemberService from "../services/memberService.js";
import MemberTransferService from "../services/memberTransferService.js";
import { emitMemberChanged } from '../services/socketService.js';
import { devLog } from '../utils/logger.js';
const getClubMembers = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { id: clubId, showAll } = req.params;
if (showAll === null) {
showAll = false;
}
const { id: clubId } = req.params;
const showAll = req.params.showAll ?? 'false';
res.status(200).json(await MemberService.getClubMembers(userToken, clubId, showAll));
} catch(error) {
devLog('[getClubMembers] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
}
const getWaitingApprovals = async(req, res) => {
try {
devLog('[getWaitingApprovals] - Start');
const { id: clubId } = req.params;
devLog('[getWaitingApprovals] - get token');
const { authcode: userToken } = req.headers;
devLog('[getWaitingApprovals] - load for waiting approvals');
const waitingApprovals = await MemberService.getApprovalRequests(userToken, clubId);
devLog('[getWaitingApprovals] - set response');
res.status(200).json(waitingApprovals);
devLog('[getWaitingApprovals] - done');
} catch(error) {
devLog('[getWaitingApprovals] - Error: ', error);
res.status(403).json({ error: error });
}
}
const setClubMembers = async (req, res) => {
try {
const { id: memberId, firstname: firstName, lastname: lastName, street, city, birthdate, phone, email, active,
testMembership, picsInInternetAllowed, gender, ttr, qttr } = req.body;
const { id: memberId, firstname: firstName, lastname: lastName, street, city, postalCode, birthdate, phone, email, active,
testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts } = 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, ttr, qttr);
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, postalCode, birthdate,
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts);
// Emit Socket-Event wenn Member erfolgreich erstellt/aktualisiert wurde
if (addResult.status === 200) {
emitMemberChanged(clubId);
}
res.status(addResult.status || 500).json(addResult.response);
} catch (error) {
console.error('[setClubMembers] - Error:', error);
res.status(500).json({ error: 'Failed to upload image' });
res.status(500).json({ error: 'Failed to save member' });
}
}
@@ -51,8 +50,12 @@ const uploadMemberImage = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.uploadMemberImage(userToken, clubId, memberId, req.file.buffer);
res.status(result.status).json(result.message ? { message: result.message } : { error: result.error });
const makePrimary =
req.body?.makePrimary === true ||
req.body?.makePrimary === 'true' ||
req.query?.makePrimary === 'true';
const result = await MemberService.uploadMemberImage(userToken, clubId, memberId, req.file.buffer, { makePrimary });
res.status(result.status).json(result.response ?? { success: false, error: 'Unknown upload result' });
} catch (error) {
console.error('[uploadMemberImage] - Error:', error);
res.status(500).json({ error: 'Failed to upload image' });
@@ -60,11 +63,12 @@ const uploadMemberImage = async (req, res) => {
};
const getMemberImage = async (req, res) => {
devLog('[getMemberImage]');
try {
const { clubId, memberId } = req.params;
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.getMemberImage(userToken, clubId, memberId);
// Support "latest" as imageId to get the latest image
const actualImageId = imageId === 'latest' ? null : (imageId || null);
const result = await MemberService.getMemberImage(userToken, clubId, memberId, actualImageId);
if (result.status === 200) {
res.sendFile(result.imagePath);
} else {
@@ -77,7 +81,6 @@ const getMemberImage = async (req, res) => {
};
const updateRatingsFromMyTischtennis = async (req, res) => {
devLog('[updateRatingsFromMyTischtennis]');
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
@@ -89,4 +92,166 @@ const updateRatingsFromMyTischtennis = async (req, res) => {
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis };
const rotateMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { direction } = req.body;
const { authcode: userToken } = req.headers;
if (!direction || !['left', 'right'].includes(direction)) {
return res.status(400).json({
success: false,
error: 'Ungültige Drehrichtung. Verwenden Sie "left" oder "right".'
});
}
const result = await MemberService.rotateMemberImage(userToken, clubId, memberId, imageId, direction);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[rotateMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to rotate image' });
}
};
const deleteMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.deleteMemberImage(userToken, clubId, memberId, imageId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[deleteMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to delete image' });
}
};
const generateMemberGallery = async (req, res) => {
try {
const { clubId } = req.params;
const { authcode: userToken } = req.headers;
const size = parseInt(req.query.size) || 200; // Default: 200x200
const format = req.query.format || 'image'; // 'image' or 'json'
// Bei format=json wird kein Bild erstellt, nur die Mitgliederliste zurückgegeben
const createImage = format !== 'json';
const result = await MemberService.generateMemberGallery(userToken, clubId, size, createImage);
if (result.status === 200) {
if (format === 'json') {
// Return member information for interactive gallery (ohne Bild zu erstellen)
return res.status(200).json({
members: result.galleryEntries.map(entry => ({
memberId: entry.memberId,
firstName: entry.firstName,
lastName: entry.lastName,
fullName: entry.fullName
}))
});
}
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'no-store');
return res.status(200).send(result.buffer);
}
return res.status(result.status).json({ error: result.error || 'Galerie konnte nicht erstellt werden' });
} catch (error) {
console.error('[generateMemberGallery] - Error:', error);
res.status(500).json({ error: 'Failed to generate member gallery' });
}
};
const setPrimaryMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.setPrimaryMemberImage(userToken, clubId, memberId, imageId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[setPrimaryMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to update primary image' });
}
};
const quickUpdateTestMembership = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.quickUpdateTestMembership(userToken, clubId, memberId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[quickUpdateTestMembership] - Error:', error);
res.status(500).json({ error: 'Failed to update test membership' });
}
};
const quickUpdateMemberFormHandedOver = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.quickUpdateMemberFormHandedOver(userToken, clubId, memberId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[quickUpdateMemberFormHandedOver] - Error:', error);
res.status(500).json({ error: 'Failed to update member form status' });
}
};
const quickDeactivateMember = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.quickDeactivateMember(userToken, clubId, memberId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[quickDeactivateMember] - Error:', error);
res.status(500).json({ error: 'Failed to deactivate member' });
}
};
const transferMembers = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const config = req.body;
// Validierung
if (!config.transferEndpoint) {
return res.status(400).json({
success: false,
error: 'Übertragungs-Endpoint ist erforderlich'
});
}
if (!config.transferTemplate) {
return res.status(400).json({
success: false,
error: 'Übertragungs-Template ist erforderlich'
});
}
const result = await MemberTransferService.transferMembers(userToken, clubId, config);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[transferMembers] - Error:', error);
res.status(500).json({
success: false,
error: 'Fehler bei der Übertragung: ' + error.message
});
}
};
export {
getClubMembers,
getWaitingApprovals,
setClubMembers,
uploadMemberImage,
getMemberImage,
updateRatingsFromMyTischtennis,
rotateMemberImage,
transferMembers,
quickUpdateTestMembership,
quickUpdateMemberFormHandedOver,
quickDeactivateMember,
deleteMemberImage,
setPrimaryMemberImage,
generateMemberGallery
};

View File

@@ -1,4 +1,5 @@
import MemberNoteService from "../services/memberNoteService.js";
import MemberNote from '../models/MemberNote.js';
import { devLog } from '../utils/logger.js';
const getMemberNotes = async (req, res) => {
@@ -6,11 +7,9 @@ const getMemberNotes = async (req, res) => {
const { authcode: userToken } = req.headers;
const { memberId } = req.params;
const { clubId } = req.query;
devLog('[getMemberNotes]', userToken, memberId, clubId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
devLog('[getMemberNotes] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -19,12 +18,11 @@ const addMemberNote = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { memberId, content, clubId } = req.body;
devLog('[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) {
devLog('[addMemberNote] - Error: ', error);
console.error('[addMemberNote] - Error:', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -34,13 +32,16 @@ const deleteMemberNote = async (req, res) => {
const { authcode: userToken } = req.headers;
const { noteId } = req.params;
const { clubId } = req.body;
devLog('[deleteMemberNote]', userToken, noteId, clubId);
const memberId = await MemberNoteService.getMemberIdForNote(noteId); // Member ID ermitteln
const note = await MemberNote.findByPk(noteId);
if (!note) {
return res.status(404).json({ error: 'notfound' });
}
const memberId = note.memberId;
await MemberNoteService.deleteNoteForMember(userToken, clubId, noteId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
devLog('[deleteMemberNote] - Error: ', error);
console.error('[deleteMemberNote] - Error:', error);
res.status(500).json({ error: 'systemerror' });
}
};

View File

@@ -0,0 +1,51 @@
import MemberTransferConfigService from '../services/memberTransferConfigService.js';
export const getConfig = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberTransferConfigService.getConfig(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[getConfig] Error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Laden der Konfiguration'
});
}
};
export const saveConfig = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const configData = req.body;
const result = await MemberTransferConfigService.saveConfig(userToken, clubId, configData);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[saveConfig] Error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Speichern der Konfiguration'
});
}
};
export const deleteConfig = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberTransferConfigService.deleteConfig(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[deleteConfig] Error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Löschen der Konfiguration'
});
}
};

View File

@@ -1,5 +1,6 @@
import myTischtennisService from '../services/myTischtennisService.js';
import HttpError from '../exceptions/HttpError.js';
import axios from 'axios';
class MyTischtennisController {
/**
@@ -42,15 +43,15 @@ class MyTischtennisController {
async upsertAccount(req, res, next) {
try {
const userId = req.user.id;
const { email, password, savePassword, userPassword } = req.body;
const { email, password, savePassword, autoUpdateRatings, userPassword } = req.body;
if (!email) {
throw new HttpError(400, 'E-Mail-Adresse erforderlich');
throw new HttpError('E-Mail-Adresse erforderlich', 400);
}
// 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');
throw new HttpError('App-Passwort erforderlich zum Setzen des myTischtennis-Passworts', 400);
}
const account = await myTischtennisService.upsertAccount(
@@ -58,6 +59,7 @@ class MyTischtennisController {
email,
password,
savePassword || false,
autoUpdateRatings || false,
userPassword
);
@@ -80,7 +82,7 @@ class MyTischtennisController {
const deleted = await myTischtennisService.deleteAccount(userId);
if (!deleted) {
throw new HttpError(404, 'Kein myTischtennis-Account gefunden');
throw new HttpError('Kein myTischtennis-Account gefunden', 404);
}
res.status(200).json({ message: 'myTischtennis-Account gelöscht' });
@@ -127,6 +129,363 @@ class MyTischtennisController {
next(error);
}
}
/**
* GET /api/mytischtennis/update-history
* Get update ratings history
*/
async getUpdateHistory(req, res, next) {
try {
const userId = req.user.id;
const history = await myTischtennisService.getUpdateHistory(userId);
res.status(200).json({ history });
} catch (error) {
next(error);
}
}
/**
* Get fetch logs for current user
*/
async getFetchLogs(req, res, next) {
try {
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
const logs = await fetchLogService.getFetchLogs(userId, {
limit: req.query.limit ? parseInt(req.query.limit) : 50,
fetchType: req.query.type
});
res.status(200).json({ logs });
} catch (error) {
next(error);
}
}
/**
* Get latest successful fetches for each type
*/
async getLatestFetches(req, res, next) {
try {
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
const latestFetches = await fetchLogService.getLatestSuccessfulFetches(userId);
res.status(200).json({ latestFetches });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/login-page
* Proxy für Login-Seite (für iframe)
* Lädt die Login-Seite von mytischtennis.de und modifiziert sie, sodass Form-Submissions über unseren Proxy gehen
* Authentifizierung ist optional - Token kann als Query-Parameter übergeben werden
*/
async getLoginPage(req, res, next) {
try {
// Versuche, userId aus Token zu bekommen (optional)
let userId = null;
const token = req.query.token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode'];
if (token) {
try {
const jwt = (await import('jsonwebtoken')).default;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
userId = decoded.userId;
} catch (err) {
// Token ungültig - ignorieren
}
}
// Speichere userId im Request für submitLogin
req.userId = userId;
// Lade die Login-Seite von mytischtennis.de
const response = await axios.get('https://www.mytischtennis.de/login?next=%2F', {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
},
maxRedirects: 5,
validateStatus: () => true // Akzeptiere alle Status-Codes
});
// Setze Cookies aus der Response
const setCookieHeaders = response.headers['set-cookie'];
if (setCookieHeaders) {
res.setHeader('Set-Cookie', setCookieHeaders);
}
// Modifiziere HTML: Ändere Form-Action auf unseren Proxy
let html = response.data;
if (typeof html === 'string') {
// Füge Token als Hidden-Input hinzu, damit submitLogin die userId bekommt
const tokenInput = userId ? `<input type="hidden" name="__token" value="${token}" />` : '';
// Ersetze Form-Action URLs und füge Token-Input hinzu
html = html.replace(
/(<form[^>]*action="[^"]*\/login[^"]*"[^>]*>)/g,
`$1${tokenInput}`
);
html = html.replace(
/action="([^"]*\/login[^"]*)"/g,
'action="/api/mytischtennis/login-submit"'
);
// Ersetze auch relative URLs
html = html.replace(
/action="\/login/g,
'action="/api/mytischtennis/login-submit'
);
}
// Setze Content-Type
res.setHeader('Content-Type', response.headers['content-type'] || 'text/html; charset=utf-8');
// Sende den modifizierten HTML-Inhalt
res.status(response.status).send(html);
} catch (error) {
console.error('Fehler beim Laden der Login-Seite:', error);
next(error);
}
}
/**
* POST /api/mytischtennis/login-submit
* Proxy für Login-Form-Submission
* Leitet den Login-Request durch, damit Cookies im Backend-Kontext bleiben
* Authentifizierung ist optional - iframe kann keinen Token mitsenden
*/
async submitLogin(req, res, next) {
try {
// Versuche, userId aus Token zu bekommen (aus Query-Parameter oder Hidden-Input)
let userId = null;
const token = req.query.token || req.body.__token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode'];
if (token) {
try {
const jwt = (await import('jsonwebtoken')).default;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
userId = decoded.userId;
} catch (err) {
// Token ungültig - ignorieren
}
}
// Entferne __token aus req.body, damit es nicht an mytischtennis.de gesendet wird
if (req.body.__token) {
delete req.body.__token;
}
// Hole Cookies aus dem Request
const cookies = req.headers.cookie || '';
// Leite den Login-Request an mytischtennis.de weiter
const response = await axios.post(
'https://www.mytischtennis.de/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
req.body, // Form-Daten
{
headers: {
'Cookie': cookies,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': '*/*',
'Referer': 'https://www.mytischtennis.de/login?next=%2F'
},
maxRedirects: 0,
validateStatus: () => true
}
);
// Setze Cookies aus der Response
const setCookieHeaders = response.headers['set-cookie'];
if (setCookieHeaders) {
res.setHeader('Set-Cookie', setCookieHeaders);
}
// Setze andere relevante Headers
if (response.headers['content-type']) {
res.setHeader('Content-Type', response.headers['content-type']);
}
if (response.headers['location']) {
res.setHeader('Location', response.headers['location']);
}
// Prüfe, ob Login erfolgreich war (durch Prüfung der Cookies)
const authCookie = setCookieHeaders?.find(cookie => cookie.startsWith('sb-10-auth-token='));
if (authCookie && userId) {
// Login erfolgreich - speichere Session (nur wenn userId vorhanden)
await this.saveSessionFromCookie(userId, authCookie);
}
// Sende Response weiter
res.status(response.status).send(response.data);
} catch (error) {
console.error('Fehler beim Login-Submit:', error);
next(error);
}
}
/**
* Speichere Session-Daten aus Cookie
*/
async saveSessionFromCookie(userId, cookieString) {
try {
const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/);
if (!tokenMatch) {
throw new Error('Token-Format ungültig');
}
const base64Token = tokenMatch[1];
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
const tokenData = JSON.parse(decodedToken);
const MyTischtennis = (await import('../models/MyTischtennis.js')).default;
const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } });
if (myTischtennisAccount) {
myTischtennisAccount.accessToken = tokenData.access_token;
myTischtennisAccount.refreshToken = tokenData.refresh_token;
myTischtennisAccount.expiresAt = tokenData.expires_at;
myTischtennisAccount.cookie = cookieString.split(';')[0].trim();
myTischtennisAccount.userData = tokenData.user;
myTischtennisAccount.lastLoginSuccess = new Date();
myTischtennisAccount.lastLoginAttempt = new Date();
// Hole Club-Informationen
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
const profileResult = await myTischtennisClient.getUserProfile(myTischtennisAccount.cookie);
if (profileResult.success) {
myTischtennisAccount.clubId = profileResult.clubId;
myTischtennisAccount.clubName = profileResult.clubName;
myTischtennisAccount.fedNickname = profileResult.fedNickname;
}
await myTischtennisAccount.save();
}
} catch (error) {
console.error('Fehler beim Speichern der Session:', error);
throw error;
}
}
/**
* POST /api/mytischtennis/extract-session
* Extrahiere Session nach Login im iframe
* Versucht, die Session-Daten aus den Cookies zu extrahieren
* Authentifizierung ist optional - iframe kann keinen Token mitsenden
*/
async extractSession(req, res, next) {
try {
// Versuche, userId aus Token zu bekommen (optional)
let userId = req.user?.id;
// Falls kein Token vorhanden, versuche userId aus Account zu bekommen (falls E-Mail bekannt)
if (!userId) {
// Kann nicht ohne Authentifizierung arbeiten - Session kann nicht gespeichert werden
return res.status(401).json({
error: 'Authentifizierung erforderlich zum Speichern der Session'
});
}
// Hole die Cookies aus dem Request
const cookies = req.headers.cookie || '';
// Versuche, die Session zu verifizieren, indem wir einen Request mit den Cookies machen
const response = await axios.get('https://www.mytischtennis.de/?_data=root', {
headers: {
'Cookie': cookies,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json'
},
validateStatus: () => true
});
// Prüfe, ob wir eingeloggt sind (durch Prüfung der Response)
if (response.status === 200 && response.data?.userProfile) {
// Session erfolgreich - speichere die Daten
const account = await myTischtennisService.getAccount(userId);
if (!account) {
throw new HttpError('Kein myTischtennis-Account verknüpft', 404);
}
// Extrahiere Cookie-String
const cookieString = cookies.split(';').find(c => c.trim().startsWith('sb-10-auth-token='));
if (!cookieString) {
throw new HttpError('Kein Auth-Token in Cookies gefunden', 400);
}
// Parse Token aus Cookie
const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/);
if (!tokenMatch) {
throw new HttpError('Token-Format ungültig', 400);
}
const base64Token = tokenMatch[1];
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
const tokenData = JSON.parse(decodedToken);
// Aktualisiere Account mit Session-Daten
const MyTischtennis = (await import('../models/MyTischtennis.js')).default;
const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } });
if (myTischtennisAccount) {
myTischtennisAccount.accessToken = tokenData.access_token;
myTischtennisAccount.refreshToken = tokenData.refresh_token;
myTischtennisAccount.expiresAt = tokenData.expires_at;
myTischtennisAccount.cookie = cookieString.trim();
myTischtennisAccount.userData = tokenData.user;
myTischtennisAccount.lastLoginSuccess = new Date();
myTischtennisAccount.lastLoginAttempt = new Date();
// Hole Club-Informationen
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
const profileResult = await myTischtennisClient.getUserProfile(cookieString.trim());
if (profileResult.success) {
myTischtennisAccount.clubId = profileResult.clubId;
myTischtennisAccount.clubName = profileResult.clubName;
myTischtennisAccount.fedNickname = profileResult.fedNickname;
}
await myTischtennisAccount.save();
}
res.status(200).json({
success: true,
message: 'Session erfolgreich extrahiert und gespeichert'
});
} else {
throw new HttpError('Nicht eingeloggt oder Session ungültig', 401);
}
} catch (error) {
console.error('Fehler beim Extrahieren der Session:', error);
next(error);
}
}
}
export default new MyTischtennisController();

View File

@@ -0,0 +1,601 @@
import myTischtennisUrlParserService from '../services/myTischtennisUrlParserService.js';
import myTischtennisService from '../services/myTischtennisService.js';
import MemberService from '../services/memberService.js';
import autoFetchMatchResultsService from '../services/autoFetchMatchResultsService.js';
import apiLogService from '../services/apiLogService.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import User from '../models/User.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class MyTischtennisUrlController {
/**
* Parse myTischtennis URL and return configuration data
* POST /api/mytischtennis/parse-url
* Body: { url: string }
*/
async parseUrl(req, res, next) {
try {
const { url } = req.body;
if (!url) {
throw new HttpError('URL is required', 400);
}
// Validate URL
if (!myTischtennisUrlParserService.isValidTeamUrl(url)) {
throw new HttpError('Invalid myTischtennis URL format', 400);
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
// Try to fetch additional data if user is authenticated
const userIdOrEmail = req.headers.userid;
let completeData = parsedData;
if (userIdOrEmail) {
// Get actual user ID
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (user) userId = user.id;
}
try {
const account = await myTischtennisService.getAccount(userId);
if (account && account.accessToken) {
completeData = await myTischtennisUrlParserService.fetchTeamData(
parsedData,
account.cookie,
account.accessToken
);
}
} catch (error) {
// Continue with parsed data only
}
}
res.json({
success: true,
data: completeData
});
} catch (error) {
next(error);
}
}
/**
* Configure team from myTischtennis URL
* POST /api/mytischtennis/configure-team
* Body: { url: string, clubTeamId: number, createLeague?: boolean, createSeason?: boolean }
*/
async configureTeam(req, res, next) {
try {
const { url, clubTeamId, createLeague, createSeason } = req.body;
const userIdOrEmail = req.headers.userid;
if (!url || !clubTeamId) {
throw new HttpError('URL and clubTeamId are required', 400);
}
// Get actual user ID
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
throw new HttpError('User not found', 404);
}
userId = user.id;
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
// Try to fetch additional data
let completeData = parsedData;
const account = await myTischtennisService.getAccount(userId);
if (account && account.accessToken) {
try {
completeData = await myTischtennisUrlParserService.fetchTeamData(
parsedData,
account.cookie,
account.accessToken
);
} catch (error) {
}
}
// Find or create season
let season = await Season.findOne({
where: { season: completeData.season }
});
if (!season && createSeason) {
season = await Season.create({
season: completeData.season
});
}
if (!season) {
throw new HttpError(`Season ${completeData.season} not found. Set createSeason=true to create it.`, 404);
}
// Find or create league
const team = await ClubTeam.findByPk(clubTeamId);
if (!team) {
throw new HttpError('Club team not found', 404);
}
let league;
// First, try to find existing league by name and season
const leagueName = completeData.leagueName || completeData.groupname;
league = await League.findOne({
where: {
name: leagueName,
seasonId: season.id,
clubId: team.clubId
}
});
if (league) {
devLog(`Found existing league: ${league.name} (ID: ${league.id})`);
// Update myTischtennis fields
await league.update({
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
} else if (team.leagueId) {
// Team has a league assigned, update it
league = await League.findByPk(team.leagueId);
if (league) {
devLog(`Updating team's existing league: ${league.name} (ID: ${league.id})`);
await league.update({
name: leagueName,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
}
} else if (createLeague) {
// Create new league
devLog(`Creating new league: ${leagueName}`);
league = await League.create({
name: leagueName,
seasonId: season.id,
clubId: team.clubId,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
} else {
throw new HttpError('League not found and team has no league assigned. Set createLeague=true to create one.', 400);
}
// Update team
await team.update({
myTischtennisTeamId: completeData.teamId,
leagueId: league.id,
seasonId: season.id
});
res.json({
success: true,
message: 'Team configured successfully',
data: {
team: {
id: team.id,
name: team.name,
myTischtennisTeamId: completeData.teamId
},
league: {
id: league.id,
name: league.name,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
},
season: {
id: season.id,
name: season.season
},
parsedData: completeData
}
});
} catch (error) {
next(error);
}
}
/**
* Manually fetch team data from myTischtennis
* POST /api/mytischtennis/fetch-team-data
* Body: { clubTeamId: number }
*/
async fetchTeamData(req, res, next) {
// Define outside of try/catch so catch has access
let account = null;
let team = null;
let myTischtennisUrl = null;
let requestStartTime = null;
try {
const { clubTeamId } = req.body;
const userIdOrEmail = req.headers.userid;
if (!clubTeamId) {
throw new HttpError('clubTeamId is required', 400);
}
if (!userIdOrEmail) {
throw new HttpError('User-ID fehlt. Bitte melden Sie sich an.', 401);
}
// Get actual user ID (userid header might be email address)
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
// It's an email, find the user
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
throw new HttpError('User not found', 404);
}
userId = user.id;
}
// Get myTischtennis session (similar to memberService.updateRatingsFromMyTischtennis)
let session;
try {
session = await myTischtennisService.getSession(userId);
} catch (sessionError) {
// Versuche automatischen Login mit gespeicherten Credentials
try {
// Check if account exists and has password
const accountCheck = await myTischtennisService.getAccount(userId);
if (!accountCheck) {
throw new Error('MyTischtennis-Account nicht gefunden');
}
if (!accountCheck.encryptedPassword) {
throw new Error('Kein Passwort gespeichert. Bitte melden Sie sich in den MyTischtennis-Einstellungen an und speichern Sie Ihr Passwort.');
}
await myTischtennisService.verifyLogin(userId);
session = await myTischtennisService.getSession(userId);
} catch (loginError) {
const errorMessage = loginError.message || 'Automatischer Login fehlgeschlagen';
throw new HttpError(`MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen: ${errorMessage}. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.`, 401);
}
}
// Get account data (for clubId, etc.)
account = await myTischtennisService.getAccount(userId);
if (!account) {
throw new HttpError('MyTischtennis-Account nicht verknüpft. Bitte verknüpfen Sie Ihren Account in den MyTischtennis-Einstellungen.', 404);
}
// Get team with league and season
team = await ClubTeam.findByPk(clubTeamId, {
include: [
{
model: League,
as: 'league',
include: [
{
model: Season,
as: 'season'
}
]
}
]
});
if (!team) {
throw new HttpError(`Team mit ID ${clubTeamId} nicht gefunden`, 404);
}
// Verbesserte Validierung mit detaillierten Fehlermeldungen
if (!team.myTischtennisTeamId) {
throw new HttpError(`Team "${team.name}" (interne ID: ${team.id}) ist nicht für myTischtennis konfiguriert: myTischtennisTeamId fehlt. Bitte konfigurieren Sie das Team zuerst über die MyTischtennis-URL.`, 400);
}
if (!team.league) {
throw new HttpError('Team ist keiner Liga zugeordnet. Bitte ordnen Sie das Team einer Liga zu.', 400);
}
if (!team.league.myTischtennisGroupId) {
throw new HttpError('Liga ist nicht für myTischtennis konfiguriert: myTischtennisGroupId fehlt. Bitte konfigurieren Sie die Liga zuerst über die MyTischtennis-URL.', 400);
}
// Validate season before proceeding
if (!team.league.season || !team.league.season.season) {
throw new HttpError('Liga ist keiner Saison zugeordnet. Bitte ordnen Sie die Liga einer Saison zu.', 400);
}
// Build the URL that will be used - do this early so we can log it even if errors occur
const seasonFull = team.league.season.season;
const seasonParts = seasonFull.split('/');
const seasonShort = seasonParts.length === 2
? `${seasonParts[0].slice(-2)}/${seasonParts[1].slice(-2)}`
: seasonFull;
const seasonStr = seasonShort.replace('/', '--');
const teamnameEncoded = encodeURIComponent(team.name.replace(/\s/g, '_'));
myTischtennisUrl = `https://www.mytischtennis.de/click-tt/${team.league.association}/${seasonStr}/ligen/${team.league.groupname}/gruppe/${team.league.myTischtennisGroupId}/mannschaft/${team.myTischtennisTeamId}/${teamnameEncoded}/spielerbilanzen/gesamt`;
// Log the request to myTischtennis BEFORE making the call
// This ensures we always see what WILL BE sent, even if the call fails
requestStartTime = Date.now();
try {
await apiLogService.logRequest({
userId: account.userId,
method: 'GET',
path: myTischtennisUrl.replace('https://www.mytischtennis.de', ''),
statusCode: null,
requestBody: JSON.stringify({
url: myTischtennisUrl,
myTischtennisTeamId: team.myTischtennisTeamId,
clubTeamId: team.id,
teamName: team.name,
leagueName: team.league.name,
association: team.league.association,
groupId: team.league.myTischtennisGroupId,
groupname: team.league.groupname,
season: seasonFull
}),
responseBody: null,
executionTime: null,
errorMessage: 'Request wird ausgeführt...',
logType: 'api_request',
schedulerJobType: 'mytischtennis_fetch'
});
} catch (logError) {
// Silent fail - logging errors shouldn't break the request
}
// Fetch data for this specific team
// Note: fetchTeamResults will also log and update with actual response
const result = await autoFetchMatchResultsService.fetchTeamResults(
{
userId: account.userId,
email: account.email,
cookie: session.cookie,
accessToken: session.accessToken,
expiresAt: session.expiresAt,
getPassword: () => null // Not needed for manual fetch
},
team
);
// Also fetch and update league table data
let tableUpdateResult = null;
try {
await autoFetchMatchResultsService.fetchAndUpdateLeagueTable(account.userId, team.league.id);
tableUpdateResult = 'League table updated successfully';
} catch (error) {
tableUpdateResult = 'League table update failed: ' + error.message;
// Don't fail the entire request if table update fails
}
// Additionally update (Q)TTR ratings for the club
let ratingsUpdate = null;
try {
// Use already resolved userId instead of authcode to avoid header dependency
const ratingsResult = await MemberService.updateRatingsFromMyTischtennisByUserId(userId, team.clubId);
ratingsUpdate = ratingsResult?.response?.message || `Ratings update status: ${ratingsResult?.status}`;
} catch (ratingsErr) {
ratingsUpdate = 'Ratings update failed: ' + (ratingsErr.message || String(ratingsErr));
}
res.json({
success: true,
message: `${result.fetchedCount} Datensätze abgerufen und verarbeitet`,
data: {
fetchedCount: result.fetchedCount,
teamName: team.name,
tableUpdate: tableUpdateResult,
ratingsUpdate
}
});
} catch (error) {
// Update log with error information if we got far enough to build the URL
if (myTischtennisUrl && account && team) {
const requestExecutionTime = requestStartTime ? (Date.now() - requestStartTime) : null;
try {
await apiLogService.logRequest({
userId: account.userId,
method: 'GET',
path: myTischtennisUrl.replace('https://www.mytischtennis.de', ''),
statusCode: 0,
requestBody: JSON.stringify({
url: myTischtennisUrl,
myTischtennisTeamId: team.myTischtennisTeamId,
clubTeamId: team.id,
teamName: team.name,
leagueName: team.league?.name,
association: team.league?.association,
groupname: team.league?.groupname,
groupId: team.league?.myTischtennisGroupId
}),
responseBody: null,
executionTime: requestExecutionTime,
errorMessage: error.message || String(error),
logType: 'api_request',
schedulerJobType: 'mytischtennis_fetch'
});
} catch (logError) {
// Silent fail - logging errors shouldn't break the request
}
}
// Normalize HTTP status code (guard against strings)
const rawCode = error && (error.statusCode != null ? error.statusCode : error.status);
const parsed = Number(rawCode);
const status = Number.isInteger(parsed) && parsed >= 100 && parsed <= 599 ? parsed : 500;
const debug = {
message: error.message || String(error),
name: error.name,
stack: (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development') ? (error.stack || null) : undefined,
team: team ? { id: team.id, name: team.name } : null,
league: team && team.league ? { id: team.league.id, name: team.league.name, association: team.league.association, groupId: team.league.myTischtennisGroupId, groupname: team.league.groupname } : null,
url: typeof myTischtennisUrl !== 'undefined' ? myTischtennisUrl : null
};
try {
if (!res.headersSent) {
// Spezieller Fall: myTischtennis-Reauth nötig → nicht 401 an FE senden, um App-Logout zu vermeiden
const isMyTischtennisAuthIssue = status === 401 && /MyTischtennis-Session abgelaufen|Automatischer Login fehlgeschlagen|Passwort gespeichert/i.test(debug.message || '');
if (isMyTischtennisAuthIssue) {
return res.status(200).json({ success: false, error: debug.message, debug, needsMyTischtennisReauth: true });
}
res.status(status).json({ success: false, error: debug.message, debug });
}
} catch (writeErr) {
// Fallback, falls Headers schon gesendet wurden
// eslint-disable-next-line no-console
console.error('[fetchTeamData] Response write failed:', writeErr);
}
}
}
/**
* Get myTischtennis URL for a team
* GET /api/mytischtennis/team-url/:teamId
*/
async getTeamUrl(req, res, next) {
try {
const { teamId } = req.params;
const team = await ClubTeam.findByPk(teamId, {
include: [
{
model: League,
as: 'league',
include: [
{
model: Season,
as: 'season'
}
]
}
]
});
if (!team) {
throw new HttpError('Team not found', 404);
}
if (!team.myTischtennisTeamId || !team.league || !team.league.myTischtennisGroupId) {
throw new HttpError('Team is not configured for myTischtennis', 400);
}
const url = myTischtennisUrlParserService.buildUrl({
association: team.league.association,
season: team.league.season?.season,
groupname: team.league.groupname,
groupId: team.league.myTischtennisGroupId,
teamId: team.myTischtennisTeamId,
teamname: team.name
});
res.json({
success: true,
url
});
} catch (error) {
next(error);
}
}
/**
* Configure league from myTischtennis table URL
* POST /api/mytischtennis/configure-league
* Body: { url: string, createSeason?: boolean }
*/
async configureLeague(req, res, next) {
try {
const { url, createSeason } = req.body;
const userIdOrEmail = req.headers.userid;
if (!url) {
throw new HttpError('URL is required', 400);
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
if (parsedData.urlType !== 'table') {
throw new HttpError('URL must be a table URL (not a team URL)', 400);
}
// Find or create season
let season = await Season.findOne({
where: { season: parsedData.season }
});
if (!season && createSeason) {
season = await Season.create({
season: parsedData.season,
startDate: new Date(),
endDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 Jahr später
});
}
// Find or create league
let league = await League.findOne({
where: {
myTischtennisGroupId: parsedData.groupId,
association: parsedData.association
}
});
if (!league) {
league = await League.create({
name: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version
myTischtennisGroupId: parsedData.groupId,
association: parsedData.association,
groupname: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version
seasonId: season?.id || null
});
} else {
// Update existing league - aber nur wenn es sich wirklich geändert hat
if (league.name !== parsedData.groupnameOriginal) {
league.name = parsedData.groupnameOriginal;
league.groupname = parsedData.groupnameOriginal;
}
league.seasonId = season?.id || league.seasonId;
await league.save();
}
res.json({
success: true,
message: 'League configured successfully',
data: {
league: {
id: league.id,
name: league.name,
myTischtennisGroupId: league.myTischtennisGroupId,
association: league.association,
groupname: league.groupname
},
season: season ? {
id: season.id,
name: season.season
} : null,
parsedData
}
});
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisUrlController();

View File

@@ -233,9 +233,11 @@ export const listOfficialTournaments = async (req, res) => {
const { clubId } = req.params;
await checkAccess(userToken, clubId);
const list = await OfficialTournament.findAll({ where: { clubId } });
res.status(200).json(list);
res.status(200).json(Array.isArray(list) ? list : []);
} catch (e) {
res.status(500).json({ error: 'Failed to list tournaments' });
console.error('[listOfficialTournaments] Error:', e);
const errorMessage = e.message || 'Failed to list tournaments';
res.status(e.statusCode || 500).json({ error: errorMessage });
}
};

View File

@@ -1,10 +1,14 @@
import Participant from '../models/Participant.js';
import DiaryDates from '../models/DiaryDates.js';
import { devLog } from '../utils/logger.js';
import { emitParticipantAdded, emitParticipantRemoved, emitParticipantUpdated } from '../services/socketService.js';
export const getParticipants = async (req, res) => {
try {
const { dateId } = req.params;
const participants = await Participant.findAll({ where: { diaryDateId: dateId } });
const participants = await Participant.findAll({
where: { diaryDateId: dateId },
attributes: ['id', 'diaryDateId', 'memberId', 'groupId', 'notes', 'createdAt', 'updatedAt']
});
res.status(200).json(participants);
} catch (error) {
devLog(error);
@@ -12,10 +16,66 @@ export const getParticipants = async (req, res) => {
}
};
export const updateParticipantGroup = async (req, res) => {
try {
const { dateId, memberId } = req.params;
const { groupId } = req.body;
const participant = await Participant.findOne({
where: {
diaryDateId: dateId,
memberId: memberId
},
include: [{
model: DiaryDates,
as: 'diaryDate',
attributes: ['clubId']
}]
});
if (!participant) {
return res.status(404).json({ error: 'Teilnehmer nicht gefunden' });
}
participant.groupId = groupId || null;
await participant.save();
// Lade den Participant erneut aus der DB, um sicherzustellen, dass wir den aktuellen Wert haben
const updatedParticipant = await Participant.findOne({
where: {
diaryDateId: dateId,
memberId: memberId
},
include: [{
model: DiaryDates,
as: 'diaryDate',
attributes: ['clubId']
}]
});
// Emit Socket-Event mit dem aktualisierten Participant
if (updatedParticipant?.diaryDate?.clubId) {
emitParticipantUpdated(updatedParticipant.diaryDate.clubId, dateId, updatedParticipant);
}
res.status(200).json(updatedParticipant || participant);
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Fehler beim Aktualisieren der Teilnehmer-Gruppenzuordnung' });
}
};
export const addParticipant = async (req, res) => {
try {
const { diaryDateId, memberId } = req.body;
const participant = await Participant.create({ diaryDateId, memberId });
// Hole DiaryDate für clubId
const diaryDate = await DiaryDates.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitParticipantAdded(diaryDate.clubId, diaryDateId, participant);
}
res.status(201).json(participant);
} catch (error) {
devLog(error);
@@ -26,7 +86,18 @@ export const addParticipant = async (req, res) => {
export const removeParticipant = async (req, res) => {
try {
const { diaryDateId, memberId } = req.body;
// Hole DiaryDate für clubId vor dem Löschen
const diaryDate = await DiaryDates.findByPk(diaryDateId);
const clubId = diaryDate?.clubId;
await Participant.destroy({ where: { diaryDateId, memberId } });
// Emit Socket-Event
if (clubId) {
emitParticipantRemoved(clubId, diaryDateId, memberId);
}
res.status(200).json({ message: 'Teilnehmer entfernt' });
} catch (error) {
devLog(error);

View File

@@ -0,0 +1,167 @@
import permissionService from '../services/permissionService.js';
/**
* Get user's permissions for a club
*/
export const getUserPermissions = async (req, res) => {
try {
const { clubId } = req.params;
const userId = req.user.id;
// Validierung: clubId muss eine gültige Zahl sein
const parsedClubId = parseInt(clubId, 10);
if (isNaN(parsedClubId) || parsedClubId <= 0) {
return res.status(400).json({ error: 'Ungültige Club-ID' });
}
const permissions = await permissionService.getUserClubPermissions(userId, parsedClubId);
if (!permissions) {
return res.status(404).json({ error: 'Keine Berechtigungen gefunden' });
}
res.json(permissions);
} catch (error) {
console.error('Error getting user permissions:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Berechtigungen' });
}
};
/**
* Get all club members with their permissions
*/
export const getClubMembersWithPermissions = async (req, res) => {
try {
const { clubId } = req.params;
const userId = req.user.id;
const members = await permissionService.getClubMembersWithPermissions(
parseInt(clubId),
userId
);
res.json(members);
} catch (error) {
console.error('Error getting club members with permissions:', error);
if (error.message === 'Keine Berechtigung zum Anzeigen von Berechtigungen') {
return res.status(403).json({ error: error.message });
}
res.status(500).json({ error: 'Fehler beim Abrufen der Mitglieder' });
}
};
/**
* Update user role
*/
export const updateUserRole = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { role } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setUserRole(
parseInt(targetUserId),
parseInt(clubId),
role,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user role:', error);
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
return res.status(403).json({ error: error.message });
}
res.status(400).json({ error: error.message });
}
};
/**
* Update user custom permissions
*/
export const updateUserPermissions = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { permissions } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setCustomPermissions(
parseInt(targetUserId),
parseInt(clubId),
permissions,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user permissions:', error);
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
return res.status(403).json({ error: error.message });
}
res.status(400).json({ error: error.message });
}
};
/**
* Get available roles
*/
export const getAvailableRoles = async (req, res) => {
try {
const roles = permissionService.getAvailableRoles();
res.json(roles);
} catch (error) {
console.error('Error getting available roles:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Rollen' });
}
};
/**
* Get permission structure
*/
export const getPermissionStructure = async (req, res) => {
try {
const structure = permissionService.getPermissionStructure();
res.json(structure);
} catch (error) {
console.error('Error getting permission structure:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Berechtigungsstruktur' });
}
};
/**
* Update user status (activate/deactivate)
*/
export const updateUserStatus = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { approved } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setUserStatus(
parseInt(targetUserId),
parseInt(clubId),
approved,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user status:', error);
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
return res.status(403).json({ error: error.message });
}
res.status(400).json({ error: error.message });
}
};
export default {
getUserPermissions,
getClubMembersWithPermissions,
updateUserRole,
updateUserPermissions,
updateUserStatus,
getAvailableRoles,
getPermissionStructure
};

View File

@@ -36,7 +36,6 @@ export const uploadPredefinedActivityImage = async (req, res) => {
// Extrahiere Zeichnungsdaten aus dem Request
const drawingData = req.body.drawingData ? JSON.parse(req.body.drawingData) : null;
devLog('[uploadPredefinedActivityImage] - drawingData:', drawingData);
const imageRecord = await PredefinedActivityImage.create({
predefinedActivityId: id,

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,223 @@
import multer from 'multer';
import path from 'path';
import fs from 'fs';
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) => {
try {
fs.mkdirSync('uploads/temp', { recursive: true });
} catch (mkdirError) {
console.error('[multer] - Failed to ensure temp upload directory exists:', mkdirError);
}
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 allowedExtensions = ['.pdf', '.doc', '.docx', '.txt', '.csv'];
const allowedMimePatterns = ['pdf', 'msword', 'wordprocessingml.document', 'text/plain', 'csv', 'excel'];
const extensionValid = allowedExtensions.includes(path.extname(file.originalname).toLowerCase());
const mimetypeValid = allowedMimePatterns.some((pattern) => file.mimetype && file.mimetype.toLowerCase().includes(pattern));
if (extensionValid && mimetypeValid) {
return cb(null, true);
}
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

@@ -1,5 +1,8 @@
// controllers/tournamentController.js
import tournamentService from "../services/tournamentService.js";
import { emitTournamentChanged } from '../services/socketService.js';
import TournamentClass from '../models/TournamentClass.js';
import HttpError from '../exceptions/HttpError.js';
// 1. Alle Turniere eines Vereins
export const getTournaments = async (req, res) => {
@@ -10,6 +13,11 @@ export const getTournaments = async (req, res) => {
res.status(200).json(tournaments);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
res.set('x-debug-tournament-clubid', String(clubId));
res.set('x-debug-tournament-clubid-num', String(Number(clubId)));
return res.status(error.statusCode || 500).json({ error: error.message });
}
res.status(500).json({ error: error.message });
}
};
@@ -17,36 +25,66 @@ export const getTournaments = async (req, res) => {
// 2. Neues Turnier anlegen
export const addTournament = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentName, date } = req.body;
const { clubId, tournamentName, date, winningSets, allowsExternal } = req.body;
try {
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date);
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal);
// Emit Socket-Event
if (clubId && tournament && tournament.id) {
emitTournamentChanged(clubId, tournament.id);
}
res.status(201).json(tournament);
} catch (error) {
console.error(error);
console.error('[addTournament] Error:', error);
res.status(500).json({ error: error.message });
}
};
// 3. Teilnehmer hinzufügen
// 3. Teilnehmer hinzufügen - klassengebunden
export const addParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participant: participantId } = req.body;
const { clubId, classId, participant: participantId, tournamentId } = req.body;
try {
await tournamentService.addParticipant(token, clubId, tournamentId, participantId);
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
// Payloads:
// - Mit Klasse (klassengebunden): { clubId, classId, participant }
// - Ohne Klasse (turnierweit): { clubId, tournamentId, participant, classId: null }
if (!participantId) {
return res.status(400).json({ error: 'Teilnehmer-ID ist erforderlich' });
}
// Allow adding a participant either to a specific class (classId) or to the whole tournament (no class)
if (!classId && !tournamentId) {
return res.status(400).json({ error: 'Klasse oder tournamentId ist erforderlich' });
}
// Pass through to service. If classId is present it will be used, otherwise the service should add the participant with classId = null for the given tournamentId
await tournamentService.addParticipant(token, clubId, classId || null, participantId, tournamentId || null);
// Determine tournamentId for response and event emission
let respTournamentId = tournamentId;
if (classId && !respTournamentId) {
const tournamentClass = await TournamentClass.findByPk(classId);
if (!tournamentClass) {
return res.status(404).json({ error: 'Klasse nicht gefunden' });
}
respTournamentId = tournamentClass.tournamentId;
}
// Fetch updated participants for the (optional) class or whole tournament
const participants = await tournamentService.getParticipants(token, clubId, respTournamentId, classId || null);
// Emit Socket-Event
if (respTournamentId) emitTournamentChanged(clubId, respTournamentId);
res.status(200).json(participants);
} catch (error) {
console.error(error);
console.error('[addParticipant] Error:', error);
res.status(500).json({ error: error.message });
}
};
// 4. Teilnehmerliste abrufen
// 4. Teilnehmerliste abrufen - nach Klasse oder Turnier
export const getParticipants = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
const { clubId, tournamentId, classId } = req.body;
try {
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
const participants = await tournamentService.getParticipants(token, clubId, tournamentId, classId || null);
res.status(200).json(participants);
} catch (error) {
console.error(error);
@@ -60,6 +98,8 @@ export const setModus = async (req, res) => {
const { clubId, tournamentId, type, numberOfGroups, advancingPerGroup } = req.body;
try {
await tournamentService.setModus(token, clubId, tournamentId, type, numberOfGroups, advancingPerGroup);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
@@ -70,9 +110,48 @@ export const setModus = async (req, res) => {
// 6. Gruppen-Strukturen anlegen (leere Gruppen)
export const createGroups = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
const { clubId, tournamentId, numberOfGroups } = req.body;
try {
await tournamentService.createGroups(token, clubId, tournamentId);
// DEBUG: Eingehende Daten sichtbar machen (temporär)
console.log('[tournamentController.createGroups] body:', req.body);
console.log('[tournamentController.createGroups] types:', {
clubId: typeof clubId,
tournamentId: typeof tournamentId,
numberOfGroups: typeof numberOfGroups,
});
// Turniere ohne Klassen: `numberOfGroups: 0` kommt aus der UI (Default) vor.
// Statt „nichts passiert“ normalisieren wir auf mindestens 1 Gruppe.
let normalizedNumberOfGroups = numberOfGroups;
if (normalizedNumberOfGroups !== undefined && normalizedNumberOfGroups !== null) {
const n = Number(normalizedNumberOfGroups);
console.log('[tournamentController.createGroups] parsed numberOfGroups:', n);
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
return res.status(400).json({ error: 'numberOfGroups muss eine ganze Zahl >= 0 sein' });
}
normalizedNumberOfGroups = Math.max(1, n);
}
console.log('[tournamentController.createGroups] normalizedNumberOfGroups:', normalizedNumberOfGroups);
await tournamentService.createGroups(token, clubId, tournamentId, normalizedNumberOfGroups);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
res.status(500).json({ error: error.message });
}
};
// 6b. Gruppen-Strukturen pro Klasse anlegen
export const createGroupsPerClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, groupsPerClass } = req.body;
try {
await tournamentService.createGroupsPerClass(token, clubId, tournamentId, groupsPerClass);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
@@ -86,6 +165,8 @@ export const fillGroups = async (req, res) => {
const { clubId, tournamentId } = req.body;
try {
const updatedMembers = await tournamentService.fillGroups(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(updatedMembers);
} catch (error) {
console.error(error);
@@ -93,6 +174,21 @@ export const fillGroups = async (req, res) => {
}
};
// 7b. Gruppenspiele erstellen ohne Gruppenzuordnungen zu ändern
export const createGroupMatches = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.body;
try {
await tournamentService.createGroupMatches(token, clubId, tournamentId, classId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
res.status(500).json({ error: error.message });
}
};
// 8. Gruppen mit ihren Teilnehmern abfragen
export const getGroups = async (req, res) => {
const { authcode: token } = req.headers;
@@ -119,6 +215,23 @@ export const getTournament = async (req, res) => {
}
};
// Update Turnier
export const updateTournament = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
const { name, date, winningSets } = req.body;
try {
const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournament);
} catch (error) {
console.error('[updateTournament] Error:', error);
const status = error.message.includes('existiert bereits') ? 400 : 500;
res.status(status).json({ error: error.message });
}
};
// 10. Alle Spiele eines Turniers abfragen
export const getTournamentMatches = async (req, res) => {
const { authcode: token } = req.headers;
@@ -138,6 +251,8 @@ export const addMatchResult = async (req, res) => {
const { clubId, tournamentId, matchId, set, result } = req.body;
try {
await tournamentService.addMatchResult(token, clubId, tournamentId, matchId, set, result);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "Result added successfully" });
} catch (error) {
console.error(error);
@@ -151,6 +266,8 @@ export const finishMatch = async (req, res) => {
const { clubId, tournamentId, matchId } = req.body;
try {
await tournamentService.finishMatch(token, clubId, tournamentId, matchId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "Match finished successfully" });
} catch (error) {
console.error(error);
@@ -164,6 +281,8 @@ export const startKnockout = async (req, res) => {
try {
await tournamentService.startKnockout(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "K.o.-Runde erfolgreich gestartet" });
} catch (error) {
const status = /Gruppenmodus|Zu wenige Qualifikanten/.test(error.message) ? 400 : 500;
@@ -190,6 +309,8 @@ export const manualAssignGroups = async (req, res) => {
numberOfGroups, // neu
maxGroupSize // neu
);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(groupsWithParts);
} catch (error) {
console.error('Error in manualAssignGroups:', error);
@@ -197,11 +318,35 @@ export const manualAssignGroups = async (req, res) => {
}
};
export const assignParticipantToGroup = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId, groupNumber, isExternal } = req.body;
try {
const groups = await tournamentService.assignParticipantToGroup(
token,
clubId,
tournamentId,
participantId,
groupNumber,
isExternal || false
);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(groups);
} catch (error) {
console.error('Error in assignParticipantToGroup:', error);
res.status(500).json({ error: error.message });
}
};
export const resetGroups = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
try {
await tournamentService.resetGroups(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (err) {
console.error(err);
@@ -211,9 +356,11 @@ export const resetGroups = async (req, res) => {
export const resetMatches = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
const { clubId, tournamentId, classId } = req.body;
try {
await tournamentService.resetMatches(token, clubId, tournamentId);
await tournamentService.resetMatches(token, clubId, tournamentId, classId || null);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (err) {
console.error(err);
@@ -227,6 +374,8 @@ export const removeParticipant = async (req, res) => {
try {
await tournamentService.removeParticipant(token, clubId, tournamentId, participantId);
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(participants);
} catch (err) {
console.error(err);
@@ -234,6 +383,21 @@ export const removeParticipant = async (req, res) => {
}
};
export const updateParticipantSeeded = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.params;
const { seeded } = req.body;
try {
await tournamentService.updateParticipantSeeded(token, clubId, tournamentId, participantId, seeded);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Gesetzt-Status aktualisiert' });
} catch (err) {
console.error('[updateParticipantSeeded] Error:', err);
res.status(500).json({ error: err.message });
}
};
export const deleteMatchResult = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, matchId, set } = req.body;
@@ -245,6 +409,8 @@ export const deleteMatchResult = async (req, res) => {
matchId,
set
);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Einzelsatz gelöscht' });
} catch (error) {
console.error('Error in deleteMatchResult:', error);
@@ -258,6 +424,8 @@ export const reopenMatch = async (req, res) => {
const { clubId, tournamentId, matchId } = req.body;
try {
await tournamentService.reopenMatch(token, clubId, tournamentId, matchId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
// Gib optional das aktualisierte Match zurück
res.status(200).json({ message: "Match reopened" });
} catch (error) {
@@ -268,13 +436,210 @@ export const reopenMatch = async (req, res) => {
export const deleteKnockoutMatches = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
const { clubId, tournamentId, classId } = req.body;
try {
await tournamentService.resetKnockout(token, clubId, tournamentId);
await tournamentService.resetKnockout(token, clubId, tournamentId, classId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "K.o.-Runde gelöscht" });
} catch (error) {
console.error("Error in deleteKnockoutMatches:", error);
res.status(500).json({ error: error.message });
}
};
export const setMatchActive = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, matchId } = req.params;
const { isActive } = req.body;
try {
await tournamentService.setMatchActive(token, clubId, tournamentId, matchId, isActive);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Match-Status aktualisiert' });
} catch (err) {
console.error('[setMatchActive] Error:', err);
res.status(500).json({ error: err.message });
}
};
// Externe Teilnehmer hinzufügen
export const addExternalParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId, firstName, lastName, club, birthDate, gender } = req.body;
try {
await tournamentService.addExternalParticipant(token, clubId, classId, firstName, lastName, club, birthDate, gender);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Externer Teilnehmer hinzugefügt' });
} catch (error) {
console.error('[addExternalParticipant] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Externe Teilnehmer abrufen - nach Klasse oder Turnier
export const getExternalParticipants = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.body;
try {
const participants = await tournamentService.getExternalParticipants(token, clubId, tournamentId, classId || null);
res.status(200).json(participants);
} catch (error) {
console.error('[getExternalParticipants] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Externe Teilnehmer löschen
export const removeExternalParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.body;
try {
await tournamentService.removeExternalParticipant(token, clubId, tournamentId, participantId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Externer Teilnehmer entfernt' });
} catch (error) {
console.error('[removeExternalParticipant] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Gesetzt-Status für externe Teilnehmer aktualisieren
export const updateExternalParticipantSeeded = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.params;
const { seeded } = req.body;
try {
await tournamentService.updateExternalParticipantSeeded(token, clubId, tournamentId, participantId, seeded);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Gesetzt-Status aktualisiert' });
} catch (error) {
console.error('[updateExternalParticipantSeeded] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Tournament Classes
export const getTournamentClasses = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
try {
const classes = await tournamentService.getTournamentClasses(token, clubId, tournamentId);
res.status(200).json(classes);
} catch (error) {
console.error('[getTournamentClasses] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const addTournamentClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
const { name, isDoubles, gender, minBirthYear } = req.body;
try {
const tournamentClass = await tournamentService.addTournamentClass(token, clubId, tournamentId, name, isDoubles, gender, minBirthYear);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournamentClass);
} catch (error) {
console.error('[addTournamentClass] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const updateTournamentClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.params;
const { name, sortOrder, isDoubles, gender, minBirthYear } = req.body;
try {
console.log('[updateTournamentClass] Request body:', { name, sortOrder, isDoubles, gender, minBirthYear });
const tournamentClass = await tournamentService.updateTournamentClass(token, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear);
console.log('[updateTournamentClass] Updated class:', JSON.stringify(tournamentClass.toJSON(), null, 2));
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournamentClass);
} catch (error) {
console.error('[updateTournamentClass] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const deleteTournamentClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.params;
try {
await tournamentService.deleteTournamentClass(token, clubId, tournamentId, classId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Klasse gelöscht' });
} catch (error) {
console.error('[deleteTournamentClass] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const updateParticipantClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.params;
const { classId, isExternal } = req.body;
try {
await tournamentService.updateParticipantClass(token, clubId, tournamentId, participantId, classId, isExternal);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Klasse aktualisiert' });
} catch (error) {
console.error('[updateParticipantClass] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Tournament Pairings
export const getPairings = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.params;
try {
const pairings = await tournamentService.getPairings(token, clubId, tournamentId, classId);
res.status(200).json(pairings);
} catch (error) {
console.error('[getPairings] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const createPairing = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.params;
const { player1Type, player1Id, player2Type, player2Id, seeded, groupId } = req.body;
try {
const pairing = await tournamentService.createPairing(token, clubId, tournamentId, classId, player1Type, player1Id, player2Type, player2Id, seeded, groupId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(pairing);
} catch (error) {
console.error('[createPairing] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const updatePairing = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, pairingId } = req.params;
const { player1Type, player1Id, player2Type, player2Id, seeded, groupId } = req.body;
try {
const pairing = await tournamentService.updatePairing(token, clubId, tournamentId, pairingId, player1Type, player1Id, player2Type, player2Id, seeded, groupId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(pairing);
} catch (error) {
console.error('[updatePairing] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const deletePairing = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, pairingId } = req.params;
try {
await tournamentService.deletePairing(token, clubId, tournamentId, pairingId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Paarung gelöscht' });
} catch (error) {
console.error('[deletePairing] Error:', error);
res.status(500).json({ error: error.message });
}
};

View File

@@ -0,0 +1,70 @@
import tournamentService from '../services/tournamentService.js';
import HttpError from '../exceptions/HttpError.js';
export const getStages = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.query;
try {
if (clubId == null || tournamentId == null) {
return res.status(400).json({ error: 'clubId und tournamentId sind erforderlich.' });
}
const data = await tournamentService.getTournamentStages(token, Number(clubId), Number(tournamentId));
res.status(200).json(data);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
// Debug-Hilfe: zeigt, welche IDs tatsächlich am Endpoint ankamen (ohne sensible Daten)
res.set('x-debug-stages-clubid', String(clubId));
res.set('x-debug-stages-tournamentid', String(tournamentId));
res.set('x-debug-stages-clubid-num', String(Number(clubId)));
return res.status(error.statusCode || 500).json({ error: error.message });
}
res.status(500).json({ error: error.message });
}
};
export const upsertStages = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, stages, advancement, advancements } = req.body;
try {
const data = await tournamentService.upsertTournamentStages(
token,
Number(clubId),
Number(tournamentId),
stages,
advancement,
advancements
);
res.status(200).json(data);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
res.set('x-debug-stages-clubid', String(clubId));
res.set('x-debug-stages-tournamentid', String(tournamentId));
res.set('x-debug-stages-clubid-num', String(Number(clubId)));
return res.status(error.statusCode || 500).json({ error: error.message });
}
res.status(500).json({ error: error.message });
}
};
export const advanceStage = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, fromStageIndex, toStageIndex } = req.body;
try {
const data = await tournamentService.advanceTournamentStage(
token,
Number(clubId),
Number(tournamentId),
Number(fromStageIndex || 1),
(toStageIndex == null ? null : Number(toStageIndex))
);
res.status(200).json(data);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
return res.status(error.statusCode || 500).json({ error: error.message });
}
res.status(500).json({ error: error.message });
}
};

View File

@@ -0,0 +1,128 @@
import trainingGroupService from '../services/trainingGroupService.js';
import { getSafeErrorMessage } from '../utils/errorUtils.js';
export const getTrainingGroups = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const groups = await trainingGroupService.getTrainingGroups(userToken, clubId);
res.status(200).json(groups);
} catch (error) {
console.error('[getTrainingGroups] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingsgruppen');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const createTrainingGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const { name, sortOrder } = req.body;
const group = await trainingGroupService.createTrainingGroup(userToken, clubId, name, sortOrder);
res.status(201).json(group);
} catch (error) {
console.error('[createTrainingGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Trainingsgruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const updateTrainingGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupId } = req.params;
const { name, sortOrder } = req.body;
const group = await trainingGroupService.updateTrainingGroup(userToken, clubId, groupId, name, sortOrder);
res.status(200).json(group);
} catch (error) {
console.error('[updateTrainingGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Trainingsgruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const deleteTrainingGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupId } = req.params;
await trainingGroupService.deleteTrainingGroup(userToken, clubId, groupId);
res.status(200).json({ success: true });
} catch (error) {
console.error('[deleteTrainingGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Trainingsgruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const addMemberToGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupId, memberId } = req.params;
const memberGroup = await trainingGroupService.addMemberToGroup(userToken, clubId, groupId, memberId);
res.status(201).json(memberGroup);
} catch (error) {
console.error('[addMemberToGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen des Mitglieds zur Gruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const removeMemberFromGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupId, memberId } = req.params;
await trainingGroupService.removeMemberFromGroup(userToken, clubId, groupId, memberId);
res.status(200).json({ success: true });
} catch (error) {
console.error('[removeMemberFromGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen des Mitglieds aus der Gruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const getMemberGroups = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
const groups = await trainingGroupService.getMemberGroups(userToken, clubId, memberId);
res.status(200).json(groups);
} catch (error) {
console.error('[getMemberGroups] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Gruppen des Mitglieds');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const ensurePresetGroups = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const groups = await trainingGroupService.ensurePresetGroups(userToken, clubId);
res.status(200).json({
message: 'Preset-Gruppen wurden erstellt/überprüft',
groups: groups.length
});
} catch (error) {
console.error('[ensurePresetGroups] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Preset-Gruppen');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const enablePresetGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, presetType } = req.params;
const group = await trainingGroupService.enablePresetGroup(userToken, clubId, presetType);
res.status(200).json({
message: 'Preset-Gruppe wurde aktiviert',
group
});
} catch (error) {
console.error('[enablePresetGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Aktivieren der Preset-Gruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};

View File

@@ -0,0 +1,80 @@
import trainingTimeService from '../services/trainingTimeService.js';
import { getSafeErrorMessage } from '../utils/errorUtils.js';
export const getTrainingTimes = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const groups = await trainingTimeService.getTrainingTimes(userToken, clubId);
res.status(200).json(groups);
} catch (error) {
console.error('[getTrainingTimes] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingszeiten');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const createTrainingTime = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const { trainingGroupId, weekday, startTime, endTime } = req.body;
if (!trainingGroupId || weekday === undefined || !startTime || !endTime) {
return res.status(400).json({ error: 'Alle Felder müssen ausgefüllt sein' });
}
const trainingTime = await trainingTimeService.createTrainingTime(
userToken,
clubId,
trainingGroupId,
weekday,
startTime,
endTime
);
res.status(201).json(trainingTime);
} catch (error) {
console.error('[createTrainingTime] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Trainingszeit');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const updateTrainingTime = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, timeId } = req.params;
const { weekday, startTime, endTime } = req.body;
const trainingTime = await trainingTimeService.updateTrainingTime(
userToken,
clubId,
timeId,
weekday,
startTime,
endTime
);
res.status(200).json(trainingTime);
} catch (error) {
console.error('[updateTrainingTime] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Trainingszeit');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const deleteTrainingTime = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, timeId } = req.params;
const result = await trainingTimeService.deleteTrainingTime(userToken, clubId, timeId);
res.status(200).json(result);
} catch (error) {
console.error('[deleteTrainingTime] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Trainingszeit');
res.status(error.statusCode || 500).json({ error: msg });
}
};

View File

@@ -10,7 +10,8 @@ const sequelize = new Sequelize(
host: development.host,
dialect: development.dialect,
define: development.define,
logging: false, // SQL-Logging deaktivieren
logging: development.logging ?? false,
storage: development.storage,
}
);

View File

@@ -1,10 +1,54 @@
/**
* HttpError mit Unterstützung für Fehlercodes
*
* Verwendung:
* - new HttpError('Fehlermeldung', 400) - Legacy, wird weiterhin unterstützt
* - new HttpError({ code: 'ERROR_USER_NOT_FOUND' }, 404) - Mit Fehlercode
* - new HttpError({ code: 'ERROR_MEMBER_NOT_FOUND', params: { memberId: 123 } }, 404) - Mit Parametern
*/
class HttpError extends Error {
constructor(message, statusCode) {
super(message);
constructor(messageOrError, statusCode) {
// Unterstützung für beide Formate:
// 1. Legacy: new HttpError('Fehlermeldung', 400)
// 2. Neu: new HttpError({ code: 'ERROR_CODE', params: {...} }, 400)
if (typeof messageOrError === 'string') {
// Legacy-Format
super(messageOrError);
this.errorCode = null;
this.errorParams = null;
} else if (messageOrError && typeof messageOrError === 'object' && messageOrError.code) {
// Neues Format mit Fehlercode
super(messageOrError.code); // Für Stack-Trace
this.errorCode = messageOrError.code;
this.errorParams = messageOrError.params || null;
} else {
// Fallback
super('Unknown error');
this.errorCode = null;
this.errorParams = null;
}
this.name = this.constructor.name;
this.statusCode = statusCode;
this.statusCode = statusCode || 500;
Error.captureStackTrace(this, this.constructor);
}
/**
* Gibt das Fehler-Objekt für die API-Antwort zurück
* @returns {object} Fehler-Objekt mit code und optional params
*/
toJSON() {
if (this.errorCode) {
return {
code: this.errorCode,
...(this.errorParams && { params: this.errorParams })
};
}
// Legacy: Gib die Nachricht zurück
return {
message: this.message
};
}
}
export default HttpError;

View File

@@ -6,6 +6,9 @@ export const authenticate = async (req, res, next) => {
if (!token) {
token = req.headers['authcode'];
}
if (!token) {
token = req.query?.authcode || req.query?.token || null;
}
if (!token) {
return res.status(401).json({ error: 'Unauthorized: Token fehlt' });
}

View File

@@ -0,0 +1,215 @@
import permissionService from '../services/permissionService.js';
/**
* Authorization Middleware
* Checks if user has permission to access a resource
*/
/**
* Check if user has permission for a specific resource and action
* @param {string} resource - Resource name (diary, members, teams, etc.)
* @param {string} action - Action type (read, write, delete)
* @returns {Function} Express middleware function
*/
export const authorize = (resource, action = 'read') => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
// Get clubId from various possible sources
const clubId =
req.params.clubId ??
req.params.clubid ??
req.params.id ??
req.body.clubId ??
req.body.clubid ??
req.query.clubId ??
req.query.clubid;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
// Check permission
const hasPermission = await permissionService.hasPermission(
userId,
parseInt(clubId),
resource,
action
);
if (!hasPermission) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: `Fehlende Berechtigung: ${resource}.${action}`
});
}
// Store permissions in request for later use
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Authorization error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
/**
* Check if user is club owner
* @returns {Function} Express middleware function
*/
export const requireOwner = () => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const clubId =
req.params.clubId ??
req.params.clubid ??
req.params.id ??
req.body.clubId ??
req.body.clubid ??
req.query.clubId ??
req.query.clubid;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
if (!userPermissions || !userPermissions.isOwner) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: 'Nur der Club-Ersteller hat Zugriff'
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Owner check error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
/**
* Check if user is admin (owner or admin role)
* @returns {Function} Express middleware function
*/
export const requireAdmin = () => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const clubId =
req.params.clubId ??
req.params.clubid ??
req.params.id ??
req.body.clubId ??
req.body.clubid ??
req.query.clubId ??
req.query.clubid;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
if (!userPermissions || (userPermissions.role !== 'admin' && !userPermissions.isOwner)) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: 'Administrator-Rechte erforderlich'
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Admin check error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
/**
* Check if user has any of the specified roles
* @param {string[]} roles - Array of allowed roles
* @returns {Function} Express middleware function
*/
export const requireRole = (roles) => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const clubId =
req.params.clubId ??
req.params.clubid ??
req.params.id ??
req.body.clubId ??
req.body.clubid ??
req.query.clubId ??
req.query.clubid;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
if (!userPermissions || !roles.includes(userPermissions.role)) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: `Erforderliche Rolle: ${roles.join(', ')}`
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Role check error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
export default {
authorize,
requireOwner,
requireAdmin,
requireRole
};

View File

@@ -0,0 +1,13 @@
/**
* Middleware to log all API requests and responses
* Should be added early in the middleware chain, but after authentication
*
* HINWEIS: Logging wurde deaktiviert - keine API-Requests werden mehr geloggt
* (früher wurden nur MyTischtennis-Requests geloggt, dies wurde entfernt)
*/
export const requestLoggingMiddleware = async (req, res, next) => {
// Logging wurde deaktiviert - keine API-Requests werden mehr geloggt
// (früher wurden nur MyTischtennis-Requests geloggt, dies wurde entfernt)
next();
};

View File

@@ -0,0 +1,17 @@
-- Create table for storing multiple images per member
CREATE TABLE IF NOT EXISTS `member_image` (
`id` INT NOT NULL AUTO_INCREMENT,
`member_id` INT NOT NULL,
`file_name` VARCHAR(255) NOT NULL,
`sort_order` INT NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_member_image_member_id` (`member_id`),
CONSTRAINT `fk_member_image_member`
FOREIGN KEY (`member_id`)
REFERENCES `member` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,58 @@
-- Adds multi-stage tournaments (rounds) support
-- MariaDB/MySQL compatible migration (manual execution)
-- 1) New table: tournament_stage
CREATE TABLE IF NOT EXISTS tournament_stage (
id INT NOT NULL AUTO_INCREMENT,
tournament_id INT NOT NULL,
stage_index INT NOT NULL,
name VARCHAR(255) NULL,
type VARCHAR(32) NOT NULL, -- 'groups' | 'knockout'
number_of_groups INT NULL,
advancing_per_group INT NULL,
max_group_size INT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT fk_tournament_stage_tournament
FOREIGN KEY (tournament_id) REFERENCES tournament(id)
ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE INDEX idx_tournament_stage_tournament_id ON tournament_stage (tournament_id);
CREATE UNIQUE INDEX uq_tournament_stage_tournament_id_index ON tournament_stage (tournament_id, stage_index);
-- 2) New table: tournament_stage_advancement
CREATE TABLE IF NOT EXISTS tournament_stage_advancement (
id INT NOT NULL AUTO_INCREMENT,
tournament_id INT NOT NULL,
from_stage_id INT NOT NULL,
to_stage_id INT NOT NULL,
mode VARCHAR(32) NOT NULL DEFAULT 'pools',
config JSON NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT fk_tournament_stage_adv_tournament
FOREIGN KEY (tournament_id) REFERENCES tournament(id)
ON DELETE CASCADE,
CONSTRAINT fk_tournament_stage_adv_from
FOREIGN KEY (from_stage_id) REFERENCES tournament_stage(id)
ON DELETE CASCADE,
CONSTRAINT fk_tournament_stage_adv_to
FOREIGN KEY (to_stage_id) REFERENCES tournament_stage(id)
ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE INDEX idx_tournament_stage_adv_tournament_id ON tournament_stage_advancement (tournament_id);
CREATE INDEX idx_tournament_stage_adv_from_stage_id ON tournament_stage_advancement (from_stage_id);
CREATE INDEX idx_tournament_stage_adv_to_stage_id ON tournament_stage_advancement (to_stage_id);
-- 3) Add stage_id to tournament_group and tournament_match
-- MariaDB has no IF NOT EXISTS for columns; run each ALTER once.
-- If you rerun, comment out the ALTERs or check INFORMATION_SCHEMA first.
ALTER TABLE tournament_group ADD COLUMN stage_id INT NULL;
ALTER TABLE tournament_match ADD COLUMN stage_id INT NULL;
CREATE INDEX idx_tournament_group_tournament_stage ON tournament_group (tournament_id, stage_id);
CREATE INDEX idx_tournament_match_tournament_stage ON tournament_match (tournament_id, stage_id);

View File

@@ -0,0 +1,16 @@
-- Allow NULL placeholders for KO (e.g. "Spiel um Platz 3")
-- MariaDB/MySQL manual migration
--
-- Background: We create placeholder matches with player1_id/player2_id = NULL.
-- Some prod DBs still have NOT NULL on these columns.
-- 1) Make player columns nullable
ALTER TABLE tournament_match MODIFY COLUMN player1_id INT NULL;
ALTER TABLE tournament_match MODIFY COLUMN player2_id INT NULL;
-- 2) (Optional) If you have foreign keys to tournament_member/external participant IDs,
-- ensure they also allow NULL. (Not adding here because not all installations have FKs.)
-- 3) Verify
-- SHOW COLUMNS FROM tournament_match LIKE 'player1_id';
-- SHOW COLUMNS FROM tournament_match LIKE 'player2_id';

View File

@@ -0,0 +1,77 @@
# Liste aller Tabellen im Trainingstagebuch-Projekt
## Basis-Tabellen
1. `user` - Benutzer
2. `user_club` - Verknüpfung Benutzer ↔ Verein
3. `user_token` - Authentifizierungs-Tokens
4. `clubs` - Vereine
5. `log` - System-Logs
## Mitglieder-Verwaltung
6. `member` - Mitglieder
7. `member_contact` - Kontaktdaten der Mitglieder (Telefon, E-Mail)
8. `member_image` - Bilder der Mitglieder
9. `member_notes` - Notizen zu Mitgliedern
10. `member_transfer_config` - Konfiguration für Mitgliederübertragung
## Trainingsgruppen (NEU)
11. `training_group` - Trainingsgruppen
12. `member_training_group` - Verknüpfung Mitglied ↔ Trainingsgruppe
13. `club_disabled_preset_groups` - Deaktivierte Preset-Gruppen pro Verein
14. `training_times` - Trainingszeiten pro Gruppe (NEU)
## Tagebuch
15. `diary_dates` - Trainingstage
16. `participants` - Teilnehmer an Trainingstagen
17. `activities` - Aktivitäten
18. `diary_notes` - Notizen zu Trainingstagen
19. `diary_tags` - Tags für Tagebuch
20. `member_diary_tags` - Verknüpfung Mitglied ↔ Tagebuch-Tag
21. `diary_date_tags` - Verknüpfung Trainingstag ↔ Tag
22. `diary_member_notes` - Notizen zu Mitgliedern an Trainingstagen
23. `diary_member_tags` - Tags für Mitglieder an Trainingstagen
24. `diary_date_activities` - Aktivitäten an Trainingstagen
25. `diary_member_activities` - Verknüpfung Teilnehmer ↔ Aktivität
26. `group` - Gruppen (für Trainingsplan)
27. `group_activity` - Gruppenaktivitäten
## Vordefinierte Aktivitäten
28. `predefined_activities` - Vordefinierte Aktivitäten
29. `predefined_activity_images` - Bilder zu vordefinierten Aktivitäten
## Unfälle
30. `accident` - Unfälle
## Teams & Ligen
31. `season` - Saisons
32. `league` - Ligen
33. `team` - Teams
34. `club_team` - Verknüpfung Verein ↔ Team
35. `team_document` - Dokumente zu Teams
36. `match` - Spiele
37. `location` - Spielorte
## Turniere
38. `tournament` - Turniere
39. `tournament_class` - Turnierklassen
40. `tournament_group` - Turniergruppen
41. `tournament_member` - Teilnehmer an Turnieren
42. `tournament_match` - Spiele in Turnieren
43. `tournament_result` - Ergebnisse von Turnierspielen
44. `external_tournament_participant` - Externe Teilnehmer an Turnieren
## Offizielle Turniere (myTischtennis)
45. `official_tournaments` - Offizielle Turniere
46. `official_competitions` - Wettbewerbe in offiziellen Turnieren
47. `official_competition_members` - Teilnehmer an offiziellen Wettbewerben
## myTischtennis Integration
48. `my_tischtennis` - myTischtennis-Verbindungen
49. `my_tischtennis_update_history` - Update-Historie
50. `my_tischtennis_fetch_log` - Fetch-Logs
## API & Logging
51. `api_log` - API-Logs
## Gesamt: 51 Tabellen

View File

@@ -0,0 +1,22 @@
-- Migration: Add 'allows_external' column to tournament table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament';
SET @columnname = 'allows_external';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `winning_sets`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,13 @@
-- Migration: Add auto update ratings fields to my_tischtennis table
-- Date: 2025-01-27
-- Add auto_update_ratings column
ALTER TABLE my_tischtennis
ADD COLUMN auto_update_ratings BOOLEAN NOT NULL DEFAULT FALSE;
-- Add last_update_ratings column
ALTER TABLE my_tischtennis
ADD COLUMN last_update_ratings TIMESTAMP NULL;
-- Create index for auto_update_ratings for efficient querying
CREATE INDEX idx_my_tischtennis_auto_update_ratings ON my_tischtennis(auto_update_ratings);

View File

@@ -0,0 +1,27 @@
-- Migration: Add 'class_id' column to external_tournament_participant table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'external_tournament_participant';
SET @columnname = 'class_id';
-- Check if column exists
SET @column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
);
-- Add column if it doesn't exist
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `external_tournament_participant` ADD COLUMN `class_id` INT(11) NULL AFTER `seeded`',
'SELECT 1 AS column_already_exists'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,27 @@
-- Migration: Add 'class_id' column to tournament_group table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_group';
SET @columnname = 'class_id';
-- Check if column exists
SET @column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
);
-- Add column if it doesn't exist
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `tournament_group` ADD COLUMN `class_id` INT(11) NULL AFTER `tournament_id`',
'SELECT 1 AS column_already_exists'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,27 @@
-- Migration: Add 'class_id' column to tournament_match table
-- Date: 2025-01-16
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_match';
SET @columnname = 'class_id';
-- Check if column exists
SET @column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
);
-- Add column if it doesn't exist
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `tournament_match` ADD COLUMN `class_id` INT(11) NULL AFTER `group_id`',
'SELECT 1 AS column_already_exists'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,22 @@
-- Migration: Add 'class_id' column to tournament_member table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_member';
SET @columnname = 'class_id';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` INT(11) NULL AFTER `seeded`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,8 @@
-- Migration: Geschlecht zu externen Turnierteilnehmern hinzufügen
-- Datum: 2025-01-XX
ALTER TABLE `external_tournament_participant`
ADD COLUMN `gender` ENUM('male', 'female', 'diverse', 'unknown') NULL DEFAULT 'unknown' AFTER `birth_date`;

View File

@@ -0,0 +1,8 @@
-- Migration: Geschlecht zu Turnierklassen hinzufügen
-- Datum: 2025-01-XX
ALTER TABLE `tournament_class`
ADD COLUMN `gender` ENUM('male', 'female', 'mixed') NULL DEFAULT NULL AFTER `is_doubles`;

View File

@@ -0,0 +1,5 @@
ALTER TABLE clubs
ADD COLUMN IF NOT EXISTS greeting_text TEXT NULL,
ADD COLUMN IF NOT EXISTS association_member_number VARCHAR(255) NULL;

View File

@@ -0,0 +1,9 @@
-- Migration: Add group_id to participants table
-- This allows assigning participants to groups for training organization
ALTER TABLE participants
ADD COLUMN group_id INTEGER NULL REFERENCES "group"(id) ON DELETE SET NULL ON UPDATE CASCADE;
-- Add index for better query performance
CREATE INDEX IF NOT EXISTS idx_participants_group_id ON participants(group_id);

View File

@@ -0,0 +1,22 @@
-- Migration: Add 'is_active' column to tournament_match table
-- Date: 2025-01-14
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_match';
SET @columnname = 'is_active';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `is_finished`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,7 @@
-- Migration: Add is_doubles column to tournament_class table
-- Date: 2025-01-23
-- For MariaDB/MySQL
ALTER TABLE `tournament_class`
ADD COLUMN `is_doubles` TINYINT(1) NOT NULL DEFAULT 0 AFTER `sort_order`;

View File

@@ -0,0 +1,28 @@
-- Migration: Add match result fields to match table
-- Date: 2025-01-27
-- For MariaDB
-- Add myTischtennis meeting ID
ALTER TABLE `match`
ADD COLUMN my_tischtennis_meeting_id VARCHAR(255) NULL UNIQUE COMMENT 'Meeting ID from myTischtennis (e.g. 15440488)';
-- Add home match points
ALTER TABLE `match`
ADD COLUMN home_match_points INT DEFAULT 0 NULL COMMENT 'Match points won by home team';
-- Add guest match points
ALTER TABLE `match`
ADD COLUMN guest_match_points INT DEFAULT 0 NULL COMMENT 'Match points won by guest team';
-- Add is_completed flag
ALTER TABLE `match`
ADD COLUMN is_completed BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Whether the match is completed';
-- Add PDF URL
ALTER TABLE `match`
ADD COLUMN pdf_url VARCHAR(512) NULL COMMENT 'PDF URL from myTischtennis';
-- Create indexes
CREATE INDEX idx_match_my_tischtennis_meeting_id ON `match`(my_tischtennis_meeting_id);
CREATE INDEX idx_match_is_completed ON `match`(is_completed);

View File

@@ -0,0 +1,4 @@
-- Add matches_tied column to team table
ALTER TABLE team
ADD COLUMN matches_tied INTEGER NOT NULL DEFAULT 0 AFTER matches_lost;

View File

@@ -0,0 +1,27 @@
-- Migration: Geburtsjahr-Beschränkung zu Turnierklassen hinzufügen
-- Datum: 2025-01-XX
-- Beschreibung: Fügt max_birth_year Feld hinzu für "geboren im Jahr X oder früher" (<=)
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_class';
SET @columnname = 'max_birth_year';
-- Check if column exists
SET @column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
);
-- Add column if it doesn't exist
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `tournament_class` ADD COLUMN `max_birth_year` INT(11) NULL DEFAULT NULL AFTER `gender`',
'SELECT 1 AS column_already_exists'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,57 @@
-- Add postal_code column to member table
ALTER TABLE `member`
ADD COLUMN `postal_code` TEXT NULL COMMENT 'Postal code (PLZ)' AFTER `city`;
-- Create member_contact table for multiple phone numbers and email addresses
CREATE TABLE IF NOT EXISTS `member_contact` (
`id` INT NOT NULL AUTO_INCREMENT,
`member_id` INT NOT NULL,
`type` ENUM('phone', 'email') NOT NULL COMMENT 'Type of contact: phone or email',
`value` TEXT NOT NULL,
`is_parent` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Whether this contact belongs to a parent',
`parent_name` TEXT NULL COMMENT 'Name of the parent (e.g. "Mutter", "Vater", "Elternteil 1")',
`is_primary` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Whether this is the primary contact of this type',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_member_id` (`member_id`),
INDEX `idx_type` (`type`),
CONSTRAINT `fk_member_contact_member`
FOREIGN KEY (`member_id`)
REFERENCES `member` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Migrate existing phone numbers from member.phone to member_contact
INSERT INTO `member_contact` (`member_id`, `type`, `value`, `is_parent`, `parent_name`, `is_primary`, `created_at`, `updated_at`)
SELECT
`id` AS `member_id`,
'phone' AS `type`,
`phone` AS `value`,
FALSE AS `is_parent`,
NULL AS `parent_name`,
TRUE AS `is_primary`,
NOW() AS `created_at`,
NOW() AS `updated_at`
FROM `member`
WHERE `phone` IS NOT NULL
AND `phone` != ''
AND TRIM(`phone`) != '';
-- Migrate existing email addresses from member.email to member_contact
INSERT INTO `member_contact` (`member_id`, `type`, `value`, `is_parent`, `parent_name`, `is_primary`, `created_at`, `updated_at`)
SELECT
`id` AS `member_id`,
'email' AS `type`,
`email` AS `value`,
FALSE AS `is_parent`,
NULL AS `parent_name`,
TRUE AS `is_primary`,
NOW() AS `created_at`,
NOW() AS `updated_at`
FROM `member`
WHERE `email` IS NOT NULL
AND `email` != ''
AND TRIM(`email`) != '';

View File

@@ -0,0 +1,19 @@
-- Migration: Add myTischtennis fields to league table
-- Date: 2025-01-27
-- For MariaDB
-- Add my_tischtennis_group_id column
ALTER TABLE league
ADD COLUMN my_tischtennis_group_id VARCHAR(255) NULL COMMENT 'Group ID from myTischtennis (e.g. 504417)';
-- Add association column
ALTER TABLE league
ADD COLUMN association VARCHAR(255) NULL COMMENT 'Association/Verband (e.g. HeTTV)';
-- Add groupname column
ALTER TABLE league
ADD COLUMN groupname VARCHAR(255) NULL COMMENT 'Group name for URL (e.g. 1.Kreisklasse)';
-- Create index for efficient querying
CREATE INDEX idx_league_my_tischtennis_group_id ON league(my_tischtennis_group_id);

View File

@@ -0,0 +1,11 @@
-- Migration: Add myTischtennis player ID to member table
-- Date: 2025-01-27
-- For MariaDB
-- Add my_tischtennis_player_id column
ALTER TABLE member
ADD COLUMN my_tischtennis_player_id VARCHAR(255) NULL COMMENT 'Player ID from myTischtennis (e.g. NU2705037)';
-- Create index for efficient querying
CREATE INDEX idx_member_my_tischtennis_player_id ON member(my_tischtennis_player_id);

View File

@@ -0,0 +1,11 @@
-- Migration: Add myTischtennis team ID to club_team table
-- Date: 2025-01-27
-- For MariaDB
-- Add my_tischtennis_team_id column
ALTER TABLE club_team
ADD COLUMN my_tischtennis_team_id VARCHAR(255) NULL COMMENT 'Team ID from myTischtennis (e.g. 2995094)';
-- Create index for efficient querying
CREATE INDEX idx_club_team_my_tischtennis_team_id ON club_team(my_tischtennis_team_id);

View File

@@ -0,0 +1,29 @@
-- Migration: Add name column to tournament table
-- Date: 2025-01-13
-- For MariaDB/MySQL
-- Add name column if it doesn't exist
-- Check if column exists and add it if not
SET @dbname = DATABASE();
SET @tablename = 'tournament';
SET @columnname = 'name';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` VARCHAR(255) NOT NULL DEFAULT "" AFTER `id`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
-- Update existing tournaments: set name to formatted date if name is empty
UPDATE `tournament`
SET `name` = DATE_FORMAT(`date`, '%d.%m.%Y')
WHERE `name` = '' OR `name` IS NULL;

View File

@@ -0,0 +1,17 @@
-- Add role and permissions columns to user_club table
ALTER TABLE `user_club`
ADD COLUMN `role` VARCHAR(50) DEFAULT 'member' COMMENT 'User role: admin, trainer, team_manager, member' AFTER `approved`,
ADD COLUMN `permissions` JSON NULL COMMENT 'Specific permissions: {diary: {read: true, write: true}, members: {...}, ...}' AFTER `role`,
ADD COLUMN `is_owner` BOOLEAN DEFAULT FALSE COMMENT 'True if user created the club' AFTER `permissions`;
-- Create index for faster role lookups
CREATE INDEX `idx_user_club_role` ON `user_club` (`role`);
CREATE INDEX `idx_user_club_owner` ON `user_club` (`is_owner`);
-- Set existing approved users as members
UPDATE `user_club` SET `role` = 'member' WHERE `approved` = 1 AND `role` IS NULL;
-- If there's a user who created the club (we need to identify them somehow)
-- For now, we'll need to manually set the owner after migration

View File

@@ -0,0 +1,8 @@
-- Add player tracking fields to match table
-- These fields store arrays of member IDs for different participation states
ALTER TABLE `match`
ADD COLUMN `players_ready` JSON NULL COMMENT 'Array of member IDs who are ready to play' AFTER `pdf_url`,
ADD COLUMN `players_planned` JSON NULL COMMENT 'Array of member IDs who are planned to play' AFTER `players_ready`,
ADD COLUMN `players_played` JSON NULL COMMENT 'Array of member IDs who actually played' AFTER `players_planned`;

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,24 @@
-- Migration: Add seeded column to tournament_member table
-- Date: 2025-01-13
-- For MariaDB/MySQL
-- Add seeded column if it doesn't exist
-- Check if column exists and add it if not
SET @dbname = DATABASE();
SET @tablename = 'tournament_member';
SET @columnname = 'seeded';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `club_member_id`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,11 @@
-- Migration: Add table fields to team table
-- Add fields for league table calculations
ALTER TABLE team ADD COLUMN matches_played INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN matches_won INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN matches_lost INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN sets_won INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN sets_lost INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN points_won INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN points_lost INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN table_points INT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,5 @@
-- Add table_points_won and table_points_lost columns to team table
ALTER TABLE team
ADD COLUMN table_points_won INTEGER NOT NULL DEFAULT 0 AFTER table_points,
ADD COLUMN table_points_lost INTEGER NOT NULL DEFAULT 0 AFTER table_points_won;

View File

@@ -0,0 +1,22 @@
-- Migration: Add 'winning_sets' column to tournament table
-- Date: 2025-01-14
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament';
SET @columnname = 'winning_sets';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` INT NOT NULL DEFAULT 3 AFTER `advancing_per_group`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,41 @@
-- Migration: Change 'ttr' column to 'birth_date' in external_tournament_participant table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'external_tournament_participant';
SET @oldcolumnname = 'ttr';
SET @newcolumnname = 'birth_date';
-- Check if old column exists
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @oldcolumnname)
) > 0,
CONCAT('ALTER TABLE `', @tablename, '` CHANGE COLUMN `', @oldcolumnname, '` `', @newcolumnname, '` VARCHAR(255) NULL AFTER `club`'),
'SELECT 1'
));
PREPARE alterIfExists FROM @preparedStatement;
EXECUTE alterIfExists;
DEALLOCATE PREPARE alterIfExists;
-- If old column didn't exist, check if new column exists and add it if not
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @newcolumnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @newcolumnname, '` VARCHAR(255) NULL AFTER `club`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,62 @@
-- Diagnose-Skript: Prüfe Seasons und Teams auf dem Server
-- Führe diese Queries auf dem Server aus, um das Problem zu identifizieren
-- 1. Prüfe, ob die season-Tabelle existiert und Daten enthält
SELECT '=== SEASONS ===' as info;
SELECT * FROM `season` ORDER BY `id` DESC;
-- 2. Prüfe, ob die club_team-Tabelle existiert und welche season_id verwendet wird
SELECT '=== CLUB_TEAMS ===' as info;
SELECT
id,
name,
club_id,
season_id,
league_id,
created_at,
updated_at
FROM `club_team`
ORDER BY `id`;
-- 3. Prüfe, ob es Teams gibt, die auf nicht-existierende Seasons verweisen
SELECT '=== TEAMS MIT FEHLENDEN SEASONS ===' as info;
SELECT
ct.id,
ct.name,
ct.season_id,
s.season
FROM `club_team` ct
LEFT JOIN `season` s ON ct.season_id = s.id
WHERE s.id IS NULL;
-- 4. Prüfe, ob es Teams gibt, die keine season_id haben
SELECT '=== TEAMS OHNE SEASON_ID ===' as info;
SELECT
id,
name,
club_id,
season_id
FROM `club_team`
WHERE season_id IS NULL;
-- 5. Prüfe die Struktur der club_team-Tabelle
SELECT '=== CLUB_TEAM TABELLENSTRUKTUR ===' as info;
DESCRIBE `club_team`;
-- 6. Prüfe die Struktur der season-Tabelle
SELECT '=== SEASON TABELLENSTRUKTUR ===' as info;
DESCRIBE `season`;
-- 7. Prüfe Foreign Key Constraints
SELECT '=== FOREIGN KEYS ===' as info;
SELECT
CONSTRAINT_NAME,
TABLE_NAME,
COLUMN_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND (TABLE_NAME = 'club_team' OR TABLE_NAME = 'season')
AND REFERENCED_TABLE_NAME IS NOT NULL;

View File

@@ -0,0 +1,30 @@
-- Vereinfachtes Diagnose-Skript: Prüfe nur die wichtigsten Punkte
-- 1. Gibt es Seasons in der Datenbank?
SELECT 'SEASONS:' as check_type, COUNT(*) as count FROM `season`;
SELECT * FROM `season` ORDER BY `id` DESC;
-- 2. Gibt es Teams in der Datenbank?
SELECT 'CLUB_TEAMS:' as check_type, COUNT(*) as count FROM `club_team`;
SELECT id, name, club_id, season_id, league_id FROM `club_team` ORDER BY `id`;
-- 3. Haben alle Teams eine season_id?
SELECT 'TEAMS OHNE SEASON_ID:' as check_type, COUNT(*) as count
FROM `club_team` WHERE season_id IS NULL;
-- 4. Verweisen alle Teams auf existierende Seasons?
SELECT 'TEAMS MIT FEHLENDEN SEASONS:' as check_type, COUNT(*) as count
FROM `club_team` ct
LEFT JOIN `season` s ON ct.season_id = s.id
WHERE s.id IS NULL;
-- 5. Welche season_id verwenden die Teams?
SELECT 'SEASON_ID VERWENDUNG:' as check_type, season_id, COUNT(*) as team_count
FROM `club_team`
GROUP BY season_id;
-- 6. Welche Seasons existieren?
SELECT 'EXISTIERENDE SEASONS:' as check_type, id, season
FROM `season`
ORDER BY id;

View File

@@ -0,0 +1,26 @@
-- Migration: Create api_log table for comprehensive request/response and execution logging
CREATE TABLE IF NOT EXISTS api_log (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
method VARCHAR(10) NOT NULL COMMENT 'HTTP method (GET, POST, PUT, DELETE, etc.)',
path VARCHAR(500) NOT NULL COMMENT 'Request path',
status_code INT NULL COMMENT 'HTTP status code',
request_body TEXT NULL COMMENT 'Request body (truncated if too long)',
response_body TEXT NULL COMMENT 'Response body (truncated if too long)',
execution_time INT NULL COMMENT 'Execution time in milliseconds',
error_message TEXT NULL COMMENT 'Error message if request failed',
ip_address VARCHAR(45) NULL COMMENT 'Client IP address',
user_agent VARCHAR(500) NULL COMMENT 'User agent string',
log_type ENUM('api_request', 'scheduler', 'cron_job', 'manual') NOT NULL DEFAULT 'api_request' COMMENT 'Type of log entry',
scheduler_job_type VARCHAR(50) NULL COMMENT 'Type of scheduler job (rating_updates, match_results, etc.)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE SET NULL ON UPDATE CASCADE,
INDEX idx_api_log_user_id (user_id, created_at),
INDEX idx_api_log_path (path, created_at),
INDEX idx_api_log_log_type (log_type, created_at),
INDEX idx_api_log_created_at (created_at),
INDEX idx_api_log_status_code (status_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,17 @@
-- Migration: Create club_disabled_preset_groups table
-- Date: 2025-01-16
-- For MariaDB/MySQL
-- Stores which preset groups are disabled for each club
CREATE TABLE IF NOT EXISTS `club_disabled_preset_groups` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`club_id` INT(11) NOT NULL,
`preset_type` ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe') NOT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_club_preset_type` (`club_id`, `preset_type`),
KEY `club_id` (`club_id`),
CONSTRAINT `club_disabled_preset_groups_ibfk_1` FOREIGN KEY (`club_id`) REFERENCES `clubs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,22 @@
-- Migration: Create external_tournament_participant table
-- Date: 2025-01-15
-- For MariaDB/MySQL
CREATE TABLE IF NOT EXISTS `external_tournament_participant` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`tournament_id` INT(11) NOT NULL,
`group_id` INT(11) NULL,
`first_name` VARCHAR(255) NOT NULL,
`last_name` VARCHAR(255) NOT NULL,
`club` VARCHAR(255) NULL,
`birth_date` VARCHAR(255) NULL,
`seeded` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
INDEX `idx_tournament_id` (`tournament_id`),
INDEX `idx_group_id` (`group_id`),
CONSTRAINT `fk_external_participant_tournament` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_external_participant_group` FOREIGN KEY (`group_id`) REFERENCES `tournament_group` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,20 @@
-- Create my_tischtennis_fetch_log table for tracking data fetches
CREATE TABLE IF NOT EXISTS my_tischtennis_fetch_log (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
fetch_type ENUM('ratings', 'match_results', 'league_table') NOT NULL COMMENT 'Type of data fetch',
success BOOLEAN NOT NULL DEFAULT FALSE,
message TEXT,
error_details TEXT,
records_processed INT NOT NULL DEFAULT 0 COMMENT 'Number of records processed',
execution_time INT COMMENT 'Execution time in milliseconds',
is_automatic BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Automatic or manual fetch',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
INDEX idx_user_fetch_type_created (user_id, fetch_type, created_at),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,23 @@
-- Migration: Create my_tischtennis_update_history table
-- Date: 2025-01-27
-- For MariaDB
CREATE TABLE IF NOT EXISTS my_tischtennis_update_history (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
success BOOLEAN NOT NULL DEFAULT FALSE,
message TEXT,
error_details TEXT,
updated_count INT DEFAULT 0,
execution_time INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_my_tischtennis_update_history_user_id
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create indexes for efficient querying
CREATE INDEX idx_my_tischtennis_update_history_user_id ON my_tischtennis_update_history(user_id);
CREATE INDEX idx_my_tischtennis_update_history_created_at ON my_tischtennis_update_history(created_at);
CREATE INDEX idx_my_tischtennis_update_history_success ON my_tischtennis_update_history(success);

View File

@@ -0,0 +1,16 @@
-- Migration: Create tournament_class table
-- Date: 2025-01-15
-- For MariaDB/MySQL
CREATE TABLE IF NOT EXISTS `tournament_class` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`tournament_id` INT(11) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`sort_order` INT(11) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `tournament_id` (`tournament_id`),
CONSTRAINT `tournament_class_ibfk_1` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,33 @@
-- Migration: Create tournament_pairing table
-- Date: 2025-01-23
-- For MariaDB/MySQL
CREATE TABLE IF NOT EXISTS `tournament_pairing` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`tournament_id` INT(11) NOT NULL,
`class_id` INT(11) NOT NULL,
`group_id` INT(11) NULL,
`member1_id` INT(11) NULL,
`external1_id` INT(11) NULL,
`member2_id` INT(11) NULL,
`external2_id` INT(11) NULL,
`seeded` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `tournament_id` (`tournament_id`),
KEY `class_id` (`class_id`),
KEY `group_id` (`group_id`),
KEY `member1_id` (`member1_id`),
KEY `member2_id` (`member2_id`),
KEY `external1_id` (`external1_id`),
KEY `external2_id` (`external2_id`),
CONSTRAINT `tournament_pairing_ibfk_1` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `tournament_pairing_ibfk_2` FOREIGN KEY (`class_id`) REFERENCES `tournament_class` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `tournament_pairing_ibfk_3` FOREIGN KEY (`group_id`) REFERENCES `tournament_group` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,36 @@
-- Migration: Create training_group and member_training_group tables
-- Date: 2025-01-16
-- For MariaDB/MySQL
-- Create training_group table
CREATE TABLE IF NOT EXISTS `training_group` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`club_id` INT(11) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`is_preset` TINYINT(1) NOT NULL DEFAULT 0,
`preset_type` ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe') NULL,
`sort_order` INT(11) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `club_id` (`club_id`),
CONSTRAINT `training_group_ibfk_1` FOREIGN KEY (`club_id`) REFERENCES `clubs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create member_training_group junction table
CREATE TABLE IF NOT EXISTS `member_training_group` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`member_id` INT(11) NOT NULL,
`training_group_id` INT(11) NOT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_member_group` (`member_id`, `training_group_id`),
KEY `member_id` (`member_id`),
KEY `training_group_id` (`training_group_id`),
CONSTRAINT `member_training_group_ibfk_1` FOREIGN KEY (`member_id`) REFERENCES `member` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `member_training_group_ibfk_2` FOREIGN KEY (`training_group_id`) REFERENCES `training_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,19 @@
-- Migration: Create training_times table
-- Date: 2025-01-16
-- For MariaDB/MySQL
-- Stores training times for training groups
CREATE TABLE IF NOT EXISTS `training_times` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`training_group_id` INT(11) NOT NULL,
`weekday` TINYINT(1) NOT NULL COMMENT '0 = Sunday, 1 = Monday, ..., 6 = Saturday',
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`sort_order` INT(11) NOT NULL DEFAULT 0 COMMENT 'Order for displaying multiple times on the same weekday',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `training_group_id` (`training_group_id`),
CONSTRAINT `training_times_ibfk_1` FOREIGN KEY (`training_group_id`) REFERENCES `training_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,92 @@
-- Fix-Skript: Behebt häufige Probleme mit Seasons und Teams
-- Führe dieses Skript auf dem Server aus, wenn die Diagnose Probleme zeigt
-- 1. Stelle sicher, dass die season-Tabelle existiert und die richtige Struktur hat
-- (Falls die Tabelle nicht existiert, wird sie erstellt)
CREATE TABLE IF NOT EXISTS `season` (
`id` INT NOT NULL AUTO_INCREMENT,
`season` VARCHAR(255) NOT NULL UNIQUE,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 2. Stelle sicher, dass die club_team-Tabelle die season_id-Spalte hat
-- (Falls die Spalte nicht existiert, wird sie hinzugefügt)
ALTER TABLE `club_team`
ADD COLUMN IF NOT EXISTS `season_id` INT NULL;
-- 3. Erstelle die Seasons, falls sie fehlen
INSERT IGNORE INTO `season` (`season`) VALUES ('2024/2025');
INSERT IGNORE INTO `season` (`season`) VALUES ('2025/2026');
-- 4. Aktualisiere Teams ohne season_id auf die aktuelle Saison
-- (Verwendet die neueste Saison basierend auf dem aktuellen Datum)
UPDATE `club_team`
SET `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
)
WHERE `season_id` IS NULL;
-- 5. Falls keine aktuelle Saison existiert, erstelle sie
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
);
-- 6. Aktualisiere Teams mit ungültigen season_id auf die aktuelle Saison
UPDATE `club_team` ct
LEFT JOIN `season` s ON ct.season_id = s.id
SET ct.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
)
WHERE s.id IS NULL;
-- 7. Füge Foreign Key Constraint hinzu, falls er fehlt
-- (Hinweis: MySQL/MariaDB unterstützt "IF NOT EXISTS" nicht für Constraints,
-- daher müssen wir prüfen, ob der Constraint bereits existiert)
SET @constraint_exists = (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'club_team'
AND CONSTRAINT_NAME = 'club_team_season_id_foreign_idx'
AND REFERENCED_TABLE_NAME = 'season'
);
SET @sql = IF(@constraint_exists = 0,
'ALTER TABLE `club_team` ADD CONSTRAINT `club_team_season_id_foreign_idx` FOREIGN KEY (`season_id`) REFERENCES `season` (`id`) ON DELETE CASCADE ON UPDATE CASCADE',
'SELECT "Foreign key constraint already exists" as message'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 8. Zeige die Ergebnisse
SELECT '=== ERGEBNIS ===' as info;
SELECT
ct.id,
ct.name,
ct.season_id,
s.season
FROM `club_team` ct
LEFT JOIN `season` s ON ct.season_id = s.id
ORDER BY ct.id;

View File

@@ -0,0 +1,8 @@
-- Migration: Make locationId optional in match table
-- Date: 2025-01-27
-- For MariaDB
-- Modify locationId to allow NULL
ALTER TABLE `match`
MODIFY COLUMN location_id INT NULL;

View File

@@ -0,0 +1,41 @@
-- Migration: Umbenennen von max_birth_year zu min_birth_year
-- Datum: 2025-01-XX
-- Beschreibung: Ändert die Logik von "geboren <= X" zu "geboren >= X"
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_class';
SET @oldcolumnname = 'max_birth_year';
SET @newcolumnname = 'min_birth_year';
-- Check if old column exists
SET @old_column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @oldcolumnname)
);
-- Check if new column already exists
SET @new_column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @newcolumnname)
);
-- Rename column if old exists and new doesn't
SET @sql = IF(@old_column_exists > 0 AND @new_column_exists = 0,
CONCAT('ALTER TABLE `', @tablename, '` CHANGE COLUMN `', @oldcolumnname, '` `', @newcolumnname, '` INT(11) NULL DEFAULT NULL AFTER `gender`'),
IF(@new_column_exists > 0,
'SELECT 1 AS column_already_renamed',
'SELECT 1 AS old_column_not_found'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

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