# API Contract (extrahiert aus Frontend + Backend-Routen)

## 1) Basis
- Base URL (Frontend): `${VITE_BACKEND || (DEV ? 'http://localhost:3005' : window.location.origin)}/api`
- Transport: JSON, bei Uploads `multipart/form-data`
- Auth-Header (via Axios-Interceptor):
  - `authcode: <token>`
  - `userid: <username/email>`
- Timeout: 60s (`apiClient`), einzelne Aufrufe überschreiben lokal

## 2) Globales Response-/Error-Format

### Erfolgsantwort
- Meist `response.data` als fachliches Objekt/Array
- Bei einigen Endpunkten explizit:
  - `{ success: true, ... }`
  - oder direkt Array/Objekt

### Fehlerantwort (Backend-Error-Handler)
```json
{
  "success": false,
  "code": "ERROR_...",   
  "params": { },
  "error": "...",
  "message": "..."       
}
```

### Beobachtete HTTP-Fehler
- `400` Bad Request
- `401` Unauthorized (führt im Frontend zu auto-logout)
- `403` Forbidden
- `404` Not Found
- `409` Conflict (z. B. Club existiert bereits)
- `5xx` Serverfehler

### Bekannte Error-Codes (global)
Quelle: `backend/constants/errorCodes.js`
- Allgemein: `ERROR_INTERNAL_SERVER_ERROR`, `ERROR_UNKNOWN_ERROR`, `ERROR_VALIDATION_FAILED`, `ERROR_NOT_FOUND`, `ERROR_UNAUTHORIZED`, `ERROR_FORBIDDEN`, `ERROR_BAD_REQUEST`
- Auth: `ERROR_USER_NOT_FOUND`, `ERROR_INVALID_PASSWORD`, `ERROR_LOGIN_FAILED`, `ERROR_SESSION_EXPIRED`
- MyTischtennis: `ERROR_MYTISCHTENNIS_*`
- Member: `ERROR_MEMBER_*`
- Group: `ERROR_GROUP_*`
- Tournament: `ERROR_TOURNAMENT_*`
- Diary: `ERROR_DIARY_*`, `SUCCESS_DIARY_*`
- Team: `ERROR_TEAM_*`
- Activity: `ERROR_ACTIVITY_IMAGE_DELETE_FAILED`
- Official Tournament: `SUCCESS_OFFICIAL_TOURNAMENT_PDF_UPLOAD`, `ERROR_OFFICIAL_TOURNAMENT_PDF_UPLOAD`
- Club: `ERROR_CLUB_*`
- Member Transfer: `ERROR_MEMBER_TRANSFER_BULK_FAILED`
- Training Stats: `ERROR_TRAINING_STATS_LOAD_FAILED`
- Logs: `ERROR_LOG_NOT_FOUND`

## 3) Endpoint-Katalog (im Frontend verwendet)

Hinweis: Payload/Response sind als **beobachtete Struktur** aus Aufrufstellen dokumentiert.

### Auth & Session
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| POST | `/auth/register` | `{ email, password }` | Erfolg ohne feste Struktur verwendet |
| POST | `/auth/login` | `{ email, password }` | `{ token, ... }` |
| GET | `/auth/activate/:activationCode` | - | Erfolg ohne Feldauswertung |
| POST | `/auth/forgot-password` | `{ email }` | Erfolg ohne Feldauswertung |
| POST | `/auth/reset-password` | `{ token, password }` | Erfolg ohne Feldauswertung |
| GET | `/session/status` | - | `{ valid: boolean }` |

### Clubs & Approvals
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/clubs` | - | `Club[]` |
| POST | `/clubs` | `{ name }` | neuer Club (ID wird weiterverwendet) |
| GET | `/clubs/:clubId` | - | Club-Objekt |
| PUT | `/clubs/:clubId/settings` | Club-Settings-Objekt | Erfolg |
| GET | `/clubs/request/:clubId` | - | Erfolg |
| GET | `/clubs/pending/:clubId` | - | Pending-Approvals-Liste |
| POST | `/clubs/approve` | `{ clubId, userId, ... }` | Erfolg |
| POST | `/clubs/reject` | `{ clubId, userId, ... }` | Erfolg |

### Permissions
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/permissions/:clubId` | - | `{ role, isOwner, permissions }` |
| GET | `/permissions/roles/available` | - | `Role[]` |
| GET | `/permissions/structure/all` | - | Permission-Struktur |
| GET | `/permissions/:clubId/members` | optional `?t=<timestamp>` | Mitglieder + Rollen/Permissions |
| PUT | `/permissions/:clubId/user/:userId/role` | `{ role }` | Erfolg |
| PUT | `/permissions/:clubId/user/:userId/status` | `{ approved }` | Erfolg |
| PUT | `/permissions/:clubId/user/:userId/permissions` | `{ permissions: {resource:{action:boolean}} }` | Erfolg |

