Membership-System und Bug Fixing (inkl. Nutrition) #8
|
|
@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
import { calcBodyFat, getBfCategory, METHOD_POINTS } from '../utils/calc'
|
import { calcBodyFat, getBfCategory, METHOD_POINTS } from '../utils/calc'
|
||||||
import { CALIPER_POINTS, CALIPER_METHODS } from '../utils/guideData'
|
import { CALIPER_POINTS, CALIPER_METHODS } from '../utils/guideData'
|
||||||
|
import UsageBadge from '../components/UsageBadge'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
function emptyForm() {
|
function emptyForm() {
|
||||||
|
|
@ -15,7 +16,7 @@ function emptyForm() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function CaliperForm({ form, setForm, profile, onSave, onCancel, saveLabel='Speichern' }) {
|
function CaliperForm({ form, setForm, profile, onSave, onCancel, saveLabel='Speichern', saving=false, error=null, usage=null }) {
|
||||||
const sex = profile?.sex||'m'
|
const sex = profile?.sex||'m'
|
||||||
const age = profile?.dob ? Math.floor((Date.now()-new Date(profile.dob))/(365.25*24*3600*1000)) : 30
|
const age = profile?.dob ? Math.floor((Date.now()-new Date(profile.dob))/(365.25*24*3600*1000)) : 30
|
||||||
const weight = form.weight || 80
|
const weight = form.weight || 80
|
||||||
|
|
@ -65,8 +66,21 @@ function CaliperForm({ form, setForm, profile, onSave, onCancel, saveLabel='Spei
|
||||||
<input type="text" className="form-input" placeholder="optional" value={form.notes||''} onChange={e=>set('notes',e.target.value)}/>
|
<input type="text" className="form-input" placeholder="optional" value={form.notes||''} onChange={e=>set('notes',e.target.value)}/>
|
||||||
<span className="form-unit"/>
|
<span className="form-unit"/>
|
||||||
</div>
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div style={{padding:'10px',background:'var(--danger-bg)',border:'1px solid var(--danger)',borderRadius:8,fontSize:13,color:'var(--danger)',marginBottom:8}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div style={{display:'flex',gap:6,marginTop:8}}>
|
<div style={{display:'flex',gap:6,marginTop:8}}>
|
||||||
<button className="btn btn-primary" style={{flex:1}} onClick={()=>onSave(bfPct, sex)}>{saveLabel}</button>
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
style={{flex:1, cursor: (usage && !usage.allowed) ? 'not-allowed' : 'pointer'}}
|
||||||
|
onClick={()=>onSave(bfPct, sex)}
|
||||||
|
disabled={saving || (usage && !usage.allowed)}
|
||||||
|
title={usage && !usage.allowed ? `Limit erreicht (${usage.used}/${usage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
||||||
|
>
|
||||||
|
{(usage && !usage.allowed) ? '🔒 Limit erreicht' : saveLabel}
|
||||||
|
</button>
|
||||||
{onCancel && <button className="btn btn-secondary" style={{flex:1}} onClick={onCancel}><X size={13}/> Abbrechen</button>}
|
{onCancel && <button className="btn btn-secondary" style={{flex:1}} onClick={onCancel}><X size={13}/> Abbrechen</button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,12 +92,26 @@ export default function CaliperScreen() {
|
||||||
const [profile, setProfile] = useState(null)
|
const [profile, setProfile] = useState(null)
|
||||||
const [form, setForm] = useState(emptyForm())
|
const [form, setForm] = useState(emptyForm())
|
||||||
const [editing, setEditing] = useState(null)
|
const [editing, setEditing] = useState(null)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const [caliperUsage, setCaliperUsage] = useState(null) // Phase 4: Usage badge
|
||||||
const nav = useNavigate()
|
const nav = useNavigate()
|
||||||
|
|
||||||
const load = () => Promise.all([api.listCaliper(), api.getProfile()])
|
const load = () => Promise.all([api.listCaliper(), api.getProfile()])
|
||||||
.then(([e,p])=>{ setEntries(e); setProfile(p) })
|
.then(([e,p])=>{ setEntries(e); setProfile(p) })
|
||||||
useEffect(()=>{ load() },[])
|
|
||||||
|
const loadUsage = () => {
|
||||||
|
api.getFeatureUsage().then(features => {
|
||||||
|
const caliperFeature = features.find(f => f.feature_id === 'caliper_entries')
|
||||||
|
setCaliperUsage(caliperFeature)
|
||||||
|
}).catch(err => console.error('Failed to load usage:', err))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
load()
|
||||||
|
loadUsage()
|
||||||
|
},[])
|
||||||
|
|
||||||
const buildPayload = (f, bfPct, sex) => {
|
const buildPayload = (f, bfPct, sex) => {
|
||||||
const weight = profile?.weight || null
|
const weight = profile?.weight || null
|
||||||
|
|
@ -97,11 +125,23 @@ export default function CaliperScreen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async (bfPct, sex) => {
|
const handleSave = async (bfPct, sex) => {
|
||||||
|
setSaving(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
const payload = buildPayload(form, bfPct, sex)
|
const payload = buildPayload(form, bfPct, sex)
|
||||||
await api.upsertCaliper(payload)
|
await api.upsertCaliper(payload)
|
||||||
setSaved(true); await load()
|
setSaved(true)
|
||||||
|
await load()
|
||||||
|
await loadUsage() // Reload usage after save
|
||||||
setTimeout(()=>setSaved(false),2000)
|
setTimeout(()=>setSaved(false),2000)
|
||||||
setForm(emptyForm())
|
setForm(emptyForm())
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Save failed:', err)
|
||||||
|
setError(err.message || 'Fehler beim Speichern')
|
||||||
|
setTimeout(()=>setError(null), 5000)
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdate = async (bfPct, sex) => {
|
const handleUpdate = async (bfPct, sex) => {
|
||||||
|
|
@ -125,9 +165,13 @@ export default function CaliperScreen() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card section-gap">
|
<div className="card section-gap">
|
||||||
<div className="card-title">Neue Messung</div>
|
<div className="card-title badge-container-right">
|
||||||
|
<span>Neue Messung</span>
|
||||||
|
{caliperUsage && <UsageBadge {...caliperUsage} />}
|
||||||
|
</div>
|
||||||
<CaliperForm form={form} setForm={setForm} profile={profile}
|
<CaliperForm form={form} setForm={setForm} profile={profile}
|
||||||
onSave={handleSave} saveLabel={saved?'✓ Gespeichert!':'Speichern'}/>
|
onSave={handleSave} saveLabel={saved?'✓ Gespeichert!':'Speichern'}
|
||||||
|
saving={saving} error={error} usage={caliperUsage}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section-gap">
|
<div className="section-gap">
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Pencil, Trash2, Check, X, Camera, BookOpen } from 'lucide-react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
import { CIRCUMFERENCE_POINTS } from '../utils/guideData'
|
import { CIRCUMFERENCE_POINTS } from '../utils/guideData'
|
||||||
|
import UsageBadge from '../components/UsageBadge'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
const FIELDS = ['c_neck','c_chest','c_waist','c_belly','c_hip','c_thigh','c_calf','c_arm']
|
const FIELDS = ['c_neck','c_chest','c_waist','c_belly','c_hip','c_thigh','c_calf','c_arm']
|
||||||
|
|
@ -16,18 +17,32 @@ export default function CircumScreen() {
|
||||||
const [editing, setEditing] = useState(null)
|
const [editing, setEditing] = useState(null)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
const [photoFile, setPhotoFile] = useState(null)
|
const [photoFile, setPhotoFile] = useState(null)
|
||||||
const [photoPreview, setPhotoPreview] = useState(null)
|
const [photoPreview, setPhotoPreview] = useState(null)
|
||||||
|
const [circumUsage, setCircumUsage] = useState(null) // Phase 4: Usage badge
|
||||||
const fileRef = useRef()
|
const fileRef = useRef()
|
||||||
const nav = useNavigate()
|
const nav = useNavigate()
|
||||||
|
|
||||||
const load = () => api.listCirc().then(setEntries)
|
const load = () => api.listCirc().then(setEntries)
|
||||||
useEffect(()=>{ load() },[])
|
|
||||||
|
const loadUsage = () => {
|
||||||
|
api.getFeatureUsage().then(features => {
|
||||||
|
const circumFeature = features.find(f => f.feature_id === 'circumference_entries')
|
||||||
|
setCircumUsage(circumFeature)
|
||||||
|
}).catch(err => console.error('Failed to load usage:', err))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
load()
|
||||||
|
loadUsage()
|
||||||
|
},[])
|
||||||
|
|
||||||
const set = (k,v) => setForm(f=>({...f,[k]:v}))
|
const set = (k,v) => setForm(f=>({...f,[k]:v}))
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const payload = {}
|
const payload = {}
|
||||||
payload.date = form.date
|
payload.date = form.date
|
||||||
|
|
@ -38,10 +53,18 @@ export default function CircumScreen() {
|
||||||
payload.photo_id = pr.id
|
payload.photo_id = pr.id
|
||||||
}
|
}
|
||||||
await api.upsertCirc(payload)
|
await api.upsertCirc(payload)
|
||||||
setSaved(true); await load()
|
setSaved(true)
|
||||||
|
await load()
|
||||||
|
await loadUsage() // Reload usage after save
|
||||||
setTimeout(()=>setSaved(false),2000)
|
setTimeout(()=>setSaved(false),2000)
|
||||||
setForm(empty()); setPhotoFile(null); setPhotoPreview(null)
|
setForm(empty()); setPhotoFile(null); setPhotoPreview(null)
|
||||||
} finally { setSaving(false) }
|
} catch (err) {
|
||||||
|
console.error('Save failed:', err)
|
||||||
|
setError(err.message || 'Fehler beim Speichern')
|
||||||
|
setTimeout(()=>setError(null), 5000)
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startEdit = (e) => setEditing({...e})
|
const startEdit = (e) => setEditing({...e})
|
||||||
|
|
@ -72,7 +95,10 @@ export default function CircumScreen() {
|
||||||
|
|
||||||
{/* Eingabe */}
|
{/* Eingabe */}
|
||||||
<div className="card section-gap">
|
<div className="card section-gap">
|
||||||
<div className="card-title">Neue Messung</div>
|
<div className="card-title badge-container-right">
|
||||||
|
<span>Neue Messung</span>
|
||||||
|
{circumUsage && <UsageBadge {...circumUsage} />}
|
||||||
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-label">Datum</label>
|
<label className="form-label">Datum</label>
|
||||||
<input type="date" className="form-input" style={{width:140}} value={form.date} onChange={e=>set('date',e.target.value)}/>
|
<input type="date" className="form-input" style={{width:140}} value={form.date} onChange={e=>set('date',e.target.value)}/>
|
||||||
|
|
@ -99,8 +125,22 @@ export default function CircumScreen() {
|
||||||
<button className="btn btn-secondary btn-full" onClick={()=>fileRef.current.click()}>
|
<button className="btn btn-secondary btn-full" onClick={()=>fileRef.current.click()}>
|
||||||
<Camera size={14}/> {photoPreview?'Foto ändern':'Foto hinzufügen'}
|
<Camera size={14}/> {photoPreview?'Foto ändern':'Foto hinzufügen'}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-primary btn-full" style={{marginTop:8}} onClick={handleSave} disabled={saving}>
|
{error && (
|
||||||
{saved ? <><Check size={14}/> Gespeichert!</> : saving ? '…' : 'Speichern'}
|
<div style={{padding:'10px',background:'var(--danger-bg)',border:'1px solid var(--danger)',borderRadius:8,fontSize:13,color:'var(--danger)',marginTop:8}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-full"
|
||||||
|
style={{marginTop:8, cursor: (circumUsage && !circumUsage.allowed) ? 'not-allowed' : 'pointer'}}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving || (circumUsage && !circumUsage.allowed)}
|
||||||
|
title={circumUsage && !circumUsage.allowed ? `Limit erreicht (${circumUsage.used}/${circumUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
||||||
|
>
|
||||||
|
{saved ? <><Check size={14}/> Gespeichert!</>
|
||||||
|
: saving ? '…'
|
||||||
|
: (circumUsage && !circumUsage.allowed) ? '🔒 Limit erreicht'
|
||||||
|
: 'Speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,28 +22,42 @@ export default function WeightScreen() {
|
||||||
const [newNote, setNewNote] = useState('')
|
const [newNote, setNewNote] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
const [weightUsage, setWeightUsage] = useState(null) // Phase 3: Usage badge
|
const [weightUsage, setWeightUsage] = useState(null) // Phase 3: Usage badge
|
||||||
|
|
||||||
const load = () => api.listWeight(365).then(data => setEntries(data))
|
const load = () => api.listWeight(365).then(data => setEntries(data))
|
||||||
|
|
||||||
useEffect(()=>{
|
const loadUsage = () => {
|
||||||
load()
|
|
||||||
// Load feature usage for badge
|
// Load feature usage for badge
|
||||||
api.getFeatureUsage().then(features => {
|
api.getFeatureUsage().then(features => {
|
||||||
const weightFeature = features.find(f => f.feature_id === 'weight_entries')
|
const weightFeature = features.find(f => f.feature_id === 'weight_entries')
|
||||||
setWeightUsage(weightFeature)
|
setWeightUsage(weightFeature)
|
||||||
}).catch(err => console.error('Failed to load usage:', err))
|
}).catch(err => console.error('Failed to load usage:', err))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
load()
|
||||||
|
loadUsage()
|
||||||
},[])
|
},[])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!newWeight) return
|
if (!newWeight) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
setError(null)
|
||||||
try {
|
try {
|
||||||
await api.upsertWeight(newDate, parseFloat(newWeight), newNote)
|
await api.upsertWeight(newDate, parseFloat(newWeight), newNote)
|
||||||
setSaved(true); await load()
|
setSaved(true)
|
||||||
|
await load()
|
||||||
|
await loadUsage() // Reload usage after save
|
||||||
setTimeout(()=>setSaved(false), 2000)
|
setTimeout(()=>setSaved(false), 2000)
|
||||||
setNewWeight(''); setNewNote('')
|
setNewWeight(''); setNewNote('')
|
||||||
} finally { setSaving(false) }
|
} catch (err) {
|
||||||
|
console.error('Save failed:', err)
|
||||||
|
setError(err.message || 'Fehler beim Speichern')
|
||||||
|
setTimeout(()=>setError(null), 5000)
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
|
|
@ -92,9 +106,21 @@ export default function WeightScreen() {
|
||||||
value={newNote} onChange={e=>setNewNote(e.target.value)}/>
|
value={newNote} onChange={e=>setNewNote(e.target.value)}/>
|
||||||
<span className="form-unit"/>
|
<span className="form-unit"/>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-primary btn-full" onClick={handleSave} disabled={saving||!newWeight}>
|
{error && (
|
||||||
|
<div style={{padding:'10px',background:'var(--danger-bg)',border:'1px solid var(--danger)',borderRadius:8,fontSize:13,color:'var(--danger)',marginBottom:12}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-full"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving || !newWeight || (weightUsage && !weightUsage.allowed)}
|
||||||
|
title={weightUsage && !weightUsage.allowed ? `Limit erreicht (${weightUsage.used}/${weightUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
||||||
|
style={{cursor: (weightUsage && !weightUsage.allowed) ? 'not-allowed' : 'pointer'}}
|
||||||
|
>
|
||||||
{saved ? <><Check size={15}/> Gespeichert!</>
|
{saved ? <><Check size={15}/> Gespeichert!</>
|
||||||
: saving ? <><div className="spinner" style={{width:14,height:14}}/> …</>
|
: saving ? <><div className="spinner" style={{width:14,height:14}}/> …</>
|
||||||
|
: (weightUsage && !weightUsage.allowed) ? '🔒 Limit erreicht'
|
||||||
: 'Speichern'}
|
: 'Speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user