feat: auto-calculate fatMass and muscleMass from weight + percentages

This commit is contained in:
Eddy de Vink 2026-06-27 23:03:53 +02:00
commit 326d16a659

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react' import React, { useState, useMemo } from 'react'
import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native' import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native'
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker' import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker'
import { Measurement } from '../types' import { Measurement } from '../types'
@ -9,17 +9,27 @@ type Props = {
initialValues?: Partial<Measurement> initialValues?: Partial<Measurement>
} }
const fields: { key: keyof Measurement; label: string; placeholder: string; keyboardType: 'decimal-pad' | 'number-pad' }[] = [ type FieldDef = {
key: keyof Measurement
label: string
placeholder: string
keyboardType: 'decimal-pad' | 'number-pad'
computed?: boolean
}
const fields: FieldDef[] = [
{ key: 'weight', label: 'Gewicht (kg)', placeholder: '75.5', keyboardType: 'decimal-pad' }, { key: 'weight', label: 'Gewicht (kg)', placeholder: '75.5', keyboardType: 'decimal-pad' },
{ key: 'bmi', label: 'BMI', placeholder: '22.3', keyboardType: 'decimal-pad' }, { key: 'bmi', label: 'BMI', placeholder: '22.3', keyboardType: 'decimal-pad' },
{ key: 'bodyFatPercentage', label: 'Vetpercentage (%)', placeholder: '15.2', keyboardType: 'decimal-pad' }, { key: 'bodyFatPercentage', label: 'Vetpercentage (%)', placeholder: '15.2', keyboardType: 'decimal-pad' },
{ key: 'fatMass', label: 'Vetmassa (kg)', placeholder: '11.5', keyboardType: 'decimal-pad' }, { key: 'fatMass', label: 'Vetmassa (kg)', placeholder: '11.5', keyboardType: 'decimal-pad', computed: true },
{ key: 'musclePercentage', label: 'Spierpercentage (%)', placeholder: '40.1', keyboardType: 'decimal-pad' }, { key: 'musclePercentage', label: 'Spierpercentage (%)', placeholder: '40.1', keyboardType: 'decimal-pad' },
{ key: 'muscleMass', label: 'Spiermassa (kg)', placeholder: '30.3', keyboardType: 'decimal-pad' }, { key: 'muscleMass', label: 'Spiermassa (kg)', placeholder: '30.3', keyboardType: 'decimal-pad', computed: true },
{ key: 'visceralFat', label: 'Buikvet (index)', placeholder: '8', keyboardType: 'number-pad' }, { key: 'visceralFat', label: 'Buikvet (index)', placeholder: '8', keyboardType: 'number-pad' },
{ key: 'bmr', label: 'Stofwisseling (kcal)', placeholder: '1650', keyboardType: 'number-pad' }, { key: 'bmr', label: 'Stofwisseling (kcal)', placeholder: '1650', keyboardType: 'number-pad' },
] ]
const computedKeys = new Set<keyof Measurement>(['fatMass', 'muscleMass'])
export default function MeasurementForm({ onSubmit, initialValues }: Props) { export default function MeasurementForm({ onSubmit, initialValues }: Props) {
const [pickerOpen, setPickerOpen] = useState(false) const [pickerOpen, setPickerOpen] = useState(false)
const [measurementDate, setMeasurementDate] = useState<Date>(() => { const [measurementDate, setMeasurementDate] = useState<Date>(() => {
@ -29,21 +39,31 @@ export default function MeasurementForm({ onSubmit, initialValues }: Props) {
const [values, setValues] = useState<Record<string, string>>(() => { const [values, setValues] = useState<Record<string, string>>(() => {
const init: Record<string, string> = {} const init: Record<string, string> = {}
for (const f of fields) { for (const f of fields) {
if (f.computed) continue
init[f.key] = initialValues?.[f.key]?.toString() ?? '' init[f.key] = initialValues?.[f.key]?.toString() ?? ''
} }
return init return init
}) })
const weight = parseFloat(values.weight) || 0
const bodyFatPct = parseFloat(values.bodyFatPercentage) || 0
const musclePct = parseFloat(values.musclePercentage) || 0
const computed = useMemo(() => ({
fatMass: weight > 0 && bodyFatPct > 0 ? ((weight * bodyFatPct) / 100).toFixed(1) : '',
muscleMass: weight > 0 && musclePct > 0 ? ((weight * musclePct) / 100).toFixed(1) : '',
}), [weight, bodyFatPct, musclePct])
const handleSubmit = () => { const handleSubmit = () => {
const now = new Date().toISOString() const now = new Date().toISOString()
const m: Measurement = { const m: Measurement = {
id: generateId(), id: generateId(),
weight: parseFloat(values.weight) || 0, weight,
bmi: parseFloat(values.bmi) || 0, bmi: parseFloat(values.bmi) || 0,
bodyFatPercentage: parseFloat(values.bodyFatPercentage) || 0, bodyFatPercentage: bodyFatPct,
fatMass: parseFloat(values.fatMass) || 0, fatMass: parseFloat(computed.fatMass) || 0,
musclePercentage: parseFloat(values.musclePercentage) || 0, musclePercentage: musclePct,
muscleMass: parseFloat(values.muscleMass) || 0, muscleMass: parseFloat(computed.muscleMass) || 0,
visceralFat: parseInt(values.visceralFat, 10) || 0, visceralFat: parseInt(values.visceralFat, 10) || 0,
bmr: parseFloat(values.bmr) || 0, bmr: parseFloat(values.bmr) || 0,
measuredAt: measurementDate.toISOString(), measuredAt: measurementDate.toISOString(),
@ -91,19 +111,32 @@ export default function MeasurementForm({ onSubmit, initialValues }: Props) {
/> />
)} )}
{fields.map(f => ( {fields.map(f => {
<View key={f.key} style={styles.field}> if (f.computed) {
<Text style={styles.label}>{f.label}</Text> const calcValue = f.key === 'fatMass' ? computed.fatMass : computed.muscleMass
<TextInput return (
style={styles.input} <View key={f.key} style={styles.field}>
keyboardType={f.keyboardType} <Text style={styles.label}>{f.label}</Text>
placeholder={f.placeholder} <View style={[styles.input, styles.computedInput]}>
value={values[f.key]} <Text style={styles.computedText}>{calcValue || '—'}</Text>
onChangeText={v => setField(f.key, v)} </View>
placeholderTextColor="#999" </View>
/> )
</View> }
))} return (
<View key={f.key} style={styles.field}>
<Text style={styles.label}>{f.label}</Text>
<TextInput
style={styles.input}
keyboardType={f.keyboardType}
placeholder={f.placeholder}
value={values[f.key] ?? ''}
onChangeText={v => setField(f.key, v)}
placeholderTextColor="#999"
/>
</View>
)
})}
<TouchableOpacity style={styles.button} onPress={handleSubmit}> <TouchableOpacity style={styles.button} onPress={handleSubmit}>
<Text style={styles.buttonText}>Opslaan</Text> <Text style={styles.buttonText}>Opslaan</Text>
</TouchableOpacity> </TouchableOpacity>
@ -118,6 +151,8 @@ const styles = StyleSheet.create({
field: { marginBottom: 12 }, field: { marginBottom: 12 },
label: { fontSize: 14, fontWeight: '600', color: '#333', marginBottom: 4 }, label: { fontSize: 14, fontWeight: '600', color: '#333', marginBottom: 4 },
input: { backgroundColor: '#fff', borderRadius: 8, padding: 12, fontSize: 16, borderWidth: 1, borderColor: '#ddd' }, input: { backgroundColor: '#fff', borderRadius: 8, padding: 12, fontSize: 16, borderWidth: 1, borderColor: '#ddd' },
computedInput: { backgroundColor: '#f0f0f0' },
computedText: { fontSize: 16, color: '#666' },
dateButton: { backgroundColor: '#fff', borderRadius: 8, padding: 12, borderWidth: 1, borderColor: '#ddd' }, dateButton: { backgroundColor: '#fff', borderRadius: 8, padding: 12, borderWidth: 1, borderColor: '#ddd' },
dateButtonText: { fontSize: 16, color: '#333' }, dateButtonText: { fontSize: 16, color: '#333' },
button: { backgroundColor: '#4CAF50', padding: 16, borderRadius: 8, alignItems: 'center', marginTop: 8 }, button: { backgroundColor: '#4CAF50', padding: 16, borderRadius: 8, alignItems: 'center', marginTop: 8 },