### Members
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/clubmembers/get/:clubId/:showAll` | - | `Member[]` |
| GET | `/clubmembers/notapproved/:clubId` | - | `Member[]` |
| POST | `/clubmembers/set/:clubId` | Member-/Bulk-Payload | Erfolg/Member |
| POST | `/clubmembers/update-ratings/:clubId` | - | Erfolg |
| POST | `/clubmembers/quick-update-test-membership/:clubId/:memberId` | - | Erfolg |
| POST | `/clubmembers/quick-update-member-form/:clubId/:memberId` | - | Erfolg |
| POST | `/clubmembers/quick-deactivate/:clubId/:memberId` | - | Erfolg |
| GET | `/clubmembers/gallery/:clubId?format=json&size=:size` | - | Galerie-Metadaten |
| POST | `/clubmembers/image/:clubId/:memberId` | `multipart/form-data` | Erfolg |
| GET | `/clubmembers/image/:clubId/:memberId` | - | Bild/Binärdaten |
| GET | `/clubmembers/image/:clubId/:memberId/:imageId` | - | Bild/Binärdaten |
| DELETE | `/clubmembers/image/:clubId/:memberId/:imageId` | - | Erfolg |
| POST | `/clubmembers/image/:clubId/:memberId/:imageId/primary` | - | Erfolg |
| POST | `/clubmembers/rotate-image/:clubId/:memberId/:imageId` | `{ direction }` | Erfolg |
| POST | `/clubmembers/transfer/:clubId` | Transfer-Konfig inkl. Endpoint/Method/Template/Login | `{ success, transferred, total, invalidMembers?, errors? }` |

### Diary, Participants, Activities, Tags, Notes
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/diary/:clubId` | - | DiaryDate[] |
| POST | `/diary/:clubId` | Datum/Trainingsdaten | neue Date |
| PUT | `/diary/:clubId` | Trainingszeiten-Update | Erfolg |
| DELETE | `/diary/:clubId/:dateId` | - | Erfolg |
| GET | `/participants/:dateId` | - | Participant[] |
| POST | `/participants/add` | `{ dateId, memberId, ... }` | neuer Participant |
| POST | `/participants/remove` | `{ dateId, participantId/memberId }` | Erfolg |
| PUT | `/participants/:dateId/:memberId/group` | `{ groupId }` | Erfolg |
| GET | `/activities/:dateId` | - | Activity[] |
| POST | `/activities/add` | Activity-Payload | neue Activity |
| GET | `/group/:clubId/:dateId` | - | Group[] |
| POST | `/group` | Group-Form | Erfolg |
| PUT | `/group/:groupId` | Group-Änderung | Erfolg |
| DELETE | `/group/:groupId` | - | Erfolg |
| GET | `/tags` | - | Tag[] |
| POST | `/tags` | `{ name }` | Tag |
| POST | `/diary/tag/:clubId/add-tag` | `{ diaryDateId, tagId }` | Erfolg |
| DELETE | `/diary/:clubId/tag` | Payload in `data` | Erfolg |
| GET | `/notes?diaryDateId=:id&memberId=:id` | - | Note[] |
| GET | `/diarymember/:clubId/note` | Query-Filter | Note[] |
| POST | `/diarymember/:clubId/note` | Note-Payload | Erfolg |
| DELETE | `/diarymember/:clubId/note/:noteId` | Query-Filter möglich | Erfolg |
| POST | `/diarymember/:clubId/note/remove` | Entfernen-Bulk | Erfolg |
| GET | `/diarymember/:clubId/tag` | Query-Filter | Tag[] |
| POST | `/diarymember/:clubId/tag` | Tag-Zuordnung | Erfolg |
| POST | `/diarymember/:clubId/tag/remove` | Tag-Entfernung | Erfolg |
| POST | `/diarydatetags/:clubId` | Tag-Payload | Erfolg |
| GET | `/diary-date-activities/:clubId/:dateId` | - | Plan-Items[] |
| POST | `/diary-date-activities/:clubId` | Plan-Item | Erfolg |
| PUT | `/diary-date-activities/:clubId/:id` | Plan-Update | Erfolg |
| PUT | `/diary-date-activities/:clubId/:id/order` | `{ order }` | Erfolg |
| DELETE | `/diary-date-activities/:clubId/:id` | - | Erfolg |
| POST | `/diary-date-activities/group` | Group-Plan-Payload | Erfolg |
| PUT | `/diary-date-activities/group/:clubId/:groupActivityId` | Group-Plan-Update | Erfolg |
| DELETE | `/diary-date-activities/group/:clubId/:groupActivityId` | - | Erfolg |
| GET | `/diary-member-activities/:clubId/:diaryDateActivityId` | - | ParticipantIds/Member[] |
| POST | `/diary-member-activities/:clubId/:diaryDateActivityId` | `{ participantIds: [] }` | Erfolg |
| DELETE | `/diary-member-activities/:clubId/:diaryDateActivityId/:participantId` | - | Erfolg |
| POST | `/accident` | Accident-Objekt | Erfolg |
| GET | `/accident/:clubId/:dateId` | - | Accident[] |

