feat: improve ProfileBuilder mobile UX and clarity
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s

Changes:
- Responsive layout: fields stack vertically, no more cramped grid
- Clear labels: 'WAS?', 'BEDINGUNG', 'WICHTIGKEIT'
- Weight field only shown when using 'weighted_score' strategy
- Weight explanation: '1 = unwichtig, 10 = sehr wichtig'
- Success message replaces alert() dialog (auto-dismiss after 2s)
- Delete button moved to rule header
- Better visual hierarchy with sections

User feedback:
- Felder lassen sich auf Handy nicht gut bearbeiten
- Überschriften nicht eindeutig
- Gewicht-Feld Verwirrung
- Keine OK-Dialoge

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-23 14:18:58 +01:00
parent 65846042e2
commit b73c77d811
2 changed files with 124 additions and 84 deletions

View File

@ -22,6 +22,7 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
const [profile, setProfile] = useState(null)
const [loading, setLoading] = useState(false)
const [expandedSections, setExpandedSections] = useState({ minReq: true })
const [successMessage, setSuccessMessage] = useState(null)
useEffect(() => {
// Initialize or load existing profile
@ -146,6 +147,12 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
setLoading(true)
try {
await onSave(profile)
setSuccessMessage('✓ Profil gespeichert!')
setTimeout(() => {
setSuccessMessage(null)
}, 2000)
} catch (err) {
// Error is already handled by parent
} finally {
setLoading(false)
}
@ -166,6 +173,21 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
</p>
</div>
{/* Success Message */}
{successMessage && (
<div style={{
padding: '12px',
background: 'var(--accent)',
color: 'white',
borderRadius: '8px',
marginBottom: '16px',
fontSize: '14px',
fontWeight: '600'
}}>
{successMessage}
</div>
)}
{/* Minimum Requirements */}
<div className="card" style={{ marginBottom: '16px' }}>
<div
@ -239,6 +261,7 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
const availableOps = OPERATORS.filter(op =>
param ? op.types.includes(param.data_type) : true
)
const useWeights = minReq.pass_strategy === 'weighted_score'
return (
<div
@ -250,23 +273,30 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
marginBottom: '8px'
}}
>
{/* Labels Row */}
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1.5fr 1fr 0.8fr 40px', gap: '8px', marginBottom: '4px' }}>
<div style={{ fontSize: '10px', color: 'var(--text3)', fontWeight: '600' }}>PARAMETER</div>
<div style={{ fontSize: '10px', color: 'var(--text3)', fontWeight: '600' }}>OPERATOR</div>
<div style={{ fontSize: '10px', color: 'var(--text3)', fontWeight: '600' }}>SCHWELLENWERT</div>
<div style={{ fontSize: '10px', color: 'var(--text3)', fontWeight: '600' }}>GEWICHT</div>
<div></div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '12px' }}>
<div style={{ fontSize: '12px', fontWeight: '600', color: 'var(--text2)' }}>
Regel {idx + 1}
</div>
<button
className="btn"
onClick={() => removeRule(idx)}
style={{ padding: '4px 8px', minWidth: 'auto', fontSize: '11px' }}
title="Regel löschen"
>
<Trash2 size={12} /> Löschen
</button>
</div>
{/* Input Row */}
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1.5fr 1fr 0.8fr 40px', gap: '8px', marginBottom: '8px' }}>
{/* Parameter */}
{/* Parameter */}
<div style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '11px', color: 'var(--text3)', fontWeight: '600', display: 'block', marginBottom: '4px' }}>
WAS soll geprüft werden?
</label>
<select
className="form-select"
value={rule.parameter}
onChange={(e) => updateRule(idx, { parameter: e.target.value })}
style={{ fontSize: '13px' }}
style={{ fontSize: '13px', width: '100%' }}
>
{parameters.map(p => (
<option key={p.key} value={p.key}>
@ -274,90 +304,97 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
</option>
))}
</select>
</div>
{/* Operator */}
<select
className="form-select"
value={rule.operator}
onChange={(e) => updateRule(idx, { operator: e.target.value })}
style={{ fontSize: '13px' }}
>
{availableOps.map(op => (
<option key={op.value} value={op.value}>{op.label}</option>
))}
</select>
{/* Operator + Value */}
<div style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '11px', color: 'var(--text3)', fontWeight: '600', display: 'block', marginBottom: '4px' }}>
BEDINGUNG
</label>
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
<select
className="form-select"
value={rule.operator}
onChange={(e) => updateRule(idx, { operator: e.target.value })}
style={{ fontSize: '13px', flex: rule.operator === 'between' ? '0 0 80px' : '0 0 100px' }}
>
{availableOps.map(op => (
<option key={op.value} value={op.value}>{op.label}</option>
))}
</select>
{/* Value */}
{rule.operator === 'between' ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{rule.operator === 'between' ? (
<div style={{ flex: 1, display: 'flex', gap: '4px' }}>
<input
type="number"
className="form-input"
placeholder="Min"
value={Array.isArray(rule.value) ? rule.value[0] : 0}
onChange={(e) => updateRule(idx, {
value: [parseFloat(e.target.value) || 0, Array.isArray(rule.value) ? rule.value[1] : 0]
})}
style={{ fontSize: '13px', flex: 1 }}
/>
<input
type="number"
className="form-input"
placeholder="Max"
value={Array.isArray(rule.value) ? rule.value[1] : 0}
onChange={(e) => updateRule(idx, {
value: [Array.isArray(rule.value) ? rule.value[0] : 0, parseFloat(e.target.value) || 0]
})}
style={{ fontSize: '13px', flex: 1 }}
/>
</div>
) : (
<input
type="number"
className="form-input"
placeholder="Min"
value={Array.isArray(rule.value) ? rule.value[0] : 0}
onChange={(e) => updateRule(idx, {
value: [parseFloat(e.target.value) || 0, Array.isArray(rule.value) ? rule.value[1] : 0]
})}
style={{ fontSize: '13px', padding: '4px 8px' }}
placeholder="z.B. 90"
value={rule.value}
onChange={(e) => updateRule(idx, { value: parseFloat(e.target.value) || 0 })}
style={{ fontSize: '13px', flex: 1 }}
/>
<input
type="number"
className="form-input"
placeholder="Max"
value={Array.isArray(rule.value) ? rule.value[1] : 0}
onChange={(e) => updateRule(idx, {
value: [Array.isArray(rule.value) ? rule.value[0] : 0, parseFloat(e.target.value) || 0]
})}
style={{ fontSize: '13px', padding: '4px 8px' }}
/>
</div>
) : (
)}
</div>
</div>
{/* Weight - nur bei weighted_score */}
{useWeights && (
<div style={{ marginBottom: '8px', paddingTop: '8px', borderTop: '1px dashed var(--border)' }}>
<label style={{ fontSize: '11px', color: 'var(--text3)', fontWeight: '600', display: 'block', marginBottom: '4px' }}>
WICHTIGKEIT dieser Regel (1 = unwichtig, 10 = sehr wichtig)
</label>
<input
type="number"
className="form-input"
placeholder="z.B. 90"
value={rule.value}
onChange={(e) => updateRule(idx, { value: parseFloat(e.target.value) || 0 })}
style={{ fontSize: '13px' }}
placeholder="1-10"
min="1"
max="10"
value={rule.weight}
onChange={(e) => updateRule(idx, { weight: parseInt(e.target.value) || 1 })}
style={{ fontSize: '13px', width: '80px' }}
/>
)}
{/* Weight */}
<input
type="number"
className="form-input"
placeholder="1-10"
min="1"
max="10"
value={rule.weight}
onChange={(e) => updateRule(idx, { weight: parseInt(e.target.value) || 1 })}
style={{ fontSize: '13px' }}
title="Gewichtung der Regel (1=niedrig, 10=hoch)"
/>
{/* Delete */}
<button
className="btn"
onClick={() => removeRule(idx)}
style={{ padding: '6px', minWidth: 'auto' }}
title="Regel löschen"
>
<Trash2 size={14} />
</button>
</div>
</div>
)}
{/* Reason */}
<input
type="text"
className="form-input"
placeholder="Begründung (optional)"
value={rule.reason}
onChange={(e) => updateRule(idx, { reason: e.target.value })}
style={{ fontSize: '12px' }}
/>
<div style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '11px', color: 'var(--text3)', fontWeight: '600', display: 'block', marginBottom: '4px' }}>
Begründung (optional)
</label>
<input
type="text"
className="form-input"
placeholder="z.B. 'Techniktraining sollte ruhig sein'"
value={rule.reason}
onChange={(e) => updateRule(idx, { reason: e.target.value })}
style={{ fontSize: '12px', width: '100%' }}
/>
</div>
{/* Optional Checkbox */}
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px', marginTop: '4px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', color: 'var(--text2)' }}>
<input
type="checkbox"
checked={rule.optional}

View File

@ -128,10 +128,13 @@ export default function AdminTrainingTypesPage() {
try {
await api.adminUpdateTrainingType(editingProfileId, { profile })
await load()
setEditingProfileId(null)
alert('✓ Profil gespeichert!')
// Success message is shown by ProfileBuilder component
// Don't close editor immediately - let user see success message
setTimeout(() => {
setEditingProfileId(null)
}, 2000)
} catch (err) {
alert('Speichern fehlgeschlagen: ' + err.message)
// Error will be thrown to ProfileBuilder
throw err
}
}