From 0f019f87a4a4d78ed14279ef570fe5949769380c Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 21 Mar 2026 07:14:34 +0100 Subject: [PATCH] feat: add feature limit enforcement UI (Phase 4 Batch 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementiert User-freundliches Limit-Feedback für Daten-Einträge: - Button wird deaktiviert wenn Limit erreicht - Hover-Tooltip erklärt warum ("Limit erreicht X/Y") - Button-Text zeigt "🔒 Limit erreicht" - Error-Handling für alle API-Fehler - Usage-Badge wird nach Speichern aktualisiert Betrifft: Weight, Circumference, Caliper Screens Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/CaliperScreen.jsx | 64 +++++++++++++++++++++++----- frontend/src/pages/CircumScreen.jsx | 52 +++++++++++++++++++--- frontend/src/pages/WeightScreen.jsx | 36 +++++++++++++--- 3 files changed, 131 insertions(+), 21 deletions(-) diff --git a/frontend/src/pages/CaliperScreen.jsx b/frontend/src/pages/CaliperScreen.jsx index f81bf2e..8940d8c 100644 --- a/frontend/src/pages/CaliperScreen.jsx +++ b/frontend/src/pages/CaliperScreen.jsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom' import { api } from '../utils/api' import { calcBodyFat, getBfCategory, METHOD_POINTS } from '../utils/calc' import { CALIPER_POINTS, CALIPER_METHODS } from '../utils/guideData' +import UsageBadge from '../components/UsageBadge' import dayjs from 'dayjs' 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 age = profile?.dob ? Math.floor((Date.now()-new Date(profile.dob))/(365.25*24*3600*1000)) : 30 const weight = form.weight || 80 @@ -65,8 +66,21 @@ function CaliperForm({ form, setForm, profile, onSave, onCancel, saveLabel='Spei set('notes',e.target.value)}/> + {error && ( +
+ {error} +
+ )}
- + {onCancel && }
@@ -78,12 +92,26 @@ export default function CaliperScreen() { const [profile, setProfile] = useState(null) const [form, setForm] = useState(emptyForm()) const [editing, setEditing] = useState(null) + const [saving, setSaving] = 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 load = () => Promise.all([api.listCaliper(), api.getProfile()]) .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 weight = profile?.weight || null @@ -97,11 +125,23 @@ export default function CaliperScreen() { } const handleSave = async (bfPct, sex) => { - const payload = buildPayload(form, bfPct, sex) - await api.upsertCaliper(payload) - setSaved(true); await load() - setTimeout(()=>setSaved(false),2000) - setForm(emptyForm()) + setSaving(true) + setError(null) + try { + const payload = buildPayload(form, bfPct, sex) + await api.upsertCaliper(payload) + setSaved(true) + await load() + await loadUsage() // Reload usage after save + setTimeout(()=>setSaved(false),2000) + 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) => { @@ -125,9 +165,13 @@ export default function CaliperScreen() {
-
Neue Messung
+
+ Neue Messung + {caliperUsage && } +
+ onSave={handleSave} saveLabel={saved?'✓ Gespeichert!':'Speichern'} + saving={saving} error={error} usage={caliperUsage}/>
diff --git a/frontend/src/pages/CircumScreen.jsx b/frontend/src/pages/CircumScreen.jsx index 22b37e4..8d876b8 100644 --- a/frontend/src/pages/CircumScreen.jsx +++ b/frontend/src/pages/CircumScreen.jsx @@ -3,6 +3,7 @@ import { Pencil, Trash2, Check, X, Camera, BookOpen } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { api } from '../utils/api' import { CIRCUMFERENCE_POINTS } from '../utils/guideData' +import UsageBadge from '../components/UsageBadge' import dayjs from 'dayjs' 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 [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) + const [error, setError] = useState(null) const [photoFile, setPhotoFile] = useState(null) const [photoPreview, setPhotoPreview] = useState(null) + const [circumUsage, setCircumUsage] = useState(null) // Phase 4: Usage badge const fileRef = useRef() const nav = useNavigate() 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 handleSave = async () => { setSaving(true) + setError(null) try { const payload = {} payload.date = form.date @@ -38,10 +53,18 @@ export default function CircumScreen() { payload.photo_id = pr.id } await api.upsertCirc(payload) - setSaved(true); await load() + setSaved(true) + await load() + await loadUsage() // Reload usage after save setTimeout(()=>setSaved(false),2000) 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}) @@ -72,7 +95,10 @@ export default function CircumScreen() { {/* Eingabe */}
-
Neue Messung
+
+ Neue Messung + {circumUsage && } +
set('date',e.target.value)}/> @@ -99,8 +125,22 @@ export default function CircumScreen() { -
diff --git a/frontend/src/pages/WeightScreen.jsx b/frontend/src/pages/WeightScreen.jsx index ffdfdcf..d6d2e13 100644 --- a/frontend/src/pages/WeightScreen.jsx +++ b/frontend/src/pages/WeightScreen.jsx @@ -22,28 +22,42 @@ export default function WeightScreen() { const [newNote, setNewNote] = useState('') const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) + const [error, setError] = useState(null) const [weightUsage, setWeightUsage] = useState(null) // Phase 3: Usage badge const load = () => api.listWeight(365).then(data => setEntries(data)) - useEffect(()=>{ - load() + const loadUsage = () => { // Load feature usage for badge api.getFeatureUsage().then(features => { const weightFeature = features.find(f => f.feature_id === 'weight_entries') setWeightUsage(weightFeature) }).catch(err => console.error('Failed to load usage:', err)) + } + + useEffect(()=>{ + load() + loadUsage() },[]) const handleSave = async () => { if (!newWeight) return setSaving(true) + setError(null) try { 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) 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 () => { @@ -92,9 +106,21 @@ export default function WeightScreen() { value={newNote} onChange={e=>setNewNote(e.target.value)}/>
-