### Predefined Activities
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/predefined-activities` | - | ActivityTemplate[] |
| GET | `/predefined-activities/:id` | - | ActivityTemplate-Detail |
| GET | `/predefined-activities/search/query?q=:term&limit=:n` | - | Trefferliste |
| POST | `/predefined-activities` | Template-Payload | neues Template |
| PUT | `/predefined-activities/:id` | Update-Payload | aktualisiertes Template |
| POST | `/predefined-activities/merge` | `{ sourceId, targetId }` | Erfolg |
| POST | `/predefined-activities/deduplicate` | `{}` | Erfolg |
| DELETE | `/predefined-activities/:id/image/:imageId` | - | Erfolg |

### Training Groups / Times / Stats
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/training-groups/:clubId` | - | Group[] |
| GET | `/training-groups/:clubId/member/:memberId` | - | Group[] |
| POST | `/training-groups/:clubId` | Group-Payload | Erfolg |
| PUT | `/training-groups/:clubId/:groupId` | Group-Payload | Erfolg |
| DELETE | `/training-groups/:clubId/:groupId` | - | Erfolg |
| POST | `/training-groups/:clubId/:groupId/member/:memberId` | - | Erfolg |
| DELETE | `/training-groups/:clubId/:groupId/member/:memberId` | - | Erfolg |
| GET | `/training-times/:clubId` | - | Time[] |
| POST | `/training-times/:clubId` | Time-Payload | Erfolg |
| PUT | `/training-times/:clubId/:timeId` | Time-Payload | Erfolg |
| DELETE | `/training-times/:clubId/:timeId` | - | Erfolg |
| GET | `/training-stats/:clubId` | - | Statistikobjekt |
| GET | `/member-activities/:clubId/:memberId` | `period` als Query | Aktivitäten-Stats |
| GET | `/member-activities/:clubId/:memberId/last-participations?limit=3` | - | letzte Teilnahmen |

### Matches / Schedule
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| POST | `/matches/import` | CSV `multipart/form-data` | Erfolg/Import-Ergebnis |
| GET | `/matches/leagues/:clubId/matches/:leagueId` | optional `seasonid` | Match[] |
| GET | `/matches/leagues/:clubId/matches` | optional `seasonid` | Match[] |
| GET | `/matches/leagues/:clubId/table/:leagueId` | - | Tabellenobjekt |
| POST | `/matches/leagues/:clubId/table/:leagueId/fetch` | - | Fetch-Ergebnis |
| GET | `/matches/leagues/:clubId/stats/:leagueId` | optional `seasonid` | Player-Stats[] |
| PATCH | `/matches/:matchId/players` | Spielerzuordnung | Erfolg |

