Replaces hardcoded mappings with database-driven, self-learning system. Backend: - Migration 007: activity_type_mappings table - Supports global and user-specific mappings - Seeded with 40+ default mappings (German + English) - Unique constraint: (activity_type, profile_id) - Refactored: get_training_type_for_activity() queries DB - Priority: user-specific → global → NULL - Bulk categorization now saves mapping automatically - Source: 'bulk' for learned mappings - admin_activity_mappings.py: Full CRUD endpoints - List, Get, Create, Update, Delete - Coverage stats endpoint - CSV import uses DB mappings (no hardcoded logic) Frontend: - AdminActivityMappingsPage: Full mapping management UI - Coverage stats (% mapped, unmapped count) - Filter: All / Global - Create/Edit/Delete mappings - Tip: System learns from bulk categorization - Added route + admin link - API methods: adminList/Get/Create/Update/DeleteActivityMapping Benefits: - No code changes needed for new activity types - System learns from user bulk categorizations - User-specific mappings override global defaults - Admin can manage all mappings via UI - Migration pre-populates 40+ common German/English types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
216 lines
10 KiB
JavaScript
216 lines
10 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'),
|
|
listUncategorizedActivities: () => req('/activity/uncategorized'),
|
|
bulkCategorizeActivities: (d) => req('/activity/bulk-categorize', json(d)),
|
|
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})),
|
|
register: (name,email,password) => req('/auth/register',json({name,email,password})),
|
|
verifyEmail: (token) => req(`/auth/verify/${token}`),
|
|
resendVerification: (email) => req('/auth/resend-verification',json({email})),
|
|
|
|
// 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'}),
|
|
|
|
// v9d: Training Types
|
|
listTrainingTypes: () => req('/training-types'), // Grouped by category
|
|
listTrainingTypesFlat:() => req('/training-types/flat'), // Flat list
|
|
getTrainingCategories:() => req('/training-types/categories'), // Category metadata
|
|
|
|
// Admin: Training Types (v9d Phase 1b)
|
|
adminListTrainingTypes: () => req('/admin/training-types'),
|
|
adminGetTrainingType: (id) => req(`/admin/training-types/${id}`),
|
|
adminCreateTrainingType: (d) => req('/admin/training-types', json(d)),
|
|
adminUpdateTrainingType: (id,d) => req(`/admin/training-types/${id}`, jput(d)),
|
|
adminDeleteTrainingType: (id) => req(`/admin/training-types/${id}`, {method:'DELETE'}),
|
|
getAbilitiesTaxonomy: () => req('/admin/training-types/taxonomy/abilities'),
|
|
|
|
// Admin: Activity Type Mappings (v9d Phase 1b - Learnable System)
|
|
adminListActivityMappings: (profileId, globalOnly) => req(`/admin/activity-mappings${profileId?'?profile_id='+profileId:''}${globalOnly?'?global_only=true':''}`),
|
|
adminGetActivityMapping: (id) => req(`/admin/activity-mappings/${id}`),
|
|
adminCreateActivityMapping: (d) => req('/admin/activity-mappings', json(d)),
|
|
adminUpdateActivityMapping: (id,d) => req(`/admin/activity-mappings/${id}`, jput(d)),
|
|
adminDeleteActivityMapping: (id) => req(`/admin/activity-mappings/${id}`, {method:'DELETE'}),
|
|
adminGetMappingCoverage: () => req('/admin/activity-mappings/stats/coverage'),
|
|
}
|