Android overhaul: S-7's state types in ExpenseEditorState.kt
ExpenseEditorViewModel.kt (555 lines) held the editor's form, state, overlay and event types beside the ViewModel; they move unchanged to their own file (471 + 97 lines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package se.hagstedt.kvitt.feature.expense
|
||||||
|
|
||||||
|
import se.hagstedt.kvitt.design.component.AccountChoice
|
||||||
|
import se.hagstedt.kvitt.design.format.AmountInput
|
||||||
|
import se.hagstedt.kvitt.model.Account
|
||||||
|
import se.hagstedt.kvitt.model.AccountId
|
||||||
|
import se.hagstedt.kvitt.model.Category
|
||||||
|
import se.hagstedt.kvitt.model.CategoryId
|
||||||
|
import se.hagstedt.kvitt.model.ExpenseId
|
||||||
|
import se.hagstedt.kvitt.model.FieldError
|
||||||
|
import se.hagstedt.kvitt.model.Share
|
||||||
|
import se.hagstedt.kvitt.model.SplitMode
|
||||||
|
import se.hagstedt.kvitt.model.User
|
||||||
|
import se.hagstedt.kvitt.model.UserId
|
||||||
|
|
||||||
|
// What S-7 renders and the one-off events of its state holder, ExpenseEditorViewModel.
|
||||||
|
|
||||||
|
/** Which keypad-driven field has the in-app keypad (design-spec §3.7, §5.2); null hides the keypad. */
|
||||||
|
enum class KeypadTarget { Amount, RatioViewer, RatioPartner, Exact }
|
||||||
|
|
||||||
|
/** What is shown over the editor: a sheet, the date picker or a confirmation (S-7). */
|
||||||
|
enum class EditorOverlay { None, AccountPicker, CategoryGrid, DatePicker, Discard, Delete }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The values the user edits (S-7). Percent fields hold hundredths of a percent in an [AmountInput] (so "60" is 6000
|
||||||
|
* bp) and money fields öre; all parsing is string-exact (SPEC §9.6).
|
||||||
|
*/
|
||||||
|
data class EditorForm(
|
||||||
|
val id: ExpenseId?,
|
||||||
|
val accountId: AccountId?,
|
||||||
|
val categoryId: CategoryId?,
|
||||||
|
val amount: AmountInput,
|
||||||
|
val description: String,
|
||||||
|
val notes: String,
|
||||||
|
val date: String,
|
||||||
|
val payer: UserId,
|
||||||
|
val mode: SplitMode,
|
||||||
|
val ratioViewer: AmountInput,
|
||||||
|
val ratioPartner: AmountInput,
|
||||||
|
val exactTyped: UserId,
|
||||||
|
val exactInput: AmountInput,
|
||||||
|
val fullOn: UserId,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** The editor's whole state as S-7 renders it. [ready] is false until the options (and, editing, the expense) are read. */
|
||||||
|
data class ExpenseEditorState(
|
||||||
|
val ready: Boolean = false,
|
||||||
|
val missing: Boolean = false,
|
||||||
|
val isEdit: Boolean = false,
|
||||||
|
val me: User? = null,
|
||||||
|
val partner: User? = null,
|
||||||
|
val form: EditorForm? = null,
|
||||||
|
val accounts: List<AccountChoice> = emptyList(),
|
||||||
|
val account: Account? = null,
|
||||||
|
val accountColorIndex: Int = 0,
|
||||||
|
val categories: List<Category> = emptyList(),
|
||||||
|
val quickRow: List<Category> = emptyList(),
|
||||||
|
val category: Category? = null,
|
||||||
|
val preview: List<Share>? = null,
|
||||||
|
val keypad: KeypadTarget? = null,
|
||||||
|
val splitOpen: Boolean = false,
|
||||||
|
val splitReveal: Int = 0,
|
||||||
|
val notesExpanded: Boolean = false,
|
||||||
|
val overlay: EditorOverlay = EditorOverlay.None,
|
||||||
|
val errors: List<FieldError> = emptyList(),
|
||||||
|
val online: Boolean = true,
|
||||||
|
val today: String = "",
|
||||||
|
val saving: Boolean = false,
|
||||||
|
) {
|
||||||
|
/** The typed exact share exceeds the amount (AC-8.7): inline error, saving blocked. */
|
||||||
|
val exactTooLarge: Boolean
|
||||||
|
get() {
|
||||||
|
val f = form ?: return false
|
||||||
|
val typed = f.exactInput.ore ?: return false
|
||||||
|
val amount = f.amount.ore ?: return false
|
||||||
|
return f.mode == SplitMode.EXACT && typed > amount
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "Spara": amount > 0, a description, a category and a computable preview (design-spec S-7). */
|
||||||
|
val canSave: Boolean
|
||||||
|
get() {
|
||||||
|
val f = form ?: return false
|
||||||
|
return !saving && (f.amount.ore ?: 0L) > 0 && f.description.isNotBlank() && f.categoryId != null && f.accountId != null &&
|
||||||
|
preview != null && !exactTooLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One-off results the entry point turns into navigation. */
|
||||||
|
sealed interface EditorEvent {
|
||||||
|
/** Saved: [isNew] goes back to the opener with a snackbar ([offline] picks its wording), an edit back to S-8. */
|
||||||
|
data class Saved(val id: ExpenseId, val isNew: Boolean, val offline: Boolean) : EditorEvent
|
||||||
|
data object Deleted : EditorEvent
|
||||||
|
data object Closed : EditorEvent
|
||||||
|
|
||||||
|
/** Validation moved focus to the description field. */
|
||||||
|
data object FocusDescription : EditorEvent
|
||||||
|
}
|
||||||
-84
@@ -27,7 +27,6 @@ import se.hagstedt.kvitt.model.Account
|
|||||||
import se.hagstedt.kvitt.model.AccountId
|
import se.hagstedt.kvitt.model.AccountId
|
||||||
import se.hagstedt.kvitt.model.AccountRepository
|
import se.hagstedt.kvitt.model.AccountRepository
|
||||||
import se.hagstedt.kvitt.model.AccountSummary
|
import se.hagstedt.kvitt.model.AccountSummary
|
||||||
import se.hagstedt.kvitt.model.Category
|
|
||||||
import se.hagstedt.kvitt.model.CategoryId
|
import se.hagstedt.kvitt.model.CategoryId
|
||||||
import se.hagstedt.kvitt.model.ConnectivityRepository
|
import se.hagstedt.kvitt.model.ConnectivityRepository
|
||||||
import se.hagstedt.kvitt.model.ExpenseDraft
|
import se.hagstedt.kvitt.model.ExpenseDraft
|
||||||
@@ -37,93 +36,10 @@ import se.hagstedt.kvitt.model.ExpenseRepository
|
|||||||
import se.hagstedt.kvitt.model.FieldError
|
import se.hagstedt.kvitt.model.FieldError
|
||||||
import se.hagstedt.kvitt.model.HomeRepository
|
import se.hagstedt.kvitt.model.HomeRepository
|
||||||
import se.hagstedt.kvitt.model.SaveResult
|
import se.hagstedt.kvitt.model.SaveResult
|
||||||
import se.hagstedt.kvitt.model.Share
|
|
||||||
import se.hagstedt.kvitt.model.SplitMode
|
import se.hagstedt.kvitt.model.SplitMode
|
||||||
import se.hagstedt.kvitt.model.User
|
|
||||||
import se.hagstedt.kvitt.model.UserId
|
import se.hagstedt.kvitt.model.UserId
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/** Which keypad-driven field has the in-app keypad (design-spec §3.7, §5.2); null hides the keypad. */
|
|
||||||
enum class KeypadTarget { Amount, RatioViewer, RatioPartner, Exact }
|
|
||||||
|
|
||||||
/** What is shown over the editor: a sheet, the date picker or a confirmation (S-7). */
|
|
||||||
enum class EditorOverlay { None, AccountPicker, CategoryGrid, DatePicker, Discard, Delete }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The values the user edits (S-7). Percent fields hold hundredths of a percent in an [AmountInput] (so "60" is 6000
|
|
||||||
* bp) and money fields öre; all parsing is string-exact (SPEC §9.6).
|
|
||||||
*/
|
|
||||||
data class EditorForm(
|
|
||||||
val id: ExpenseId?,
|
|
||||||
val accountId: AccountId?,
|
|
||||||
val categoryId: CategoryId?,
|
|
||||||
val amount: AmountInput,
|
|
||||||
val description: String,
|
|
||||||
val notes: String,
|
|
||||||
val date: String,
|
|
||||||
val payer: UserId,
|
|
||||||
val mode: SplitMode,
|
|
||||||
val ratioViewer: AmountInput,
|
|
||||||
val ratioPartner: AmountInput,
|
|
||||||
val exactTyped: UserId,
|
|
||||||
val exactInput: AmountInput,
|
|
||||||
val fullOn: UserId,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** The editor's whole state as S-7 renders it. [ready] is false until the options (and, editing, the expense) are read. */
|
|
||||||
data class ExpenseEditorState(
|
|
||||||
val ready: Boolean = false,
|
|
||||||
val missing: Boolean = false,
|
|
||||||
val isEdit: Boolean = false,
|
|
||||||
val me: User? = null,
|
|
||||||
val partner: User? = null,
|
|
||||||
val form: EditorForm? = null,
|
|
||||||
val accounts: List<AccountChoice> = emptyList(),
|
|
||||||
val account: Account? = null,
|
|
||||||
val accountColorIndex: Int = 0,
|
|
||||||
val categories: List<Category> = emptyList(),
|
|
||||||
val quickRow: List<Category> = emptyList(),
|
|
||||||
val category: Category? = null,
|
|
||||||
val preview: List<Share>? = null,
|
|
||||||
val keypad: KeypadTarget? = null,
|
|
||||||
val splitOpen: Boolean = false,
|
|
||||||
val splitReveal: Int = 0,
|
|
||||||
val notesExpanded: Boolean = false,
|
|
||||||
val overlay: EditorOverlay = EditorOverlay.None,
|
|
||||||
val errors: List<FieldError> = emptyList(),
|
|
||||||
val online: Boolean = true,
|
|
||||||
val today: String = "",
|
|
||||||
val saving: Boolean = false,
|
|
||||||
) {
|
|
||||||
/** The typed exact share exceeds the amount (AC-8.7): inline error, saving blocked. */
|
|
||||||
val exactTooLarge: Boolean
|
|
||||||
get() {
|
|
||||||
val f = form ?: return false
|
|
||||||
val typed = f.exactInput.ore ?: return false
|
|
||||||
val amount = f.amount.ore ?: return false
|
|
||||||
return f.mode == SplitMode.EXACT && typed > amount
|
|
||||||
}
|
|
||||||
|
|
||||||
/** "Spara": amount > 0, a description, a category and a computable preview (design-spec S-7). */
|
|
||||||
val canSave: Boolean
|
|
||||||
get() {
|
|
||||||
val f = form ?: return false
|
|
||||||
return !saving && (f.amount.ore ?: 0L) > 0 && f.description.isNotBlank() && f.categoryId != null && f.accountId != null &&
|
|
||||||
preview != null && !exactTooLarge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One-off results the entry point turns into navigation. */
|
|
||||||
sealed interface EditorEvent {
|
|
||||||
/** Saved: [isNew] goes back to the opener with a snackbar ([offline] picks its wording), an edit back to S-8. */
|
|
||||||
data class Saved(val id: ExpenseId, val isNew: Boolean, val offline: Boolean) : EditorEvent
|
|
||||||
data object Deleted : EditorEvent
|
|
||||||
data object Closed : EditorEvent
|
|
||||||
|
|
||||||
/** Validation moved focus to the description field. */
|
|
||||||
data object FocusDescription : EditorEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* S-7 state holder (design-spec §4 S-7 and §5; SPEC AC-8.3 to AC-8.9, §9.11 CS-1 … CS-7). Arguments: [ARG_EXPENSE_ID]
|
* S-7 state holder (design-spec §4 S-7 and §5; SPEC AC-8.3 to AC-8.9, §9.11 CS-1 … CS-7). Arguments: [ARG_EXPENSE_ID]
|
||||||
* edits that expense; otherwise a new expense, in [ARG_ACCOUNT_ID] when opened from an account (S-5).
|
* edits that expense; otherwise a new expense, in [ARG_ACCOUNT_ID] when opened from an account (S-5).
|
||||||
|
|||||||
Reference in New Issue
Block a user