mitai-jinkendo/frontend/src/utils/api.js
Lars 02ca9772d6
All checks were successful
Deploy Development / deploy (push) Successful in 34s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
feat: add manual nutrition entry form with auto-detect
Features:
- Manual entry form above data list
- Date picker with auto-load existing entries
- Upsert logic: creates new or updates existing entry
- Smart button text: "Hinzufügen" vs "Aktualisieren"
- Prevents duplicate entries per day
- Feature enforcement for nutrition_entries

Backend:
- POST /nutrition - Create or update entry (upsert)
- GET /nutrition/by-date/{date} - Load entry by date
- Auto-detects existing entry and switches to UPDATE mode
- Increments usage counter only on INSERT

Frontend:
- EntryForm component with date picker + macros inputs
- Auto-loads data when date changes
- Shows info message when entry exists
- Success/error feedback
- Disabled state while loading/saving

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 08:37:01 +01:00

190 lines
8.4 KiB
JavaScript

import { getToken } from '../context/AuthContext'
let _profileId = null
export function setProfileId(id) { _profileId = id }
const BASE = '/api'
function hdrs(extra={}) {
const h = {...extra}
if (_profileId) h['X-Profile-Id'] = _profileId
const token = getToken()
if (token) h['X-Auth-Token'] = token
return h
}
async function req(path, opts={}) {
const res = await fetch(BASE+path, {...opts, headers:hdrs(opts.headers||{})})
if (!res.ok) { const err=await res.text(); throw new Error(err) }
return res.json()
}
const json=(d)=>({method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)})
const jput=(d)=>({method:'PUT', headers:{'Content-Type':'application/json'},body:JSON.stringify(d)})
export const api = {
// Profiles
getActiveProfile: () => req('/profile'),
listProfiles: () => req('/profiles'),
createProfile: (d) => req('/profiles', json(d)),
updateProfile: (id,d) => req(`/profiles/${id}`, jput(d)),
deleteProfile: (id) => req(`/profiles/${id}`, {method:'DELETE'}),
getProfile: () => req('/profile'),
updateActiveProfile:(d)=> req('/profile', jput(d)),
// Weight
listWeight: (l=365) => req(`/weight?limit=${l}`),
upsertWeight: (date,weight,note='') => req('/weight',json({date,weight,note})),
updateWeight: (id,date,weight,note='') => req(`/weight/${id}`,jput({date,weight,note})),
deleteWeight: (id) => req(`/weight/${id}`,{method:'DELETE'}),
weightStats: () => req('/weight/stats'),
// Circumferences
listCirc: (l=100) => req(`/circumferences?limit=${l}`),
upsertCirc: (d) => req('/circumferences',json(d)),
updateCirc: (id,d) => req(`/circumferences/${id}`,jput(d)),
deleteCirc: (id) => req(`/circumferences/${id}`,{method:'DELETE'}),
// Caliper
listCaliper: (l=100) => req(`/caliper?limit=${l}`),
upsertCaliper: (d) => req('/caliper',json(d)),
updateCaliper: (id,d) => req(`/caliper/${id}`,jput(d)),
deleteCaliper: (id) => req(`/caliper/${id}`,{method:'DELETE'}),
// Activity
listActivity: (l=200)=> req(`/activity?limit=${l}`),
createActivity: (d) => req('/activity',json(d)),
updateActivity: (id,d) => req(`/activity/${id}`,jput(d)),
deleteActivity: (id) => req(`/activity/${id}`,{method:'DELETE'}),
activityStats: () => req('/activity/stats'),
importActivityCsv: async(file)=>{
const fd=new FormData();fd.append('file',file)
const r=await fetch(`${BASE}/activity/import-csv`,{method:'POST',body:fd,headers:hdrs()})
const d=await r.json();if(!r.ok)throw new Error(d.detail||JSON.stringify(d));return d
},
// Photos
uploadPhoto: (file,date='')=>{
const fd=new FormData();fd.append('file',file);fd.append('date',date)
return fetch(`${BASE}/photos`,{method:'POST',body:fd,headers:hdrs()}).then(r=>r.json())
},
listPhotos: () => req('/photos'),
photoUrl: (pid) => {
const token = getToken()
return `${BASE}/photos/${pid}${token ? `?token=${token}` : ''}`
},
// Nutrition
importCsv: async(file)=>{
const fd=new FormData();fd.append('file',file)
const r=await fetch(`${BASE}/nutrition/import-csv`,{method:'POST',body:fd,headers:hdrs()})
const d=await r.json();if(!r.ok)throw new Error(d.detail||JSON.stringify(d));return d
},
listNutrition: (l=365) => req(`/nutrition?limit=${l}`),
nutritionCorrelations: () => req('/nutrition/correlations'),
nutritionWeekly: (w=16) => req(`/nutrition/weekly?weeks=${w}`),
nutritionImportHistory: () => req('/nutrition/import-history'),
getNutritionByDate: (date) => req(`/nutrition/by-date/${date}`),
createNutrition: (date,kcal,protein,fat,carbs) => req(`/nutrition?date=${date}&kcal=${kcal}&protein_g=${protein}&fat_g=${fat}&carbs_g=${carbs}`,{method:'POST'}),
updateNutrition: (id,kcal,protein,fat,carbs) => req(`/nutrition/${id}?kcal=${kcal}&protein_g=${protein}&fat_g=${fat}&carbs_g=${carbs}`,{method:'PUT'}),
deleteNutrition: (id) => req(`/nutrition/${id}`,{method:'DELETE'}),
// Stats & AI
getStats: () => req('/stats'),
insightTrend: () => req('/insights/trend',{method:'POST'}),
listPrompts: () => req('/prompts'),
runInsight: (slug) => req(`/insights/run/${slug}`,{method:'POST'}),
insightPipeline: () => req('/insights/pipeline',{method:'POST'}),
listInsights: () => req('/insights'),
latestInsights: () => req('/insights/latest'),
exportZip: async () => {
const res = await fetch(`${BASE}/export/zip`, {headers: hdrs()})
if (!res.ok) throw new Error('Export failed')
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `mitai-export-${new Date().toISOString().split('T')[0]}.zip`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
},
exportJson: async () => {
const res = await fetch(`${BASE}/export/json`, {headers: hdrs()})
if (!res.ok) throw new Error('Export failed')
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `mitai-export-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
},
exportCsv: async () => {
const res = await fetch(`${BASE}/export/csv`, {headers: hdrs()})
if (!res.ok) throw new Error('Export failed')
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `mitai-export-${new Date().toISOString().split('T')[0]}.csv`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
},
// Admin
adminListProfiles: () => req('/admin/profiles'),
adminCreateProfile: (d) => req('/admin/profiles',json(d)),
adminDeleteProfile: (id) => req(`/admin/profiles/${id}`,{method:'DELETE'}),
adminSetPermissions: (id,d) => req(`/admin/profiles/${id}/permissions`,jput(d)),
changePin: (pin) => req('/auth/pin',json({pin})),
// v9c Subscription System
// User-facing
getMySubscription: () => req('/subscription/me'),
getMyUsage: () => req('/subscription/usage'),
getMyLimits: () => req('/subscription/limits'),
redeemCoupon: (code) => req('/coupons/redeem',json({code})),
getFeatureUsage: () => req('/features/usage'), // Phase 3: Usage overview
// Admin: Features
listFeatures: () => req('/features'),
createFeature: (d) => req('/features',json(d)),
updateFeature: (id,d) => req(`/features/${id}`,jput(d)),
deleteFeature: (id) => req(`/features/${id}`,{method:'DELETE'}),
// Admin: Tiers
listTiers: () => req('/tiers'),
createTier: (d) => req('/tiers',json(d)),
updateTier: (id,d) => req(`/tiers/${id}`,jput(d)),
deleteTier: (id) => req(`/tiers/${id}`,{method:'DELETE'}),
// Admin: Tier Limits (Matrix)
getTierLimitsMatrix: () => req('/tier-limits'),
updateTierLimit: (d) => req('/tier-limits',jput(d)),
updateTierLimitsBatch:(updates) => req('/tier-limits/batch',jput({updates})),
// Admin: User Restrictions
listUserRestrictions: (pid) => req(`/user-restrictions${pid?'?profile_id='+pid:''}`),
createUserRestriction:(d) => req('/user-restrictions',json(d)),
updateUserRestriction:(id,d) => req(`/user-restrictions/${id}`,jput(d)),
deleteUserRestriction:(id) => req(`/user-restrictions/${id}`,{method:'DELETE'}),
// Admin: Coupons
listCoupons: () => req('/coupons'),
createCoupon: (d) => req('/coupons',json(d)),
updateCoupon: (id,d) => req(`/coupons/${id}`,jput(d)),
deleteCoupon: (id) => req(`/coupons/${id}`,{method:'DELETE'}),
getCouponRedemptions: (id) => req(`/coupons/${id}/redemptions`),
// Admin: Access Grants
listAccessGrants: (pid,active)=> req(`/access-grants${pid?'?profile_id='+pid:''}${active?'&active_only=true':''}`),
createAccessGrant: (d) => req('/access-grants',json(d)),
updateAccessGrant: (id,d) => req(`/access-grants/${id}`,jput(d)),
revokeAccessGrant: (id) => req(`/access-grants/${id}`,{method:'DELETE'}),
}