fix(home): resolve OutcomeDay import for outcome dialogs

This commit is contained in:
null 2026-06-21 00:02:02 -05:00
parent 57a3e35359
commit b70463274f
3 changed files with 149 additions and 4 deletions

View File

@ -32,9 +32,12 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.TrendingUp
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.LocalFireDepartment
import app.closer.domain.model.OutcomeDay
import app.closer.ui.components.OutcomeCheckInDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -43,7 +46,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@ -97,6 +102,60 @@ fun HomeScreen(
}
}
var showBaselineDialog by remember { mutableStateOf(false) }
var showFollowUpDialog by remember { mutableStateOf(false) }
var pendingFollowUpDay by remember { mutableStateOf<OutcomeDay?>(null) }
LaunchedEffect(state.showOutcomeBaselineDialog) {
showBaselineDialog = state.showOutcomeBaselineDialog
}
LaunchedEffect(state.showOutcomeFollowUpDialog, state.outcomeFollowUpDay) {
showFollowUpDialog = state.showOutcomeFollowUpDialog
pendingFollowUpDay = state.outcomeFollowUpDay
}
if (showBaselineDialog) {
OutcomeCheckInDialog(
title = "Quick check-in",
subtitle = "Before you start, how are you feeling about your relationship right now?",
onDismiss = {
viewModel.markBaselineOutcomeShown()
showBaselineDialog = false
},
onSubmit = { scores ->
viewModel.submitOutcome(OutcomeDay.BASELINE.key, scores)
showBaselineDialog = false
}
)
}
pendingFollowUpDay?.let { day ->
if (showFollowUpDialog) {
OutcomeCheckInDialog(
title = "${day.label} check-in",
subtitle = "How are you feeling now compared to when you started?",
onDismiss = {
viewModel.markFollowUpOutcomeShown(day.key)
showFollowUpDialog = false
pendingFollowUpDay = null
},
onSubmit = { scores ->
viewModel.submitOutcome(day.key, scores)
showFollowUpDialog = false
pendingFollowUpDay = null
}
)
}
}
LaunchedEffect(state.outcomeSubmitSuccess, state.outcomeError) {
state.outcomeError?.let { snackbarHostState.showSnackbar(it); viewModel.consumeOutcomeError() }
if (state.outcomeSubmitSuccess) {
snackbarHostState.showSnackbar("Check-in saved")
viewModel.consumeOutcomeSuccess()
}
}
HomeContent(
state = state,
snackbarHostState = snackbarHostState,

View File

@ -20,6 +20,11 @@ import app.closer.domain.repository.LocalAnswerRepository
import app.closer.domain.repository.QuestionRepository
import app.closer.domain.repository.QuestionSessionRepository
import app.closer.domain.repository.UserRepository
import app.closer.domain.model.Outcome
import app.closer.domain.model.OutcomeDay
import app.closer.domain.model.OutcomeScores
import app.closer.domain.repository.OutcomeRepository
import app.closer.domain.repository.SettingsRepository
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.functions.FirebaseFunctions
import dagger.hilt.android.lifecycle.HiltViewModel
@ -27,6 +32,7 @@ import app.closer.ui.home.HomePriorityEngine.Input as PriorityInput
import app.closer.ui.home.HomePriorityEngine.Priority
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.ZoneId
import javax.inject.Inject
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
@ -36,6 +42,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.first
data class HomeCategorySummary(
val category: QuestionCategory,
@ -126,7 +133,13 @@ data class HomeUiState(
val hasUpcomingDatePlan: Boolean = false,
val hasUnlockedCapsule: Boolean = false,
val weeklyRecapReady: Boolean = false,
val reminderSentEvent: Boolean = false
val reminderSentEvent: Boolean = false,
// Outcome check-in state
val outcomeSubmitSuccess: Boolean = false,
val outcomeError: String? = null,
val showOutcomeBaselineDialog: Boolean = false,
val showOutcomeFollowUpDialog: Boolean = false,
val outcomeFollowUpDay: OutcomeDay? = null
)
@HiltViewModel
@ -143,7 +156,9 @@ class HomeViewModel @Inject constructor(
private val challengeDataSource: FirestoreChallengeDataSource,
private val capsuleDataSource: FirestoreCapsuleDataSource,
private val datePlanRepository: DatePlanRepository,
private val sealedRevealManager: SealedRevealManager
private val sealedRevealManager: SealedRevealManager,
private val outcomeRepository: OutcomeRepository,
private val settingsRepository: SettingsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
@ -196,6 +211,17 @@ class HomeViewModel @Inject constructor(
else -> false
}
// Outcome check-in due-state calculation
val outcomeBaselineShownAt = settingsRepository.settings.first().outcomeBaselineShownAt
val outcomes = couple?.let {
runCatching { outcomeRepository.getOutcomes(it.id) }
.onFailure { Log.w(TAG, "Could not load outcomes", it) }
.getOrDefault(emptyList())
} ?: emptyList()
val baselineRecorded = outcomes.any { it.dayKey == OutcomeDay.BASELINE.key }
val showBaselineDialog = couple != null && !baselineRecorded && outcomeBaselineShownAt == 0L
val followUpDay = couple?.let { dueFollowUpDay(it.createdAt, outcomes) }
// Retention signal fetches — run in parallel, failures silently default to false.
var hasWaitingGame = false
var hasActiveChallenge = false
@ -253,7 +279,10 @@ class HomeViewModel @Inject constructor(
hasWaitingGame = hasWaitingGame,
hasActiveChallenge = hasActiveChallenge,
hasUpcomingDatePlan = hasUpcomingDatePlan,
hasUnlockedCapsule = hasUnlockedCapsule
hasUnlockedCapsule = hasUnlockedCapsule,
showOutcomeBaselineDialog = showBaselineDialog,
showOutcomeFollowUpDialog = followUpDay != null,
outcomeFollowUpDay = followUpDay
).refreshDailyQuestionState().withHomeActions()
}
observePartnerAnswer(couple?.id, couple?.userIds, dailyQuestion?.id)
@ -311,6 +340,63 @@ class HomeViewModel @Inject constructor(
loadHome()
}
private fun dueFollowUpDay(createdAt: Long, outcomes: List<Outcome>): OutcomeDay? {
val paired = java.time.Instant.ofEpochMilli(createdAt).atZone(ZoneId.systemDefault()).toLocalDate()
val today = LocalDate.now()
val ageDays = java.time.temporal.ChronoUnit.DAYS.between(paired, today)
val due = listOf(
OutcomeDay.DAY_30 to 30,
OutcomeDay.DAY_60 to 60,
OutcomeDay.DAY_90 to 90
).firstOrNull { (_, days) -> ageDays >= days } ?: return null
return if (outcomes.any { it.dayKey == due.first.key }) null else due.first
}
fun submitOutcome(dayKey: String, scores: OutcomeScores) {
viewModelScope.launch {
_uiState.update { it.copy(outcomeSubmitSuccess = false, outcomeError = null) }
try {
val uid = authRepository.currentUserId
?: throw IllegalStateException("Not signed in.")
val couple = coupleRepository.getCoupleForUser(uid)
?: throw IllegalStateException("Not paired.")
outcomeRepository.submitOutcome(couple.id, dayKey, scores).getOrThrow()
settingsRepository.setOutcomeLastPromptedDay(dayKey)
if (dayKey == OutcomeDay.BASELINE.key) {
settingsRepository.markOutcomeBaselineShown()
}
_uiState.update {
it.copy(
outcomeSubmitSuccess = true,
showOutcomeBaselineDialog = false,
showOutcomeFollowUpDialog = false,
outcomeFollowUpDay = null
)
}
loadHome()
} catch (e: Exception) {
_uiState.update { it.copy(outcomeError = e.message ?: "Couldnt save check-in.") }
}
}
}
fun markBaselineOutcomeShown() {
viewModelScope.launch {
settingsRepository.markOutcomeBaselineShown()
_uiState.update { it.copy(showOutcomeBaselineDialog = false) }
}
}
fun markFollowUpOutcomeShown(dayKey: String) {
viewModelScope.launch {
settingsRepository.setOutcomeLastPromptedDay(dayKey)
_uiState.update { it.copy(showOutcomeFollowUpDialog = false, outcomeFollowUpDay = null) }
}
}
fun consumeOutcomeSuccess() = _uiState.update { it.copy(outcomeSubmitSuccess = false) }
fun consumeOutcomeError() = _uiState.update { it.copy(outcomeError = null) }
/**
* Sends a gentle reminder to the partner that the daily question is waiting.
* Notification wiring is intentionally a no-op until Batch 6.

View File

@ -20,10 +20,10 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.TrendingUp
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.TrendingUp
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Palette