### Club Teams & Team Documents
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/club-teams/club/:clubId` | optional `seasonid` | Team[] |
| GET | `/club-teams/leagues/:clubId` | optional `seasonid` | League[] |
| POST | `/club-teams/club/:clubId` | `teamData` | Team |
| PUT | `/club-teams/:clubTeamId` | `teamData` | Erfolg |
| DELETE | `/club-teams/:clubTeamId` | - | Erfolg |
| GET | `/team-documents/club-team/:clubTeamId` | - | Document[] |
| POST | `/team-documents/club-team/:clubTeamId/upload` | PDF `multipart/form-data` | `{ id, ... }` |
| POST | `/team-documents/:documentId/parse?leagueid=:leagueId` | - | Parse-Ergebnis |
| GET | `/team-documents/:documentId/download` | - | PDF/Binärdaten |

### Tournaments
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/tournament/:clubId` | optional `?type=mini` | Tournament[] |
| GET | `/tournament/:clubId/:tournamentId` | - | Tournament-Detail |
| PUT | `/tournament/:clubId/:tournamentId` | `{ numberOfTables, winningSets, ... }` | Erfolg |
| POST | `/tournament` | neues Turnier | Erfolg |
| POST | `/tournament/mini` | Mini-Format-Payload | Erfolg |
| POST | `/tournament/participant` | Teilnehmer hinzufügen | Erfolg |
| DELETE | `/tournament/participant` | `data: { clubId, tournamentId, ... }` | Erfolg |
| POST | `/tournament/participants` | `{ clubId, tournamentId, classId }` | Participant[] |
| PUT | `/tournament/participant/:clubId/:tournamentId/:participantId/seeded` | `{ seeded }` | Erfolg |
| PUT | `/tournament/participant/:clubId/:tournamentId/:participantId/gave-up` | `{ gaveUp }` | Erfolg |
| PUT | `/tournament/participant/:clubId/:tournamentId/:participantId/class` | `{ classId }` | Erfolg |
| POST | `/tournament/external-participant` | externer Teilnehmer | Erfolg |
| DELETE | `/tournament/external-participant` | `data` mit IDs | Erfolg |
| POST | `/tournament/external-participants` | `{ clubId, tournamentId, classId }` | ExternalParticipant[] |
| PUT | `/tournament/external-participant/:clubId/:tournamentId/:participantId/seeded` | `{ seeded }` | Erfolg |
| PUT | `/tournament/external-participant/:clubId/:tournamentId/:participantId/gave-up` | `{ gaveUp }` | Erfolg |
| GET | `/tournament/groups` | Query `{ clubId, tournamentId }` | Group[] |
| PUT | `/tournament/groups` | Gruppenkonfiguration | Erfolg |
| POST | `/tournament/groups/create` | Gruppen erstellen | Erfolg |
| POST | `/tournament/groups` | Gruppen füllen | Erfolg |
| POST | `/tournament/groups/reset` | IDs/Turnier | Erfolg |
| POST | `/tournament/modus` | Modus-Einstellungen | Erfolg |
| POST | `/tournament/matches/create` | Match-Erstellung | Erfolg |
| POST | `/tournament/matches/reset` | Reset-Payload | Erfolg |
| POST | `/tournament/matches/cleanup-orphaned` | IDs/Turnier | Cleanup-Result |
| GET | `/tournament/matches/:clubId/:tournamentId` | - | Match[] |
| POST | `/tournament/match/result` | Match-Resultat | Erfolg |
| DELETE | `/tournament/match/result` | `data` mit Match-ID | Erfolg |
| POST | `/tournament/match/finish` | Match-Finish-Payload | Erfolg |
| POST | `/tournament/match/reopen` | Reopen-Payload | Erfolg |
| PUT | `/tournament/match/:clubId/:tournamentId/:matchId/active` | `{ isActive }` | Erfolg |
| PUT | `/tournament/match/:clubId/:tournamentId/:matchId/table` | `{ tableNumber }` | Erfolg |
| POST | `/tournament/knockout` | Start-KO-Payload | Erfolg |
| DELETE | `/tournament/matches/knockout` | `data: payload` | Erfolg |
| POST | `/tournament/pools/merge` | Merge-Payload | Erfolg |
| PUT | `/tournament/participant/group` | `{ participantId, groupId, ... }` | Erfolg |
| GET | `/tournament/classes/:clubId/:tournamentId` | - | Class[] |
| POST | `/tournament/class/:clubId/:tournamentId` | Klassen-Payload | Erfolg |
| PUT | `/tournament/class/:clubId/:tournamentId/:classId` | Klassen-Update | Erfolg |
| DELETE | `/tournament/class/:clubId/:tournamentId/:classId` | - | Erfolg |
| GET | `/tournament/pairings/:clubId/:tournamentId/:classId` | - | Pairing[] |
| POST | `/tournament/pairing/:clubId/:tournamentId/:classId` | Pairing-Payload | Erfolg |
| PUT | `/tournament/pairing/:clubId/:tournamentId/:pairingId` | Pairing-Update | Erfolg |
| DELETE | `/tournament/pairing/:clubId/:tournamentId/:pairingId` | - | Erfolg |
| GET | `/tournament/stages` | Query `{ clubId, tournamentId }` | Stage[] |
| PUT | `/tournament/stages` | Stage-Konfiguration | Erfolg |
| POST | `/tournament/stages/advance` | Advancement-Payload | Erfolg |

