feat: Refactor Item Edit Screen with all API fields and user-friendly UI

This commit is contained in:
2025-09-28 11:33:57 +03:00
parent aa69776807
commit 5eb23eed5b
11 changed files with 1221 additions and 149 deletions

View File

@@ -5,28 +5,48 @@
package com.homebox.lens.ui.screen.itemedit
// [IMPORTS]
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
@@ -36,13 +56,16 @@ import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
// [END_IMPORTS]
// [ENTITY: Function('ItemEditScreen')]
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')]
// [RELATION: Function('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')]
// [RELATION: Function('ItemEditScreen')] -> [CALLS] -> [Function('MainScaffold')]
// [ENTITY: Composable('ItemEditScreen')]
// [RELATION: Composable('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Composable('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')]
// [RELATION: Composable('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')]
// [RELATION: Composable('ItemEditScreen')] -> [CALLS] -> [Composable('MainScaffold')]
/**
* @summary Composable-функция для экрана "Редактирование элемента".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
@@ -51,6 +74,7 @@ import timber.log.Timber
* @param viewModel ViewModel для управления состоянием экрана.
* @param onSaveSuccess Callback, вызываемый после успешного сохранения товара.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemEditScreen(
currentRoute: String?,
@@ -75,7 +99,7 @@ fun ItemEditScreen(
}
LaunchedEffect(Unit) {
viewModel.saveCompleted.collect {
viewModel.saveCompleted.collect {
Timber.i("[INFO][ACTION][save_completed_callback] Item save completed. Triggering onSaveSuccess.")
onSaveSuccess()
}
@@ -85,7 +109,7 @@ fun ItemEditScreen(
topBarTitle = stringResource(id = R.string.item_edit_title),
currentRoute = currentRoute,
navigationActions = navigationActions
) {
) { paddingValues ->
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = {
@@ -100,40 +124,389 @@ fun ItemEditScreen(
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
} else {
uiState.item?.let { item ->
OutlinedTextField(
value = item.name,
onValueChange = { viewModel.updateName(it) },
label = { Text(stringResource(R.string.item_name)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.description ?: "",
onValueChange = { viewModel.updateDescription(it) },
label = { Text(stringResource(R.string.item_description)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.quantity.toString(),
onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) },
label = { Text(stringResource(R.string.item_quantity)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
// Add more fields as needed
// [AI_NOTE]: General Information section for basic item details.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_general_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.name,
onValueChange = { viewModel.updateName(it) },
label = { Text(stringResource(R.string.item_name)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.description ?: "",
onValueChange = { viewModel.updateDescription(it) },
label = { Text(stringResource(R.string.item_description)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.quantity.toString(),
onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) },
label = { Text(stringResource(R.string.item_quantity)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Location selection will require a separate component or screen.
OutlinedTextField(
value = item.location?.name ?: "",
onValueChange = { /* TODO: Implement location selection */ },
label = { Text(stringResource(R.string.item_location)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { /* TODO: Implement location selection */ }) {
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.select_location))
}
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Label selection will require a separate component or screen.
OutlinedTextField(
value = item.labels.joinToString { it.name },
onValueChange = { /* TODO: Implement label selection */ },
label = { Text(stringResource(R.string.item_labels)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { /* TODO: Implement label selection */ }) {
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.select_labels))
}
},
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Purchase Information section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_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)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.purchaseFrom ?: "",
onValueChange = { viewModel.updatePurchaseFrom(it) },
label = { Text(stringResource(R.string.item_purchase_from)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Date picker for purchase time.
var showPurchaseDatePicker by remember { mutableStateOf(false) }
val purchaseDateState = rememberDatePickerState()
OutlinedTextField(
value = item.purchaseTime ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_purchase_time)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showPurchaseDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.select_date))
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showPurchaseDatePicker = true }
)
if (showPurchaseDatePicker) {
DatePickerDialog(
onDismissRequest = { showPurchaseDatePicker = false },
confirmButton = {
Text(stringResource(R.string.ok), modifier = Modifier.clickable {
val selectedDate = purchaseDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
if (selectedDate != null) {
viewModel.updatePurchaseTime(selectedDate)
}
showPurchaseDatePicker = false
})
},
dismissButton = {
Text(stringResource(R.string.cancel), modifier = Modifier.clickable { showPurchaseDatePicker = false })
}
) {
DatePicker(state = purchaseDateState)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Warranty Information section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_warranty_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_lifetime_warranty))
Switch(
checked = item.lifetimeWarranty,
onCheckedChange = { viewModel.updateLifetimeWarranty(it) }
)
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.warrantyDetails ?: "",
onValueChange = { viewModel.updateWarrantyDetails(it) },
label = { Text(stringResource(R.string.item_warranty_details)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Date picker for warranty expiration.
var showWarrantyDatePicker by remember { mutableStateOf(false) }
val warrantyDateState = rememberDatePickerState()
OutlinedTextField(
value = item.warrantyExpires ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_warranty_expires)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showWarrantyDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.select_date))
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showWarrantyDatePicker = true }
)
if (showWarrantyDatePicker) {
DatePickerDialog(
onDismissRequest = { showWarrantyDatePicker = false },
confirmButton = {
Text(stringResource(R.string.ok), modifier = Modifier.clickable {
val selectedDate = warrantyDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
if (selectedDate != null) {
viewModel.updateWarrantyExpires(selectedDate)
}
showWarrantyDatePicker = false
})
},
dismissButton = {
Text(stringResource(R.string.cancel), modifier = Modifier.clickable { showWarrantyDatePicker = false })
}
) {
DatePicker(state = warrantyDateState)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Identification section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_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)) },
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)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.manufacturer ?: "",
onValueChange = { viewModel.updateManufacturer(it) },
label = { Text(stringResource(R.string.item_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)) },
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Status & Notes section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_status_notes),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_archived))
Switch(
checked = item.archived,
onCheckedChange = { viewModel.updateArchived(it) }
)
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_insured))
Switch(
checked = item.insured,
onCheckedChange = { viewModel.updateInsured(it) }
)
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.notes ?: "",
onValueChange = { viewModel.updateNotes(it) },
label = { Text(stringResource(R.string.item_notes)) },
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Sold Information section (conditionally displayed).
if (item.soldTime != null || item.soldPrice != null || item.soldTo != null || item.soldNotes != null) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_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)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.soldTo ?: "",
onValueChange = { viewModel.updateSoldTo(it) },
label = { Text(stringResource(R.string.item_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)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Date picker for sold time.
var showSoldDatePicker by remember { mutableStateOf(false) }
val soldDateState = rememberDatePickerState()
OutlinedTextField(
value = item.soldTime ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_sold_time)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showSoldDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.select_date))
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showSoldDatePicker = true }
)
if (showSoldDatePicker) {
DatePickerDialog(
onDismissRequest = { showSoldDatePicker = false },
confirmButton = {
Text(stringResource(R.string.ok), modifier = Modifier.clickable {
val selectedDate = soldDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
if (selectedDate != null) {
viewModel.updateSoldTime(selectedDate)
}
showSoldDatePicker = false
})
},
dismissButton = {
Text(stringResource(R.string.cancel), modifier = Modifier.clickable { showSoldDatePicker = false })
}
) {
DatePicker(state = soldDateState)
}
}
}
}
}
}
}
}
}
}
// [END_ENTITY: Function('ItemEditScreen')]
// [END_ENTITY: Composable('ItemEditScreen')]
// [END_FILE_ItemEditScreen.kt]

