v9d Phase 2d: Vitals Module Refactoring (Baseline + Blood Pressure) #22
|
|
@ -22,6 +22,7 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
|
||||||
const [profile, setProfile] = useState(null)
|
const [profile, setProfile] = useState(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [expandedSections, setExpandedSections] = useState({ minReq: true })
|
const [expandedSections, setExpandedSections] = useState({ minReq: true })
|
||||||
|
const [successMessage, setSuccessMessage] = useState(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialize or load existing profile
|
// Initialize or load existing profile
|
||||||
|
|
@ -146,6 +147,12 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await onSave(profile)
|
await onSave(profile)
|
||||||
|
setSuccessMessage('✓ Profil gespeichert!')
|
||||||
|
setTimeout(() => {
|
||||||
|
setSuccessMessage(null)
|
||||||
|
}, 2000)
|
||||||
|
} catch (err) {
|
||||||
|
// Error is already handled by parent
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +173,21 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 */}
|
{/* Minimum Requirements */}
|
||||||
<div className="card" style={{ marginBottom: '16px' }}>
|
<div className="card" style={{ marginBottom: '16px' }}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -239,6 +261,7 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
|
||||||
const availableOps = OPERATORS.filter(op =>
|
const availableOps = OPERATORS.filter(op =>
|
||||||
param ? op.types.includes(param.data_type) : true
|
param ? op.types.includes(param.data_type) : true
|
||||||
)
|
)
|
||||||
|
const useWeights = minReq.pass_strategy === 'weighted_score'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -250,23 +273,30 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
|
||||||
marginBottom: '8px'
|
marginBottom: '8px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Labels Row */}
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '12px' }}>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1.5fr 1fr 0.8fr 40px', gap: '8px', marginBottom: '4px' }}>
|
<div style={{ fontSize: '12px', fontWeight: '600', color: 'var(--text2)' }}>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--text3)', fontWeight: '600' }}>PARAMETER</div>
|
Regel {idx + 1}
|
||||||
<div style={{ fontSize: '10px', color: 'var(--text3)', fontWeight: '600' }}>OPERATOR</div>
|
</div>
|
||||||
<div style={{ fontSize: '10px', color: 'var(--text3)', fontWeight: '600' }}>SCHWELLENWERT</div>
|
<button
|
||||||
<div style={{ fontSize: '10px', color: 'var(--text3)', fontWeight: '600' }}>GEWICHT</div>
|
className="btn"
|
||||||
<div></div>
|
onClick={() => removeRule(idx)}
|
||||||
|
style={{ padding: '4px 8px', minWidth: 'auto', fontSize: '11px' }}
|
||||||
|
title="Regel löschen"
|
||||||
|
>
|
||||||
|
<Trash2 size={12} /> Löschen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input Row */}
|
{/* Parameter */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1.5fr 1fr 0.8fr 40px', gap: '8px', marginBottom: '8px' }}>
|
<div style={{ marginBottom: '8px' }}>
|
||||||
{/* Parameter */}
|
<label style={{ fontSize: '11px', color: 'var(--text3)', fontWeight: '600', display: 'block', marginBottom: '4px' }}>
|
||||||
|
WAS soll geprüft werden?
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={rule.parameter}
|
value={rule.parameter}
|
||||||
onChange={(e) => updateRule(idx, { parameter: e.target.value })}
|
onChange={(e) => updateRule(idx, { parameter: e.target.value })}
|
||||||
style={{ fontSize: '13px' }}
|
style={{ fontSize: '13px', width: '100%' }}
|
||||||
>
|
>
|
||||||
{parameters.map(p => (
|
{parameters.map(p => (
|
||||||
<option key={p.key} value={p.key}>
|
<option key={p.key} value={p.key}>
|
||||||
|
|
@ -274,90 +304,97 @@ export default function ProfileBuilder({ trainingType, onSave, onCancel, paramet
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Operator */}
|
{/* Operator + Value */}
|
||||||
<select
|
<div style={{ marginBottom: '8px' }}>
|
||||||
className="form-select"
|
<label style={{ fontSize: '11px', color: 'var(--text3)', fontWeight: '600', display: 'block', marginBottom: '4px' }}>
|
||||||
value={rule.operator}
|
BEDINGUNG
|
||||||
onChange={(e) => updateRule(idx, { operator: e.target.value })}
|
</label>
|
||||||
style={{ fontSize: '13px' }}
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
|
||||||
>
|
<select
|
||||||
{availableOps.map(op => (
|
className="form-select"
|
||||||
<option key={op.value} value={op.value}>{op.label}</option>
|
value={rule.operator}
|
||||||
))}
|
onChange={(e) => updateRule(idx, { operator: e.target.value })}
|
||||||
</select>
|
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' ? (
|
||||||
{rule.operator === 'between' ? (
|
<div style={{ flex: 1, display: 'flex', gap: '4px' }}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', 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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-input"
|
className="form-input"
|
||||||
placeholder="Min"
|
placeholder="z.B. 90"
|
||||||
value={Array.isArray(rule.value) ? rule.value[0] : 0}
|
value={rule.value}
|
||||||
onChange={(e) => updateRule(idx, {
|
onChange={(e) => updateRule(idx, { value: parseFloat(e.target.value) || 0 })}
|
||||||
value: [parseFloat(e.target.value) || 0, Array.isArray(rule.value) ? rule.value[1] : 0]
|
style={{ fontSize: '13px', flex: 1 }}
|
||||||
})}
|
|
||||||
style={{ fontSize: '13px', padding: '4px 8px' }}
|
|
||||||
/>
|
/>
|
||||||
<input
|
)}
|
||||||
type="number"
|
</div>
|
||||||
className="form-input"
|
</div>
|
||||||
placeholder="Max"
|
|
||||||
value={Array.isArray(rule.value) ? rule.value[1] : 0}
|
{/* Weight - nur bei weighted_score */}
|
||||||
onChange={(e) => updateRule(idx, {
|
{useWeights && (
|
||||||
value: [Array.isArray(rule.value) ? rule.value[0] : 0, parseFloat(e.target.value) || 0]
|
<div style={{ marginBottom: '8px', paddingTop: '8px', borderTop: '1px dashed var(--border)' }}>
|
||||||
})}
|
<label style={{ fontSize: '11px', color: 'var(--text3)', fontWeight: '600', display: 'block', marginBottom: '4px' }}>
|
||||||
style={{ fontSize: '13px', padding: '4px 8px' }}
|
WICHTIGKEIT dieser Regel (1 = unwichtig, 10 = sehr wichtig)
|
||||||
/>
|
</label>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-input"
|
className="form-input"
|
||||||
placeholder="z.B. 90"
|
placeholder="1-10"
|
||||||
value={rule.value}
|
min="1"
|
||||||
onChange={(e) => updateRule(idx, { value: parseFloat(e.target.value) || 0 })}
|
max="10"
|
||||||
style={{ fontSize: '13px' }}
|
value={rule.weight}
|
||||||
|
onChange={(e) => updateRule(idx, { weight: parseInt(e.target.value) || 1 })}
|
||||||
|
style={{ fontSize: '13px', width: '80px' }}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
{/* Reason */}
|
{/* Reason */}
|
||||||
<input
|
<div style={{ marginBottom: '8px' }}>
|
||||||
type="text"
|
<label style={{ fontSize: '11px', color: 'var(--text3)', fontWeight: '600', display: 'block', marginBottom: '4px' }}>
|
||||||
className="form-input"
|
Begründung (optional)
|
||||||
placeholder="Begründung (optional)"
|
</label>
|
||||||
value={rule.reason}
|
<input
|
||||||
onChange={(e) => updateRule(idx, { reason: e.target.value })}
|
type="text"
|
||||||
style={{ fontSize: '12px' }}
|
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 */}
|
{/* 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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={rule.optional}
|
checked={rule.optional}
|
||||||
|
|
|
||||||
|
|
@ -128,10 +128,13 @@ export default function AdminTrainingTypesPage() {
|
||||||
try {
|
try {
|
||||||
await api.adminUpdateTrainingType(editingProfileId, { profile })
|
await api.adminUpdateTrainingType(editingProfileId, { profile })
|
||||||
await load()
|
await load()
|
||||||
setEditingProfileId(null)
|
// Success message is shown by ProfileBuilder component
|
||||||
alert('✓ Profil gespeichert!')
|
// Don't close editor immediately - let user see success message
|
||||||
|
setTimeout(() => {
|
||||||
|
setEditingProfileId(null)
|
||||||
|
}, 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Speichern fehlgeschlagen: ' + err.message)
|
// Error will be thrown to ProfileBuilder
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user