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>
This commit is contained in:
130
app/src/main/java/com/hsdiary/ui/trends/HealthTrendsViewModel.kt
Normal file
130
app/src/main/java/com/hsdiary/ui/trends/HealthTrendsViewModel.kt
Normal file
@@ -0,0 +1,130 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user