View File

@@ -7,8 +7,10 @@ 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.usecase.CreateItemUseCase
@@ -23,6 +25,7 @@ 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]
@@ -73,24 +76,45 @@ class ItemEditViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
if (itemId == null) {
Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.")
_uiState.value = _uiState.value.copy(isLoading = false, item = Item(id = "", name = "", description = null, quantity = 0, image = null, location = null, labels = emptyList(), value = null, createdAt = null))
_uiState.value = _uiState.value.copy(
isLoading = false,
item = Item(
id = "",
name = "",
description = null,
quantity = 1,
image = null,
location = null,
labels = emptyList(),
purchasePrice = null,
createdAt = null,
archived = false,
assetId = null,
fields = emptyList(),
insured = false,
lifetimeWarranty = false,
manufacturer = null,
modelNumber = null,
notes = null,
parentId = null,
purchaseFrom = null,
purchaseTime = null,
serialNumber = null,
soldNotes = null,
soldPrice = null,
soldTime = null,
soldTo = null,
syncChildItemsLocations = false,
warrantyDetails = null,
warrantyExpires = null
)
)
} else {
try {
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.")
val item = Item(
id = itemOut.id,
name = itemOut.name,
description = itemOut.description,
quantity = itemOut.quantity,
image = itemOut.images.firstOrNull()?.path, // Assuming first image is the main one
location = itemOut.location?.let { Location(it.id, it.name) }, // Simplified mapping
labels = itemOut.labels.map { Label(it.id, it.name) }, // Simplified mapping
value = itemOut.value?.toBigDecimal(),
createdAt = itemOut.createdAt
)
_uiState.value = _uiState.value.copy(isLoading = false, item = item)
_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)
} catch (e: Exception) {
Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId)
@@ -117,53 +141,72 @@ class ItemEditViewModel @Inject constructor(
try {
if (currentItem.id.isBlank()) {
Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name)
val createdItemSummary = createItemUseCase(ItemCreate(
name = currentItem.name,
description = currentItem.description,
quantity = currentItem.quantity,
assetId = null, // Item does not have assetId
notes = null, // Item does not have notes
serialNumber = null, // Item does not have serialNumber
value = currentItem.value?.toDouble(), // Convert BigDecimal to Double
purchasePrice = null, // Item does not have purchasePrice
purchaseDate = null, // Item does not have purchaseDate
warrantyUntil = null, // Item does not have warrantyUntil
locationId = currentItem.location?.id,
parentId = null, // Item does not have parentId
labelIds = currentItem.labels.map { it.id }
))
Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.")
val createdItem = Item(
id = createdItemSummary.id,
name = createdItemSummary.name,
description = null, // ItemSummary does not have description
quantity = 0, // ItemSummary does not have quantity
image = null, // ItemSummary does not have image
location = null, // ItemSummary does not have location
labels = emptyList(), // ItemSummary does not have labels
value = null, // ItemSummary does not have value
createdAt = null // ItemSummary does not have createdAt
val createdItemOut = createItemUseCase(
ItemCreate(
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 }
)
)
_uiState.value = _uiState.value.copy(isLoading = false, item = createdItem)
Timber.i("[INFO][ACTION][new_item_created] Successfully created new item with ID: %s", createdItem.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)
_saveCompleted.emit(Unit)
} else {
Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id)
val updatedItemOut = updateItemUseCase(currentItem)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
val updatedItem = Item(
id = updatedItemOut.id,
name = updatedItemOut.name,
description = updatedItemOut.description,
quantity = updatedItemOut.quantity,
image = updatedItemOut.images.firstOrNull()?.path,
location = updatedItemOut.location?.let { Location(it.id, it.name) },
labels = updatedItemOut.labels.map { Label(it.id, it.name) },
value = updatedItemOut.value.toBigDecimal(),
createdAt = updatedItemOut.createdAt
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 }
)
)
_uiState.value = _uiState.value.copy(isLoading = false, item = updatedItem)
Timber.i("[INFO][ACTION][item_updated] Successfully updated item with ID: %s", updatedItem.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)
_saveCompleted.emit(Unit)
}
} catch (e: Exception) {
@@ -209,6 +252,234 @@ class ItemEditViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity))
}
// [END_ENTITY: Function('updateQuantity')]
}
// [END_ENTITY: ViewModel('ItemEditViewModel')]
// [END_FILE_ItemEditViewModel.kt]
// [ENTITY: Function('updateArchived')]
/**
* @summary Updates the archived status of the item in the UI state.
* @param newArchived The new archived status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateArchived(newArchived: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_archived] Updating item archived status to: %s", newArchived)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(archived = newArchived))
}
// [END_ENTITY: Function('updateArchived')]
// [ENTITY: Function('updateAssetId')]
/**
* @summary Updates the asset ID of the item in the UI state.
* @param newAssetId The new asset ID for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateAssetId(newAssetId: String) {
Timber.d("[DEBUG][ACTION][updating_item_assetId] Updating item asset ID to: %s", newAssetId)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(assetId = newAssetId))
}
// [END_ENTITY: Function('updateAssetId')]
// [ENTITY: Function('updateInsured')]
/**
* @summary Updates the insured status of the item in the UI state.
* @param newInsured The new insured status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateInsured(newInsured: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_insured] Updating item insured status to: %s", newInsured)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(insured = newInsured))
}
// [END_ENTITY: Function('updateInsured')]
// [ENTITY: Function('updateLifetimeWarranty')]
/**
* @summary Updates the lifetime warranty status of the item in the UI state.
* @param newLifetimeWarranty The new lifetime warranty status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateLifetimeWarranty(newLifetimeWarranty: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_lifetime_warranty] Updating item lifetime warranty status to: %s", newLifetimeWarranty)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(lifetimeWarranty = newLifetimeWarranty))
}
// [END_ENTITY: Function('updateLifetimeWarranty')]
// [ENTITY: Function('updateManufacturer')]
/**
* @summary Updates the manufacturer of the item in the UI state.
* @param newManufacturer The new manufacturer for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateManufacturer(newManufacturer: String) {
Timber.d("[DEBUG][ACTION][updating_item_manufacturer] Updating item manufacturer to: %s", newManufacturer)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(manufacturer = newManufacturer))
}
// [END_ENTITY: Function('updateManufacturer')]
// [ENTITY: Function('updateModelNumber')]
/**
* @summary Updates the model number of the item in the UI state.
* @param newModelNumber The new model number for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateModelNumber(newModelNumber: String) {
Timber.d("[DEBUG][ACTION][updating_item_model_number] Updating item model number to: %s", newModelNumber)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(modelNumber = newModelNumber))
}
// [END_ENTITY: Function('updateModelNumber')]
// [ENTITY: Function('updateNotes')]
/**
* @summary Updates the notes of the item in the UI state.
* @param newNotes The new notes for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateNotes(newNotes: String) {
Timber.d("[DEBUG][ACTION][updating_item_notes] Updating item notes to: %s", newNotes)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(notes = newNotes))
}
// [END_ENTITY: Function('updateNotes')]
// [ENTITY: Function('updateParentId')]
/**
* @summary Updates the parent ID of the item in the UI state.
* @param newParentId The new parent ID for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateParentId(newParentId: String) {
Timber.d("[DEBUG][ACTION][updating_item_parent_id] Updating item parent ID to: %s", newParentId)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(parentId = newParentId))
}
// [END_ENTITY: Function('updateParentId')]
// [ENTITY: Function('updatePurchaseFrom')]
/**
* @summary Updates the purchase source of the item in the UI state.
* @param newPurchaseFrom The new purchase source for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchaseFrom(newPurchaseFrom: String) {
Timber.d("[DEBUG][ACTION][updating_item_purchase_from] Updating item purchase from to: %s", newPurchaseFrom)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseFrom = newPurchaseFrom))
}
// [END_ENTITY: Function('updatePurchaseFrom')]
// [ENTITY: Function('updatePurchasePrice')]
/**
* @summary Updates the purchase price of the item in the UI state.
* @param newPurchasePrice The new purchase price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchasePrice(newPurchasePrice: BigDecimal?) {
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))
}
// [END_ENTITY: Function('updatePurchasePrice')]
// [ENTITY: Function('updatePurchaseTime')]
/**
* @summary Updates the purchase time of the item in the UI state.
* @param newPurchaseTime The new purchase time for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchaseTime(newPurchaseTime: String) {
Timber.d("[DEBUG][ACTION][updating_item_purchase_time] Updating item purchase time to: %s", newPurchaseTime)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseTime = newPurchaseTime))
}
// [END_ENTITY: Function('updatePurchaseTime')]
// [ENTITY: Function('updateSerialNumber')]
/**
* @summary Updates the serial number of the item in the UI state.
* @param newSerialNumber The new serial number for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSerialNumber(newSerialNumber: String) {
Timber.d("[DEBUG][ACTION][updating_item_serial_number] Updating item serial number to: %s", newSerialNumber)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(serialNumber = newSerialNumber))
}
// [END_ENTITY: Function('updateSerialNumber')]
// [ENTITY: Function('updateSoldNotes')]
/**
* @summary Updates the sold notes of the item in the UI state.
* @param newSoldNotes The new sold notes for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldNotes(newSoldNotes: String) {
Timber.d("[DEBUG][ACTION][updating_item_sold_notes] Updating item sold notes to: %s", newSoldNotes)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldNotes = newSoldNotes))
}
// [END_ENTITY: Function('updateSoldNotes')]
// [ENTITY: Function('updateSoldPrice')]
/**
* @summary Updates the sold price of the item in the UI state.
* @param newSoldPrice The new sold price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldPrice(newSoldPrice: BigDecimal?) {
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))
}
// [END_ENTITY: Function('updateSoldPrice')]
// [ENTITY: Function('updateSoldTime')]
/**
* @summary Updates the sold time of the item in the UI state.
* @param newSoldTime The new sold time for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldTime(newSoldTime: String) {
Timber.d("[DEBUG][ACTION][updating_item_sold_time] Updating item sold time to: %s", newSoldTime)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTime = newSoldTime))
}
// [END_ENTITY: Function('updateSoldTime')]
// [ENTITY: Function('updateSoldTo')]
/**
* @summary Updates the sold to field of the item in the UI state.
* @param newSoldTo The new sold to for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldTo(newSoldTo: String) {
Timber.d("[DEBUG][ACTION][updating_item_sold_to] Updating item sold to to: %s", newSoldTo)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTo = newSoldTo))
}
// [END_ENTITY: Function('updateSoldTo')]
// [ENTITY: Function('updateSyncChildItemsLocations')]
/**
* @summary Updates the sync child items locations status of the item in the UI state.
* @param newSyncChildItemsLocations The new sync child items locations status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSyncChildItemsLocations(newSyncChildItemsLocations: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_sync_child_items_locations] Updating item sync child items locations status to: %s", newSyncChildItemsLocations)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(syncChildItemsLocations = newSyncChildItemsLocations))
}
// [END_ENTITY: Function('updateSyncChildItemsLocations')]
// [ENTITY: Function('updateWarrantyDetails')]
/**
* @summary Updates the warranty details of the item in the UI state.
* @param newWarrantyDetails The new warranty details for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateWarrantyDetails(newWarrantyDetails: String) {
Timber.d("[DEBUG][ACTION][updating_item_warranty_details] Updating item warranty details to: %s", newWarrantyDetails)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyDetails = newWarrantyDetails))
}
// [END_ENTITY: Function('updateWarrantyDetails')]
// [ENTITY: Function('updateWarrantyExpires')]
/**
* @summary Updates the warranty expires date of the item in the UI state.
* @param newWarrantyExpires The new warranty expires date for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateWarrantyExpires(newWarrantyExpires: String) {
Timber.d("[DEBUG][ACTION][updating_item_warranty_expires] Updating item warranty expires date to: %s", newWarrantyExpires)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyExpires = newWarrantyExpires))
}
// [END_ENTITY: Function('updateWarrantyExpires')]
}
// [END_ENTITY: ViewModel('ItemEditViewModel')]
// [END_FILE_ItemEditViewModel.kt]

View File

@@ -3,7 +3,7 @@
plugins {
// [PLUGIN] Android Application plugin
id("com.android.application") version "8.12.2" apply false
id("com.android.application") version "8.13.0" apply false
// [PLUGIN] Kotlin Android plugin
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
// [PLUGIN] Hilt Android plugin

View File

@@ -17,17 +17,28 @@ import com.homebox.lens.domain.model.ItemCreate
@JsonClass(generateAdapter = true)
data class ItemCreateDto(
@Json(name = "name") val name: String,
@Json(name = "assetId") val assetId: String?,
@Json(name = "description") val description: String?,
@Json(name = "notes") val notes: String?,
@Json(name = "serialNumber") val serialNumber: String?,
@Json(name = "quantity") val quantity: Int?,
@Json(name = "value") val value: Double?,
@Json(name = "purchasePrice") val purchasePrice: Double?,
@Json(name = "purchaseDate") val purchaseDate: String?,
@Json(name = "warrantyUntil") val warrantyUntil: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "archived") val archived: Boolean?,
@Json(name = "assetId") val assetId: String?,
@Json(name = "insured") val insured: Boolean?,
@Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
@Json(name = "manufacturer") val manufacturer: String?,
@Json(name = "modelNumber") val modelNumber: String?,
@Json(name = "notes") val notes: String?,
@Json(name = "parentId") val parentId: String?,
@Json(name = "purchaseFrom") val purchaseFrom: String?,
@Json(name = "purchasePrice") val purchasePrice: Double?,
@Json(name = "purchaseTime") val purchaseTime: String?,
@Json(name = "serialNumber") val serialNumber: String?,
@Json(name = "soldNotes") val soldNotes: String?,
@Json(name = "soldPrice") val soldPrice: Double?,
@Json(name = "soldTime") val soldTime: String?,
@Json(name = "soldTo") val soldTo: String?,
@Json(name = "syncChildItemsLocations") val syncChildItemsLocations: Boolean?,
@Json(name = "warrantyDetails") val warrantyDetails: String?,
@Json(name = "warrantyExpires") val warrantyExpires: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>?
)
// [END_ENTITY: DataClass('ItemCreateDto')]
@@ -40,17 +51,28 @@ data class ItemCreateDto(
fun ItemCreate.toDto(): ItemCreateDto {
return ItemCreateDto(
name = this.name,
assetId = this.assetId,
description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity,
value = this.value,
purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate,
warrantyUntil = this.warrantyUntil,
locationId = this.locationId,
archived = this.archived,
assetId = this.assetId,
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchasePrice = this.purchasePrice,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires,
locationId = this.locationId,
labelIds = this.labelIds
)
}

View File

@@ -24,10 +24,20 @@ data class ItemOutDto(
@Json(name = "serialNumber") val serialNumber: String?,
@Json(name = "quantity") val quantity: Int,
@Json(name = "isArchived") val isArchived: Boolean,
@Json(name = "value") val value: Double,
@Json(name = "purchasePrice") val purchasePrice: Double?,
@Json(name = "purchaseDate") val purchaseDate: String?,
@Json(name = "warrantyUntil") val warrantyUntil: String?,
@Json(name = "purchaseTime") val purchaseTime: String?,
@Json(name = "purchaseFrom") val purchaseFrom: String?,
@Json(name = "warrantyExpires") val warrantyExpires: String?,
@Json(name = "warrantyDetails") val warrantyDetails: String?,
@Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
@Json(name = "insured") val insured: Boolean?,
@Json(name = "manufacturer") val manufacturer: String?,
@Json(name = "modelNumber") val modelNumber: String?,
@Json(name = "soldPrice") val soldPrice: Double?,
@Json(name = "soldTime") val soldTime: String?,
@Json(name = "soldTo") val soldTo: String?,
@Json(name = "soldNotes") val soldNotes: String?,
@Json(name = "syncChildItemsLocations") val syncChildItemsLocations: Boolean?,
@Json(name = "location") val location: LocationOutDto?,
@Json(name = "parent") val parent: ItemSummaryDto?,
@Json(name = "children") val children: List<ItemSummaryDto>,
@@ -56,10 +66,20 @@ fun ItemOutDto.toDomain(): ItemOut {
serialNumber = this.serialNumber,
quantity = this.quantity,
isArchived = this.isArchived,
value = this.value,
purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate,
warrantyUntil = this.warrantyUntil,
purchaseTime = this.purchaseTime,
purchaseFrom = this.purchaseFrom,
warrantyExpires = this.warrantyExpires,
warrantyDetails = this.warrantyDetails,
lifetimeWarranty = this.lifetimeWarranty,
insured = this.insured,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
soldNotes = this.soldNotes,
syncChildItemsLocations = this.syncChildItemsLocations,
location = this.location?.toDomain(),
parent = this.parent?.toDomain(),
children = this.children.map { it.toDomain() },

View File

@@ -17,18 +17,28 @@ import com.homebox.lens.domain.model.ItemUpdate
@JsonClass(generateAdapter = true)
data class ItemUpdateDto(
@Json(name = "name") val name: String?,
@Json(name = "assetId") val assetId: String?,
@Json(name = "description") val description: String?,
@Json(name = "notes") val notes: String?,
@Json(name = "serialNumber") val serialNumber: String?,
@Json(name = "quantity") val quantity: Int?,
@Json(name = "isArchived") val isArchived: Boolean?,
@Json(name = "value") val value: Double?,
@Json(name = "purchasePrice") val purchasePrice: Double?,
@Json(name = "purchaseDate") val purchaseDate: String?,
@Json(name = "warrantyUntil") val warrantyUntil: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "archived") val archived: Boolean?,
@Json(name = "assetId") val assetId: String?,
@Json(name = "insured") val insured: Boolean?,
@Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
@Json(name = "manufacturer") val manufacturer: String?,
@Json(name = "modelNumber") val modelNumber: String?,
@Json(name = "notes") val notes: String?,
@Json(name = "parentId") val parentId: String?,
@Json(name = "purchaseFrom") val purchaseFrom: String?,
@Json(name = "purchasePrice") val purchasePrice: Double?,
@Json(name = "purchaseTime") val purchaseTime: String?,
@Json(name = "serialNumber") val serialNumber: String?,
@Json(name = "soldNotes") val soldNotes: String?,
@Json(name = "soldPrice") val soldPrice: Double?,
@Json(name = "soldTime") val soldTime: String?,
@Json(name = "soldTo") val soldTo: String?,
@Json(name = "syncChildItemsLocations") val syncChildItemsLocations: Boolean?,
@Json(name = "warrantyDetails") val warrantyDetails: String?,
@Json(name = "warrantyExpires") val warrantyExpires: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>?
)
// [END_ENTITY: DataClass('ItemUpdateDto')]
@@ -41,18 +51,28 @@ data class ItemUpdateDto(
fun ItemUpdate.toDto(): ItemUpdateDto {
return ItemUpdateDto(
name = this.name,
assetId = this.assetId,
description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity,
isArchived = this.isArchived,
value = this.value,
purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate,
warrantyUntil = this.warrantyUntil,
locationId = this.locationId,
archived = this.archived,
assetId = this.assetId,
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchasePrice = this.purchasePrice,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires,
locationId = this.locationId,
labelIds = this.labelIds
)
}

View File

@@ -18,10 +18,29 @@ data class ItemEntity(
@PrimaryKey val id: String,
val name: String,
val description: String?,
val quantity: Int,
val image: String?,
val locationId: String?,
val value: BigDecimal?,
val createdAt: String?
val purchasePrice: BigDecimal?,
val createdAt: String?,
val archived: Boolean,
val assetId: String?,
val insured: Boolean,
val lifetimeWarranty: Boolean,
val manufacturer: String?,
val modelNumber: String?,
val notes: String?,
val parentId: String?,
val purchaseFrom: String?,
val purchaseTime: String?,
val serialNumber: String?,
val soldNotes: String?,
val soldPrice: BigDecimal?,
val soldTime: String?,
val soldTo: String?,
val syncChildItemsLocations: Boolean,
val warrantyDetails: String?,
val warrantyExpires: String?
)
// [END_ENTITY: DatabaseTable('ItemEntity')]

View File

@@ -4,39 +4,236 @@
package com.homebox.lens.data.db.entity
// [IMPORTS]
import com.homebox.lens.data.api.dto.CustomFieldDto
import com.homebox.lens.data.api.dto.ItemCreateDto
import com.homebox.lens.data.api.dto.ItemOutDto
import com.homebox.lens.data.api.dto.ItemUpdateDto
import com.homebox.lens.domain.model.CustomField
import com.homebox.lens.domain.model.Image
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.model.ItemUpdate
import com.homebox.lens.domain.model.Label
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.Location
import com.homebox.lens.domain.model.LocationOut
import java.math.BigDecimal
// [END_IMPORTS]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
// [ENTITY: Function('ItemWithLabels.toDomainItemSummary')]
// [RELATION: Function('ItemWithLabels.toDomainItemSummary')] -> [RETURNS] -> [DataClass('ItemSummary')]
/**
* @summary Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
*/
fun ItemWithLabels.toDomain(): ItemSummary {
fun ItemWithLabels.toDomainItemSummary(): ItemSummary {
return ItemSummary(
id = this.item.id,
name = this.item.name,
image = this.item.image?.let { Image(id = "", path = it, isPrimary = true) },
location = this.item.locationId?.let { LocationOut(id = it, name = "", color = "", isArchived = false, createdAt = "", updatedAt = "") },
labels = this.labels.map { it.toDomain() },
assetId = null,
isArchived = false,
value = this.item.value?.toDouble() ?: 0.0,
labels = this.labels.map { it.toDomainLabelOut() },
assetId = this.item.assetId,
isArchived = this.item.archived,
value = this.item.purchasePrice?.toDouble() ?: 0.0, // Assuming value maps to purchasePrice
createdAt = this.item.createdAt ?: "",
updatedAt = ""
updatedAt = "" // ItemEntity does not have updatedAt
)
}
// [END_ENTITY: Function('toDomain')]
// [END_ENTITY: Function('ItemWithLabels.toDomainItemSummary')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
// [ENTITY: Function('ItemEntity.toDomainItem')]
// [RELATION: Function('ItemEntity.toDomainItem')] -> [RETURNS] -> [DataClass('Item')]
/**
* @summary Преобразует [ItemEntity] (сущность БД) в [Item] (доменную модель).
*/
fun ItemEntity.toDomainItem(): Item {
return Item(
id = this.id,
name = this.name,
description = this.description,
quantity = this.quantity,
image = this.image,
location = this.locationId?.let { Location(it, "") }, // Simplified, name is not in ItemEntity
labels = emptyList(), // Labels are handled via ItemWithLabels
purchasePrice = this.purchasePrice,
createdAt = this.createdAt,
archived = this.archived,
assetId = this.assetId,
fields = emptyList(), // Custom fields are not stored in ItemEntity
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires
)
}
// [END_ENTITY: Function('ItemEntity.toDomainItem')]
// [ENTITY: Function('Item.toItemEntity')]
// [RELATION: Function('Item.toItemEntity')] -> [RETURNS] -> [DataClass('ItemEntity')]
/**
* @summary Преобразует [Item] (доменную модель) в [ItemEntity] (сущность БД).
*/
fun Item.toItemEntity(): ItemEntity {
return ItemEntity(
id = this.id,
name = this.name,
description = this.description,
quantity = this.quantity,
image = this.image,
locationId = this.location?.id,
purchasePrice = this.purchasePrice,
createdAt = this.createdAt,
archived = this.archived,
assetId = this.assetId,
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires
)
}
// [END_ENTITY: Function('Item.toItemEntity')]
// [ENTITY: Function('ItemOutDto.toDomainItem')]
// [RELATION: Function('ItemOutDto.toDomainItem')] -> [RETURNS] -> [DataClass('Item')]
/**
* @summary Маппер из ItemOutDto в доменную модель Item.
*/
fun ItemOutDto.toDomainItem(): Item {
return Item(
id = this.id,
name = this.name,
description = this.description,
quantity = this.quantity,
image = this.images.firstOrNull()?.path,
location = this.location?.toDomain(),
labels = this.labels.map { Label(it.id, it.name) },
purchasePrice = this.purchasePrice?.toBigDecimal(),
createdAt = this.createdAt,
archived = this.isArchived,
assetId = this.assetId,
fields = this.fields.map { it.toDomainCustomField() },
insured = this.insured ?: false,
lifetimeWarranty = this.lifetimeWarranty ?: false,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parent?.id,
purchaseFrom = this.purchaseFrom,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice?.toBigDecimal(),
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations ?: false,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires
)
}
// [END_ENTITY: Function('ItemOutDto.toDomainItem')]
// [ENTITY: Function('ItemCreate.toItemCreateDto')]
// [RELATION: Function('ItemCreate.toItemCreateDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')]
/**
* @summary Маппер из доменной модели ItemCreate в ItemCreateDto.
*/
fun ItemCreate.toItemCreateDto(): ItemCreateDto {
return ItemCreateDto(
name = this.name,
description = this.description,
quantity = this.quantity,
archived = null, // Not applicable for creation
assetId = this.assetId,
insured = null, // Not applicable for creation
lifetimeWarranty = null, // Not applicable for creation
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchasePrice = this.purchasePrice?.toDouble(),
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = null, // Not applicable for creation
soldPrice = null, // Not applicable for creation
soldTime = null, // Not applicable for creation
soldTo = null, // Not applicable for creation
syncChildItemsLocations = null, // Not applicable for creation
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires,
locationId = this.locationId,
labelIds = this.labelIds
)
}
// [END_ENTITY: Function('ItemCreate.toItemCreateDto')]
// [ENTITY: Function('ItemUpdate.toItemUpdateDto')]
// [RELATION: Function('ItemUpdate.toItemUpdateDto')] -> [RETURNS] -> [DataClass('ItemUpdateDto')]
/**
* @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto.
*/
fun ItemUpdate.toItemUpdateDto(): ItemUpdateDto {
return ItemUpdateDto(
name = this.name,
description = this.description,
quantity = this.quantity,
archived = this.archived,
assetId = this.assetId,
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchasePrice = this.purchasePrice?.toDouble(),
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice?.toDouble(),
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires,
locationId = this.locationId,
labelIds = this.labelIds
)
}
// [END_ENTITY: Function('ItemUpdate.toItemUpdateDto')]
// [ENTITY: Function('LabelEntity.toDomainLabelOut')]
// [RELATION: Function('LabelEntity.toDomainLabelOut')] -> [RETURNS] -> [DataClass('LabelOut')]
/**
* @summary Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
*/
fun LabelEntity.toDomain(): LabelOut {
fun LabelEntity.toDomainLabelOut(): LabelOut {
return LabelOut(
id = this.id,
name = this.name,
@@ -46,4 +243,17 @@ fun LabelEntity.toDomain(): LabelOut {
updatedAt = ""
)
}
// [END_ENTITY: Function('toDomain')]
// [END_ENTITY: Function('LabelEntity.toDomainLabelOut')]
// [ENTITY: Function('CustomFieldDto.toDomainCustomField')]
// [RELATION: Function('CustomFieldDto.toDomainCustomField')] -> [RETURNS] -> [DataClass('CustomField')]
/**
* @summary Преобразует [CustomFieldDto] (DTO API) в [CustomField] (доменную модель).
*/
fun CustomFieldDto.toDomainCustomField(): CustomField {
return CustomField(
name = this.name,
value = this.value
)
}
// [END_ENTITY: Function('CustomFieldDto.toDomainCustomField')]

View File

@@ -5,6 +5,8 @@ package com.homebox.lens.domain.model
// [IMPORTS]
import java.math.BigDecimal
import com.homebox.lens.domain.model.CustomField
import com.homebox.lens.domain.model.Image
// [END_IMPORTS]
// [ENTITY: DataClass('Item')]
@@ -18,8 +20,27 @@ import java.math.BigDecimal
* @param image Url изображения.
* @param location Местоположение вещи.
* @param labels Список меток, присвоенных вещи.
* @param value Стоимость вещи.
* @param purchasePrice Цена покупки вещи.
* @param createdAt Дата создания.
* @param archived Архивирована ли вещь.
* @param assetId Идентификатор актива.
* @param fields Пользовательские поля.
* @param insured Застрахована ли вещь.
* @param lifetimeWarranty Пожизненная гарантия.
* @param manufacturer Производитель.
* @param modelNumber Номер модели.
* @param notes Дополнительные заметки.
* @param parentId ID родительского элемента.
* @param purchaseFrom Место покупки.
* @param purchaseTime Время покупки.
* @param serialNumber Серийный номер.
* @param soldNotes Заметки о продаже.
* @param soldPrice Цена продажи.
* @param soldTime Время продажи.
* @param soldTo Кому продано.
* @param syncChildItemsLocations Синхронизировать местоположения дочерних элементов.
* @param warrantyDetails Детали гарантии.
* @param warrantyExpires Дата окончания гарантии.
*/
data class Item(
val id: String,
@@ -29,8 +50,27 @@ data class Item(
val image: String?,
val location: Location?,
val labels: List<Label>,
val value: BigDecimal?,
val createdAt: String?
val purchasePrice: BigDecimal?,
val createdAt: String?,
val archived: Boolean = false,
val assetId: String? = null,
val fields: List<CustomField> = emptyList(),
val insured: Boolean = false,
val lifetimeWarranty: Boolean = false,
val manufacturer: String? = null,
val modelNumber: String? = null,
val notes: String? = null,
val parentId: String? = null,
val purchaseFrom: String? = null,
val purchaseTime: String? = null,
val serialNumber: String? = null,
val soldNotes: String? = null,
val soldPrice: BigDecimal? = null,
val soldTime: String? = null,
val soldTo: String? = null,
val syncChildItemsLocations: Boolean = false,
val warrantyDetails: String? = null,
val warrantyExpires: String? = null
)
// [END_ENTITY: DataClass('Item')]

View File

@@ -18,7 +18,6 @@ distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
org.gradle.java.home=/snap/android-studio/197/jbr
android.useAndroidX=true

View File

@@ -0,0 +1,98 @@
<![CDATA[
<WORK_ORDER>
<META>
<VERSION>1.0</VERSION>
<CREATED_AT>{timestamp}</CREATED_AT>
<TITLE>[ARCHITECT -> DEV] Refactor Item Edit Screen</TITLE>
<DESCRIPTION>
This work order instructs the developer agent to refactor the Item Edit screen to include all available API fields and implement a user-friendly, grouped layout.
</DESCRIPTION>
<ASSIGNED_TO>agent-developer</ASSIGNED_TO>
<LABELS>type::refactoring,feature::item-edit,status::pending</LABELS>
</META>
<SPECIFICATION>
<GOAL>
The primary goal is to refactor the `ItemEditScreen` to include all available fields from the Homebox API for creating and updating items, and to present them in a user-friendly manner.
</GOAL>
<CONTEXT>
<FILE_LIST>
<FILE path="domain/src/main/java/com/homebox/lens/domain/model/Item.kt" />
<FILE path="data/src/main/java/com/homebox/lens/data/db/entity/ItemEntity.kt" />
<FILE path="data/src/main/java/com/homebox/lens/data/api/dto/ItemCreateDto.kt" />
<FILE path="data/src/main/java/com/homebox/lens/data/api/dto/ItemUpdateDto.kt" />
<FILE path="data/src/main/java/com/homebox/lens/data/api/dto/ItemOutDto.kt" />
<FILE path="data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt" />
<FILE path="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt" />
<FILE path="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" />
</FILE_LIST>
</CONTEXT>
</SPECIFICATION>
<EXECUTION_PLAN>
<STEP id="1" name="Update Domain Model">
<ACTION>Modify `domain/src/main/java/com/homebox/lens/domain/model/Item.kt`.</ACTION>
<DETAILS>
- Add the following fields to the `Item` data class:
- `archived: Boolean`
- `assetId: String?`
- `fields: List<CustomField>`
- `insured: Boolean`
- `lifetimeWarranty: Boolean`
- `manufacturer: String?`
- `modelNumber: String?`
- `notes: String?`
- `parentId: String?`
- `purchaseFrom: String?`
- `purchaseTime: String?`
- `serialNumber: String?`
- `soldNotes: String?`
- `soldPrice: Double?`
- `soldTime: String?`
- `soldTo: String?`
- `syncChildItemsLocations: Boolean`
- `warrantyDetails: String?`
- `warrantyExpires: String?`
- Rename the existing `value: BigDecimal?` field to `purchasePrice: Double?`.
</DETAILS>
</STEP>
<STEP id="2" name="Update Data Layer">
<ACTION>Update database entity, DTOs, and mappers.</ACTION>
<DETAILS>
- Modify `data/src/main/java/com/homebox/lens/data/db/entity/ItemEntity.kt` to reflect the new `Item` model.
- Ensure `ItemCreateDto`, `ItemUpdateDto`, and `ItemOutDto` contain all necessary fields according to the API spec.
- Update the mapping functions in `data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt` to handle all new fields correctly across all layers.
</DETAILS>
</STEP>
<STEP id="3" name="Update ViewModel">
<ACTION>Modify `app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt`.</ACTION>
<DETAILS>
- Update `loadItem` and `saveItem` to handle the state for all new fields.
- Add public functions to update the state for each new field (e.g., `updateManufacturer(String)`, `toggleInsured(Boolean)`).
</DETAILS>
</STEP>
<STEP id="4" name="Implement UI">
<ACTION>Refactor `app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt`.</ACTION>
<DETAILS>
- Implement a user-friendly layout using tabs or expandable cards for the following groups: General, Purchase, Warranty, Identification, Status & Notes.
- Use appropriate controls for each field type (e.g., `Switch` for booleans, Date Picker for dates).
- Connect all UI controls to the ViewModel.
- Implement basic input validation.
</DETAILS>
</STEP>
</EXECUTION_PLAN>
<VERIFICATION>
<CHECK>The application compiles successfully.</CHECK>
<CHECK>The Item Edit screen displays all new fields, grouped logically.</CHECK>
<CHECK>Creating a new item with all fields populated works correctly.</CHECK>
<CHECK>Editing an existing item and modifying the new fields works correctly.</CHECK>
<CHECK>Data is persisted correctly and is visible in the Item Details screen after saving.</CHECK>
</VERIFICATION>
</WORK_ORDER>
]]>