fix(home): resolve OutcomeDay import for outcome dialogs
This commit is contained in:
parent
b84377a8fa
commit
391ea68793
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ?: "Couldn’t 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue