Files
H-S-Diary/app/src/main/java/com/hsdiary/ui/trends/HealthTrendsViewModel.kt
whitlocktech bba09dc131 Initial commit: Implement base health and cycle tracking application.
- Set up Android project structure with Gradle 8.7, Kotlin 2.0, and Hilt.
- Implement Room database with entities for Profiles, Day Logs, Conditions, Cycle Records, and Intimacy Logs.
- Integrate Jetpack Compose for the UI layer including Navigation and Material3.
- Develop a cycle prediction engine to calculate menstruation, fertile windows, and ovulation based on user history.
- Implement core screens:
    - **Onboarding:** Initial profile setup for female/male users.
    - **Calendar:** Monthly view showing cycle phases, logged symptoms, and intimacy records.
    - **Day Detail:** Detailed logging for symptoms (with ratings), period status, notes, and intimacy encounters.
    - **Cycle Insights:** Visualization of cycle phases and historical cycle length trends.
    - **Health Trends:** Frequency and recurrence analysis of logged health conditions over various time ranges.
    - **Settings:** Profile management, data clearing, and app theme configuration.
- Add DataStore for managing user preferences such as active profile and onboarding status.

Signed-off-by: whitlocktech <whitlocktech@gmail.com>
2026-05-22 17:42:09 -05:00

131 lines
5.2 KiB
Kotlin

package com.hsdiary.ui.trends
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.hsdiary.data.db.entity.ConditionDefinitionEntity
import com.hsdiary.data.db.entity.ConditionEntryEntity
import com.hsdiary.data.db.entity.DayLogEntity
import com.hsdiary.data.db.entity.ProfileEntity
import com.hsdiary.data.model.ProfileType
import com.hsdiary.data.preferences.UserPreferences
import com.hsdiary.data.repository.CycleRepository
import com.hsdiary.data.repository.DayLogRepository
import com.hsdiary.data.repository.ProfileRepository
import com.hsdiary.domain.CyclePredictionEngine
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.time.LocalDate
import javax.inject.Inject
enum class TrendsRange { DAYS_30, MONTHS_3, MONTHS_6, ALL }
data class ConditionFrequency(
val definition: ConditionDefinitionEntity,
val count: Int,
val isRecurring: Boolean,
val avgRating: Float,
val weeklyData: List<Int>
)
data class HealthTrendsUiState(
val profile: ProfileEntity? = null,
val range: TrendsRange = TrendsRange.DAYS_30,
val conditionFrequencies: List<ConditionFrequency> = emptyList(),
val selectedCondition: ConditionDefinitionEntity? = null,
val isLoading: Boolean = true,
val isFemale: Boolean = false
)
@HiltViewModel
class HealthTrendsViewModel @Inject constructor(
private val profileRepository: ProfileRepository,
private val dayLogRepository: DayLogRepository,
private val userPreferences: UserPreferences
) : ViewModel() {
private val _range = MutableStateFlow(TrendsRange.DAYS_30)
private val _selectedCondition = MutableStateFlow<ConditionDefinitionEntity?>(null)
private val _uiState = MutableStateFlow(HealthTrendsUiState())
val uiState: StateFlow<HealthTrendsUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch { load() }
}
private suspend fun load() {
val activeId = userPreferences.activeProfileId.first()
val profiles = profileRepository.getAllProfilesOnce()
val profile = profiles.find { it.id == activeId } ?: profiles.firstOrNull() ?: return
val isFemale = profile.profileType == ProfileType.FEMALE.name
val allDefs = dayLogRepository.getAllDefinitions()
.filter { it.profileVisibility == "ALL" || (isFemale && it.profileVisibility == "FEMALE_ONLY") }
_range.combine(_selectedCondition) { range, sel -> range to sel }
.onEach { (range, sel) ->
val today = LocalDate.now()
val startDate = when (range) {
TrendsRange.DAYS_30 -> today.minusDays(29)
TrendsRange.MONTHS_3 -> today.minusMonths(3)
TrendsRange.MONTHS_6 -> today.minusMonths(6)
TrendsRange.ALL -> today.minusYears(5)
}
val entries = dayLogRepository.getConditionsInRange(
profile.id, startDate.toString(), today.toString()
)
val dayLogs = dayLogRepository.getDayLogsInRangeOnce(
profile.id, startDate.toString(), today.toString()
)
val dayLogMap = dayLogs.associateBy { it.id }
val grouped = entries.groupBy { it.conditionKey }
val frequencies = allDefs.mapNotNull { def ->
val entriesForKey = grouped[def.conditionKey] ?: return@mapNotNull null
val weeklyData = buildWeeklyData(entriesForKey, dayLogMap, startDate, today)
ConditionFrequency(
definition = def,
count = entriesForKey.size,
isRecurring = entriesForKey.size >= 3,
avgRating = entriesForKey.map { it.rating }.average().toFloat(),
weeklyData = weeklyData
)
}.sortedByDescending { it.count }
_uiState.value = HealthTrendsUiState(
profile = profile,
range = range,
conditionFrequencies = frequencies,
selectedCondition = sel,
isLoading = false,
isFemale = isFemale
)
}
.launchIn(viewModelScope)
}
private fun buildWeeklyData(
entries: List<ConditionEntryEntity>,
dayLogMap: Map<Long, DayLogEntity>,
startDate: LocalDate,
today: LocalDate
): List<Int> {
val weeks = mutableListOf<Int>()
var weekStart = startDate
while (!weekStart.isAfter(today)) {
val weekEnd = weekStart.plusDays(6)
val count = entries.count { entry ->
val log = dayLogMap[entry.dayLogId] ?: return@count false
val date = LocalDate.parse(log.date)
!date.isBefore(weekStart) && !date.isAfter(weekEnd)
}
weeks.add(count)
weekStart = weekStart.plusWeeks(1)
}
return weeks
}
fun setRange(range: TrendsRange) { _range.value = range }
fun selectCondition(def: ConditionDefinitionEntity?) { _selectedCondition.value = def }
}