fix: add dashboard weight enforcement and fix hover tooltips
- Dashboard QuickWeight: Feature limit enforcement hinzugefügt - Hover-Tooltip Fix: Button in div wrapper (disabled buttons zeigen keine nativen tooltips) - Error handling für Dashboard weight input - Konsistentes UX über alle Eingabe-Screens Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0f019f87a4
commit
d13c2c7e25
|
|
@ -72,15 +72,19 @@ function CaliperForm({ form, setForm, profile, onSave, onCancel, saveLabel='Spei
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{display:'flex',gap:6,marginTop:8}}>
|
<div style={{display:'flex',gap:6,marginTop:8}}>
|
||||||
|
<div
|
||||||
|
title={usage && !usage.allowed ? `Limit erreicht (${usage.used}/${usage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
||||||
|
style={{flex:1,display:'inline-block'}}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
style={{flex:1, cursor: (usage && !usage.allowed) ? 'not-allowed' : 'pointer'}}
|
style={{width:'100%', cursor: (usage && !usage.allowed) ? 'not-allowed' : 'pointer'}}
|
||||||
onClick={()=>onSave(bfPct, sex)}
|
onClick={()=>onSave(bfPct, sex)}
|
||||||
disabled={saving || (usage && !usage.allowed)}
|
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}
|
{(usage && !usage.allowed) ? '🔒 Limit erreicht' : saveLabel}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
{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>
|
||||||
|
|
|
||||||
|
|
@ -130,12 +130,15 @@ export default function CircumScreen() {
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
title={circumUsage && !circumUsage.allowed ? `Limit erreicht (${circumUsage.used}/${circumUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
||||||
|
style={{display:'inline-block',width:'100%',marginTop:8}}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-full"
|
className="btn btn-primary btn-full"
|
||||||
style={{marginTop:8, cursor: (circumUsage && !circumUsage.allowed) ? 'not-allowed' : 'pointer'}}
|
style={{cursor: (circumUsage && !circumUsage.allowed) ? 'not-allowed' : 'pointer'}}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving || (circumUsage && !circumUsage.allowed)}
|
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!</>
|
{saved ? <><Check size={14}/> Gespeichert!</>
|
||||||
: saving ? '…'
|
: saving ? '…'
|
||||||
|
|
@ -143,6 +146,7 @@ export default function CircumScreen() {
|
||||||
: 'Speichern'}
|
: 'Speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Liste */}
|
{/* Liste */}
|
||||||
<div className="section-gap">
|
<div className="section-gap">
|
||||||
|
|
|
||||||
|
|
@ -27,33 +27,76 @@ function QuickWeight({ onSaved }) {
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = 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)
|
||||||
const today = dayjs().format('YYYY-MM-DD')
|
const today = dayjs().format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
const loadUsage = () => {
|
||||||
|
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(()=>{
|
useEffect(()=>{
|
||||||
api.weightStats().then(s=>{
|
api.weightStats().then(s=>{
|
||||||
if(s?.latest?.date===today) setInput(String(s.latest.weight))
|
if(s?.latest?.date===today) setInput(String(s.latest.weight))
|
||||||
})
|
})
|
||||||
|
loadUsage()
|
||||||
},[])
|
},[])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const w=parseFloat(input); if(!w||w<20||w>300) return
|
const w=parseFloat(input); if(!w||w<20||w>300) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try{ await api.upsertWeight(today,w); setSaved(true); onSaved?.(); setTimeout(()=>setSaved(false),2000) }
|
setError(null)
|
||||||
finally{ setSaving(false) }
|
try{
|
||||||
|
await api.upsertWeight(today,w)
|
||||||
|
setSaved(true)
|
||||||
|
await loadUsage() // Reload usage after save
|
||||||
|
onSaved?.()
|
||||||
|
setTimeout(()=>setSaved(false),2000)
|
||||||
|
} catch(err) {
|
||||||
|
console.error('Save failed:', err)
|
||||||
|
setError(err.message || 'Fehler beim Speichern')
|
||||||
|
setTimeout(()=>setError(null), 5000)
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDisabled = saving || !input || (weightUsage && !weightUsage.allowed)
|
||||||
|
const tooltipText = weightUsage && !weightUsage.allowed
|
||||||
|
? `Limit erreicht (${weightUsage.used}/${weightUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.`
|
||||||
|
: ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
{error && (
|
||||||
|
<div style={{padding:'8px 10px',background:'var(--danger-bg)',border:'1px solid var(--danger)',borderRadius:8,fontSize:12,color:'var(--danger)',marginBottom:8}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div style={{display:'flex',gap:8,alignItems:'center'}}>
|
<div style={{display:'flex',gap:8,alignItems:'center'}}>
|
||||||
<input type="number" min={20} max={300} step={0.1} className="form-input"
|
<input type="number" min={20} max={300} step={0.1} className="form-input"
|
||||||
style={{flex:1,fontSize:17,fontWeight:600,textAlign:'center'}}
|
style={{flex:1,fontSize:17,fontWeight:600,textAlign:'center'}}
|
||||||
placeholder="kg eingeben" value={input} onChange={e=>setInput(e.target.value)}
|
placeholder="kg eingeben" value={input} onChange={e=>setInput(e.target.value)}
|
||||||
onKeyDown={e=>e.key==='Enter'&&handleSave()}/>
|
onKeyDown={e=>e.key==='Enter'&&!isDisabled&&handleSave()}/>
|
||||||
<span style={{fontSize:13,color:'var(--text3)'}}>kg</span>
|
<span style={{fontSize:13,color:'var(--text3)'}}>kg</span>
|
||||||
<button className="btn btn-primary" style={{padding:'8px 14px'}}
|
<div title={tooltipText} style={{display:'inline-block'}}>
|
||||||
onClick={handleSave} disabled={saving||!input}>
|
<button
|
||||||
{saved?<Check size={15}/>:saving?<div className="spinner" style={{width:14,height:14}}/>:'Speichern'}
|
className="btn btn-primary"
|
||||||
|
style={{padding:'8px 14px', cursor: isDisabled ? 'not-allowed' : 'pointer'}}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
{saved ? <Check size={15}/>
|
||||||
|
: saving ? <div className="spinner" style={{width:14,height:14}}/>
|
||||||
|
: (weightUsage && !weightUsage.allowed) ? '🔒 Limit'
|
||||||
|
: 'Speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,11 +111,14 @@ export default function WeightScreen() {
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
title={weightUsage && !weightUsage.allowed ? `Limit erreicht (${weightUsage.used}/${weightUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
||||||
|
style={{display:'inline-block',width:'100%'}}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-full"
|
className="btn btn-primary btn-full"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving || !newWeight || (weightUsage && !weightUsage.allowed)}
|
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'}}
|
style={{cursor: (weightUsage && !weightUsage.allowed) ? 'not-allowed' : 'pointer'}}
|
||||||
>
|
>
|
||||||
{saved ? <><Check size={15}/> Gespeichert!</>
|
{saved ? <><Check size={15}/> Gespeichert!</>
|
||||||
|
|
@ -124,6 +127,7 @@ export default function WeightScreen() {
|
||||||
: 'Speichern'}
|
: 'Speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Chart */}
|
{/* Chart */}
|
||||||
{chartData.length >= 2 && (
|
{chartData.length >= 2 && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user