All checks were successful
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 24s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Successful in 29s
- Added a new API endpoint for bulk uploading media assets, allowing users to upload multiple files in a single request. - Implemented validation for file types and sizes during the upload process, ensuring compliance with allowed formats and limits. - Enhanced the MediaLibraryPage component to support bulk file selection and visibility options, improving user experience. - Updated CSS styles for the upload interface to enhance layout and accessibility. - Added tests to verify the functionality of the new bulk upload feature and its integration with existing media asset management.
1479 lines
43 KiB
JavaScript
1479 lines
43 KiB
JavaScript
/**
|
||
* Shinkan Jinkendo API Client
|
||
*
|
||
* Zentrale API-Kommunikation mit automatischer Token-Injektion
|
||
*/
|
||
|
||
import { stripHtmlToText } from './htmlUtils'
|
||
|
||
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),
|
||
})
|
||
}
|
||
|
||
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({}),
|
||
})
|
||
}
|
||
|
||
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 }))
|
||
|
||
return {
|
||
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,
|
||
...extras,
|
||
}
|
||
}
|
||
|
||
export async function uploadExerciseMedia(exerciseId, formData) {
|
||
const token = localStorage.getItem('authToken')
|
||
const headers = {}
|
||
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 err = await response.json().catch(() => ({ detail: 'Unknown error' }))
|
||
const d = err.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
|
||
}
|
||
const msg =
|
||
typeof d === 'string'
|
||
? d
|
||
: d != null && typeof d === 'object' && typeof d.message === 'string'
|
||
? d.message
|
||
: d != null
|
||
? JSON.stringify(d)
|
||
: `HTTP ${response.status}`
|
||
throw new Error(msg)
|
||
}
|
||
return response.json()
|
||
}
|
||
|
||
export async function updateExerciseMedia(exerciseId, mediaId, data) {
|
||
return request(`/api/exercises/${exerciseId}/media/${mediaId}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function deleteExerciseMedia(exerciseId, mediaId) {
|
||
return request(`/api/exercises/${exerciseId}/media/${mediaId}`, { method: 'DELETE' })
|
||
}
|
||
|
||
export async function reorderExerciseMedia(exerciseId, mediaIds) {
|
||
return request(`/api/exercises/${exerciseId}/media/reorder`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ media_ids: mediaIds }),
|
||
})
|
||
}
|
||
|
||
/** Papierkorb / Recovery — `action`: trash_soft | trash_hidden | recover | purge | reactivate */
|
||
export async function postMediaAssetLifecycle(assetId, action, extra = {}) {
|
||
return request(`/api/media-assets/${assetId}/lifecycle`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ action, ...extra }),
|
||
})
|
||
}
|
||
|
||
/** Archiv: aktive media_assets sichtbar für den Nutzer (Bibliotheksrechte). */
|
||
export async function listMediaAssets(params = {}) {
|
||
const sp = new URLSearchParams()
|
||
if (params.q) sp.set('q', params.q)
|
||
if (params.limit != null) sp.set('limit', String(params.limit))
|
||
if (params.offset != null) sp.set('offset', String(params.offset))
|
||
if (params.lifecycle) sp.set('lifecycle', String(params.lifecycle))
|
||
if (params.media_kind) sp.set('media_kind', String(params.media_kind))
|
||
if (params.club_id != null && params.club_id !== '') sp.set('club_id', String(params.club_id))
|
||
if (params.uploaded_by != null && params.uploaded_by !== '') sp.set('uploaded_by', String(params.uploaded_by))
|
||
if (params.include_filter_meta) sp.set('include_filter_meta', 'true')
|
||
const qs = sp.toString()
|
||
return request(`/api/media-assets${qs ? `?${qs}` : ''}`)
|
||
}
|
||
|
||
export async function patchMediaAsset(assetId, data) {
|
||
return request(`/api/media-assets/${assetId}`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function bulkMediaLifecycle(data) {
|
||
return request('/api/media-assets/bulk-lifecycle', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function bulkPatchMediaAssets(data) {
|
||
return request('/api/media-assets/bulk-patch', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Mehrere Dateien ins Medienarchiv (`POST /api/media-assets/bulk-upload`).
|
||
* @param {File[]} files
|
||
* @param {{ visibility?: string, club_id?: number }} [options]
|
||
*/
|
||
export async function bulkUploadMediaAssets(files, options = {}) {
|
||
const visibility = options.visibility || 'private'
|
||
const token = localStorage.getItem('authToken')
|
||
const headers = mergeActiveClubHeader({})
|
||
if (token) headers['X-Auth-Token'] = token
|
||
const formData = new FormData()
|
||
formData.append('visibility', String(visibility))
|
||
if (options.club_id != null && options.club_id !== '') {
|
||
formData.append('club_id', String(options.club_id))
|
||
}
|
||
const arr = Array.isArray(files) ? files : [files]
|
||
for (const f of arr) {
|
||
if (f) formData.append('files', f)
|
||
}
|
||
const url = `${API_URL}/api/media-assets/bulk-upload`
|
||
const response = await fetch(url, {
|
||
method: 'POST',
|
||
headers,
|
||
body: formData,
|
||
})
|
||
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))
|
||
}
|
||
const snippet = (text || '').replace(/\s+/g, ' ').trim().slice(0, 180)
|
||
throw new Error(snippet ? `HTTP ${response.status}: ${snippet}` : `HTTP ${response.status}`)
|
||
}
|
||
return response.json()
|
||
}
|
||
|
||
/** Übung: bestehendes Archiv-Medium verknüpfen (`POST /api/exercises/{id}/media/from-asset`). */
|
||
export async function attachExerciseMediaFromAsset(exerciseId, body) {
|
||
return request(`/api/exercises/${exerciseId}/media/from-asset`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(body),
|
||
})
|
||
}
|
||
|
||
export async function getExercise(id) {
|
||
return request(`/api/exercises/${id}`)
|
||
}
|
||
|
||
export async function createExercise(data) {
|
||
return request('/api/exercises', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data)
|
||
})
|
||
}
|
||
|
||
export async function updateExercise(id, data) {
|
||
const token = localStorage.getItem('authToken')
|
||
const headers = mergeActiveClubHeader({ 'Content-Type': 'application/json' })
|
||
if (token) headers['X-Auth-Token'] = token
|
||
const url = `${API_URL}/api/exercises/${id}`
|
||
const response = await fetch(url, {
|
||
method: 'PUT',
|
||
headers,
|
||
body: JSON.stringify(data),
|
||
})
|
||
if (!response.ok) {
|
||
const text = await response.text()
|
||
let parsed = null
|
||
try {
|
||
parsed = JSON.parse(text)
|
||
} catch {
|
||
parsed = null
|
||
}
|
||
const d = parsed?.detail
|
||
if (
|
||
response.status === 422 &&
|
||
d &&
|
||
typeof d === 'object' &&
|
||
!Array.isArray(d) &&
|
||
typeof d.code === 'string'
|
||
) {
|
||
const e = new Error(typeof d.message === 'string' ? d.message : 'Validierung fehlgeschlagen')
|
||
e.status = 422
|
||
e.code = d.code
|
||
e.payload = d
|
||
throw e
|
||
}
|
||
if (parsed?.detail != null) {
|
||
const msg = typeof parsed.detail === 'string' ? parsed.detail : JSON.stringify(parsed.detail)
|
||
throw new Error(msg)
|
||
}
|
||
const snippet = (text || '').replace(/\s+/g, ' ').trim().slice(0, 180)
|
||
throw new Error(snippet ? `HTTP ${response.status}: ${snippet}` : `HTTP ${response.status}`)
|
||
}
|
||
return response.json()
|
||
}
|
||
|
||
/** Massenänderung Übungen: Sichtbarkeit, Status, Katalog-Zuordnungen (`PATCH /api/exercises/bulk-metadata`). */
|
||
export async function bulkPatchExercisesMetadata(data) {
|
||
return request('/api/exercises/bulk-metadata', {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function deleteExercise(id) {
|
||
return request(`/api/exercises/${id}`, { method: 'DELETE' })
|
||
}
|
||
|
||
export async function createExerciseVariant(exerciseId, data) {
|
||
return request(`/api/exercises/${exerciseId}/variants`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function updateExerciseVariant(exerciseId, variantId, data) {
|
||
return request(`/api/exercises/${exerciseId}/variants/${variantId}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function deleteExerciseVariant(exerciseId, variantId) {
|
||
return request(`/api/exercises/${exerciseId}/variants/${variantId}`, { method: 'DELETE' })
|
||
}
|
||
|
||
export async function reorderExerciseVariants(exerciseId, variantIds) {
|
||
return request(`/api/exercises/${exerciseId}/variants/reorder`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ variant_ids: variantIds }),
|
||
})
|
||
}
|
||
|
||
// Progressionsgraphen (Übung → Übung), Migration 032/033
|
||
export async function listExerciseProgressionGraphs() {
|
||
return request('/api/exercise-progression-graphs')
|
||
}
|
||
|
||
export async function getExerciseProgressionGraph(id, { includeEdges = false } = {}) {
|
||
const q = includeEdges ? '?include_edges=true' : ''
|
||
return request(`/api/exercise-progression-graphs/${id}${q}`)
|
||
}
|
||
|
||
export async function createExerciseProgressionGraph(data) {
|
||
return request('/api/exercise-progression-graphs', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function updateExerciseProgressionGraph(id, data) {
|
||
return request(`/api/exercise-progression-graphs/${id}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function deleteExerciseProgressionGraph(id) {
|
||
return request(`/api/exercise-progression-graphs/${id}`, { method: 'DELETE' })
|
||
}
|
||
|
||
export async function listExerciseProgressionEdges(graphId, query = {}) {
|
||
const q = new URLSearchParams()
|
||
if (query.from_exercise_id != null) q.set('from_exercise_id', String(query.from_exercise_id))
|
||
if (query.to_exercise_id != null) q.set('to_exercise_id', String(query.to_exercise_id))
|
||
const qs = q.toString()
|
||
return request(`/api/exercise-progression-graphs/${graphId}/edges${qs ? `?${qs}` : ''}`)
|
||
}
|
||
|
||
export async function createExerciseProgressionEdge(graphId, data) {
|
||
return request(`/api/exercise-progression-graphs/${graphId}/edges`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function updateExerciseProgressionEdge(graphId, edgeId, data) {
|
||
return request(`/api/exercise-progression-graphs/${graphId}/edges/${edgeId}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function deleteExerciseProgressionEdge(graphId, edgeId) {
|
||
return request(`/api/exercise-progression-graphs/${graphId}/edges/${edgeId}`, { method: 'DELETE' })
|
||
}
|
||
|
||
export async function createExerciseProgressionSequence(graphId, data) {
|
||
return request(`/api/exercise-progression-graphs/${graphId}/edges/sequence`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
})
|
||
}
|
||
|
||
export async function deleteExerciseProgressionEdgesBatch(graphId, edgeIds) {
|
||
return request(`/api/exercise-progression-graphs/${graphId}/edges/delete-batch`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ edge_ids: edgeIds }),
|
||
})
|
||
}
|
||
|
||
/** KI-Ausbaustufe (EXERCISES_API_SPEC): benötigt Backend + z. B. OPENROUTER_API_KEY. */
|
||
export async function suggestExerciseAi(payload) {
|
||
return request('/api/exercises/ai/suggest', {
|
||
method: 'POST',
|
||
body: JSON.stringify(payload),
|
||
})
|
||
}
|
||
|
||
export async function regenerateExerciseAi(exerciseId, payload) {
|
||
return request(`/api/exercises/${exerciseId}/ai/regenerate`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(payload),
|
||
})
|
||
}
|
||
|
||
// ============================================================================
|
||
// Catalogs (Admin-verwaltbare Stammdaten)
|
||
// ============================================================================
|
||
|
||
// Focus Areas
|
||
export async function listFocusAreas(filters = {}) {
|
||
const query = new URLSearchParams(filters).toString()
|
||
return request(`/api/focus-areas${query ? '?' + query : ''}`)
|
||
}
|
||
|
||
export async function createFocusArea(data) {
|
||
return request('/api/focus-areas', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data)
|
||
})
|
||
}
|
||
|
||
export async function updateFocusArea(id, data) {
|
||
return request(`/api/focus-areas/${id}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data)
|
||
})
|
||
}
|
||
|
||
export async function deleteFocusArea(id) {
|
||
return request(`/api/focus-areas/${id}`, { method: 'DELETE' })
|
||
}
|
||
|
||
// Admin Hierarchy (Tree View)
|
||
export async function getAdminHierarchy() {
|
||
return request('/api/admin/hierarchy')
|
||
}
|
||
|
||
// ============================================================================
|
||
// Reifegradmodelle / Fähigkeitsmatrix
|
||
// ============================================================================
|
||
|
||
export async function listMaturityModels(filters = {}) {
|
||
const query = new URLSearchParams(
|
||
Object.entries(filters).filter(([, v]) => 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 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,
|
||
changePassword,
|
||
verifyEmail,
|
||
resendVerification,
|
||
|
||
// Clubs & Groups
|
||
listClubs,
|
||
getClub,
|
||
createClub,
|
||
updateClub,
|
||
deleteClub,
|
||
listClubMembers,
|
||
getClubMember,
|
||
addClubMember,
|
||
updateClubMember,
|
||
removeClubMember,
|
||
listPublicClubsDirectory,
|
||
clubMembersDirectory,
|
||
getMyClubJoinRequests,
|
||
createClubJoinRequest,
|
||
withdrawClubJoinRequest,
|
||
listClubJoinRequests,
|
||
acceptClubJoinRequest,
|
||
rejectClubJoinRequest,
|
||
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,
|
||
attachExerciseMediaFromAsset,
|
||
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,
|
||
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' })
|
||
}
|
||
|
||
export default api
|