### Official Tournaments
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/official-tournaments/:clubId` | - | Tournament[] |
| GET | `/official-tournaments/:clubId/participations/summary` | - | Summary-Objekt |
| POST | `/official-tournaments/:clubId/upload` | PDF `multipart/form-data` | `{ id, ... }` |
| GET | `/official-tournaments/:clubId/:id` | - | Parsed Tournament |
| POST | `/official-tournaments/:clubId/:id/participation` | Teilnahme-Payload | Erfolg |
| POST | `/official-tournaments/:clubId/:id/status` | Status-Payload | Erfolg |
| DELETE | `/official-tournaments/:clubId/:id` | - | Erfolg |

### MyTischtennis
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/mytischtennis/account` | - | Account-Objekt |
| GET | `/mytischtennis/status` | - | Status-Objekt |
| POST | `/mytischtennis/verify` | `{}` oder Login-Daten | Verify-Ergebnis |
| POST | `/mytischtennis/account` | Account-Payload | Erfolg |
| DELETE | `/mytischtennis/account` | - | Erfolg |
| GET | `/mytischtennis/update-history` | - | Historie[] |
| POST | `/mytischtennis/parse-url` | `{ url, ... }` | Parse-Result |
| POST | `/mytischtennis/configure-team` | Team-Konfig | Erfolg |
| POST | `/mytischtennis/configure-league` | League-Konfig | Erfolg |
| POST | `/mytischtennis/fetch-team-data/async` | `{ clubTeamId }` | `{ jobId }` |
| GET | `/mytischtennis/fetch-team-data/jobs/:jobId` | - | `{ job: { status, result?, error? } }` |

### Logs
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| GET | `/logs` | Query-Filter (`params`) | Log-Liste |
| GET | `/logs/:id` | - | Log-Detail |
| GET | `/logs/scheduler/last-executions` | optional Club-Query | Scheduler-Logs |

### NuScore (direkte `fetch`-Aufrufe)
| Methode | URL | Request-Payload | Response (beobachtet) |
|---|---|---|---|
| POST | `/nuscore/init-cookies/:code` | - | Erfolg |
| GET | `/nuscore/meetinginfo/:code` | - | Meeting-Objekt |
| GET | `/nuscore/meetingdetails/:uuid` | - | Meeting-Detail |
| PUT | `/nuscore/validate/:uuid` | komplettes `matchData` | `{ validationWarnings?, validationErrors?, ... }` |
| PUT | `/nuscore/submit/:uuid` | komplettes `matchData` | Erfolgs-/Fehlerobjekt |
| POST | `/nuscore/broadcast-draft` | `{ clubId, gameCode, matchData }` | Erfolg |

## 4) Dynamische/indirekte API-Aufrufe
- `GET image.apiPath` in `MembersView.vue` (Blob-Download)
- `GET /:path` in `DiaryView.vue` (Blob-Download über dynamischen Pfad)
- `fetch(resource.url)` in `NuscoreAnalyzer.vue` ist extern (kein Backend-API-Contract)

