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.AccountRepository
|
||||
import se.hagstedt.kvitt.model.AccountSummary
|
||||
import se.hagstedt.kvitt.model.Category
|
||||
import se.hagstedt.kvitt.model.CategoryId
|
||||
import se.hagstedt.kvitt.model.ConnectivityRepository
|
||||
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.HomeRepository
|
||||
import se.hagstedt.kvitt.model.SaveResult
|
||||
import se.hagstedt.kvitt.model.Share
|
||||
import se.hagstedt.kvitt.model.SplitMode
|
||||
import se.hagstedt.kvitt.model.User
|
||||
import se.hagstedt.kvitt.model.UserId
|
||||
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]
|
||||
* 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