- 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>
131 lines
5.2 KiB
Kotlin
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 }
|
|
}
|