feat: add MeasurementDetailScreen, 365-day charts, and edit support
- New MeasurementDetailScreen with wijzigen/verwijderen - WeightChart now supports 30 and 365 days - NewMeasurementScreen now handles both create and edit - Updated README to match current code - Add build-toolchain documentation - Ignore *.apk build artifacts
This commit is contained in:
parent
c499f7df13
commit
ed714af8d1
15 changed files with 730 additions and 130 deletions
|
|
@ -47,8 +47,8 @@ const styles = StyleSheet.create({
|
|||
card: { backgroundColor: '#fff', borderRadius: 12, padding: 16, margin: 16, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 8, elevation: 3 },
|
||||
title: { fontSize: 18, fontWeight: '700', marginBottom: 4 },
|
||||
date: { fontSize: 14, color: '#666', marginBottom: 12 },
|
||||
row: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 4 },
|
||||
rowLabel: { fontSize: 14, color: '#555' },
|
||||
row: { flexDirection: 'row', paddingVertical: 4 },
|
||||
rowLabel: { fontSize: 14, color: '#555', flex: 1 },
|
||||
rowValue: { fontSize: 14, fontWeight: '600' },
|
||||
emptyText: { fontSize: 16, color: '#999', textAlign: 'center', padding: 20 },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { generateId } from '../utils/uuid'
|
|||
type Props = {
|
||||
onSubmit: (m: Measurement) => void
|
||||
initialValues?: Partial<Measurement>
|
||||
isEdit?: boolean
|
||||
}
|
||||
|
||||
type FieldDef = {
|
||||
|
|
@ -29,13 +30,10 @@ const fields: FieldDef[] = [
|
|||
{ key: 'bmr', label: 'Stofwisseling (kcal)', placeholder: '1650', keyboardType: 'number-pad' },
|
||||
]
|
||||
|
||||
export default function MeasurementForm({ onSubmit, initialValues }: Props) {
|
||||
export default function MeasurementForm({ onSubmit, initialValues, isEdit }: Props) {
|
||||
const insets = useSafeAreaInsets()
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
const [measurementDate, setMeasurementDate] = useState<Date>(() => {
|
||||
if (initialValues?.measuredAt) return new Date(initialValues.measuredAt)
|
||||
return new Date()
|
||||
})
|
||||
const [measurementDate, setMeasurementDate] = useState<Date>(new Date())
|
||||
const [values, setValues] = useState<Record<string, string>>(() => {
|
||||
const init: Record<string, string> = {}
|
||||
for (const f of fields) {
|
||||
|
|
@ -47,9 +45,18 @@ export default function MeasurementForm({ onSubmit, initialValues }: Props) {
|
|||
|
||||
|
||||
|
||||
const weight = parseFloat(values.weight) || 0
|
||||
const bodyFatPct = parseFloat(values.bodyFatPercentage) || 0
|
||||
const musclePct = parseFloat(values.musclePercentage) || 0
|
||||
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) : '',
|
||||
|
|
@ -59,17 +66,17 @@ export default function MeasurementForm({ onSubmit, initialValues }: Props) {
|
|||
const handleSubmit = () => {
|
||||
const now = new Date().toISOString()
|
||||
const m: Measurement = {
|
||||
id: generateId(),
|
||||
id: isEdit && initialValues?.id ? initialValues.id : generateId(),
|
||||
weight,
|
||||
bmi: parseFloat(values.bmi) || 0,
|
||||
bmi: toFloat(values.bmi),
|
||||
bodyFatPercentage: bodyFatPct,
|
||||
fatMass: parseFloat(computed.fatMass) || 0,
|
||||
fatMass: toFloat(computed.fatMass),
|
||||
musclePercentage: musclePct,
|
||||
muscleMass: parseFloat(computed.muscleMass) || 0,
|
||||
visceralFat: parseInt(values.visceralFat, 10) || 0,
|
||||
bmr: parseFloat(values.bmr) || 0,
|
||||
muscleMass: toFloat(computed.muscleMass),
|
||||
visceralFat: toInt(values.visceralFat),
|
||||
bmr: toFloat(values.bmr),
|
||||
measuredAt: measurementDate.toISOString(),
|
||||
createdAt: now,
|
||||
createdAt: isEdit && initialValues?.createdAt ? initialValues.createdAt : now,
|
||||
updatedAt: now,
|
||||
syncStatus: 'local',
|
||||
}
|
||||
|
|
@ -140,7 +147,7 @@ export default function MeasurementForm({ onSubmit, initialValues }: Props) {
|
|||
)
|
||||
})}
|
||||
</ScrollView>
|
||||
<TouchableOpacity style={[styles.button, { marginBottom: insets.bottom + 16 }]} onPress={handleSubmit}>
|
||||
<TouchableOpacity style={[styles.button, { marginBottom: insets.bottom + 48 }]} onPress={handleSubmit}>
|
||||
<Text style={styles.buttonText}>Opslaan</Text>
|
||||
</TouchableOpacity>
|
||||
</KeyboardAvoidingView>
|
||||
|
|
@ -149,7 +156,7 @@ export default function MeasurementForm({ onSubmit, initialValues }: Props) {
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#f5f5f5' },
|
||||
scroll: { padding: 16, paddingBottom: 0 },
|
||||
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' },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react'
|
||||
import { View, Text, StyleSheet, Dimensions } from 'react-native'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { View, Text, StyleSheet, Dimensions, ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native'
|
||||
import { LineChart } from 'react-native-chart-kit'
|
||||
import { Measurement } from '../types'
|
||||
|
||||
|
|
@ -11,6 +11,9 @@ const COLORS = {
|
|||
muscleMass: '#2196F3',
|
||||
}
|
||||
|
||||
const screenWidth = Dimensions.get('window').width
|
||||
const chartWidth = screenWidth - 32
|
||||
|
||||
function SingleChart({
|
||||
title,
|
||||
data,
|
||||
|
|
@ -29,7 +32,7 @@ function SingleChart({
|
|||
<Text style={styles.chartTitle}>{title}</Text>
|
||||
<LineChart
|
||||
data={{ labels, datasets: [{ data, color: () => color, strokeWidth: 2 }] }}
|
||||
width={Dimensions.get('window').width - 32}
|
||||
width={chartWidth}
|
||||
height={180}
|
||||
yAxisSuffix={unit}
|
||||
chartConfig={{
|
||||
|
|
@ -57,7 +60,56 @@ function hexToRgb(hex: string): string {
|
|||
return `${r}, ${g}, ${b}`
|
||||
}
|
||||
|
||||
function filterData(sorted: Measurement[], days: number, lastDate: Date) {
|
||||
const cutoff = new Date(lastDate)
|
||||
cutoff.setDate(cutoff.getDate() - days)
|
||||
const filtered = sorted.filter(m => new Date(m.measuredAt) >= cutoff)
|
||||
return filtered.length >= 2 ? filtered : sorted
|
||||
}
|
||||
|
||||
function prepareLabels(dataPoints: Measurement[]) {
|
||||
const labels = dataPoints.map(m => {
|
||||
const d = new Date(m.measuredAt)
|
||||
return `${d.getDate()}/${d.getMonth() + 1}`
|
||||
})
|
||||
const labelStep = labels.length > 8 ? Math.ceil(labels.length / 8) : 1
|
||||
return labels.map((l, i) => (i % labelStep === 0 ? l : ''))
|
||||
}
|
||||
|
||||
function ChartSet({ dataPoints, title }: { dataPoints: Measurement[]; title: string }) {
|
||||
const labels = prepareLabels(dataPoints)
|
||||
return (
|
||||
<View style={styles.page}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<SingleChart
|
||||
title="Gewicht"
|
||||
data={dataPoints.map(m => m.weight)}
|
||||
labels={labels}
|
||||
color={COLORS.weight}
|
||||
unit=" kg"
|
||||
/>
|
||||
<SingleChart
|
||||
title="Vetmassa"
|
||||
data={dataPoints.map(m => m.fatMass)}
|
||||
labels={labels}
|
||||
color={COLORS.fatMass}
|
||||
unit=" kg"
|
||||
/>
|
||||
<SingleChart
|
||||
title="Spiermassa"
|
||||
data={dataPoints.map(m => m.muscleMass)}
|
||||
labels={labels}
|
||||
color={COLORS.muscleMass}
|
||||
unit=" kg"
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default function WeightChart({ measurements }: Props) {
|
||||
const scrollRef = useRef<ScrollView>(null)
|
||||
const [page, setPage] = useState(0)
|
||||
|
||||
if (measurements.length < 2) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -66,52 +118,38 @@ export default function WeightChart({ measurements }: Props) {
|
|||
(a, b) => new Date(a.measuredAt).getTime() - new Date(b.measuredAt).getTime()
|
||||
)
|
||||
|
||||
const cutoff = new Date(sorted[sorted.length - 1].measuredAt)
|
||||
cutoff.setDate(cutoff.getDate() - 30)
|
||||
const filtered = sorted.filter(m => new Date(m.measuredAt) >= cutoff)
|
||||
const dataPoints = filtered.length >= 2 ? filtered : sorted
|
||||
const lastDate = new Date(sorted[sorted.length - 1].measuredAt)
|
||||
|
||||
const labels = dataPoints.map(m => {
|
||||
const d = new Date(m.measuredAt)
|
||||
return `${d.getDate()}/${d.getMonth() + 1}`
|
||||
})
|
||||
const data30 = filterData(sorted, 30, lastDate)
|
||||
const data365 = filterData(sorted, 365, lastDate)
|
||||
|
||||
const labelStep = labels.length > 8 ? Math.ceil(labels.length / 8) : 1
|
||||
const sparseLabels = labels.map((l, i) => (i % labelStep === 0 ? l : ''))
|
||||
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const offset = e.nativeEvent.contentOffset.x
|
||||
const newPage = Math.round(offset / screenWidth)
|
||||
if (newPage !== page) {
|
||||
setPage(newPage)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Voortgang (30 dagen)</Text>
|
||||
|
||||
<SingleChart
|
||||
title="Gewicht"
|
||||
data={dataPoints.map(m => m.weight)}
|
||||
labels={sparseLabels}
|
||||
color={COLORS.weight}
|
||||
unit=" kg"
|
||||
/>
|
||||
|
||||
<SingleChart
|
||||
title="Vetmassa"
|
||||
data={dataPoints.map(m => m.fatMass)}
|
||||
labels={sparseLabels}
|
||||
color={COLORS.fatMass}
|
||||
unit=" kg"
|
||||
/>
|
||||
|
||||
<SingleChart
|
||||
title="Spiermassa"
|
||||
data={dataPoints.map(m => m.muscleMass)}
|
||||
labels={sparseLabels}
|
||||
color={COLORS.muscleMass}
|
||||
unit=" kg"
|
||||
/>
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
onMomentumScrollEnd={onScroll}
|
||||
>
|
||||
<ChartSet dataPoints={data30} title="Voortgang (30 dagen)" />
|
||||
<ChartSet dataPoints={data365} title="Voortgang (365 dagen)" />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { marginHorizontal: 16, marginBottom: 16 },
|
||||
container: { marginBottom: 16 },
|
||||
page: { width: screenWidth, paddingHorizontal: 16 },
|
||||
title: { fontSize: 18, fontWeight: '700', marginBottom: 8 },
|
||||
chartBlock: { marginBottom: 12 },
|
||||
chartTitle: { fontSize: 13, fontWeight: '600', color: '#555', marginBottom: 4, marginLeft: 4 },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useCallback, useState } from 'react'
|
||||
import { View, FlatList, StyleSheet, Text, RefreshControl, Alert } from 'react-native'
|
||||
import { View, FlatList, StyleSheet, Text, RefreshControl, TouchableOpacity } from 'react-native'
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import { initDatabase, getAllMeasurements, deleteMeasurement } from '../database/db'
|
||||
import { Measurement } from '../types'
|
||||
|
|
@ -80,7 +80,9 @@ export default function HistoryScreen({ navigation }: Props) {
|
|||
data={measurements}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<MeasurementRow measurement={item} onPress={() => handlePress(item)} />
|
||||
<TouchableOpacity onPress={() => navigation.navigate('MeasurementDetail', { measurementId: item.id })}>
|
||||
<MeasurementRow measurement={item} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
contentContainerStyle={measurements.length === 0 ? styles.empty : undefined}
|
||||
ListEmptyComponent={<Text style={styles.emptyText}>Nog geen metingen</Text>}
|
||||
|
|
|
|||
135
src/screens/MeasurementDetailScreen.tsx
Normal file
135
src/screens/MeasurementDetailScreen.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import React, { useCallback, useState } from 'react'
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView } from 'react-native'
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { initDatabase, getAllMeasurements, deleteMeasurement } from '../database/db'
|
||||
import { Measurement } from '../types'
|
||||
|
||||
type Props = {
|
||||
navigation: any
|
||||
route: {
|
||||
params: {
|
||||
measurementId: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows: { label: string; key: keyof Measurement; suffix: string }[] = [
|
||||
{ label: 'Gewicht', key: 'weight', suffix: 'kg' },
|
||||
{ label: 'BMI', key: 'bmi', suffix: '' },
|
||||
{ label: 'Vetpercentage', key: 'bodyFatPercentage', suffix: '%' },
|
||||
{ label: 'Vetmassa', key: 'fatMass', suffix: 'kg' },
|
||||
{ label: 'Spierpercentage', key: 'musclePercentage', suffix: '%' },
|
||||
{ label: 'Spiermassa', key: 'muscleMass', suffix: 'kg' },
|
||||
{ label: 'Buikvet', key: 'visceralFat', suffix: '' },
|
||||
{ label: 'Stofwisseling', key: 'bmr', suffix: 'kcal' },
|
||||
]
|
||||
|
||||
export default function MeasurementDetailScreen({ navigation, route }: Props) {
|
||||
const insets = useSafeAreaInsets()
|
||||
const { measurementId } = route.params
|
||||
const [measurement, setMeasurement] = useState<Measurement | null>(null)
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const load = async () => {
|
||||
await initDatabase()
|
||||
const all = await getAllMeasurements()
|
||||
const m = all.find(x => x.id === measurementId)
|
||||
setMeasurement(m ?? null)
|
||||
}
|
||||
load()
|
||||
}, [measurementId])
|
||||
)
|
||||
|
||||
const handleEdit = () => {
|
||||
navigation.navigate('NewMeasurement', { measurement })
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
Alert.alert(
|
||||
'Meting verwijderen?',
|
||||
'Dit kan niet ongedaan worden gemaakt.',
|
||||
[
|
||||
{ text: 'Annuleren', style: 'cancel' },
|
||||
{
|
||||
text: 'Verwijderen',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await initDatabase()
|
||||
await deleteMeasurement(measurementId)
|
||||
try {
|
||||
const { syncAll } = await import('../sync/syncService')
|
||||
await syncAll(new Set([measurementId]))
|
||||
} catch {}
|
||||
navigation.goBack()
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
if (!measurement) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<Text style={{ color: '#999' }}>Laden...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
<Text style={styles.date}>
|
||||
{new Date(measurement.measuredAt).toLocaleDateString('nl-NL', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
|
||||
{rows.map(r => (
|
||||
<View key={r.key} style={styles.row}>
|
||||
<Text style={styles.label}>{r.label}</Text>
|
||||
<Text style={styles.value}>
|
||||
{typeof measurement[r.key] === 'number'
|
||||
? (measurement[r.key] as number).toFixed(1)
|
||||
: measurement[r.key]}{' '}
|
||||
{r.suffix}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
<View style={[styles.buttons, { paddingBottom: insets.bottom + 16 }]}>
|
||||
<TouchableOpacity style={styles.editButton} onPress={handleEdit}>
|
||||
<Text style={styles.buttonText}>Wijzigen</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.deleteButton} onPress={handleDelete}>
|
||||
<Text style={styles.buttonText}>Verwijderen</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#f5f5f5' },
|
||||
content: { padding: 16 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f5f5f5' },
|
||||
date: { fontSize: 18, fontWeight: '700', marginBottom: 16, textAlign: 'center' },
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#fff',
|
||||
padding: 14,
|
||||
marginBottom: 8,
|
||||
borderRadius: 8,
|
||||
},
|
||||
label: { fontSize: 15, color: '#555', flex: 1 },
|
||||
value: { fontSize: 15, fontWeight: '600' },
|
||||
buttons: { flexDirection: 'row', gap: 12, padding: 16 },
|
||||
editButton: { flex: 1, backgroundColor: '#2196F3', padding: 14, borderRadius: 8, alignItems: 'center' },
|
||||
deleteButton: { flex: 1, backgroundColor: '#e53935', padding: 14, borderRadius: 8, alignItems: 'center' },
|
||||
buttonText: { color: '#fff', fontSize: 16, fontWeight: '700' },
|
||||
})
|
||||
|
|
@ -3,88 +3,74 @@ import { View, Text, Alert } from 'react-native'
|
|||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import MeasurementForm from '../components/MeasurementForm'
|
||||
import { Measurement } from '../types'
|
||||
import {
|
||||
initDatabase,
|
||||
insertMeasurement,
|
||||
getLatestMeasurement,
|
||||
getMeasurementByDate,
|
||||
upsertMeasurement,
|
||||
} from '../database/db'
|
||||
import { initDatabase, insertMeasurement, upsertMeasurement, getLatestMeasurement, getMeasurementByDate } from '../database/db'
|
||||
|
||||
type Props = {
|
||||
navigation: any
|
||||
route: any
|
||||
route: {
|
||||
params: {
|
||||
measurement?: Measurement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function NewMeasurementScreen({ navigation, route }: Props) {
|
||||
const editing = route.params?.measurement as Measurement | undefined
|
||||
const editMeasurement = route.params?.measurement
|
||||
const isEdit = !!editMeasurement
|
||||
const [latest, setLatest] = useState<Partial<Measurement> | undefined>(undefined)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({ title: isEdit ? 'Meting wijzigen' : 'Nieuwe meting' })
|
||||
}, [navigation, isEdit])
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (isEdit) {
|
||||
setLatest(editMeasurement)
|
||||
setLoaded(true)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
const load = async () => {
|
||||
await initDatabase()
|
||||
if (editing) {
|
||||
if (!cancelled) {
|
||||
setLatest(editing)
|
||||
setLoaded(true)
|
||||
}
|
||||
} else {
|
||||
const m = await getLatestMeasurement()
|
||||
if (!cancelled) {
|
||||
setLatest(m || {})
|
||||
setLoaded(true)
|
||||
}
|
||||
const m = await getLatestMeasurement()
|
||||
if (!cancelled) {
|
||||
setLatest(m || {})
|
||||
setLoaded(true)
|
||||
}
|
||||
}
|
||||
load()
|
||||
return () => { cancelled = true }
|
||||
}, [editing])
|
||||
}, [isEdit, editMeasurement])
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) {
|
||||
navigation.setOptions({ title: 'Meting wijzigen' })
|
||||
}
|
||||
}, [editing, navigation])
|
||||
|
||||
const handleSubmit = async (m: Measurement) => {
|
||||
await initDatabase()
|
||||
if (editing) {
|
||||
m.id = editing.id
|
||||
m.createdAt = editing.createdAt
|
||||
if (isEdit) {
|
||||
await upsertMeasurement(m)
|
||||
doSync()
|
||||
return
|
||||
}
|
||||
const existing = await getMeasurementByDate(m.measuredAt)
|
||||
if (existing) {
|
||||
Alert.alert(
|
||||
'Dubbele datum',
|
||||
`Er bestaat al een meting van ${new Date(m.measuredAt).toLocaleDateString('nl-NL')}. Overschrijven?`,
|
||||
[
|
||||
{ text: 'Annuleren', style: 'cancel' },
|
||||
{ text: 'Overschrijven', style: 'destructive', onPress: () => doSave(m, existing.id) },
|
||||
]
|
||||
)
|
||||
return
|
||||
} else {
|
||||
const existing = await getMeasurementByDate(m.measuredAt)
|
||||
if (existing) {
|
||||
Alert.alert(
|
||||
'Dubbele datum',
|
||||
`Er bestaat al een meting van ${new Date(m.measuredAt).toLocaleDateString('nl-NL')}. Overschrijven?`,
|
||||
[
|
||||
{ text: 'Annuleren', style: 'cancel' },
|
||||
{ text: 'Overschrijven', style: 'destructive', onPress: () => {
|
||||
m.id = existing.id
|
||||
doSave(m)
|
||||
}},
|
||||
]
|
||||
)
|
||||
return
|
||||
}
|
||||
await insertMeasurement(m)
|
||||
}
|
||||
doSave(m)
|
||||
}
|
||||
|
||||
const doSave = async (m: Measurement, existingId?: string) => {
|
||||
if (existingId) {
|
||||
m.id = existingId
|
||||
await upsertMeasurement(m)
|
||||
} else {
|
||||
await insertMeasurement(m)
|
||||
}
|
||||
doSync()
|
||||
}
|
||||
|
||||
const doSync = async () => {
|
||||
const doSave = async (m: Measurement) => {
|
||||
try {
|
||||
const { syncAll } = await import('../sync/syncService')
|
||||
await syncAll()
|
||||
|
|
@ -110,6 +96,7 @@ export default function NewMeasurementScreen({ navigation, route }: Props) {
|
|||
<MeasurementForm
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={latest as Partial<Measurement> | undefined}
|
||||
isEdit={isEdit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@ const WEBDAV_PATH = 'weegschaal/measurements.json'
|
|||
|
||||
type SyncResult = { local: number; remote: number; merged: number }
|
||||
|
||||
export function mergeMeasurements(local: Measurement[], remote: Measurement[]): Measurement[] {
|
||||
export function mergeMeasurements(local: Measurement[], remote: Measurement[], excludeIds?: Set<string>): Measurement[] {
|
||||
const map = new Map<string, Measurement>()
|
||||
|
||||
for (const m of local) {
|
||||
if (excludeIds?.has(m.id)) continue
|
||||
map.set(m.id, m)
|
||||
}
|
||||
|
||||
for (const m of remote) {
|
||||
if (excludeIds?.has(m.id)) continue
|
||||
const existing = map.get(m.id)
|
||||
if (!existing || new Date(m.updatedAt) > new Date(existing.updatedAt)) {
|
||||
map.set(m.id, { ...m, syncStatus: 'synced' })
|
||||
|
|
@ -82,7 +84,7 @@ async function uploadRemote(
|
|||
}
|
||||
}
|
||||
|
||||
export async function syncAll(): Promise<SyncResult> {
|
||||
export async function syncAll(excludeIds?: Set<string>): Promise<SyncResult> {
|
||||
const config = await getWebDAVConfig()
|
||||
if (!config) {
|
||||
throw new Error('Geen WebDAV configuratie. Stel deze eerst in in Instellingen.')
|
||||
|
|
@ -93,7 +95,7 @@ export async function syncAll(): Promise<SyncResult> {
|
|||
const local = await getAllMeasurements()
|
||||
const remote = await fetchRemote(config)
|
||||
|
||||
const merged = mergeMeasurements(local, remote)
|
||||
const merged = mergeMeasurements(local, remote, excludeIds)
|
||||
|
||||
for (const m of merged) {
|
||||
await upsertMeasurement(m)
|
||||
|
|
|
|||
Loading…
Reference in a new issue