From 28725e9ba374bcf02758d2c662798c9fd3420bf8 Mon Sep 17 00:00:00 2001 From: oliexdev Date: Thu, 14 Aug 2025 12:02:28 +0200 Subject: [PATCH] When calculating derived values, the code now fetches the unit from the `MeasurementType` and converts the input values (e.g., weight to KG, waist to CM) before passing them to the calculation functions. This ensures calculations are performed with consistent units. --- .../core/database/DatabaseRepository.kt | 122 +++++++++++++--- .../ui/screen/settings/SettingsViewModel.kt | 136 +++++++++--------- 2 files changed, 176 insertions(+), 82 deletions(-) diff --git a/android_app/app/src/main/java/com/health/openscale/core/database/DatabaseRepository.kt b/android_app/app/src/main/java/com/health/openscale/core/database/DatabaseRepository.kt index f1a05b53..13da314e 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/database/DatabaseRepository.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/database/DatabaseRepository.kt @@ -19,13 +19,17 @@ package com.health.openscale.core.database import com.health.openscale.core.data.ActivityLevel import com.health.openscale.core.data.GenderType +import com.health.openscale.core.data.MeasureUnit import com.health.openscale.core.data.Measurement import com.health.openscale.core.data.MeasurementType import com.health.openscale.core.data.MeasurementTypeKey import com.health.openscale.core.data.MeasurementValue +import com.health.openscale.core.data.UnitType import com.health.openscale.core.data.User +import com.health.openscale.core.data.WeightUnit import com.health.openscale.core.model.MeasurementWithValues import com.health.openscale.core.utils.CalculationUtil +import com.health.openscale.core.utils.Converters import com.health.openscale.core.utils.LogManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -220,8 +224,9 @@ class DatabaseRepository( } val userId = measurement.userId + // Fetch all current values for this specific measurement and all global MeasurementType definitions val currentMeasurementValues = measurementValueDao.getValuesForMeasurement(measurementId).first() - val allGlobalTypes = measurementTypeDao.getAll().first() + val allGlobalTypes = measurementTypeDao.getAll().first() // These are MeasurementType objects, containing unit info val user = userDao.getById(userId).first() ?: run { LogManager.w(DERIVED_VALUES_TAG, "User with ID $userId not found for measurement $measurementId. Cannot recalculate derived values.") return @@ -230,16 +235,22 @@ class DatabaseRepository( LogManager.d(DERIVED_VALUES_TAG, "Fetched ${currentMeasurementValues.size} current values, " + "${allGlobalTypes.size} global types, and user '${user.name}' for measurement $measurementId.") - val findValue = { key: MeasurementTypeKey -> - val type = allGlobalTypes.find { it.key == key } - if (type == null) { + // Helper to find a raw value and its unit from the persisted MeasurementValues and MeasurementTypes + val findValueAndUnit = { key: MeasurementTypeKey -> + val measurementTypeObject = allGlobalTypes.find { it.key == key } + if (measurementTypeObject == null) { LogManager.w(DERIVED_VALUES_TAG, "MeasurementType for key '$key' not found in global types list.") + Pair(null, null) // Return nulls if the type definition is missing + } else { + val valueObject = currentMeasurementValues.find { it.typeId == measurementTypeObject.id } + val value = valueObject?.floatValue + val unit = measurementTypeObject.unit // The unit is defined in the MeasurementType object + LogManager.v(DERIVED_VALUES_TAG, "findValueAndUnit for $key (typeId: ${measurementTypeObject.id}, unit: $unit): ${value ?: "not found"}") + Pair(value, unit) } - val value = currentMeasurementValues.find { it.typeId == type?.id }?.floatValue - LogManager.v(DERIVED_VALUES_TAG, "findValue for $key (typeId: ${type?.id}): ${value ?: "not found"}") - value } + // Helper to save or update a derived measurement value val saveOrUpdateDerivedValue: suspend (value: Float?, typeKey: MeasurementTypeKey) -> Unit = save@{ derivedValue, derivedValueTypeKey -> val derivedTypeObject = allGlobalTypes.find { it.key == derivedValueTypeKey } @@ -252,6 +263,7 @@ class DatabaseRepository( val existingDerivedValueObject = currentMeasurementValues.find { it.typeId == derivedTypeObject.id } if (derivedValue == null) { + // If derived value is null, delete any existing persisted value for it if (existingDerivedValueObject != null) { measurementValueDao.deleteById(existingDerivedValueObject.id) LogManager.d(DERIVED_VALUES_TAG, "Derived value for key ${derivedTypeObject.key} is null. Deleted existing value (ID: ${existingDerivedValueObject.id}).") @@ -259,7 +271,8 @@ class DatabaseRepository( LogManager.v(DERIVED_VALUES_TAG, "Derived value for key ${derivedTypeObject.key} is null. No existing value to delete.") } } else { - val roundedValue = roundTo(derivedValue) + // If derived value is not null, insert or update it + val roundedValue = roundTo(derivedValue) // Apply rounding if (existingDerivedValueObject != null) { if (existingDerivedValueObject.floatValue != roundedValue) { measurementValueDao.update(existingDerivedValueObject.copy(floatValue = roundedValue)) @@ -280,20 +293,93 @@ class DatabaseRepository( } } - val weightKg = findValue(MeasurementTypeKey.WEIGHT) - val bodyFatPercentage = findValue(MeasurementTypeKey.BODY_FAT) - val waistCm = findValue(MeasurementTypeKey.WAIST) - val hipsCm = findValue(MeasurementTypeKey.HIPS) - val caliper1Cm = findValue(MeasurementTypeKey.CALIPER_1) - val caliper2Cm = findValue(MeasurementTypeKey.CALIPER_2) - val caliper3Cm = findValue(MeasurementTypeKey.CALIPER_3) + // Fetch raw values and their original units + val (weightValue, weightUnitType) = findValueAndUnit(MeasurementTypeKey.WEIGHT) + val (bodyFatValue, _) = findValueAndUnit(MeasurementTypeKey.BODY_FAT) // Unit usually % (UnitType.PERCENT) + val (waistValue, waistUnitType) = findValueAndUnit(MeasurementTypeKey.WAIST) + val (hipsValue, hipsUnitType) = findValueAndUnit(MeasurementTypeKey.HIPS) + val (caliper1Value, caliper1UnitType) = findValueAndUnit(MeasurementTypeKey.CALIPER_1) + val (caliper2Value, caliper2UnitType) = findValueAndUnit(MeasurementTypeKey.CALIPER_2) + val (caliper3Value, caliper3UnitType) = findValueAndUnit(MeasurementTypeKey.CALIPER_3) - processBmiCalculation(weightKg, user.heightCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.BMI) } + // --- CONVERT VALUES TO REQUIRED UNITS FOR CALCULATIONS --- + + // Convert weight to Kilograms (KG) + val weightKg: Float? = if (weightValue != null && weightUnitType != null) { + when (weightUnitType) { + UnitType.KG -> weightValue + UnitType.LB -> Converters.toKilogram(weightValue, WeightUnit.LB) + UnitType.ST -> Converters.toKilogram(weightValue, WeightUnit.ST) + else -> { + LogManager.w(DERIVED_VALUES_TAG, "Unsupported unit $weightUnitType for weight conversion. Assuming KG if value present for ${MeasurementTypeKey.WEIGHT}.") + weightValue // Fallback or handle error appropriately + } + } + } else null + + // Body fat is typically already in percentage + val bodyFatPercentage: Float? = bodyFatValue + + // Convert waist circumference to Centimeters (CM) + val waistCm: Float? = if (waistValue != null && waistUnitType != null) { + when (waistUnitType) { + UnitType.CM -> waistValue + UnitType.INCH -> Converters.toCentimeter(waistValue, MeasureUnit.INCH) + else -> { + LogManager.w(DERIVED_VALUES_TAG, "Unsupported unit $waistUnitType for waist conversion. Assuming CM if value present for ${MeasurementTypeKey.WAIST}.") + waistValue + } + } + } else null + + // Convert hips circumference to Centimeters (CM) + val hipsCm: Float? = if (hipsValue != null && hipsUnitType != null) { + when (hipsUnitType) { + UnitType.CM -> hipsValue + UnitType.INCH -> Converters.toCentimeter(hipsValue, MeasureUnit.INCH) + else -> { + LogManager.w(DERIVED_VALUES_TAG, "Unsupported unit $hipsUnitType for hips conversion. Assuming CM if value present for ${MeasurementTypeKey.HIPS}.") + hipsValue + } + } + } else null + + // Convert caliper measurements to Centimeters (CM) + val caliper1Cm: Float? = if (caliper1Value != null && caliper1UnitType != null) { + when (caliper1UnitType) { + UnitType.CM -> caliper1Value + UnitType.INCH -> Converters.toCentimeter(caliper1Value, MeasureUnit.INCH) + else -> caliper1Value // Fallback + } + } else null + val caliper2Cm: Float? = if (caliper2Value != null && caliper2UnitType != null) { + when (caliper2UnitType) { + UnitType.CM -> caliper2Value + UnitType.INCH -> Converters.toCentimeter(caliper2Value, MeasureUnit.INCH) + else -> caliper2Value + } + } else null + val caliper3Cm: Float? = if (caliper3Value != null && caliper3UnitType != null) { + when (caliper3UnitType) { + UnitType.CM -> caliper3Value + UnitType.INCH -> Converters.toCentimeter(caliper3Value, MeasureUnit.INCH) + else -> caliper3Value + } + } else null + + // User's height is assumed to be stored in CM in the User object + val userHeightCm = user.heightCm + + // --- PERFORM DERIVED VALUE CALCULATIONS --- + // Pass the converted values (e.g., weightKg, waistCm) to the processing functions + + processBmiCalculation(weightKg, userHeightCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.BMI) } processLbmCalculation(weightKg, bodyFatPercentage).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.LBM) } processWhrCalculation(waistCm, hipsCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.WHR) } - processWhtrCalculation(waistCm, user.heightCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.WHTR) } - processBmrCalculation(weightKg, user).also { bmr -> + processWhtrCalculation(waistCm, userHeightCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.WHTR) } + processBmrCalculation(weightKg, user).also { bmr -> // user object contains heightCm and other necessary details saveOrUpdateDerivedValue(bmr, MeasurementTypeKey.BMR) + // TDEE calculation depends on the BMR result processTDEECalculation(bmr, user.activityLevel).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.TDEE) } } processFatCaliperCalculation(caliper1Cm, caliper2Cm, caliper3Cm, user) diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsViewModel.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsViewModel.kt index 1011eb10..ca1b32c2 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsViewModel.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsViewModel.kt @@ -1158,7 +1158,7 @@ class SettingsViewModel( */ fun updateMeasurementTypeAndConvertDataViewModelCentric( originalType: MeasurementType, - updatedType: MeasurementType, + updatedType: MeasurementType, // This contains the new unit and other proposed changes from the UI showSnackbarMaster: Boolean = true ) { viewModelScope.launch { @@ -1170,14 +1170,54 @@ class SettingsViewModel( var conversionErrorOccurred = false var valuesConvertedCount = 0 - if (originalType.unit != updatedType.unit && originalType.inputType == InputFieldType.FLOAT && updatedType.inputType == InputFieldType.FLOAT) { + // 1. First, update the MeasurementType definition in the database. + // This ensures that any subsequent recalculations of derived values + // will use the correct (new) unit for this type. + val finalTypeToUpdate = MeasurementType( + id = originalType.id, + key = originalType.key, // Key should be immutable for an existing type + name = updatedType.name, + color = updatedType.color, + icon = updatedType.icon, + unit = updatedType.unit, // Crucial: The new unit + inputType = updatedType.inputType, + displayOrder = originalType.displayOrder, + isDerived = originalType.isDerived, + isEnabled = updatedType.isEnabled, + isPinned = updatedType.isPinned, + isOnRightYAxis = updatedType.isOnRightYAxis + ) + + try { + repository.updateMeasurementType(finalTypeToUpdate) LogManager.i( TAG, - "Unit changed for FLOAT type ID ${originalType.id} from ${originalType.unit} to ${updatedType.unit}. Converting values." + "MeasurementType (ID: ${originalType.id}) definition updated successfully to new unit '${finalTypeToUpdate.unit}'." ) + } catch (e: Exception) { + LogManager.e(TAG, "Error updating MeasurementType (ID: ${originalType.id}) definition itself", e) + sharedViewModel.showSnackbar( + messageResId = R.string.measurement_type_updated_error, + // Consider using context.getString for display names if not available in originalType + formatArgs = listOf(originalType.name ?: originalType.key.toString()) + ) + conversionErrorOccurred = true // Prevent further steps if this critical update fails + } + // 2. If the type definition was updated successfully AND the unit has changed for a FLOAT type, + // convert the associated MeasurementValue entries. + if (!conversionErrorOccurred && + originalType.unit != updatedType.unit && + originalType.inputType == InputFieldType.FLOAT && + updatedType.inputType == InputFieldType.FLOAT + ) { + LogManager.i( + TAG, + "Unit changed for FLOAT type ID ${originalType.id}. Converting values AFTER type definition update." + ) try { - val valuesToConvert = repository.getValuesForType(originalType.id).first() // .first() um den aktuellen Wert des Flows zu erhalten + // Fetch all values belonging to this type + val valuesToConvert = repository.getValuesForType(originalType.id).first() if (valuesToConvert.isNotEmpty()) { LogManager.d(TAG, "Found ${valuesToConvert.size} values of type ID ${originalType.id} to potentially convert.") @@ -1185,13 +1225,14 @@ class SettingsViewModel( valuesToConvert.forEach { valueToConvert -> valueToConvert.floatValue?.let { currentFloatVal -> + // Convert from the original unit of the values to the new target unit val convertedFloat = Converters.convertFloatValueUnit( value = currentFloatVal, - fromUnit = originalType.unit, - toUnit = updatedType.unit + fromUnit = originalType.unit, // Important: Use the unit the values currently have + toUnit = updatedType.unit // The new target unit ) - if (convertedFloat != currentFloatVal) { + if (convertedFloat != currentFloatVal) { // Add only if the value actually changes updatedValuesBatch.add(valueToConvert.copy(floatValue = convertedFloat)) valuesConvertedCount++ } @@ -1200,9 +1241,8 @@ class SettingsViewModel( if (updatedValuesBatch.isNotEmpty()) { LogManager.d(TAG, "Updating ${updatedValuesBatch.size} values in batch for type ID ${originalType.id}.") - // Aktualisiere jeden Wert einzeln. Dein Repository.updateMeasurementValue - // stößt die Neuberechnung der abgeleiteten Werte an. updatedValuesBatch.forEach { repository.updateMeasurementValue(it) } + // Consider a repository.justUpdateMeasurementValueData(it) LogManager.d(TAG, "Batch update of ${updatedValuesBatch.size} values completed for type ID ${originalType.id}.") } else { LogManager.i(TAG, "No values required actual conversion or update for type ID ${originalType.id} after checking.") @@ -1213,74 +1253,42 @@ class SettingsViewModel( } catch (e: Exception) { LogManager.e(TAG, "Error during value conversion/update for type ID ${originalType.id}", e) conversionErrorOccurred = true - // Verwende hier getDisplayName vom originalType, da updatedType möglicherweise noch nicht committet wurde. sharedViewModel.showSnackbar( messageResId = R.string.measurement_type_update_error_conversion_failed, formatArgs = listOf(originalType.name ?: originalType.key.toString()) ) + // Optional: Consider reverting the MeasurementType update if value conversion fails (adds complexity). } - } else if (originalType.unit != updatedType.unit) { + } else if (!conversionErrorOccurred && originalType.unit != updatedType.unit) { LogManager.i( TAG, - "Unit changed for type ID ${originalType.id}, but InputType is not FLOAT (Original: ${originalType.inputType}, Updated: ${updatedType.inputType}). No value conversion performed." + "Unit changed for type ID ${originalType.id}, but InputType is not FLOAT or previous type update failed. " + + "No direct value conversion, but affected measurements will be flagged for recalculation." ) } - if (!conversionErrorOccurred) { - try { - val finalTypeToUpdate = MeasurementType( - id = originalType.id, - key = originalType.key, - name = updatedType.name, - color = updatedType.color, - icon = updatedType.icon, - unit = updatedType.unit, - inputType = updatedType.inputType, - displayOrder = originalType.displayOrder, - isDerived = originalType.isDerived, - isEnabled = updatedType.isEnabled, - isPinned = updatedType.isPinned, - isOnRightYAxis = updatedType.isOnRightYAxis - ) - - repository.updateMeasurementType(finalTypeToUpdate) - LogManager.i( - TAG, - "MeasurementType (ID: ${originalType.id}) updated successfully to new unit '${finalTypeToUpdate.unit}'." - ) - - if (showSnackbarMaster) { - if (valuesConvertedCount > 0) { - sharedViewModel.showSnackbar( - messageResId = R.string.measurement_type_updated_and_values_converted_successfully, - formatArgs = listOf(updatedType.name ?: updatedType.key.toString(), valuesConvertedCount.toString()) // Context für getDisplayName wäre besser - ) - } else if (originalType.unit != updatedType.unit && originalType.inputType == InputFieldType.FLOAT) { - sharedViewModel.showSnackbar( - messageResId = R.string.measurement_type_updated_unit_changed_no_values_converted, - formatArgs = listOf(updatedType.name ?: updatedType.key.toString()) // Context für getDisplayName wäre besser - ) - } - else { - sharedViewModel.showSnackbar( - messageResId = R.string.measurement_type_updated_successfully, - formatArgs = listOf(updatedType.name ?: updatedType.key.toString()) // Context für getDisplayName wäre besser - ) - } - } - // sharedViewModel.refreshMeasurementTypes() // Optional, wenn die Liste sich nicht automatisch aktualisiert - } catch (e: Exception) { - LogManager.e(TAG, "Error updating MeasurementType (ID: ${originalType.id}) itself", e) + // 3. Show appropriate snackbar message. + if (!conversionErrorOccurred && showSnackbarMaster) { + if (valuesConvertedCount > 0) { sharedViewModel.showSnackbar( - messageResId = R.string.measurement_type_updated_error, - formatArgs = listOf(originalType.name ?: originalType.key.toString()) // Context für getDisplayName wäre besser + messageResId = R.string.measurement_type_updated_and_values_converted_successfully, + formatArgs = listOf( + updatedType.name ?: updatedType.key.toString(), + valuesConvertedCount.toString() + ) + ) + } else if (originalType.unit != updatedType.unit && (originalType.inputType == InputFieldType.FLOAT)) { + // Also show if unit changed but no values were converted (e.g., none existed or input type wasn't FLOAT but recalculation was still triggered) + sharedViewModel.showSnackbar( + messageResId = R.string.measurement_type_updated_unit_changed_no_values_converted, // Or a more specific message + formatArgs = listOf(updatedType.name ?: updatedType.key.toString()) + ) + } else { // Generic success if no unit change or no conversion needed + sharedViewModel.showSnackbar( + messageResId = R.string.measurement_type_updated_successfully, + formatArgs = listOf(updatedType.name ?: updatedType.key.toString()) ) } - } else { - LogManager.w( - TAG, - "Skipped updating MeasurementType definition (ID: ${originalType.id}) due to prior value conversion error." - ) } } }