weegschaal-app/src/components/MeasurementForm.tsx

168 lines
6.3 KiB
TypeScript

import React, { useState, useMemo } from 'react'
import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, ScrollView } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker'
import { Measurement } from '../types'
import { generateId } from '../utils/uuid'
type Props = {
onSubmit: (m: Measurement) => void
initialValues?: Partial<Measurement>
isEdit?: boolean
}
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: 'bmi', label: 'BMI', placeholder: '22.3', keyboardType: 'decimal-pad' },
{ key: 'bodyFatPercentage', label: 'Vetpercentage (%)', placeholder: '15.2', 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: 'muscleMass', label: 'Spiermassa (kg)', placeholder: '30.3', keyboardType: 'decimal-pad', computed: true },
{ key: 'visceralFat', label: 'Buikvet (index)', placeholder: '8', keyboardType: 'number-pad' },
{ key: 'bmr', label: 'Stofwisseling (kcal)', placeholder: '1650', keyboardType: 'number-pad' },
]
export default function MeasurementForm({ onSubmit, initialValues, isEdit }: Props) {
const insets = useSafeAreaInsets()
const [pickerOpen, setPickerOpen] = useState(false)
const [measurementDate, setMeasurementDate] = useState<Date>(new Date())
const [values, setValues] = useState<Record<string, string>>(() => {
const init: Record<string, string> = {}
for (const f of fields) {
if (f.computed) continue
init[f.key] = initialValues?.[f.key]?.toString() ?? ''
}
return init
})
function toFloat(s: string): number {
return parseFloat(s.replace(',', '.')) || 0
}
function toInt(s: string): number {
return parseInt(s.replace(',', '.'), 10) || 0
}
const weight = toFloat(values.weight)
const bodyFatPct = toFloat(values.bodyFatPercentage)
const musclePct = toFloat(values.musclePercentage)
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 now = new Date().toISOString()
const m: Measurement = {
id: isEdit && initialValues?.id ? initialValues.id : generateId(),
weight,
bmi: toFloat(values.bmi),
bodyFatPercentage: bodyFatPct,
fatMass: toFloat(computed.fatMass),
musclePercentage: musclePct,
muscleMass: toFloat(computed.muscleMass),
visceralFat: toInt(values.visceralFat),
bmr: toFloat(values.bmr),
measuredAt: measurementDate.toISOString(),
createdAt: isEdit && initialValues?.createdAt ? initialValues.createdAt : now,
updatedAt: now,
}
onSubmit(m)
}
const setField = (key: string, value: string) => {
setValues(prev => ({ ...prev, [key]: value }))
}
const onDateChange = (_event: DateTimePickerEvent, selectedDate?: Date) => {
setPickerOpen(false)
if (selectedDate) {
setMeasurementDate(selectedDate)
}
}
return (
<KeyboardAvoidingView behavior="padding" style={styles.container}>
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
<View style={styles.field}>
<Text style={styles.label}>Datum</Text>
<TouchableOpacity style={styles.dateButton} onPress={() => setPickerOpen(true)}>
<Text style={styles.dateButtonText}>
{measurementDate.toLocaleDateString('nl-NL', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</TouchableOpacity>
</View>
{pickerOpen && (
<DateTimePicker
value={measurementDate}
mode="date"
display="default"
onChange={onDateChange}
/>
)}
{fields.map(f => {
if (f.computed) {
const calcValue = f.key === 'fatMass' ? computed.fatMass : computed.muscleMass
return (
<View key={f.key} style={styles.field}>
<Text style={styles.label}>{f.label}</Text>
<View style={[styles.input, styles.computedInput]}>
<Text style={styles.computedText}>{calcValue || '—'}</Text>
</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>
)
})}
</ScrollView>
<TouchableOpacity style={[styles.button, { marginBottom: insets.bottom + 48 }]} onPress={handleSubmit}>
<Text style={styles.buttonText}>Opslaan</Text>
</TouchableOpacity>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f5f5' },
scroll: { padding: 16 },
field: { marginBottom: 12 },
label: { fontSize: 14, fontWeight: '600', color: '#333', marginBottom: 4 },
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' },
dateButtonText: { fontSize: 16, color: '#333' },
button: { backgroundColor: '#4CAF50', padding: 16, borderRadius: 8, alignItems: 'center', margin: 16 },
buttonText: { color: '#fff', fontSize: 16, fontWeight: '700' },
})