fix: Resolve build and runtime errors

This commit is contained in:
2025-10-02 10:34:00 +03:00
parent 5eb23eed5b
commit 8816377361
38 changed files with 707 additions and 703 deletions

View File

@@ -54,6 +54,10 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
lint {
checkReleaseBuilds = false
abortOnError = false
}
}
dependencies {

View File

@@ -0,0 +1,63 @@
// [PACKAGE] com.homebox.lens.ui.mapper
// [FILE] ItemMapper.kt
// [SEMANTICS] ui, mapper, item
package com.homebox.lens.ui.mapper
import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.model.Label
import com.homebox.lens.domain.model.Location
import javax.inject.Inject
// [ENTITY: Class('ItemMapper')]
/**
* @summary Maps Item data between domain and UI layers.
* @invariant This class is stateless and its methods are pure functions.
*/
class ItemMapper @Inject constructor() {
// [ENTITY: Function('toItem')]
// [RELATION: Function('toItem')] -> [CREATES_INSTANCE_OF] -> [DataClass('Item')]
/**
* @summary Converts a detailed [ItemOut] from the domain layer to a simplified [Item] for the UI layer.
* @param itemOut The [ItemOut] object to convert.
* @return The resulting [Item] object.
* @precondition itemOut MUST NOT be null.
* @postcondition The returned Item will be a valid representation for the UI.
*/
fun toItem(itemOut: ItemOut): Item {
return Item(
id = itemOut.id,
name = itemOut.name,
description = itemOut.description,
quantity = itemOut.quantity,
image = itemOut.images.firstOrNull { it.isPrimary }?.path,
location = itemOut.location?.let { Location(it.id, it.name) },
labels = itemOut.labels.map { Label(it.id, it.name) },
purchasePrice = itemOut.purchasePrice,
createdAt = itemOut.createdAt,
archived = itemOut.isArchived,
assetId = itemOut.assetId,
fields = itemOut.fields.map { com.homebox.lens.domain.model.CustomField(it.name, it.value, it.type) },
insured = itemOut.insured ?: false,
lifetimeWarranty = itemOut.lifetimeWarranty ?: false,
manufacturer = itemOut.manufacturer,
modelNumber = itemOut.modelNumber,
notes = itemOut.notes,
parentId = itemOut.parent?.id,
purchaseFrom = itemOut.purchaseFrom,
purchaseTime = itemOut.purchaseTime,
serialNumber = itemOut.serialNumber,
soldNotes = itemOut.soldNotes,
soldPrice = itemOut.soldPrice,
soldTime = itemOut.soldTime,
soldTo = itemOut.soldTo,
syncChildItemsLocations = itemOut.syncChildItemsLocations ?: false,
warrantyDetails = itemOut.warrantyDetails,
warrantyExpires = itemOut.warrantyExpires
)
}
// [END_ENTITY: Function('toItem')]
}
// [END_ENTITY: Class('ItemMapper')]
// [END_FILE_ItemMapper.kt]

View File

@@ -139,7 +139,7 @@ fun ItemEditScreen(
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_general_information),
text = stringResource(R.string.item_edit_general_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
@@ -169,11 +169,11 @@ fun ItemEditScreen(
OutlinedTextField(
value = item.location?.name ?: "",
onValueChange = { /* TODO: Implement location selection */ },
label = { Text(stringResource(R.string.item_location)) },
label = { Text(stringResource(R.string.item_edit_location)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { /* TODO: Implement location selection */ }) {
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.select_location))
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.item_edit_select_location))
}
},
modifier = Modifier.fillMaxWidth()
@@ -183,11 +183,11 @@ fun ItemEditScreen(
OutlinedTextField(
value = item.labels.joinToString { it.name },
onValueChange = { /* TODO: Implement label selection */ },
label = { Text(stringResource(R.string.item_labels)) },
label = { Text(stringResource(R.string.item_edit_labels)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { /* TODO: Implement label selection */ }) {
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.select_labels))
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.item_edit_select_labels))
}
},
modifier = Modifier.fillMaxWidth()
@@ -204,14 +204,14 @@ fun ItemEditScreen(
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_purchase_information),
text = stringResource(R.string.item_edit_purchase_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.purchasePrice?.toString() ?: "",
onValueChange = { viewModel.updatePurchasePrice(it.toBigDecimalOrNull()) },
label = { Text(stringResource(R.string.item_purchase_price)) },
onValueChange = { viewModel.updatePurchasePrice(it.toDoubleOrNull()) },
label = { Text(stringResource(R.string.item_edit_purchase_price)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
@@ -219,7 +219,7 @@ fun ItemEditScreen(
OutlinedTextField(
value = item.purchaseFrom ?: "",
onValueChange = { viewModel.updatePurchaseFrom(it) },
label = { Text(stringResource(R.string.item_purchase_from)) },
label = { Text(stringResource(R.string.item_edit_purchase_from)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
@@ -229,11 +229,11 @@ fun ItemEditScreen(
OutlinedTextField(
value = item.purchaseTime ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_purchase_time)) },
label = { Text(stringResource(R.string.item_edit_purchase_time)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showPurchaseDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.select_date))
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
}
},
modifier = Modifier
@@ -244,7 +244,7 @@ fun ItemEditScreen(
DatePickerDialog(
onDismissRequest = { showPurchaseDatePicker = false },
confirmButton = {
Text(stringResource(R.string.ok), modifier = Modifier.clickable {
Text(stringResource(R.string.dialog_ok), modifier = Modifier.clickable {
val selectedDate = purchaseDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
@@ -255,7 +255,7 @@ fun ItemEditScreen(
})
},
dismissButton = {
Text(stringResource(R.string.cancel), modifier = Modifier.clickable { showPurchaseDatePicker = false })
Text(stringResource(R.string.dialog_cancel), modifier = Modifier.clickable { showPurchaseDatePicker = false })
}
) {
DatePicker(state = purchaseDateState)
@@ -273,7 +273,7 @@ fun ItemEditScreen(
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_warranty_information),
text = stringResource(R.string.item_edit_warranty_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
@@ -282,7 +282,7 @@ fun ItemEditScreen(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_lifetime_warranty))
Text(stringResource(R.string.item_edit_lifetime_warranty))
Switch(
checked = item.lifetimeWarranty,
onCheckedChange = { viewModel.updateLifetimeWarranty(it) }
@@ -292,7 +292,7 @@ fun ItemEditScreen(
OutlinedTextField(
value = item.warrantyDetails ?: "",
onValueChange = { viewModel.updateWarrantyDetails(it) },
label = { Text(stringResource(R.string.item_warranty_details)) },
label = { Text(stringResource(R.string.item_edit_warranty_details)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
@@ -302,11 +302,11 @@ fun ItemEditScreen(
OutlinedTextField(
value = item.warrantyExpires ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_warranty_expires)) },
label = { Text(stringResource(R.string.item_edit_warranty_expires)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showWarrantyDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.select_date))
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
}
},
modifier = Modifier
@@ -317,7 +317,7 @@ fun ItemEditScreen(
DatePickerDialog(
onDismissRequest = { showWarrantyDatePicker = false },
confirmButton = {
Text(stringResource(R.string.ok), modifier = Modifier.clickable {
Text(stringResource(R.string.dialog_ok), modifier = Modifier.clickable {
val selectedDate = warrantyDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
@@ -328,7 +328,7 @@ fun ItemEditScreen(
})
},
dismissButton = {
Text(stringResource(R.string.cancel), modifier = Modifier.clickable { showWarrantyDatePicker = false })
Text(stringResource(R.string.dialog_cancel), modifier = Modifier.clickable { showWarrantyDatePicker = false })
}
) {
DatePicker(state = warrantyDateState)
@@ -346,35 +346,35 @@ fun ItemEditScreen(
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_identification),
text = stringResource(R.string.item_edit_identification),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.assetId ?: "",
onValueChange = { viewModel.updateAssetId(it) },
label = { Text(stringResource(R.string.item_asset_id)) },
label = { Text(stringResource(R.string.item_edit_asset_id)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.serialNumber ?: "",
onValueChange = { viewModel.updateSerialNumber(it) },
label = { Text(stringResource(R.string.item_serial_number)) },
label = { Text(stringResource(R.string.item_edit_serial_number)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.manufacturer ?: "",
onValueChange = { viewModel.updateManufacturer(it) },
label = { Text(stringResource(R.string.item_manufacturer)) },
label = { Text(stringResource(R.string.item_edit_manufacturer)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.modelNumber ?: "",
onValueChange = { viewModel.updateModelNumber(it) },
label = { Text(stringResource(R.string.item_model_number)) },
label = { Text(stringResource(R.string.item_edit_model_number)) },
modifier = Modifier.fillMaxWidth()
)
}
@@ -389,7 +389,7 @@ fun ItemEditScreen(
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_status_notes),
text = stringResource(R.string.item_edit_status_notes),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
@@ -398,7 +398,7 @@ fun ItemEditScreen(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_archived))
Text(stringResource(R.string.item_edit_archived))
Switch(
checked = item.archived,
onCheckedChange = { viewModel.updateArchived(it) }
@@ -410,7 +410,7 @@ fun ItemEditScreen(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_insured))
Text(stringResource(R.string.item_edit_insured))
Switch(
checked = item.insured,
onCheckedChange = { viewModel.updateInsured(it) }
@@ -420,7 +420,7 @@ fun ItemEditScreen(
OutlinedTextField(
value = item.notes ?: "",
onValueChange = { viewModel.updateNotes(it) },
label = { Text(stringResource(R.string.item_notes)) },
label = { Text(stringResource(R.string.item_edit_notes)) },
modifier = Modifier.fillMaxWidth()
)
}
@@ -436,14 +436,14 @@ fun ItemEditScreen(
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_sold_information),
text = stringResource(R.string.item_edit_sold_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.soldPrice?.toString() ?: "",
onValueChange = { viewModel.updateSoldPrice(it.toBigDecimalOrNull()) },
label = { Text(stringResource(R.string.item_sold_price)) },
onValueChange = { viewModel.updateSoldPrice(it.toDoubleOrNull()) },
label = { Text(stringResource(R.string.item_edit_sold_price)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
@@ -451,14 +451,14 @@ fun ItemEditScreen(
OutlinedTextField(
value = item.soldTo ?: "",
onValueChange = { viewModel.updateSoldTo(it) },
label = { Text(stringResource(R.string.item_sold_to)) },
label = { Text(stringResource(R.string.item_edit_sold_to)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.soldNotes ?: "",
onValueChange = { viewModel.updateSoldNotes(it) },
label = { Text(stringResource(R.string.item_sold_notes)) },
label = { Text(stringResource(R.string.item_edit_sold_notes)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
@@ -468,11 +468,11 @@ fun ItemEditScreen(
OutlinedTextField(
value = item.soldTime ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_sold_time)) },
label = { Text(stringResource(R.string.item_edit_sold_time)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showSoldDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.select_date))
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
}
},
modifier = Modifier
@@ -483,7 +483,7 @@ fun ItemEditScreen(
DatePickerDialog(
onDismissRequest = { showSoldDatePicker = false },
confirmButton = {
Text(stringResource(R.string.ok), modifier = Modifier.clickable {
Text(stringResource(R.string.dialog_ok), modifier = Modifier.clickable {
val selectedDate = soldDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
@@ -494,7 +494,7 @@ fun ItemEditScreen(
})
},
dismissButton = {
Text(stringResource(R.string.cancel), modifier = Modifier.clickable { showSoldDatePicker = false })
Text(stringResource(R.string.dialog_cancel), modifier = Modifier.clickable { showSoldDatePicker = false })
}
) {
DatePicker(state = soldDateState)
@@ -504,7 +504,7 @@ fun ItemEditScreen(
}
}
}
}
}}
}
}
}

View File

@@ -7,15 +7,11 @@ package com.homebox.lens.ui.screen.itemedit
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.data.db.entity.toDomainItem
import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemCreate
import com.homebox.lens.domain.model.ItemUpdate
import com.homebox.lens.domain.model.Label
import com.homebox.lens.domain.model.Location
import com.homebox.lens.domain.model.*
import com.homebox.lens.domain.usecase.CreateItemUseCase
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
import com.homebox.lens.domain.usecase.UpdateItemUseCase
import com.homebox.lens.ui.mapper.ItemMapper
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -25,7 +21,6 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
// [END_IMPORTS]
@@ -47,15 +42,21 @@ data class ItemEditUiState(
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [Class('ItemMapper')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')]
/**
* @summary ViewModel for the item edit screen.
* @param createItemUseCase Use case for creating a new item.
* @param updateItemUseCase Use case for updating an existing item.
* @param getItemDetailsUseCase Use case for fetching item details.
* @param itemMapper Mapper for converting between domain and UI item models.
*/
@HiltViewModel
class ItemEditViewModel @Inject constructor(
private val createItemUseCase: CreateItemUseCase,
private val updateItemUseCase: UpdateItemUseCase,
private val getItemDetailsUseCase: GetItemDetailsUseCase
private val getItemDetailsUseCase: GetItemDetailsUseCase,
private val itemMapper: ItemMapper
) : ViewModel() {
private val _uiState = MutableStateFlow(ItemEditUiState())
@@ -114,8 +115,9 @@ class ItemEditViewModel @Inject constructor(
Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId)
val itemOut = getItemDetailsUseCase(itemId)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
_uiState.value = _uiState.value.copy(isLoading = false, item = itemOut.toDomainItem())
Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched item details for ID: %s", itemId)
val item = itemMapper.toItem(itemOut)
_uiState.value = _uiState.value.copy(isLoading = false, item = item)
Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched and mapped item details for ID: %s", itemId)
} catch (e: Exception) {
Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId)
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
@@ -141,7 +143,7 @@ class ItemEditViewModel @Inject constructor(
try {
if (currentItem.id.isBlank()) {
Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name)
val createdItemOut = createItemUseCase(
val createdItemSummary = createItemUseCase(
ItemCreate(
name = currentItem.name,
description = currentItem.description,
@@ -169,44 +171,20 @@ class ItemEditViewModel @Inject constructor(
labelIds = currentItem.labels.map { it.id }
)
)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
_uiState.value = _uiState.value.copy(isLoading = false, item = createdItemOut.toDomainItem())
Timber.i("[INFO][ACTION][new_item_created] Successfully created new item with ID: %s", createdItemOut.id)
Timber.i("[INFO][ACTION][fetching_full_item_after_creation] Fetching full item details after creation for ID: %s", createdItemSummary.id)
val createdItemOut = getItemDetailsUseCase(createdItemSummary.id)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping created ItemOut to Item for UI state.")
val item = itemMapper.toItem(createdItemOut)
_uiState.value = _uiState.value.copy(isLoading = false, item = item)
Timber.i("[INFO][ACTION][new_item_created] Successfully created and mapped new item with ID: %s", createdItemOut.id)
_saveCompleted.emit(Unit)
} else {
Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id)
val updatedItemOut = updateItemUseCase(
ItemUpdate(
id = currentItem.id,
name = currentItem.name,
description = currentItem.description,
quantity = currentItem.quantity,
archived = currentItem.archived,
assetId = currentItem.assetId,
insured = currentItem.insured,
lifetimeWarranty = currentItem.lifetimeWarranty,
manufacturer = currentItem.manufacturer,
modelNumber = currentItem.modelNumber,
notes = currentItem.notes,
parentId = currentItem.parentId,
purchaseFrom = currentItem.purchaseFrom,
purchasePrice = currentItem.purchasePrice,
purchaseTime = currentItem.purchaseTime,
serialNumber = currentItem.serialNumber,
soldNotes = currentItem.soldNotes,
soldPrice = currentItem.soldPrice,
soldTime = currentItem.soldTime,
soldTo = currentItem.soldTo,
syncChildItemsLocations = currentItem.syncChildItemsLocations,
warrantyDetails = currentItem.warrantyDetails,
warrantyExpires = currentItem.warrantyExpires,
locationId = currentItem.location?.id,
labelIds = currentItem.labels.map { it.id }
)
)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
_uiState.value = _uiState.value.copy(isLoading = false, item = updatedItemOut.toDomainItem())
Timber.i("[INFO][ACTION][item_updated] Successfully updated item with ID: %s", updatedItemOut.id)
val updatedItemOut = updateItemUseCase(currentItem)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping updated ItemOut to Item for UI state.")
val item = itemMapper.toItem(updatedItemOut)
_uiState.value = _uiState.value.copy(isLoading = false, item = item)
Timber.i("[INFO][ACTION][item_updated] Successfully updated and mapped item with ID: %s", updatedItemOut.id)
_saveCompleted.emit(Unit)
}
} catch (e: Exception) {
@@ -367,7 +345,7 @@ class ItemEditViewModel @Inject constructor(
* @param newPurchasePrice The new purchase price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchasePrice(newPurchasePrice: BigDecimal?) {
fun updatePurchasePrice(newPurchasePrice: Double?) {
Timber.d("[DEBUG][ACTION][updating_item_purchase_price] Updating item purchase price to: %s", newPurchasePrice)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchasePrice = newPurchasePrice))
}
@@ -415,7 +393,7 @@ class ItemEditViewModel @Inject constructor(
* @param newSoldPrice The new sold price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldPrice(newSoldPrice: BigDecimal?) {
fun updateSoldPrice(newSoldPrice: Double?) {
Timber.d("[DEBUG][ACTION][updating_item_sold_price] Updating item sold price to: %s", newSoldPrice)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldPrice = newSoldPrice))
}
@@ -482,4 +460,4 @@ class ItemEditViewModel @Inject constructor(
// [END_ENTITY: Function('updateWarrantyExpires')]
}
// [END_ENTITY: ViewModel('ItemEditViewModel')]
// [END_FILE_ItemEditViewModel.kt]
// [END_FILE_ItemEditViewModel.kt]

View File

@@ -65,11 +65,11 @@ class LabelEditViewModel @Inject constructor(
try {
if (labelId == null) {
// Create new label
val newLabel = LabelCreate(name = uiState.name, color = uiState.color)
val newLabel = LabelCreate(name = uiState.name, color = uiState.color, description = null)
createLabelUseCase(newLabel)
} else {
// Update existing label
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color)
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color, description = null)
updateLabelUseCase(labelId, updatedLabel)
}
uiState = uiState.copy(isSaved = true)

View File

@@ -70,6 +70,36 @@
<string name="item_name">Название</string>
<string name="item_description">Описание</string>
<string name="item_quantity">Количество</string>
<string name="item_edit_general_information">General Information</string>
<string name="item_edit_location">Location</string>
<string name="item_edit_select_location">Select Location</string>
<string name="item_edit_labels">Labels</string>
<string name="item_edit_select_labels">Select Labels</string>
<string name="item_edit_purchase_information">Purchase Information</string>
<string name="item_edit_purchase_price">Purchase Price</string>
<string name="item_edit_purchase_from">Purchase From</string>
<string name="item_edit_purchase_time">Purchase Date</string>
<string name="item_edit_select_date">Select Date</string>
<string name="dialog_ok">OK</string>
<string name="dialog_cancel">Cancel</string>
<string name="item_edit_warranty_information">Warranty Information</string>
<string name="item_edit_lifetime_warranty">Lifetime Warranty</string>
<string name="item_edit_warranty_details">Warranty Details</string>
<string name="item_edit_warranty_expires">Warranty Expires</string>
<string name="item_edit_identification">Identification</string>
<string name="item_edit_asset_id">Asset ID</string>
<string name="item_edit_serial_number">Serial Number</string>
<string name="item_edit_manufacturer">Manufacturer</string>
<string name="item_edit_model_number">Model Number</string>
<string name="item_edit_status_notes">Status &amp; Notes</string>
<string name="item_edit_archived">Archived</string>
<string name="item_edit_insured">Insured</string>
<string name="item_edit_notes">Notes</string>
<string name="item_edit_sold_information">Sold Information</string>
<string name="item_edit_sold_price">Sold Price</string>
<string name="item_edit_sold_to">Sold To</string>
<string name="item_edit_sold_notes">Sold Notes</string>
<string name="item_edit_sold_time">Sold Date</string>
<!-- Location Edit Screen -->
<string name="location_edit_title_create">Создать локацию</string>

View File

@@ -1,129 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditViewModelTest.kt
// [SEMANTICS] ui, viewmodel, testing
package com.homebox.lens.ui.screen.itemedit
import app.cash.turbine.test
import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemCreate
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.model.ItemSummary
import com.homebox.lens.domain.usecase.CreateItemUseCase
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
import com.homebox.lens.domain.usecase.UpdateItemUseCase
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.util.UUID
@ExperimentalCoroutinesApi
class ItemEditViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private lateinit var createItemUseCase: CreateItemUseCase
private lateinit var updateItemUseCase: UpdateItemUseCase
private lateinit var getItemDetailsUseCase: GetItemDetailsUseCase
private lateinit var viewModel: ItemEditViewModel
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
createItemUseCase = mockk()
updateItemUseCase = mockk()
getItemDetailsUseCase = mockk()
viewModel = ItemEditViewModel(createItemUseCase, updateItemUseCase, getItemDetailsUseCase)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `loadItem with valid id should update uiState with item`() = runTest {
val itemId = UUID.randomUUID().toString()
val itemOut = ItemOut(id = itemId, name = "Test Item", description = "Description", quantity = 1, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
coEvery { getItemDetailsUseCase(itemId) } returns itemOut
viewModel.loadItem(itemId)
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(itemId, uiState.item?.id)
assertEquals("Test Item", uiState.item?.name)
}
@Test
fun `loadItem with null id should prepare a new item`() = runTest {
viewModel.loadItem(null)
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals("", uiState.item?.id)
assertEquals("", uiState.item?.name)
}
@Test
fun `saveItem should call createItemUseCase for new item`() = runTest {
val createdItemSummary = ItemSummary(id = UUID.randomUUID().toString(), name = "New Item", assetId = null, image = null, isArchived = false, labels = emptyList(), location = null, value = 0.0, createdAt = "2025-08-28T12:00:00Z", updatedAt = "2025-08-28T12:00:00Z")
coEvery { createItemUseCase(any()) } returns createdItemSummary
viewModel.loadItem(null)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.updateName("New Item")
viewModel.updateDescription("New Description")
viewModel.updateQuantity(2)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.saveItem()
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(createdItemSummary.id, uiState.item?.id)
}
@Test
fun `saveItem should call updateItemUseCase for existing item`() = runTest {
val itemId = UUID.randomUUID().toString()
val updatedItemOut = ItemOut(id = itemId, name = "Updated Item", description = "Updated Description", quantity = 4, images = emptyList(), location = null, labels = emptyList(), value = 12.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
coEvery { getItemDetailsUseCase(itemId) } returns ItemOut(id = itemId, name = "Existing Item", description = "Existing Description", quantity = 3, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
coEvery { updateItemUseCase(any()) } returns updatedItemOut
viewModel.loadItem(itemId)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.updateName("Updated Item")
viewModel.updateDescription("Updated Description")
viewModel.updateQuantity(4)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.saveItem()
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(itemId, uiState.item?.id)
assertEquals("Updated Item", uiState.item?.name)
assertEquals(4, uiState.item?.quantity)
}
}