fix(home): resolve OutcomeDay import for outcome dialogs

This commit is contained in:
null 2026-06-21 00:02:02 -05:00
parent b84377a8fa
commit 391ea68793
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.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.LocalFireDepartment 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.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -43,7 +46,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset 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( HomeContent(
state = state, state = state,
snackbarHostState = snackbarHostState, 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.QuestionRepository
import app.closer.domain.repository.QuestionSessionRepository import app.closer.domain.repository.QuestionSessionRepository
import app.closer.domain.repository.UserRepository 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.firestore.FirebaseFirestore
import com.google.firebase.functions.FirebaseFunctions import com.google.firebase.functions.FirebaseFunctions
import dagger.hilt.android.lifecycle.HiltViewModel 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 app.closer.ui.home.HomePriorityEngine.Priority
import java.time.DayOfWeek import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -36,6 +42,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.first
data class HomeCategorySummary( data class HomeCategorySummary(
val category: QuestionCategory, val category: QuestionCategory,
@ -126,7 +133,13 @@ data class HomeUiState(
val hasUpcomingDatePlan: Boolean = false, val hasUpcomingDatePlan: Boolean = false,
val hasUnlockedCapsule: Boolean = false, val hasUnlockedCapsule: Boolean = false,
val weeklyRecapReady: 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 @HiltViewModel
@ -143,7 +156,9 @@ class HomeViewModel @Inject constructor(
private val challengeDataSource: FirestoreChallengeDataSource, private val challengeDataSource: FirestoreChallengeDataSource,
private val capsuleDataSource: FirestoreCapsuleDataSource, private val capsuleDataSource: FirestoreCapsuleDataSource,
private val datePlanRepository: DatePlanRepository, private val datePlanRepository: DatePlanRepository,
private val sealedRevealManager: SealedRevealManager private val sealedRevealManager: SealedRevealManager,
private val outcomeRepository: OutcomeRepository,
private val settingsRepository: SettingsRepository
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState()) private val _uiState = MutableStateFlow(HomeUiState())
@ -196,6 +211,17 @@ class HomeViewModel @Inject constructor(
else -> false 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. // Retention signal fetches — run in parallel, failures silently default to false.
var hasWaitingGame = false var hasWaitingGame = false
var hasActiveChallenge = false var hasActiveChallenge = false
@ -253,7 +279,10 @@ class HomeViewModel @Inject constructor(
hasWaitingGame = hasWaitingGame, hasWaitingGame = hasWaitingGame,
hasActiveChallenge = hasActiveChallenge, hasActiveChallenge = hasActiveChallenge,
hasUpcomingDatePlan = hasUpcomingDatePlan, hasUpcomingDatePlan = hasUpcomingDatePlan,
hasUnlockedCapsule = hasUnlockedCapsule hasUnlockedCapsule = hasUnlockedCapsule,
showOutcomeBaselineDialog = showBaselineDialog,
showOutcomeFollowUpDialog = followUpDay != null,
outcomeFollowUpDay = followUpDay
).refreshDailyQuestionState().withHomeActions() ).refreshDailyQuestionState().withHomeActions()
} }
observePartnerAnswer(couple?.id, couple?.userIds, dailyQuestion?.id) observePartnerAnswer(couple?.id, couple?.userIds, dailyQuestion?.id)
@ -311,6 +340,63 @@ class HomeViewModel @Inject constructor(
loadHome() 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. * Sends a gentle reminder to the partner that the daily question is waiting.
* Notification wiring is intentionally a no-op until Batch 6. * 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.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos 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.Done
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder 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.Lock
import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Palette