/** * Shinkan Jinkendo API Client * * Zentrale API-Kommunikation mit automatischer Token-Injektion */ import { stripHtmlToText } from './htmlUtils' import { normalizeAdvanceMode } from './combinationMethodProfileUi' const API_URL = import.meta.env.VITE_API_URL || '' /** LocalStorage + Request-Header für Mandanten-Kontext */ export const ACTIVE_CLUB_STORAGE_KEY = 'shinkan_active_club_id' function mergeActiveClubHeader(headers = {}) { const cid = localStorage.getItem(ACTIVE_CLUB_STORAGE_KEY) if (cid && /^\d+$/.test(String(cid).trim())) { return { ...headers, 'X-Active-Club-Id': String(cid).trim() } } return { ...headers } } /** * Generic API request with automatic token injection */ async function request(endpoint, options = {}) { const token = localStorage.getItem('authToken') const method = (options.method || 'GET').toUpperCase() const headers = mergeActiveClubHeader({ ...options.headers, }) // GET ohne Body: kein Content-Type: application/json (manche Proxies/Headers stören sich) if (method !== 'GET' && method !== 'HEAD') { if (!headers['Content-Type'] && !headers['content-type']) { headers['Content-Type'] = 'application/json' } } if (token) { headers['X-Auth-Token'] = token } const url = `${API_URL}${endpoint}` try { const response = await fetch(url, { ...options, headers, }) if (!response.ok) { const text = await response.text() let parsed = null try { parsed = JSON.parse(text) } catch { parsed = null } if (parsed?.detail != null) { const d = parsed.detail throw new Error(typeof d === 'string' ? d : JSON.stringify(d)) } if (response.status === 502) { throw new Error( 'HTTP 502 (Bad Gateway): Der Reverse-Proxy hat die API nicht korrekt erreicht. Ist `shinkan-api` aktiv (`docker compose ps`, `docker logs shinkan-api`)? Bei Host-Routing nur einen Weg verwenden — alles auf Port 3003 (Nginx nach `backend:8000`) oder sauber `/api` → Backend-Port.' ) } const snippet = (text || '').replace(/\s+/g, ' ').trim().slice(0, 180) throw new Error(snippet ? `HTTP ${response.status}: ${snippet}` : `HTTP ${response.status}`) } return response.json() } catch (e) { if (e instanceof TypeError && (e.message === 'Failed to fetch' || e.message.includes('fetch'))) { const hint = API_URL && API_URL.length > 0 ? `Verbindung zum API unter ${API_URL} fehlgeschlagen. Läuft das Backend (z. B. Port 8098) und ist CORS erlaubt?` : 'Kein VITE_API_URL gesetzt: Anfragen gehen an die Frontend-URL und schlagen oft fehl. Setze in .env z. B. VITE_API_URL=http://localhost:8098 und starte Vite neu.' // Ursache oft: CORS (v. a. bei Fehler-Antworten ohne CORS-Header), Adblocker, falsche URL — // Login kann trotzdem klappen wenn nur eine andere Route betroffen ist. throw new Error(`${hint} [Technisch: ${e.message}; URL war ${endpoint}]`) } throw e } } // ============================================================================ // Auth // ============================================================================ export async function login(email, password) { return request('/api/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }) } export async function register(email, password, name, extra = {}) { return request('/api/auth/register', { method: 'POST', body: JSON.stringify({ email, password, name, ...extra }), }) } export async function logout() { return request('/api/auth/logout', { method: 'POST' }) } export async function getCurrentProfile() { return request('/api/profiles/me') } /** Liste aller Profile – nur für Plattform-Admins (Vereinsanlage). */ export async function listProfiles() { return request('/api/profiles') } /** Alle Nutzer inkl. Vereinsmitgliedschaften — nur Portal-Admin (UI: Admin → Nutzer). */ export async function listAdminUsers() { return request('/api/admin/users') } /** Medien-Speicher (MEDIA_ROOT + relativer Unterordner) — GET: admin/superadmin, PUT: nur superadmin. */ export async function getPlatformMediaStorage() { return request('/api/admin/platform-media-storage') } export async function putPlatformMediaStorage(payload) { return request('/api/admin/platform-media-storage', { method: 'PUT', body: JSON.stringify(payload), }) } export async function updateProfile(profileId, data) { return request(`/api/profiles/${profileId}`, { method: 'PUT', body: JSON.stringify(data), }) } /** * Passwort anderer Konten: Standard leerer Body → E-Mail mit Reset-Link (wie Passwort vergessen). * Nur Super-Admins dürfen `newPassword` setzen (direktes Überschreiben des Passwort-Hashes). */ export async function managementPasswordReset(profileId, newPassword = null) { const body = {} if (newPassword != null && String(newPassword).trim() !== '') { body.new_password = newPassword } return request(`/api/profiles/${profileId}/management-password-reset`, { method: 'POST', body: JSON.stringify(body), }) } export async function changePassword(newPassword) { return request('/api/auth/pin', { method: 'PUT', body: JSON.stringify({ pin: newPassword }), }) } /** GET /api/auth/verify/{token} — keine Auth nötig; Token gehört zur URL des Bestätigungslinks */ export async function verifyEmail(token) { const t = encodeURIComponent(token) return request(`/api/auth/verify/${t}`, { method: 'GET', }) } export async function resendVerification(email) { return request('/api/auth/resend-verification', { method: 'POST', body: JSON.stringify({ email }), }) } // ============================================================================ // Clubs & Groups // ============================================================================ export async function listClubs() { return request('/api/clubs') } export async function getClub(id) { return request(`/api/clubs/${id}`) } export async function createClub(data) { return request('/api/clubs', { method: 'POST', body: JSON.stringify(data) }) } export async function updateClub(id, data) { return request(`/api/clubs/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteClub(id) { return request(`/api/clubs/${id}`, { method: 'DELETE' }) } /** Vereinsmitglieder (API für Admin ohne eigene UI) */ export async function listClubMembers(clubId, { includeInactive = false } = {}) { const q = includeInactive ? '?include_inactive=true' : '' return request(`/api/clubs/${clubId}/members${q}`) } export async function getClubMember(clubId, profileId) { return request(`/api/clubs/${clubId}/members/${profileId}`) } export async function addClubMember(clubId, payload) { return request(`/api/clubs/${clubId}/members`, { method: 'POST', body: JSON.stringify(payload), }) } export async function updateClubMember(clubId, profileId, payload) { return request(`/api/clubs/${clubId}/members/${profileId}`, { method: 'PUT', body: JSON.stringify(payload), }) } export async function removeClubMember(clubId, profileId) { return request(`/api/clubs/${clubId}/members/${profileId}`, { method: 'DELETE' }) } /** Aktive Vereine (öffentlich, für Registrierungswahl). */ export async function listPublicClubsDirectory() { return request('/api/clubs/public-directory') } /** Vereinsinternes Mitgliederverzeichnis (Trainer-/Co-Auswahl). */ export async function clubMembersDirectory(clubId) { return request(`/api/clubs/${clubId}/members/directory`) } /** Eigene Beitrittsanträge. */ export async function getMyClubJoinRequests() { return request('/api/me/club-join-requests') } export async function createClubJoinRequest(payload) { return request('/api/me/club-join-requests', { method: 'POST', body: JSON.stringify(payload), }) } export async function withdrawClubJoinRequest(requestId) { return request(`/api/me/club-join-requests/${requestId}`, { method: 'DELETE' }) } /** Offene Anträge (Vereins-/Plattform-Admin). */ export async function listClubJoinRequests(clubId) { return request(`/api/clubs/${clubId}/join-requests`) } export async function acceptClubJoinRequest(clubId, requestId, roles = ['trainer']) { return request(`/api/clubs/${clubId}/join-requests/${requestId}/accept`, { method: 'POST', body: JSON.stringify({ roles }), }) } export async function rejectClubJoinRequest(clubId, requestId) { return request(`/api/clubs/${clubId}/join-requests/${requestId}/reject`, { method: 'POST', body: JSON.stringify({}), }) } /** Aggregierter Posteingang: offene Beitrittsanträge für Vereins-/Plattform-Admins. */ export async function getInboxJoinRequests() { return request('/api/me/inbox/join-requests') } export async function listDivisions(clubId) { const query = clubId ? `?club_id=${clubId}` : '' return request(`/api/divisions${query}`) } export async function createDivision(data) { return request('/api/divisions', { method: 'POST', body: JSON.stringify(data) }) } export async function updateDivision(id, data) { return request(`/api/divisions/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteDivision(id) { return request(`/api/divisions/${id}`, { method: 'DELETE' }) } export async function listTrainingGroups(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/groups${query ? '?' + query : ''}`) } export async function getTrainingGroup(id) { return request(`/api/groups/${id}`) } export async function createTrainingGroup(data) { return request('/api/groups', { method: 'POST', body: JSON.stringify(data) }) } export async function updateTrainingGroup(id, data) { return request(`/api/groups/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteTrainingGroup(id) { return request(`/api/groups/${id}`, { method: 'DELETE' }) } // ============================================================================ // Skills & Methods // ============================================================================ export async function listSkills(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/skills${query ? '?' + query : ''}`) } /** Admin: Fähigkeiten hierarchisch sortiert (Hauptkategorie → Kategorie → Sortierung) */ export async function listSkillsCatalog(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/skills/catalog${query ? '?' + query : ''}`) } export async function getSkill(id) { return request(`/api/skills/${id}`) } export async function createSkill(data) { return request('/api/skills', { method: 'POST', body: JSON.stringify(data) }) } export async function updateSkill(id, data) { return request(`/api/skills/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteSkill(id) { return request(`/api/skills/${id}`, { method: 'DELETE' }) } export async function listMethods(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/methods${query ? '?' + query : ''}`) } export async function getMethod(id) { return request(`/api/methods/${id}`) } export async function createMethod(data) { return request('/api/methods', { method: 'POST', body: JSON.stringify(data) }) } export async function updateMethod(id, data) { return request(`/api/methods/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteMethod(id) { return request(`/api/methods/${id}`, { method: 'DELETE' }) } // ============================================================================ // Exercises // ============================================================================ export async function listExercises(filters = {}) { const q = new URLSearchParams() Object.entries(filters).forEach(([k, v]) => { if (v === undefined || v === null) return if (typeof v === 'boolean') { q.set(k, v ? 'true' : 'false') return } if (Array.isArray(v)) { if (v.length === 0) return v.forEach((item) => { if (item !== '' && item !== undefined && item !== null && String(item).trim() !== '') { q.append(k, String(item)) } }) return } if (String(v).trim() !== '') q.set(k, String(v)) }) const query = q.toString() return request(`/api/exercises${query ? '?' + query : ''}`) } /** Formular → API-Body (M:N gemäß EXERCISES_API_SPEC + training_types) */ export function buildExerciseApiPayload(formData, extras = {}) { const num = (v) => (v === '' || v == null ? null : Number(v)) const goalHtml = formData.goal || '' const execHtml = formData.execution || '' const goalText = stripHtmlToText(goalHtml) const execText = stripHtmlToText(execHtml) if (!goalText && !execText) { throw new Error('Ziel oder Durchführung ausfüllen (mindestens eines, auch mit Formatierung).') } const mapFocus = (formData.focus_areas_multi || []) .filter((x) => x && x.focus_area_id) .map((x) => ({ focus_area_id: Number(x.focus_area_id), is_primary: !!x.is_primary })) const mapStyles = (formData.training_styles_multi || []) .filter((x) => x && x.training_style_id) .map((x) => ({ training_style_id: Number(x.training_style_id), is_primary: !!x.is_primary })) const mapTTypes = (formData.training_types_multi || []) .filter((x) => x && x.training_type_id) .map((x) => ({ training_type_id: Number(x.training_type_id), is_primary: !!x.is_primary })) const mapTg = (formData.target_groups_multi || []) .filter((x) => x && x.target_group_id) .map((x) => ({ target_group_id: Number(x.target_group_id), is_primary: !!x.is_primary })) const payload = { title: (formData.title || '').trim(), summary: formData.summary || null, goal: goalHtml.trim() ? goalHtml : null, execution: execHtml.trim() ? execHtml : null, preparation: formData.preparation || null, trainer_notes: formData.trainer_notes || null, duration_min: num(formData.duration_min), duration_max: num(formData.duration_max), group_size_min: num(formData.group_size_min), group_size_max: num(formData.group_size_max), equipment: Array.isArray(formData.equipment) ? formData.equipment : [], focus_areas_multi: mapFocus, training_styles_multi: mapStyles, training_types_multi: mapTTypes, target_groups_multi: mapTg, age_groups: [], skills: (formData.skills || []).map((s) => ({ skill_id: s.skill_id, is_primary: !!s.is_primary, intensity: s.intensity || null, required_level: s.required_level || null, target_level: s.target_level || null, })), visibility: formData.visibility || 'private', status: formData.status || 'draft', club_id: formData.club_id ?? null, exercise_kind: String(formData.exercise_kind || 'simple').toLowerCase() === 'combination' ? 'combination' : 'simple', ...extras, } const isCombo = payload.exercise_kind === 'combination' if (isCombo) { let mpObj = {} const mpRaw = typeof formData.method_profile_json === 'string' ? formData.method_profile_json.trim() : '' if (mpRaw) { try { const parsed = JSON.parse(mpRaw) if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error('Ablaufprofil muss ein JSON-Objekt sein.') } mpObj = parsed } catch (e) { if (e instanceof SyntaxError) { throw new Error('Ablaufprofil (JSON): Syntax ungültig.') } throw e } } const slotRows = Array.isArray(formData.combination_slots) ? formData.combination_slots : [] const combination_slots = [] function parseTimingField(raw) { if (raw === '' || raw == null || raw === undefined) return undefined const n = parseInt(String(raw), 10) return Number.isFinite(n) ? n : undefined } for (let i = 0; i < slotRows.length; i += 1) { const row = slotRows[i] || {} let ids = Array.isArray(row.candidate_exercise_ids) ? row.candidate_exercise_ids.map((x) => Number(x)).filter((n) => Number.isFinite(n)) : [] /** Legacy: noch idsText Unterstützung für Import von älteren FormStand */ if ((!ids || ids.length === 0) && typeof row.idsText === 'string' && row.idsText.trim()) { ids = row.idsText .split(/[\s,;]+/) .map((s) => s.trim()) .filter(Boolean) .map((s) => parseInt(s, 10)) .filter((n) => Number.isFinite(n)) } combination_slots.push({ slot_index: i, title: (typeof row.title === 'string' && row.title.trim()) || null, candidate_exercise_ids: ids, }) } const slot_profiles_v1_next = [] for (let i = 0; i < slotRows.length; i += 1) { const row = slotRows[i] || {} const o = { slot_index: i } const advanceMode = normalizeAdvanceMode(row.advance_mode) if (advanceMode !== 'timed') o.advance_mode = advanceMode const load = parseTimingField(row.load_sec) const crs = parseTimingField(row.consecutive_reps) const intra = parseTimingField(row.intra_rep_rest_sec) const tran = parseTimingField(row.transition_after_sec) if (advanceMode === 'timed' && load !== undefined && load >= 0) o.load_sec = Math.round(load) if (crs !== undefined && crs >= 1) o.consecutive_reps = Math.round(crs) if (intra !== undefined && intra >= 0) o.intra_rep_rest_sec = Math.round(intra) if (tran !== undefined && tran >= 0) o.transition_after_sec = Math.round(tran) if (Object.keys(o).length > 1) slot_profiles_v1_next.push(o) } payload.method_archetype = (formData.method_archetype || '').trim() || null if (slot_profiles_v1_next.length > 0) mpObj.slot_profiles_v1 = slot_profiles_v1_next else delete mpObj.slot_profiles_v1 payload.method_profile = mpObj payload.combination_slots = combination_slots } else { payload.method_archetype = null payload.method_profile = {} } return payload } export async function uploadExerciseMedia(exerciseId, formData) { const token = localStorage.getItem('authToken') const headers = mergeActiveClubHeader({}) if (token) headers['X-Auth-Token'] = token const response = await fetch(`${API_URL}/api/exercises/${exerciseId}/media`, { method: 'POST', headers, body: formData, }) if (!response.ok) { const text = await response.text() let parsed = null try { parsed = text ? JSON.parse(text) : null } catch { parsed = null } const d = parsed?.detail if ( response.status === 409 && d && typeof d === 'object' && !Array.isArray(d) && typeof d.code === 'string' ) { const e = new Error( typeof d.message === 'string' ? d.message : 'Upload konnte nicht verarbeitet werden', ) e.code = d.code e.status = 409 e.payload = d throw e } if (response.status === 413) { const nginx = (text || '').toLowerCase().includes('nginx') throw new Error( nginx ? 'Die Anfrage ist zu groß (413). Häufig: nginx „client_max_body_size“ — z. B. große/r mehrere Videos oder Bulk-Upload. Dateien kleiner aufteilen oder Server-Limit erhöhen (Frontend-Image Neu bauen).' : 'Die Anfrage ist zu groß (413). Dateigröße oder Server-Limit prüfen.', ) } const msg = typeof d === 'string' ? d : d != null && typeof d === 'object' && typeof d.message === 'string' ? d.message : d != null ? JSON.stringify(d) : text && text.length < 400 && !/^\s* v !== undefined && v !== null && v !== '') ).toString() return request(`/api/maturity-models${query ? '?' + query : ''}`) } export async function resolveMaturityModel(filters = {}) { const query = new URLSearchParams( Object.entries(filters).filter(([, v]) => v !== undefined && v !== null && v !== '') ).toString() return request(`/api/maturity-models/resolve${query ? '?' + query : ''}`) } export async function getMaturityModel(id) { return request(`/api/maturity-models/${id}`) } export async function createMaturityModel(data) { return request('/api/maturity-models', { method: 'POST', body: JSON.stringify(data) }) } export async function updateMaturityModel(id, data) { return request(`/api/maturity-models/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteMaturityModel(id) { return request(`/api/maturity-models/${id}`, { method: 'DELETE' }) } export async function addMaturityModelSkill(modelId, data) { return request(`/api/maturity-models/${modelId}/skills`, { method: 'POST', body: JSON.stringify(data) }) } export async function removeMaturityModelSkill(modelId, skillId) { return request(`/api/maturity-models/${modelId}/skills/${skillId}`, { method: 'DELETE' }) } export async function upsertMaturityModelSkillLevels(modelId, data) { return request(`/api/maturity-models/${modelId}/skill-levels`, { method: 'PUT', body: JSON.stringify(data) }) } /** Hierarchische Zuordnung Modell → Fokus / Stilrichtung / Trainingsstil (training_types) */ export async function listMaturityModelContextBindings() { return request('/api/maturity-model-context-bindings') } export async function upsertMaturityModelContextBinding(data) { return request('/api/maturity-model-context-bindings', { method: 'POST', body: JSON.stringify(data) }) } export async function deleteMaturityModelContextBinding(id) { return request(`/api/maturity-model-context-bindings/${id}`, { method: 'DELETE' }) } export async function importMaturityModelBundle(payload) { return request('/api/maturity-models/import', { method: 'POST', body: JSON.stringify(payload) }) } export async function exportMaturityModelBundle(modelId) { return request(`/api/maturity-models/${modelId}/export`) } export async function exportResolvedMaturityBundle(filters = {}) { const query = new URLSearchParams( Object.entries(filters).filter(([, v]) => v !== undefined && v !== null && v !== '') ).toString() return request(`/api/maturity-models/export-resolved${query ? '?' + query : ''}`) } /** Komplett: Fähigkeitskatalog + Reifegradmodelle + Kontext-Bindings (slug-/namensbasiert auf Ziel-DB) */ export async function exportMatrixStackBundle() { return request('/api/admin/matrix-stack/export') } export async function importMatrixStackBundle(payload) { return request('/api/admin/matrix-stack/import', { method: 'POST', body: JSON.stringify(payload) }) } // Style Directions (formerly Training Styles) export async function listStyleDirections(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/training-styles${query ? '?' + query : ''}`) } // Backward-compatibility alias export const listTrainingStyles = listStyleDirections export async function createStyleDirection(data) { return request('/api/training-styles', { method: 'POST', body: JSON.stringify(data) }) } export async function updateStyleDirection(id, data) { return request(`/api/training-styles/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteStyleDirection(id) { return request(`/api/training-styles/${id}`, { method: 'DELETE' }) } // Training Characters export async function listTrainingCharacters(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/training-characters${query ? '?' + query : ''}`) } export async function createTrainingCharacter(data) { return request('/api/training-characters', { method: 'POST', body: JSON.stringify(data) }) } export async function updateTrainingCharacter(id, data) { return request(`/api/training-characters/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteTrainingCharacter(id) { return request(`/api/training-characters/${id}`, { method: 'DELETE' }) } // Training Types (Breitensport, Leistungssport, etc.) export async function listTrainingTypes(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/training-types${query ? '?' + query : ''}`) } export async function createTrainingType(data) { return request('/api/training-types', { method: 'POST', body: JSON.stringify(data) }) } export async function updateTrainingType(id, data) { return request(`/api/training-types/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteTrainingType(id) { return request(`/api/training-types/${id}`, { method: 'DELETE' }) } // Skill main categories (Hauptgruppen im Fähigkeitskatalog) export async function listSkillMainCategories() { return request('/api/skill-main-categories') } export async function createSkillMainCategory(data) { return request('/api/skill-main-categories', { method: 'POST', body: JSON.stringify(data) }) } export async function updateSkillMainCategory(id, data) { return request(`/api/skill-main-categories/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteSkillMainCategory(id) { return request(`/api/skill-main-categories/${id}`, { method: 'DELETE' }) } // Skill Categories export async function listSkillCategories(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/skill-categories${query ? '?' + query : ''}`) } export async function createSkillCategory(data) { return request('/api/skill-categories', { method: 'POST', body: JSON.stringify(data) }) } export async function updateSkillCategory(id, data) { return request(`/api/skill-categories/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteSkillCategory(id) { return request(`/api/skill-categories/${id}`, { method: 'DELETE' }) } // Trainer Focus Areas export async function listTrainerFocusAreas(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/trainer-focus-areas${query ? '?' + query : ''}`) } export async function assignTrainerFocusArea(data) { return request('/api/trainer-focus-areas', { method: 'POST', body: JSON.stringify(data) }) } export async function deleteTrainerFocusArea(id) { return request(`/api/trainer-focus-areas/${id}`, { method: 'DELETE' }) } // Target Groups (Zielgruppen) export async function listTargetGroups(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/target-groups${query ? '?' + query : ''}`) } export async function createTargetGroup(data) { return request('/api/target-groups', { method: 'POST', body: JSON.stringify(data) }) } export async function updateTargetGroup(id, data) { return request(`/api/target-groups/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteTargetGroup(id) { return request(`/api/target-groups/${id}`, { method: 'DELETE' }) } // Style Direction → Target Groups (M:N Assignments) export async function listStyleDirectionTargetGroups(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/training-style-target-groups${query ? '?' + query : ''}`) } export async function createStyleDirectionTargetGroup(data) { return request('/api/training-style-target-groups', { method: 'POST', body: JSON.stringify(data) }) } export async function updateStyleDirectionTargetGroup(id, data) { return request(`/api/training-style-target-groups/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteStyleDirectionTargetGroup(id) { return request(`/api/training-style-target-groups/${id}`, { method: 'DELETE' }) } export async function getStyleDirectionsHierarchy(filters = {}) { const query = new URLSearchParams(filters).toString() return request(`/api/training-styles/hierarchy${query ? '?' + query : ''}`) } // Trainer Contexts (Fokussierte Ansichten) export async function listTrainerContexts() { return request('/api/trainer-contexts') } export async function createTrainerContext(data) { return request('/api/trainer-contexts', { method: 'POST', body: JSON.stringify(data) }) } export async function updateTrainerContext(id, data) { return request(`/api/trainer-contexts/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteTrainerContext(id) { return request(`/api/trainer-contexts/${id}`, { method: 'DELETE' }) } // ============================================================================ // Training Planning // ============================================================================ /** Query-Parameter wie GET /api/training-units. */ export async function listTrainingUnits(filters = {}) { const q = new URLSearchParams() if (filters.group_id != null && filters.group_id !== '') { q.set('group_id', String(filters.group_id)) } if (filters.club_id != null && filters.club_id !== '') { q.set('club_id', String(filters.club_id)) } if (filters.start_date) q.set('start_date', filters.start_date) if (filters.end_date) q.set('end_date', filters.end_date) if (filters.status) q.set('status', filters.status) if (filters.debrief_pending === true) q.set('debrief_pending', 'true') if (filters.assigned_to_me === true) q.set('assigned_to_me', 'true') if (filters.sort) q.set('sort', String(filters.sort)) if (filters.limit != null && filters.limit !== '') q.set('limit', String(filters.limit)) const qs = q.toString() return request(`/api/training-units${qs ? `?${qs}` : ''}`) } /** Dashboard: Übungen in geplanten Einheiten, die für den Verein noch auf Sichtbarkeit „Verein“ gehören. */ export async function getTrainingExerciseClubVisibilityQueue(filters = {}) { const q = new URLSearchParams() if (filters.start_date) q.set('start_date', String(filters.start_date)) if (filters.end_date) q.set('end_date', String(filters.end_date)) if (filters.assigned_to_me === false) q.set('assigned_to_me', 'false') if (filters.limit_units != null && filters.limit_units !== '') { q.set('limit_units', String(filters.limit_units)) } const qs = q.toString() return request(`/api/training-units/exercises-club-visibility-queue${qs ? `?${qs}` : ''}`) } export async function getTrainingUnit(id) { return request(`/api/training-units/${id}`) } export async function createTrainingUnit(data) { return request('/api/training-units', { method: 'POST', body: JSON.stringify(data) }) } export async function updateTrainingUnit(id, data) { return request(`/api/training-units/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteTrainingUnit(id) { return request(`/api/training-units/${id}`, { method: 'DELETE' }) } export async function quickCreateTrainingUnit(data) { return request('/api/training-units/quick-create', { method: 'POST', body: JSON.stringify(data) }) } /** Rahmen-Slot → geplante Einheit (tiefe Kopie, origin_framework_slot_id). */ export async function createTrainingUnitFromFrameworkSlot(data) { return request('/api/training-units/from-framework-slot', { method: 'POST', body: JSON.stringify(data) }) } export async function listTrainingPlanTemplates() { return request('/api/training-plan-templates') } export async function getTrainingPlanTemplate(id) { return request(`/api/training-plan-templates/${id}`) } export async function createTrainingPlanTemplate(data) { return request('/api/training-plan-templates', { method: 'POST', body: JSON.stringify(data) }) } export async function updateTrainingPlanTemplate(id, data) { return request(`/api/training-plan-templates/${id}`, { method: 'PUT', body: JSON.stringify(data) }) } export async function deleteTrainingPlanTemplate(id) { return request(`/api/training-plan-templates/${id}`, { method: 'DELETE' }) } export async function listTrainingModules() { return request('/api/training-modules') } export async function getTrainingModule(id) { return request(`/api/training-modules/${id}`) } export async function createTrainingModule(data) { return request('/api/training-modules', { method: 'POST', body: JSON.stringify(data), }) } export async function updateTrainingModule(id, data) { return request(`/api/training-modules/${id}`, { method: 'PUT', body: JSON.stringify(data), }) } export async function deleteTrainingModule(id) { return request(`/api/training-modules/${id}`, { method: 'DELETE' }) } /** Kopiert Modul-Inhalte ans Ende eines Abschnitts (section_order_index 0-basiert). */ export async function applyTrainingModuleToTrainingUnit(unitId, data) { return request(`/api/training-units/${unitId}/apply-training-module`, { method: 'POST', body: JSON.stringify(data), }) } export async function listTrainingFrameworkPrograms() { return request('/api/training-framework-programs') } export async function getTrainingFrameworkProgram(id) { return request(`/api/training-framework-programs/${id}`) } export async function createTrainingFrameworkProgram(data) { return request('/api/training-framework-programs', { method: 'POST', body: JSON.stringify(data), }) } export async function updateTrainingFrameworkProgram(id, data) { return request(`/api/training-framework-programs/${id}`, { method: 'PUT', body: JSON.stringify(data), }) } export async function deleteTrainingFrameworkProgram(id) { return request(`/api/training-framework-programs/${id}`, { method: 'DELETE' }) } // ============================================================================ // Version & Health // ============================================================================ export async function getVersion() { return request('/api/version') } export async function healthCheck() { return request('/health') } export const api = { // Auth login, register, logout, getCurrentProfile, listProfiles, listAdminUsers, updateProfile, managementPasswordReset, changePassword, verifyEmail, resendVerification, // Clubs & Groups listClubs, getClub, createClub, updateClub, deleteClub, listClubMembers, getClubMember, addClubMember, updateClubMember, removeClubMember, listPublicClubsDirectory, clubMembersDirectory, getMyClubJoinRequests, createClubJoinRequest, withdrawClubJoinRequest, listClubJoinRequests, acceptClubJoinRequest, rejectClubJoinRequest, getInboxJoinRequests, listDivisions, createDivision, updateDivision, deleteDivision, listTrainingGroups, getTrainingGroup, createTrainingGroup, updateTrainingGroup, deleteTrainingGroup, // Skills & Methods listSkills, listSkillsCatalog, getSkill, createSkill, updateSkill, deleteSkill, listMethods, getMethod, createMethod, updateMethod, deleteMethod, // Exercises listExercises, getExercise, createExercise, updateExercise, bulkPatchExercisesMetadata, deleteExercise, createExerciseVariant, updateExerciseVariant, deleteExerciseVariant, reorderExerciseVariants, buildExerciseApiPayload, suggestExerciseAi, regenerateExerciseAi, uploadExerciseMedia, updateExerciseMedia, deleteExerciseMedia, reorderExerciseMedia, postMediaAssetLifecycle, listMediaAssets, patchMediaAsset, bulkMediaLifecycle, bulkPatchMediaAssets, bulkUploadMediaAssets, getMediaAssetJournal, addMediaAssetDeclarationCorrection, attachExerciseMediaFromAsset, setMediaAssetLegalHold, releaseMediaAssetLegalHold, listMediaAssetsWithLegalHold, listExerciseProgressionGraphs, getExerciseProgressionGraph, createExerciseProgressionGraph, updateExerciseProgressionGraph, deleteExerciseProgressionGraph, listExerciseProgressionEdges, createExerciseProgressionEdge, updateExerciseProgressionEdge, deleteExerciseProgressionEdge, createExerciseProgressionSequence, deleteExerciseProgressionEdgesBatch, // Training Planning listTrainingUnits, getTrainingExerciseClubVisibilityQueue, getTrainingUnit, createTrainingUnit, updateTrainingUnit, deleteTrainingUnit, quickCreateTrainingUnit, createTrainingUnitFromFrameworkSlot, listTrainingPlanTemplates, getTrainingPlanTemplate, createTrainingPlanTemplate, updateTrainingPlanTemplate, deleteTrainingPlanTemplate, listTrainingModules, getTrainingModule, createTrainingModule, updateTrainingModule, deleteTrainingModule, applyTrainingModuleToTrainingUnit, listTrainingFrameworkPrograms, getTrainingFrameworkProgram, createTrainingFrameworkProgram, updateTrainingFrameworkProgram, deleteTrainingFrameworkProgram, // Catalogs listFocusAreas, createFocusArea, updateFocusArea, deleteFocusArea, getAdminHierarchy, listStyleDirections, listTrainingStyles, createStyleDirection, updateStyleDirection, deleteStyleDirection, listTrainingCharacters, createTrainingCharacter, updateTrainingCharacter, deleteTrainingCharacter, listTrainingTypes, createTrainingType, updateTrainingType, deleteTrainingType, listSkillMainCategories, createSkillMainCategory, updateSkillMainCategory, deleteSkillMainCategory, listSkillCategories, createSkillCategory, updateSkillCategory, deleteSkillCategory, listTrainerFocusAreas, assignTrainerFocusArea, deleteTrainerFocusArea, listTargetGroups, createTargetGroup, updateTargetGroup, deleteTargetGroup, listStyleDirectionTargetGroups, createStyleDirectionTargetGroup, updateStyleDirectionTargetGroup, deleteStyleDirectionTargetGroup, listMaturityModels, listMaturityModelContextBindings, upsertMaturityModelContextBinding, deleteMaturityModelContextBinding, importMaturityModelBundle, exportMaturityModelBundle, exportResolvedMaturityBundle, exportMatrixStackBundle, importMatrixStackBundle, resolveMaturityModel, getMaturityModel, createMaturityModel, updateMaturityModel, deleteMaturityModel, addMaturityModelSkill, removeMaturityModelSkill, upsertMaturityModelSkillLevels, getStyleDirectionsHierarchy, listTrainerContexts, createTrainerContext, updateTrainerContext, deleteTrainerContext, // System getVersion, healthCheck, // MediaWiki Import previewMediaWikiImport: (category, importType = 'exercise', limit = 10) => request(`/api/import/mediawiki/preview?category=${encodeURIComponent(category)}&import_type=${importType}&limit=${limit}`), executeMediaWikiImport: (data) => request('/api/import/mediawiki/execute', { method: 'POST', body: JSON.stringify(data) }), getMediaWikiImportStatus: (logId) => request(`/api/import/mediawiki/status/${logId}`), listMediaWikiImportLogs: () => request('/api/import/mediawiki/logs'), deleteMediaWikiImportReference: (refId) => request(`/api/import/mediawiki/references/${refId}`, { method: 'DELETE' }), // Legal Documents (public) getPublishedLegalDocument: (documentType) => request(`/api/legal-documents/${documentType}/published`), // Legal Documents (superadmin) listLegalDocuments: (documentType) => request(`/api/admin/legal-documents${documentType ? `?document_type=${documentType}` : ''}`), createLegalDocument: (data) => request('/api/admin/legal-documents', { method: 'POST', body: JSON.stringify(data) }), getLegalDocument: (id) => request(`/api/admin/legal-documents/${id}`), updateLegalDocument: (id, data) => request(`/api/admin/legal-documents/${id}`, { method: 'PUT', body: JSON.stringify(data) }), publishLegalDocument: (id, changeNote) => request(`/api/admin/legal-documents/${id}/publish`, { method: 'POST', body: JSON.stringify({ change_note: changeNote }), }), archiveLegalDocument: (id) => request(`/api/admin/legal-documents/${id}/archive`, { method: 'POST' }), copyLegalDocumentAsDraft: (id) => request(`/api/admin/legal-documents/${id}/copy-as-draft`, { method: 'POST' }), getLegalDocumentAudit: (id) => request(`/api/admin/legal-documents/${id}/audit`), // P-13: Content-Melde-Backend submitContentReport: (body) => request('/api/content-reports', { method: 'POST', body: JSON.stringify(body) }), getInboxContentReports: () => request('/api/me/inbox/content-reports'), getContentReport: (id) => request(`/api/content-reports/${id}`), patchContentReport: (id, body) => request(`/api/content-reports/${id}`, { method: 'PATCH', body: JSON.stringify(body) }), setLegalHoldFromReport: (id, body) => request(`/api/content-reports/${id}/legal-hold`, { method: 'POST', body: JSON.stringify(body) }), } export default api