M12e: contract pass (pair invite kind, admin name in invite links, sync failed flag, rejection actor, device version name, template id, update check result, untouched accounts, D-027 re-stamp, PushEvent in :core:model, snackbar hosts)
Server: E-3 invite_kind; E-12 link ?from=<admin name>, E-4 passes it into kvitt://invite; X-Kvitt-App-Version-Name stored in devices.app_version_name (migration 0003, schema 3); OpenAPI and examples follow. Phone: :core:model, :core:fakes, :data, features and :app updated together; goldens re-recorded for S-20 (version name) and new S-1/S-22 states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+18
-15
@@ -17,6 +17,7 @@ import se.hagstedt.kvitt.model.AccountRepository
|
||||
import se.hagstedt.kvitt.model.AllocationLine
|
||||
import se.hagstedt.kvitt.model.FieldError
|
||||
import se.hagstedt.kvitt.model.HomeRepository
|
||||
import se.hagstedt.kvitt.model.UntouchedAccount
|
||||
import se.hagstedt.kvitt.model.SaveResult
|
||||
import se.hagstedt.kvitt.model.SettleRequest
|
||||
import se.hagstedt.kvitt.model.SettleState
|
||||
@@ -48,8 +49,8 @@ data class SettleRow(val line: AllocationLine, val input: AmountInput)
|
||||
|
||||
/**
|
||||
* Everything S-9 shows. [settle] fields come from `SettlementRepository.start`/`recompute` (§14.2.4); the feature never
|
||||
* computes the proposal, the remaining amount, the warnings or [canConfirm] itself. [viewerNets] are the viewer's
|
||||
* account balances from Home, used only for the §9.4 info line about accounts left untouched.
|
||||
* computes the proposal, the remaining amount, the warnings or [canConfirm] itself. [untouchedAccounts] are the
|
||||
* repository's accounts left untouched (`SettleState.untouched`, D-074), for the §9.4 info line.
|
||||
*/
|
||||
data class SettleUiState(
|
||||
val loading: Boolean = true,
|
||||
@@ -67,7 +68,7 @@ data class SettleUiState(
|
||||
val rowsEdited: Boolean = false,
|
||||
val fixedAccount: Account? = null,
|
||||
val accounts: List<Account> = emptyList(),
|
||||
val viewerNets: Map<AccountId, Long> = emptyMap(),
|
||||
val untouchedAccounts: List<UntouchedAccount> = emptyList(),
|
||||
val errors: List<FieldError> = emptyList(),
|
||||
val dialog: SettleDialog? = null,
|
||||
val dirty: Boolean = false,
|
||||
@@ -87,18 +88,15 @@ data class SettleUiState(
|
||||
val showRemaining: Boolean get() = mode != SettleMode.Account && rows.isNotEmpty()
|
||||
|
||||
/**
|
||||
* §9.4 Consequences: accounts where the payer is owed and that are not in the rows (global variant). The viewer's
|
||||
* net sign of each says who owes there; all of them have the same sign because the payer is owed in each.
|
||||
* §9.4 Consequences: the repository's accounts where the payer is owed and that are not in the rows (global
|
||||
* variant), each with the viewer's net there; all of them have the same sign because the payer is owed in each.
|
||||
*/
|
||||
val untouched: List<Pair<Account, Long>>
|
||||
get() {
|
||||
val payer = payer ?: return emptyList()
|
||||
if (mode == SettleMode.Account || rows.isEmpty()) return emptyList()
|
||||
return accounts.mapNotNull { account ->
|
||||
val viewerNet = viewerNets[account.id] ?: 0L
|
||||
val payerNet = if (payer.id == me?.id) viewerNet else -viewerNet
|
||||
account.takeIf { payerNet > 0 && rows.none { it.line.account.id == account.id } }?.let { it to viewerNet }
|
||||
}
|
||||
return untouchedAccounts.filter { u -> rows.none { it.line.account.id == u.account.id } }
|
||||
.map { u -> u.account to if (payer.id == me?.id) u.payerNetOre else -u.payerNetOre }
|
||||
}
|
||||
|
||||
val overSettled: List<SettleRow> get() = rows.filter { it.line.overSettles }
|
||||
@@ -152,10 +150,7 @@ class SettleViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
home.home().collect { h ->
|
||||
val nets = (h.active + h.archived).associate { it.account.id to it.netOre }
|
||||
mutable.update { it.copy(me = h.me, partner = h.partner, viewerNets = nets) }
|
||||
}
|
||||
home.home().collect { h -> mutable.update { it.copy(me = h.me, partner = h.partner) } }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val start = settlements.start(editing, if (editing == null) fixedAccountId else null)
|
||||
@@ -271,5 +266,13 @@ internal fun SettleUiState.withResult(result: SettleState, propose: Boolean): Se
|
||||
rows.map { row -> SettleRow(result.lines.firstOrNull { it.account.id == row.line.account.id } ?: row.line, row.input) }
|
||||
}
|
||||
val amount = if (loading) AmountText.of(result.amountOre) else amount
|
||||
return copy(payer = result.payer, recipient = result.recipient, amount = amount, rows = rows, remainingOre = result.remainingOre, canConfirm = result.canConfirm)
|
||||
return copy(
|
||||
payer = result.payer,
|
||||
recipient = result.recipient,
|
||||
amount = amount,
|
||||
rows = rows,
|
||||
remainingOre = result.remainingOre,
|
||||
canConfirm = result.canConfirm,
|
||||
untouchedAccounts = result.untouched,
|
||||
)
|
||||
}
|
||||
|
||||
+7
-1
@@ -13,10 +13,12 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
@@ -68,6 +70,7 @@ import kotlin.time.Instant
|
||||
/**
|
||||
* S-10 Settlement detail (design-spec S-10) for [SettleArgs.SETTLEMENT_ID]. [onEdit] opens S-9 editing; after a
|
||||
* confirmed delete [onDeleted] goes back, where "Betalningen togs bort" (`snackbar_payment_deleted`) is shown.
|
||||
* [snackbarHostState] is hosted in this screen's scaffold ("Betalningen sparades" after an edit, D-071).
|
||||
*/
|
||||
@Composable
|
||||
fun SettlementDetailScreen(
|
||||
@@ -75,11 +78,12 @@ fun SettlementDetailScreen(
|
||||
onEdit: (SettlementId) -> Unit,
|
||||
onDeleted: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
viewModel: SettlementDetailViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
LaunchedEffect(state.deletedByMe) { if (state.deletedByMe) onDeleted() }
|
||||
SettlementDetailContent(state, viewModel, onBack, onEdit, modifier)
|
||||
SettlementDetailContent(state, viewModel, onBack, onEdit, modifier, snackbarHostState = snackbarHostState)
|
||||
}
|
||||
|
||||
/** The stateless S-10, rendered by [SettlementDetailScreen], previews and screenshot tests. */
|
||||
@@ -91,12 +95,14 @@ fun SettlementDetailContent(
|
||||
onEdit: (SettlementId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
overlayHost: OverlayHost = WindowOverlayHost,
|
||||
snackbarHostState: SnackbarHostState? = null,
|
||||
) {
|
||||
val me = state.me
|
||||
val detail = state.detail
|
||||
val body: @Composable () -> Unit = {
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
snackbarHostState = snackbarHostState,
|
||||
topBar = {
|
||||
KvittTopAppBar(stringResource(R.string.settle_detail_title), navigation = TopBarNavigation.Back, onNavigation = onBack) {
|
||||
if (state.actions && detail != null) {
|
||||
|
||||
Reference in New Issue
Block a user