/* ============================================================ BAN YARO — API Client Zentraler Eingang für ALLE Backend-Kommunikation. Kein fetch() wird außerhalb dieser Datei aufgerufen. ============================================================ */ const API = (() => { // ---------------------------------------------------------- // Request-Deduplication: gleiche GET-URL nur einmal in-flight // ---------------------------------------------------------- const _inflight = new Map(); // ---------------------------------------------------------- // Interner HTTP-Kern // ---------------------------------------------------------- async function _doRequest(method, path, body, attempt) { const config = { method, headers: { 'Content-Type': 'application/json' }, credentials: 'include', }; if (body && !(body instanceof FormData)) { config.body = JSON.stringify(body); } else if (body instanceof FormData) { delete config.headers['Content-Type']; config.body = body; } const token = localStorage.getItem('by_token'); if (token) config.headers['Authorization'] = `Bearer ${token}`; let response; try { response = await fetch(`/api${path}`, config); } catch { // Netzwerkfehler: bei GET bis zu 2 Retry-Versuche if (method === 'GET' && attempt < 2) { await new Promise(r => setTimeout(r, 200 * Math.pow(3, attempt))); return _doRequest(method, path, body, attempt + 1); } const msg = 'Kein Internet — du bist offline.'; if (window.UI?.toast) UI.toast.warning(msg, 4000); throw new APIError(msg, 0, 'network'); } // Versions-Check: Server meldet neue Version → Banner anzeigen (einmalig) const serverVer = response.headers.get('x-app-version'); if (serverVer && serverVer !== APP_VER && !window._byUpdatePending) { window._byUpdatePending = true; // App._showUpdateBanner wird aufgerufen sobald App initialisiert ist setTimeout(() => window.App?._triggerUpdateBanner?.(serverVer), 0); } if (response.status === 204) return null; let data; try { data = await response.json(); } catch { data = null; } if (!response.ok) { const message = data?.detail || data?.message || `Fehler ${response.status}`; const isSwOffline = response.status === 503 && message.startsWith('Offline'); // Retry: GET auf echte 5xx (nicht SW-generierte Offline-503) if (method === 'GET' && response.status >= 500 && !isSwOffline && attempt < 2) { await new Promise(r => setTimeout(r, 200 * Math.pow(3, attempt))); return _doRequest(method, path, body, attempt + 1); } if (isSwOffline && window.UI?.toast) UI.toast.warning('Kein Internet — du bist offline.', 4000); throw new APIError(message, response.status, isSwOffline ? 'network' : data?.code); } if (data?._queued) { if (typeof UI !== 'undefined' && UI.toast) UI.toast.info('Offline gespeichert — wird automatisch synchronisiert'); return data; } return data; } async function _request(method, path, body = null) { // GET-Deduplication: laufende identische Anfragen zusammenfassen if (method === 'GET') { if (_inflight.has(path)) return _inflight.get(path); const promise = _doRequest('GET', path, null, 0).finally(() => _inflight.delete(path)); _inflight.set(path, promise); return promise; } return _doRequest(method, path, body, 0); } // ---------------------------------------------------------- // Öffentliche HTTP-Methoden // ---------------------------------------------------------- const get = (path) => _request('GET', path); const post = (path, body) => _request('POST', path, body); const put = (path, body) => _request('PUT', path, body); const patch = (path, body) => _request('PATCH', path, body); const del = (path) => _request('DELETE', path); const upload = (path, form) => _request('POST', path, form); // FormData // ---------------------------------------------------------- // AUTH // ---------------------------------------------------------- const auth = { login(email, password) { return post('/auth/login', { email, password }); }, register(email, password, name, ref_code) { const body = { email, password, name }; if (ref_code) body.ref_code = ref_code; return post('/auth/register', body); }, logout() { localStorage.removeItem('by_token'); return post('/auth/logout'); }, me() { return get('/auth/me'); }, referral: () => get('/auth/referral'), }; // ---------------------------------------------------------- // HUNDE-PROFIL // ---------------------------------------------------------- const dogs = { list() { return get('/dogs'); }, get(id) { return get(`/dogs/${id}`); }, create(data) { return post('/dogs', data); }, update(id, data) { return patch(`/dogs/${id}`, data); }, delete(id) { return del(`/dogs/${id}`); }, uploadPhoto(id, formData) { return upload(`/dogs/${id}/photo`, formData); }, updatePhotoPosition(id, zoom, offsetX, offsetY) { return patch(`/dogs/${id}/photo-position`, { zoom, offset_x: offsetX, offset_y: offsetY }); }, deletePhoto(id) { return del(`/dogs/${id}/photo`); }, getSkills(id) { return get(`/dogs/${id}/skills`); }, welcomeDashboard(dogId) { return get(`/dogs/${dogId}/welcome-dashboard`); }, }; // ---------------------------------------------------------- // TAGEBUCH // ---------------------------------------------------------- const diary = { list(dogId, params = {}) { const q = new URLSearchParams(params).toString(); return get(`/dogs/${dogId}/diary${q ? '?' + q : ''}`); }, stats(dogId) { return get(`/dogs/${dogId}/diary/stats`); }, get(dogId, entryId) { return get(`/dogs/${dogId}/diary/${entryId}`); }, create(dogId, data) { return post(`/dogs/${dogId}/diary`, data); }, update(dogId, id, data){ return patch(`/dogs/${dogId}/diary/${id}`, data); }, delete(dogId, id) { return del(`/dogs/${dogId}/diary/${id}`); }, uploadMedia(dogId, id, formData) { return upload(`/dogs/${dogId}/diary/${id}/media`, formData); }, deleteMedia(dogId, id) { return del(`/dogs/${dogId}/diary/${id}/media`); }, deleteMediaItem(dogId, entryId, mediaId) { return del(`/dogs/${dogId}/diary/${entryId}/media/${mediaId}`); }, setCover(dogId, entryId, mediaId) { return patch(`/dogs/${dogId}/diary/${entryId}/media/${mediaId}/cover`, {}); }, nearby(dogId, lat, lon) { return get(`/dogs/${dogId}/diary/nearby?lat=${lat}&lon=${lon}`); }, locations(dogId) { return get(`/dogs/${dogId}/diary/locations`); }, calendar(dogId) { return get(`/dogs/${dogId}/diary/calendar`); }, }; // ---------------------------------------------------------- // GESUNDHEIT // ---------------------------------------------------------- const health = { list(dogId, typ = null) { const q = typ ? `?typ=${encodeURIComponent(typ)}` : ''; return get(`/dogs/${dogId}/health${q}`); }, create(dogId, data) { return post(`/dogs/${dogId}/health`, data); }, update(dogId, id, d) { return patch(`/dogs/${dogId}/health/${id}`, d); }, delete(dogId, id) { return del(`/dogs/${dogId}/health/${id}`); }, uploadDokument(dogId, id, formData) { return upload(`/dogs/${dogId}/health/${id}/dokument`, formData); }, deleteDocument(dogId, id) { return del(`/dogs/${dogId}/health/${id}/dokument`); }, uploadMedia(dogId, entryId, formData) { return upload(`/dogs/${dogId}/health/${entryId}/media`, formData); }, deleteMedia(dogId, entryId, mediaId) { return del(`/dogs/${dogId}/health/${entryId}/media/${mediaId}`); }, kiZusammenfassung(dogId) { return post(`/dogs/${dogId}/health/ki-zusammenfassung`); }, kiBerichte(dogId) { return get(`/dogs/${dogId}/health/ki-berichte`); }, terminvorschlaege(dogId) { return get(`/dogs/${dogId}/health/terminvorschlaege`); }, symptomCheck(dogId, symptoms) { return post(`/dogs/${dogId}/health/symptom-check`, { symptoms }); }, gewichtVerlauf(dogId) { return get(`/dogs/${dogId}/health/gewicht`); }, }; // ---------------------------------------------------------- // TIERÄRZTE // ---------------------------------------------------------- const tieraerzte = { list() { return get('/tieraerzte'); }, create(data) { return post('/tieraerzte', data); }, update(id, d) { return patch(`/tieraerzte/${id}`, d); }, osmNearby(lat, lon) { return get(`/tieraerzte/osm-nearby?lat=${lat}&lon=${lon}`); }, myFavorite() { return get('/tieraerzte/my-favorite'); }, toggleFavorite(id) { return post(`/tieraerzte/${id}/favorite`); }, bewertungen(id) { return get(`/tieraerzte/${id}/bewertungen`); }, meineBewertung(id) { return get(`/tieraerzte/${id}/meine-bewertung`); }, bewertungAbgeben(id, data) { return post(`/tieraerzte/${id}/bewertung`, data); }, }; // ---------------------------------------------------------- // GESUNDHEITSDOKUMENTE // ---------------------------------------------------------- const healthDocs = { list(dogId) { return get(`/health-docs?dog_id=${dogId}`); }, upload(formData) { return upload('/health-docs/upload', formData); }, delete(id) { return del(`/health-docs/${id}`); }, }; // ---------------------------------------------------------- // GIFTKÖDER-ALARM // ---------------------------------------------------------- const poison = { listNearby(lat, lon, radius = 5000) { return get(`/poison?lat=${lat}&lon=${lon}&radius=${radius}`); }, report(data) { return post('/poison', data); }, confirm(id) { return post(`/poison/${id}/confirm`); }, resolve(id, data={}) { return post(`/poison/${id}/resolve`, data); }, uploadPhoto(id, form){ return upload(`/poison/${id}/photo`, form); }, }; // ---------------------------------------------------------- // KARTE & ORTE // ---------------------------------------------------------- const places = { list(typ = null) { return get(`/places${typ ? '?typ=' + encodeURIComponent(typ) : ''}`); }, listNearby(lat, lon, typ = null, radius = 5000) { const params = new URLSearchParams({ lat, lon, radius }); if (typ) params.set('typ', typ); return get(`/places?${params}`); }, get(id) { return get(`/places/${id}`); }, create(data) { return post('/places', data); }, update(id, data) { return patch(`/places/${id}`, data); }, delete(id) { return del(`/places/${id}`); }, }; // ---------------------------------------------------------- // GASSI-ROUTEN // ---------------------------------------------------------- const routes = { list() { return get('/routes'); }, listNearby(lat, lon, radius = 10000) { return get(`/routes?lat=${lat}&lon=${lon}&radius=${radius}`); }, get(id) { return get(`/routes/${id}`); }, create(data) { return post('/routes', data); }, update(id, data) { return patch(`/routes/${id}`, data); }, trim(id, gps_track) { return patch(`/routes/${id}/trim`, { gps_track }); }, feedback(id, text) { return post(`/routes/${id}/feedback`, { text }); }, elevation(id) { return get(`/routes/${id}/elevation`); }, delete(id) { return del(`/routes/${id}`); }, rate(id, wertung) { return post(`/routes/${id}/rate`, { wertung }); }, walked(id, walked_km, progress_pct) { return post(`/routes/${id}/walked`, { walked_km, progress_pct }); }, reverse(id) { return post(`/routes/${id}/reverse`, {}); }, addPhoto(id, file) { const fd = new FormData(); fd.append('file', file); return fetch(`/api/routes/${id}/photo`, { method: 'POST', credentials: 'include', body: fd, }).then(r => r.ok ? r.json() : Promise.reject(new Error('Foto-Upload fehlgeschlagen'))); }, }; // ---------------------------------------------------------- // TRAINING & ÜBUNGSFORTSCHRITT // ---------------------------------------------------------- const training = { getProgress() { return get('/training/progress'); }, setProgress(id, status) { return post('/training/progress', { exercise_id: id, status }); }, getSuggestions() { return get('/training/suggestions'); }, getPlanProgress() { return get('/training/plan-progress'); }, setPlanProgress(key, checked) { return post('/training/plan-progress', { item_key: key, checked }); }, getRecommendations(dogId) { return get(`/training/recommendations?dog_id=${dogId}`); }, }; // ---------------------------------------------------------- // GASSI-TREFFEN // ---------------------------------------------------------- const walks = { list(lat = null, lon = null, radius = 20000) { const params = new URLSearchParams({ radius }); if (lat !== null) { params.set('lat', lat); params.set('lon', lon); } return get(`/walks?${params}`); }, get(id) { return get(`/walks/${id}`); }, create(data) { return post('/walks', data); }, update(id, data) { return patch(`/walks/${id}`, data); }, cancel(id) { return del(`/walks/${id}`); }, join(id, dogIds) { return post(`/walks/${id}/join`, { dog_ids: dogIds }); }, leave(id) { return del(`/walks/${id}/join`); }, nearby(lat, lon) { return get(`/walks/nearby?lat=${lat}&lon=${lon}`); }, inviteCandidates(id) { return get(`/walks/${id}/invite-candidates`); }, invite(id, friendId) { return post(`/walks/${id}/invite`, { friend_id: friendId }); }, rsvp(id, status) { return post(`/walks/${id}/rsvp`, { status }); }, participants(id) { return get(`/walks/${id}/participants`); }, }; // ---------------------------------------------------------- // EVENTS // ---------------------------------------------------------- const events = { list(params = {}) { const q = new URLSearchParams(params).toString(); return get(`/events${q ? '?' + q : ''}`); }, get(id) { return get(`/events/${id}`); }, create(data) { return post('/events', data); }, update(id, data) { return patch(`/events/${id}`, data); }, delete(id) { return del(`/events/${id}`); }, rsvp(id, status) { return post(`/events/${id}/rsvp`, { status }); }, cancelRsvp(id) { return del(`/events/${id}/rsvp`); }, listRsvp(id) { return get(`/events/${id}/rsvp`); }, }; // ---------------------------------------------------------- // SITTING // ---------------------------------------------------------- const sitting = { list(lat = null, lon = null, radius = 30000, service = null) { const p = new URLSearchParams({ radius }); if (lat !== null) { p.set('lat', lat); p.set('lon', lon); } if (service) { p.set('service', service); } return get(`/sitting?${p}`); }, me() { return get('/sitting/me'); }, create(data) { return post('/sitting', data); }, updateMe(data) { return patch('/sitting/me', data); }, requests() { return get('/sitting/requests'); }, inbox() { return get('/sitting/inbox'); }, sendRequest(data){ return post('/sitting/requests', data); }, updateRequest(id, status) { return patch(`/sitting/requests/${id}`, { status }); }, }; // ---------------------------------------------------------- // RATINGS // ---------------------------------------------------------- const ratings = { rate(targetType, targetId, stars, kommentar = null) { return post('/ratings', { target_type: targetType, target_id: targetId, stars, kommentar }); }, list(targetType, targetId) { return get(`/ratings/${targetType}/${targetId}`); }, mine(targetType, targetId) { return get(`/ratings/me/${targetType}/${targetId}`); }, }; // ---------------------------------------------------------- // FORUM // ---------------------------------------------------------- const forum = { threads(params = {}) { const q = new URLSearchParams(params).toString(); return get(`/forum/threads${q ? '?' + q : ''}`); }, thread(id) { return get(`/forum/threads/${id}`); }, create(data) { return post('/forum/threads', data); }, deleteThread(id) { return del(`/forum/threads/${id}`); }, patchThread(id, data) { return patch(`/forum/threads/${id}`, data); }, addPost(threadId, data){ return post(`/forum/threads/${threadId}/posts`, data); }, deletePost(id) { return del(`/forum/posts/${id}`); }, updateThread(id, data) { return patch(`/forum/threads/${id}/content`, data); }, updatePost(id, data) { return patch(`/forum/posts/${id}`, data); }, uploadThreadFoto(id, file) { const fd = new FormData(); fd.append('file', file); return upload(`/forum/threads/${id}/fotos`, fd); }, uploadPostFoto(id, file) { const fd = new FormData(); fd.append('file', file); return upload(`/forum/posts/${id}/fotos`, fd); }, like(targetType, targetId) { return post('/forum/like', { target_type: targetType, target_id: targetId }); }, report(targetType, targetId, grund) { return post('/forum/report', { target_type: targetType, target_id: targetId, grund }); }, reports() { return get('/forum/reports'); }, resolveReport(id) { return patch(`/forum/reports/${id}`, { resolved: 1 }); }, membersMap() { return get('/forum/members/map'); }, setLocation(lat, lon, show) { return patch('/forum/members/location', { lat, lon, show }); }, search(q) { return get(`/forum/search?q=${encodeURIComponent(q)}`); }, // Legacy aliases (keep old names working) listThreads(params = {}) { return forum.threads(params); }, getThread(id) { return forum.thread(id); }, createThread(data) { return forum.create(data); }, createPost(tid, data) { return forum.addPost(tid, data); }, }; // ---------------------------------------------------------- // VERLORENER HUND // ---------------------------------------------------------- const lost = { list(lat = null, lon = null, radius_km = 25) { const params = new URLSearchParams({ radius_km }); if (lat !== null) { params.set('lat', lat); params.set('lon', lon); } return get(`/lost?${params}`); }, report(data) { return post('/lost', data); }, uploadFoto(id, form) { return upload(`/lost/${id}/foto`, form); }, markFound(id) { return post(`/lost/${id}/found`); }, delete(id) { return del(`/lost/${id}`); }, }; // ---------------------------------------------------------- // KNIGGE // ---------------------------------------------------------- const knigge = { vote: (szenario_id, answer) => post('/knigge/vote', { szenario_id, answer }), votes: (szenario_id) => get(`/knigge/votes?szenario_id=${encodeURIComponent(szenario_id)}`), kiRat: (situation) => post('/knigge/ki-rat', { situation }), }; // ---------------------------------------------------------- // WETTER // ---------------------------------------------------------- const weather = { alerts(lat, lon) { return get(`/weather/alerts?lat=${lat}&lon=${lon}`); }, get(lat, lon) { return get(`/weather?lat=${lat}&lon=${lon}`); }, forecast(lat, lon) { return get(`/weather/forecast?lat=${lat}&lon=${lon}`); }, }; // ---------------------------------------------------------- // FREUNDE // ---------------------------------------------------------- const friends = { list() { return get('/friends/'); }, search(q) { return get(`/friends/search?q=${encodeURIComponent(q)}`); }, activity() { return get('/friends/activity'); }, sendRequest(userId) { return post(`/friends/request/${userId}`, {}); }, accept(friendshipId) { return post(`/friends/${friendshipId}/accept`, {}); }, decline(friendshipId) { return post(`/friends/${friendshipId}/decline`, {}); }, remove(friendUserId) { return del(`/friends/${friendUserId}`); }, }; // ---------------------------------------------------------- // DIREKTNACHRICHTEN // ---------------------------------------------------------- const chat = { conversations() { return get('/chat/conversations'); }, start(partnerId) { return post('/chat/conversations', { partner_id: partnerId }); }, messages(convId, offset=0) { return get(`/chat/conversations/${convId}?offset=${offset}`); }, send(convId, text) { return post(`/chat/conversations/${convId}/messages`, { text }); }, markRead(convId) { return post(`/chat/conversations/${convId}/read`, {}); }, deleteMessage(msgId) { return del(`/chat/messages/${msgId}`); }, uploadPhoto(convId, file) { const fd = new FormData(); fd.append('file', file); return upload(`/chat/conversations/${convId}/upload`, fd); }, heartbeat() { return post('/chat/heartbeat', {}); }, }; // ---------------------------------------------------------- // PUSH NOTIFICATIONS // ---------------------------------------------------------- const push = { getVapidKey() { return get('/push/vapid-key'); }, subscribe(subscription) { return post('/push/subscribe', subscription); }, unsubscribe() { return del('/push/subscribe'); }, }; // ---------------------------------------------------------- // PUSH-SUBSCRIPTION HELPER // ---------------------------------------------------------- async function subscribeToPush() { if (!('serviceWorker' in navigator) || !('PushManager' in window)) return null; const permission = await Notification.requestPermission(); if (permission !== 'granted') return null; const { vapid_public_key } = await push.getVapidKey(); const reg = await navigator.serviceWorker.ready; const subscription = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: _urlBase64ToUint8Array(vapid_public_key), }); await push.subscribe(subscription.toJSON()); return subscription; } function _urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const raw = atob(base64); return new Uint8Array([...raw].map(c => c.charCodeAt(0))); } // ---------------------------------------------------------- // GEOLOCATION HELPER // ---------------------------------------------------------- function getLocation(options = {}) { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error('Geolocation wird nicht unterstützt.')); return; } navigator.geolocation.getCurrentPosition( pos => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }), err => reject(err), { timeout: 8000, maximumAge: 60000, ...options } ); }); } // ---------------------------------------------------------- // WEBCAL // ---------------------------------------------------------- const webcal = { getToken: () => get('/webcal/token'), resetToken: () => del('/webcal/token'), }; const sharing = { create: (dogId, role) => post(`/dogs/${dogId}/share`, { role }), list: (dogId) => get(`/dogs/${dogId}/shares`), revoke: (dogId, id) => del(`/dogs/${dogId}/share/${id}`), accept: (token) => post(`/share/accept/${token}`, {}), info: (token) => get(`/share/info/${token}`), }; const widget = { snapshot: () => get('/widget/snapshot'), }; // ---------------------------------------------------------- // NOTIFICATIONS // ---------------------------------------------------------- const notifications = { list() { return get('/notifications'); }, unreadCount() { return get('/notifications/unread-count'); }, badge() { return get('/notifications/badge'); }, readAll() { return patch('/notifications/read-all', {}); }, read(id) { return patch(`/notifications/${id}/read`, {}); }, delete(id) { return del(`/notifications/${id}`); }, }; // ---------------------------------------------------------- // SERVICE-ANGEBOTE (Sitting & Walks Matching) // ---------------------------------------------------------- const services = { list(type, lat = null, lon = null, radius = 20) { const p = new URLSearchParams({ type, radius }); if (lat !== null) { p.set('lat', lat); p.set('lon', lon); } return get(`/services?${p}`); }, me() { return get('/services/me'); }, upsert(data) { return post('/services', data); }, deactivate(id) { return del(`/services/${id}`); }, }; // ---------------------------------------------------------- // GASTHUND-ZUGANG // ---------------------------------------------------------- const sittingAccess = { grant: (data) => post('/sitting-access', data), revoke: (id) => del(`/sitting-access/${id}`), my: () => get('/sitting-access/my'), }; const importData = { notestation(dogId, file) { const fd = new FormData(); fd.append('dog_id', dogId); fd.append('file', file); return upload('/import/notestation', fd); }, csv(dogId, file) { const fd = new FormData(); fd.append('dog_id', dogId); fd.append('file', file); return upload('/import/csv', fd); }, }; // ---------------------------------------------------------- // NOTIZEN // ---------------------------------------------------------- const notes = { get(parentType, parentId) { return get(`/notes/${parentType}/${parentId}`); }, getAll(params) { return get('/notes?' + new URLSearchParams(params || {}).toString()); }, analyse() { return post('/notes/ki-analyse', {}); }, create(parentType, parentId, data) { return post(`/notes/${parentType}/${parentId}`, data); }, update(id, data) { return patch(`/notes/${id}`, data); }, delete(id) { return del(`/notes/${id}`); }, }; // ---------------------------------------------------------- // ERROR-KLASSE // ---------------------------------------------------------- class APIError extends Error { constructor(message, status, code) { super(message); this.name = 'APIError'; this.status = status; this.code = code; } } // Lokale Gerätezeit als ISO-String ("2026-04-26T12:00:00") für server-seitige created_at function clientNow() { return new Date().toLocaleString('sv').replace(' ', 'T'); } // ---------------------------------------------------------- // ZÜCHTER // ---------------------------------------------------------- const breeder = { status() { return get('/breeder/status'); }, apply(form) { return upload('/breeder/apply', form); }, profile(zwingername) { return get(`/breeder/profil/${encodeURIComponent(zwingername)}`); }, mapMarkers() { return get('/breeder/map'); }, updateProfile(data) { return put('/breeder/profile', data); }, adminCreateProfile() { return post('/admin/breeder/create-profile', {}); }, pendingList() { return get('/admin/breeders/pending'); }, documents(userId) { return get(`/admin/breeder/${userId}/documents`); }, documentUrl(userId, docId) { return `/api/admin/breeder/${userId}/document/${docId}`; }, approve(userId) { return post(`/admin/breeder/${userId}/approve`, {}); }, reject(userId, grund) { return post(`/admin/breeder/${userId}/reject`, { grund }); }, }; // ---------------------------------------------------------- // WÜRFE (Züchter-Wurf-Verwaltung) // ---------------------------------------------------------- const litters = { // Züchter: eigene Würfe myList() { return get('/litters/my'); }, create(data) { return post('/litters', data); }, update(id, data) { return put(`/litters/${id}`, data); }, remove(id) { return del(`/litters/${id}`); }, welfareConfirm(id) { return post(`/litters/${id}/welfare-confirm`, {}); }, // Welpen puppies(id) { return get(`/litters/${id}/puppies`); }, addPuppy(id, data) { return post(`/litters/${id}/puppies`, data); }, updatePuppy(id, data) { return put(`/litters/puppies/${id}`, data); }, addWeight(id, data) { return post(`/litters/puppies/${id}/weight`, data); }, // Öffentlich public(params) { return get('/litters?' + new URLSearchParams(params || {}).toString()); }, detail(id) { return get(`/litters/${id}`); }, }; // ---------------------------------------------------------- // ZÜCHTER-FOTOS // ---------------------------------------------------------- const breederPhotos = { upload(form) { return upload('/breeder/photos/upload', form); }, list(entityType, entityId) { return get(`/photos/${entityType}/${entityId}`); }, updateVisibility(id, visibility) { return patch(`/breeder/photos/${id}/visibility`, { visibility }); }, setPrimary(id) { return patch(`/breeder/photos/${id}/primary`, {}); }, updateCaption(id, caption) { return patch(`/breeder/photos/${id}/caption`, { caption }); }, remove(id) { return del(`/breeder/photos/${id}`); }, }; // ---------------------------------------------------------- // ZUCHTKARTEI (Hunde-Stammdaten, Gesundheit, Genetik, Titel) // ---------------------------------------------------------- const zuchthunde = { list() { return get('/zuchthunde'); }, get(id) { return get(`/zuchthunde/${id}`); }, create(data) { return post('/zuchthunde', data); }, update(id, data) { return put(`/zuchthunde/${id}`, data); }, remove(id) { return del(`/zuchthunde/${id}`); }, pedigree(id, gen=4) { return get(`/zuchthunde/${id}/pedigree?generations=${gen}`); }, healthTests(id) { return get(`/zuchthunde/${id}/health-tests`); }, addHealthTest(id, data) { return post(`/zuchthunde/${id}/health-tests`, data); }, updateHealthTest(tid, data) { return put(`/zuchthunde/health-tests/${tid}`, data); }, deleteHealthTest(tid) { return del(`/zuchthunde/health-tests/${tid}`); }, geneticTests(id) { return get(`/zuchthunde/${id}/genetic-tests`); }, addGeneticTest(id, data) { return post(`/zuchthunde/${id}/genetic-tests`, data); }, updateGeneticTest(tid, data) { return put(`/zuchthunde/genetic-tests/${tid}`, data); }, deleteGeneticTest(tid) { return del(`/zuchthunde/genetic-tests/${tid}`); }, titles(id) { return get(`/zuchthunde/${id}/titles`); }, addTitle(id, data) { return post(`/zuchthunde/${id}/titles`, data); }, updateTitle(tid, data) { return put(`/zuchthunde/titles/${tid}`, data); }, deleteTitle(tid) { return del(`/zuchthunde/titles/${tid}`); }, trialMating(vaterId, mutterId) { return post('/zuchthunde/trial-mating', { vater_id: vaterId, mutter_id: mutterId }); }, }; // ---------------------------------------------------------- // ZÜCHTER-KI // ---------------------------------------------------------- const zuchtKi = { wurfankuendigung(litterId) { return post('/zucht-ki/wurfankuendigung', { litter_id: litterId }); }, genetikErklaerung(litterId, ziel) { return post('/zucht-ki/genetik-erklaerung', { litter_id: litterId, zielgruppe: ziel }); }, paarungAnalyse(vaterId, mutterId, ik, welfareLevel) { return post('/zucht-ki/paarung-analyse', { vater_id: vaterId, mutter_id: mutterId, ik_prozent: ik, welfare_level: welfareLevel }); }, hundBeschreibung(hundId) { return post('/zucht-ki/hund-beschreibung', { hund_id: hundId }); }, jahresbericht() { return post('/zucht-ki/jahresbericht', {}); }, jahresberichtList() { return get('/zucht-ki/jahresbericht'); }, jahresberichtGet(id) { return get(`/zucht-ki/jahresbericht/${id}`); }, }; const osm = { pois: (type, south, west, north, east) => get(`/osm/pois?type=${type}&south=${south}&west=${west}&north=${north}&east=${east}&fast=true`), }; // Öffentliche API return { get, post, put, patch, del, upload, auth, dogs, diary, health, tieraerzte, healthDocs, poison, places, routes, walks, events, sitting, forum, lost, knigge, weather, push, friends, chat, webcal, importData, sharing, widget, notifications, services, ratings, sittingAccess, training, notes, breeder, litters, breederPhotos, zuchthunde, zuchtKi, osm, subscribeToPush, getLocation, clientNow, APIError, }; })();