M7g: feature cleanups after the design-system fix pass

Resource-backed snackbars (showKvittSnackbar) in account, onboarding and
settings; SettingsScaffold replaced by KvittScaffold + SettingsMessageEffect.
KvittButton(loading) for S-6 saving and S-16 Synka nu. S-10 missing state
disables Synka nu offline (connectivity in the ViewModel, new
detail_missing_offline goldens). S-6 system back asks to discard unsaved
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 11:44:07 +02:00
parent f4dc10c7ae
commit 3dda414ec2
22 changed files with 177 additions and 160 deletions
@@ -123,7 +123,8 @@ fun SettlementDetailContent(
action = stringResource(R.string.settle_detail_sync_now),
actionIcon = KvittIcon.Sync,
actionKind = ButtonKind.Tonal,
onAction = actions::onSyncNow,
actionEnabled = state.online,
onAction = { if (state.online) actions.onSyncNow() },
)
else -> DetailBody(state, detail)
}
@@ -16,6 +16,7 @@ import se.hagstedt.kvitt.model.Account
import se.hagstedt.kvitt.model.AccountId
import se.hagstedt.kvitt.model.AccountRepository
import se.hagstedt.kvitt.model.AuditAction
import se.hagstedt.kvitt.model.ConnectivityRepository
import se.hagstedt.kvitt.model.HomeRepository
import se.hagstedt.kvitt.model.SettlementDetailState
import se.hagstedt.kvitt.model.SettlementId
@@ -25,7 +26,8 @@ import se.hagstedt.kvitt.model.TimelineLine
import se.hagstedt.kvitt.model.User
/**
* Everything S-10 shows. [detail] null after loading is the missing state (the S-8 missing layout, design-spec S-10).
* Everything S-10 shows. [detail] null after loading is the missing state (the S-8 missing layout, design-spec S-10);
* its "Synka nu" needs [online].
*/
data class SettlementDetailUiState(
val loading: Boolean = true,
@@ -35,6 +37,7 @@ data class SettlementDetailUiState(
val accounts: List<Account> = emptyList(),
val confirmDelete: Boolean = false,
val deletedByMe: Boolean = false,
val online: Boolean = true,
) {
/** Design-spec §2.4: an account's colour is its position in `AccountRepository.all()`. */
fun colorIndex(id: AccountId): Int = accounts.indexOfFirst { it.id == id }.coerceAtLeast(0)
@@ -61,6 +64,7 @@ class SettlementDetailViewModel @javax.inject.Inject constructor(
private val settlements: SettlementRepository,
accounts: AccountRepository,
home: HomeRepository,
connectivity: ConnectivityRepository,
private val sync: SyncRepository,
) : ViewModel(), SettlementDetailActions {
val id: SettlementId? = savedStateHandle.get<String>(SettleArgs.SETTLEMENT_ID)?.let(::SettlementId)
@@ -70,9 +74,10 @@ class SettlementDetailViewModel @javax.inject.Inject constructor(
id?.let { settlements.detail(it) } ?: flowOf(null),
accounts.all(),
home.home(),
connectivity.online,
local,
) { detail, all, h, l ->
l.copy(loading = false, detail = detail, accounts = all, me = h.me, partner = h.partner)
) { detail, all, h, online, l ->
l.copy(loading = false, detail = detail, accounts = all, me = h.me, partner = h.partner, online = online)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIMEOUT_MS), SettlementDetailUiState())
override fun onDelete() = local.update { it.copy(confirmDelete = true) }
@@ -30,7 +30,7 @@ class SettlementDetailViewModelTest {
fun reset() = Dispatchers.resetMain()
private fun vm(id: String, repos: FakeRepositories = FakeRepositories(SettleScenarios.detail)) =
SettlementDetailViewModel(SavedStateHandle(mapOf(SettleArgs.SETTLEMENT_ID to id)), repos.settlements, repos.accounts, repos.home, repos.sync)
SettlementDetailViewModel(SavedStateHandle(mapOf(SettleArgs.SETTLEMENT_ID to id)), repos.settlements, repos.accounts, repos.home, repos.connectivity, repos.sync)
@Test
fun loadedShowsTheSettlementAndItsActions() = runTest {
@@ -48,6 +48,14 @@ class SettlementDetailViewModelTest {
assertFalse(s.actions)
}
@Test
fun missingOfflineCarriesTheConnectivityForSyncNow() = runTest {
assertTrue(vm(SettleScenarios.missing.value).state.first { !it.loading }.online)
val offline = vm(SettleScenarios.missing.value, FakeRepositories(SettleScenarios.detailOffline)).state.first { !it.loading }
assertNull(offline.detail)
assertFalse(offline.online)
}
@Test
fun deletedHasTheDeletionLineAndNoActions() = runTest {
val s = vm(SettleScenarios.deleted.value).state.first { !it.loading }
@@ -78,6 +78,7 @@ private val cases: Map<String, Case> = linkedMapOf(
"detail_offline" to Detail(SettleScenarios.detailOffline, SettleScenarios.split.value),
"detail_delete_dialog" to Detail(SettleScenarios.detail, SettleScenarios.swish.value) { onDelete() },
"detail_missing" to Detail(SettleScenarios.detail, SettleScenarios.missing.value),
"detail_missing_offline" to Detail(SettleScenarios.detailOffline, SettleScenarios.missing.value),
"detail_long" to Detail(SettleScenarios.longDetail, SettleScenarios.long.value, height = longHeight),
)
@@ -105,7 +106,7 @@ class SettleScreenshotTest(private val name: String) {
is Detail -> {
val repos = FakeRepositories(case.scenario)
val vm = SettlementDetailViewModel(
SavedStateHandle(mapOf(SettleArgs.SETTLEMENT_ID to case.id)), repos.settlements, repos.accounts, repos.home, repos.sync,
SavedStateHandle(mapOf(SettleArgs.SETTLEMENT_ID to case.id)), repos.settlements, repos.accounts, repos.home, repos.connectivity, repos.sync,
)
vm.apply(case.steps)
ScreenSnapshots.snap(compose, name, case.height, case.largeFont) {