Compare commits
3 Commits
aa69776807
...
refactor/i
| Author | SHA1 | Date | |
|---|---|---|---|
| eccc7ee970 | |||
| 8816377361 | |||
| 5eb23eed5b |
@@ -5,8 +5,8 @@
|
|||||||
</META>
|
</META>
|
||||||
<INCLUDES>
|
<INCLUDES>
|
||||||
<INCLUDE from="../knowledge_base/semantic_linting.xml"/>
|
<INCLUDE from="../knowledge_base/semantic_linting.xml"/>
|
||||||
<INCLUDE from="../knowledge_base/graphrag_optimization.md"/>
|
<INCLUDE from="../knowledge_base/graphrag_optimization.xml"/>
|
||||||
<INCLUDE from="../knowledge_base/design_by_contract.md"/>
|
<INCLUDE from="../knowledge_base/design_by_contract.xml"/>
|
||||||
<INCLUDE from="../knowledge_base/ai_friendly_logging.md"/>
|
<INCLUDE from="../knowledge_base/ai_friendly_logging.xml"/>
|
||||||
</INCLUDES>
|
</INCLUDES>
|
||||||
</SEMANTIC_ENRICHMENT_PROTOCOL>
|
</SEMANTIC_ENRICHMENT_PROTOCOL>
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ android {
|
|||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lint {
|
||||||
|
checkReleaseBuilds = false
|
||||||
|
abortOnError = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -25,11 +25,14 @@ import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
|
|||||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||||
|
import com.homebox.lens.ui.screen.settings.SettingsScreen
|
||||||
|
import com.homebox.lens.ui.screen.splash.SplashScreen
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTITY: Function('NavGraph')]
|
// [ENTITY: Function('NavGraph')]
|
||||||
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
||||||
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
|
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
|
||||||
|
// [RELATION: Function('NavGraph')] -> [USES] -> [Screen('SplashScreen')]
|
||||||
/**
|
/**
|
||||||
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
||||||
* @param navController Контроллер навигации.
|
* @param navController Контроллер навигации.
|
||||||
@@ -47,11 +50,13 @@ fun NavGraph(
|
|||||||
val navigationActions = remember(navController) {
|
val navigationActions = remember(navController) {
|
||||||
NavigationActions(navController)
|
NavigationActions(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Screen.Setup.route
|
startDestination = Screen.Splash.route
|
||||||
) {
|
) {
|
||||||
|
composable(route = Screen.Splash.route) {
|
||||||
|
SplashScreen(navController = navController)
|
||||||
|
}
|
||||||
composable(route = Screen.Setup.route) {
|
composable(route = Screen.Setup.route) {
|
||||||
SetupScreen(onSetupComplete = {
|
SetupScreen(onSetupComplete = {
|
||||||
navController.navigate(Screen.Dashboard.route) {
|
navController.navigate(Screen.Dashboard.route) {
|
||||||
@@ -137,6 +142,12 @@ fun NavGraph(
|
|||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(route = Screen.Settings.route) {
|
||||||
|
SettingsScreen(
|
||||||
|
currentRoute = currentRoute,
|
||||||
|
navigationActions = navigationActions
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('NavGraph')]
|
// [END_ENTITY: Function('NavGraph')]
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ package com.homebox.lens.navigation
|
|||||||
* @param route Строковый идентификатор маршрута.
|
* @param route Строковый идентификатор маршрута.
|
||||||
*/
|
*/
|
||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
|
// [ENTITY: Object('Splash')]
|
||||||
|
data object Splash : Screen("splash_screen")
|
||||||
|
// [END_ENTITY: Object('Splash')]
|
||||||
|
|
||||||
// [ENTITY: Object('Setup')]
|
// [ENTITY: Object('Setup')]
|
||||||
data object Setup : Screen("setup_screen")
|
data object Setup : Screen("setup_screen")
|
||||||
// [END_ENTITY: Object('Setup')]
|
// [END_ENTITY: Object('Setup')]
|
||||||
@@ -118,6 +122,10 @@ sealed class Screen(val route: String) {
|
|||||||
// [ENTITY: Object('Search')]
|
// [ENTITY: Object('Search')]
|
||||||
data object Search : Screen("search_screen")
|
data object Search : Screen("search_screen")
|
||||||
// [END_ENTITY: Object('Search')]
|
// [END_ENTITY: Object('Search')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('Settings')]
|
||||||
|
data object Settings : Screen("settings_screen")
|
||||||
|
// [END_ENTITY: Object('Settings')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: SealedClass('Screen')]
|
// [END_ENTITY: SealedClass('Screen')]
|
||||||
// [END_FILE_Screen.kt]
|
// [END_FILE_Screen.kt]
|
||||||
|
|||||||
63
app/src/main/java/com/homebox/lens/ui/mapper/ItemMapper.kt
Normal file
63
app/src/main/java/com/homebox/lens/ui/mapper/ItemMapper.kt
Normal 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]
|
||||||
@@ -310,10 +310,10 @@ fun DashboardContentSuccessPreview() {
|
|||||||
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
|
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
|
||||||
),
|
),
|
||||||
labels = listOf(
|
labels = listOf(
|
||||||
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
|
LabelOut(id="1", name="electronics", description = null, color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
|
||||||
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
|
LabelOut(id="2", name="important", description = null, color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
|
||||||
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
|
LabelOut(id="3", name="seasonal", description = null, color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
|
||||||
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
|
LabelOut(id="4", name="hobby", description = null, color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
|
||||||
),
|
),
|
||||||
recentlyAddedItems = emptyList()
|
recentlyAddedItems = emptyList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,28 +5,48 @@
|
|||||||
package com.homebox.lens.ui.screen.itemedit
|
package com.homebox.lens.ui.screen.itemedit
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
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.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.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.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
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.navigation.NavigationActions
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
import com.homebox.lens.ui.common.MainScaffold
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTITY: Function('ItemEditScreen')]
|
// [ENTITY: Composable('ItemEditScreen')]
|
||||||
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
// [RELATION: Composable('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||||
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')]
|
// [RELATION: Composable('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')]
|
||||||
// [RELATION: Function('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')]
|
// [RELATION: Composable('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')]
|
||||||
// [RELATION: Function('ItemEditScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
// [RELATION: Composable('ItemEditScreen')] -> [CALLS] -> [Composable('MainScaffold')]
|
||||||
/**
|
/**
|
||||||
* @summary Composable-функция для экрана "Редактирование элемента".
|
* @summary Composable-функция для экрана "Редактирование элемента".
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||||
@@ -51,6 +74,7 @@ import timber.log.Timber
|
|||||||
* @param viewModel ViewModel для управления состоянием экрана.
|
* @param viewModel ViewModel для управления состоянием экрана.
|
||||||
* @param onSaveSuccess Callback, вызываемый после успешного сохранения товара.
|
* @param onSaveSuccess Callback, вызываемый после успешного сохранения товара.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemEditScreen(
|
fun ItemEditScreen(
|
||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
@@ -75,7 +99,7 @@ fun ItemEditScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.saveCompleted.collect {
|
viewModel.saveCompleted.collect {
|
||||||
Timber.i("[INFO][ACTION][save_completed_callback] Item save completed. Triggering onSaveSuccess.")
|
Timber.i("[INFO][ACTION][save_completed_callback] Item save completed. Triggering onSaveSuccess.")
|
||||||
onSaveSuccess()
|
onSaveSuccess()
|
||||||
}
|
}
|
||||||
@@ -85,7 +109,7 @@ fun ItemEditScreen(
|
|||||||
topBarTitle = stringResource(id = R.string.item_edit_title),
|
topBarTitle = stringResource(id = R.string.item_edit_title),
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
) {
|
) { paddingValues ->
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -100,40 +124,389 @@ fun ItemEditScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(it)
|
.padding(paddingValues)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
|
CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
} else {
|
} else {
|
||||||
uiState.item?.let { item ->
|
uiState.item?.let { item ->
|
||||||
OutlinedTextField(
|
// [AI_NOTE]: General Information section for basic item details.
|
||||||
value = item.name,
|
Card(
|
||||||
onValueChange = { viewModel.updateName(it) },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
label = { Text(stringResource(R.string.item_name)) },
|
elevation = CardDefaults.cardElevation(4.dp)
|
||||||
modifier = Modifier.fillMaxWidth()
|
) {
|
||||||
)
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Text(
|
||||||
OutlinedTextField(
|
text = stringResource(R.string.item_edit_general_information),
|
||||||
value = item.description ?: "",
|
style = MaterialTheme.typography.headlineSmall
|
||||||
onValueChange = { viewModel.updateDescription(it) },
|
)
|
||||||
label = { Text(stringResource(R.string.item_description)) },
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
modifier = Modifier.fillMaxWidth()
|
OutlinedTextField(
|
||||||
)
|
value = item.name,
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
onValueChange = { viewModel.updateName(it) },
|
||||||
OutlinedTextField(
|
label = { Text(stringResource(R.string.item_name)) },
|
||||||
value = item.quantity.toString(),
|
modifier = Modifier.fillMaxWidth()
|
||||||
onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) },
|
)
|
||||||
label = { Text(stringResource(R.string.item_quantity)) },
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth()
|
value = item.description ?: "",
|
||||||
)
|
onValueChange = { viewModel.updateDescription(it) },
|
||||||
// Add more fields as needed
|
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_edit_location)) },
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { /* TODO: Implement location selection */ }) {
|
||||||
|
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.item_edit_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_edit_labels)) },
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { /* TODO: Implement label selection */ }) {
|
||||||
|
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.item_edit_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_edit_purchase_information),
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = item.purchasePrice?.toString() ?: "",
|
||||||
|
onValueChange = { viewModel.updatePurchasePrice(it.toDoubleOrNull()) },
|
||||||
|
label = { Text(stringResource(R.string.item_edit_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_edit_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_edit_purchase_time)) },
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { showPurchaseDatePicker = true }) {
|
||||||
|
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { showPurchaseDatePicker = true }
|
||||||
|
)
|
||||||
|
if (showPurchaseDatePicker) {
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showPurchaseDatePicker = false },
|
||||||
|
confirmButton = {
|
||||||
|
Text(stringResource(R.string.dialog_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.dialog_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_edit_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_edit_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_edit_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_edit_warranty_expires)) },
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { showWarrantyDatePicker = true }) {
|
||||||
|
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { showWarrantyDatePicker = true }
|
||||||
|
)
|
||||||
|
if (showWarrantyDatePicker) {
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showWarrantyDatePicker = false },
|
||||||
|
confirmButton = {
|
||||||
|
Text(stringResource(R.string.dialog_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.dialog_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_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_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_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_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_edit_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_edit_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_edit_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_edit_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_edit_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_edit_sold_information),
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = item.soldPrice?.toString() ?: "",
|
||||||
|
onValueChange = { viewModel.updateSoldPrice(it.toDoubleOrNull()) },
|
||||||
|
label = { Text(stringResource(R.string.item_edit_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_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_edit_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_edit_sold_time)) },
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { showSoldDatePicker = true }) {
|
||||||
|
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { showSoldDatePicker = true }
|
||||||
|
)
|
||||||
|
if (showSoldDatePicker) {
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showSoldDatePicker = false },
|
||||||
|
confirmButton = {
|
||||||
|
Text(stringResource(R.string.dialog_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.dialog_cancel), modifier = Modifier.clickable { showSoldDatePicker = false })
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
DatePicker(state = soldDateState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('ItemEditScreen')]
|
// [END_ENTITY: Composable('ItemEditScreen')]
|
||||||
// [END_FILE_ItemEditScreen.kt]
|
// [END_FILE_ItemEditScreen.kt]
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.homebox.lens.domain.model.Item
|
import com.homebox.lens.domain.model.Item
|
||||||
import com.homebox.lens.domain.model.ItemCreate
|
import com.homebox.lens.domain.model.ItemCreate
|
||||||
import com.homebox.lens.domain.model.Label
|
import com.homebox.lens.domain.model.ItemUpdate
|
||||||
import com.homebox.lens.domain.model.Location
|
import com.homebox.lens.domain.model.Location
|
||||||
|
import com.homebox.lens.domain.model.Label
|
||||||
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
||||||
|
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||||
|
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||||
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
||||||
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
||||||
|
import com.homebox.lens.ui.mapper.ItemMapper
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -32,11 +36,15 @@ import javax.inject.Inject
|
|||||||
* @param item The item being edited, or null if creating a new item.
|
* @param item The item being edited, or null if creating a new item.
|
||||||
* @param isLoading Whether data is currently being loaded or saved.
|
* @param isLoading Whether data is currently being loaded or saved.
|
||||||
* @param error An error message if an operation failed.
|
* @param error An error message if an operation failed.
|
||||||
|
* @param allLocations A list of all available locations.
|
||||||
|
* @param allLabels A list of all available labels.
|
||||||
*/
|
*/
|
||||||
data class ItemEditUiState(
|
data class ItemEditUiState(
|
||||||
val item: Item? = null,
|
val item: Item? = null,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val error: String? = null
|
val error: String? = null,
|
||||||
|
val allLocations: List<Location> = emptyList(),
|
||||||
|
val allLabels: List<Label> = emptyList()
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('ItemEditUiState')]
|
// [END_ENTITY: DataClass('ItemEditUiState')]
|
||||||
|
|
||||||
@@ -44,15 +52,23 @@ data class ItemEditUiState(
|
|||||||
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')]
|
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')]
|
||||||
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')]
|
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')]
|
||||||
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')]
|
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')]
|
||||||
|
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [Class('ItemMapper')]
|
||||||
// [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')]
|
// [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')]
|
||||||
/**
|
/**
|
||||||
* @summary ViewModel for the item edit screen.
|
* @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
|
@HiltViewModel
|
||||||
class ItemEditViewModel @Inject constructor(
|
class ItemEditViewModel @Inject constructor(
|
||||||
private val createItemUseCase: CreateItemUseCase,
|
private val createItemUseCase: CreateItemUseCase,
|
||||||
private val updateItemUseCase: UpdateItemUseCase,
|
private val updateItemUseCase: UpdateItemUseCase,
|
||||||
private val getItemDetailsUseCase: GetItemDetailsUseCase
|
private val getItemDetailsUseCase: GetItemDetailsUseCase,
|
||||||
|
private val getAllLocationsUseCase: GetAllLocationsUseCase,
|
||||||
|
private val getAllLabelsUseCase: GetAllLabelsUseCase,
|
||||||
|
private val itemMapper: ItemMapper
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ItemEditUiState())
|
private val _uiState = MutableStateFlow(ItemEditUiState())
|
||||||
@@ -73,34 +89,93 @@ class ItemEditViewModel @Inject constructor(
|
|||||||
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
||||||
if (itemId == null) {
|
if (itemId == null) {
|
||||||
Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.")
|
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 {
|
} else {
|
||||||
try {
|
try {
|
||||||
Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId)
|
Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId)
|
||||||
val itemOut = getItemDetailsUseCase(itemId)
|
val itemOut = getItemDetailsUseCase(itemId)
|
||||||
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
|
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
|
||||||
val item = Item(
|
val item = itemMapper.toItem(itemOut)
|
||||||
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 = item)
|
||||||
Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched item details for ID: %s", itemId)
|
Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched and mapped item details for ID: %s", itemId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId)
|
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)
|
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load all locations and labels
|
||||||
|
try {
|
||||||
|
Timber.i("[INFO][ACTION][fetching_all_locations] Fetching all locations.")
|
||||||
|
val allLocations = getAllLocationsUseCase().map { Location(it.id, it.name) }
|
||||||
|
Timber.i("[INFO][ACTION][fetching_all_labels] Fetching all labels.")
|
||||||
|
val allLabels = getAllLabelsUseCase().map { Label(it.id, it.name) }
|
||||||
|
_uiState.value = _uiState.value.copy(allLocations = allLocations, allLabels = allLabels)
|
||||||
|
Timber.i("[INFO][ACTION][all_locations_labels_fetched] Successfully fetched all locations and labels.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "[ERROR][FALLBACK][locations_labels_load_failed] Failed to load locations or labels.")
|
||||||
|
_uiState.value = _uiState.value.copy(error = e.localizedMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('loadItem')]
|
// [END_ENTITY: Function('loadItem')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('updateLocation')]
|
||||||
|
/**
|
||||||
|
* @summary Updates the location of the item in the UI state.
|
||||||
|
* @param location The new location for the item.
|
||||||
|
* @sideeffect Updates the `item` in `_uiState`.
|
||||||
|
*/
|
||||||
|
fun updateLocation(location: Location) {
|
||||||
|
Timber.d("[DEBUG][ACTION][updating_item_location] Updating item location to: %s", location.name)
|
||||||
|
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(location = location))
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('updateLocation')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('updateLabels')]
|
||||||
|
/**
|
||||||
|
* @summary Updates the labels of the item in the UI state.
|
||||||
|
* @param labels The new list of labels for the item.
|
||||||
|
* @sideeffect Updates the `item` in `_uiState`.
|
||||||
|
*/
|
||||||
|
fun updateLabels(labels: List<Label>) {
|
||||||
|
Timber.d("[DEBUG][ACTION][updating_item_labels] Updating item labels to: %s", labels.map { it.name }.joinToString())
|
||||||
|
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = labels))
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('updateLabels')]
|
||||||
|
|
||||||
// [ENTITY: Function('saveItem')]
|
// [ENTITY: Function('saveItem')]
|
||||||
/**
|
/**
|
||||||
* @summary Saves the current item, either creating a new one or updating an existing one.
|
* @summary Saves the current item, either creating a new one or updating an existing one.
|
||||||
@@ -117,53 +192,48 @@ class ItemEditViewModel @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
if (currentItem.id.isBlank()) {
|
if (currentItem.id.isBlank()) {
|
||||||
Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name)
|
Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name)
|
||||||
val createdItemSummary = createItemUseCase(ItemCreate(
|
val createdItemSummary = createItemUseCase(
|
||||||
name = currentItem.name,
|
ItemCreate(
|
||||||
description = currentItem.description,
|
name = currentItem.name,
|
||||||
quantity = currentItem.quantity,
|
description = currentItem.description,
|
||||||
assetId = null, // Item does not have assetId
|
quantity = currentItem.quantity,
|
||||||
notes = null, // Item does not have notes
|
archived = currentItem.archived,
|
||||||
serialNumber = null, // Item does not have serialNumber
|
assetId = currentItem.assetId,
|
||||||
value = currentItem.value?.toDouble(), // Convert BigDecimal to Double
|
insured = currentItem.insured,
|
||||||
purchasePrice = null, // Item does not have purchasePrice
|
lifetimeWarranty = currentItem.lifetimeWarranty,
|
||||||
purchaseDate = null, // Item does not have purchaseDate
|
manufacturer = currentItem.manufacturer,
|
||||||
warrantyUntil = null, // Item does not have warrantyUntil
|
modelNumber = currentItem.modelNumber,
|
||||||
locationId = currentItem.location?.id,
|
notes = currentItem.notes,
|
||||||
parentId = null, // Item does not have parentId
|
parentId = currentItem.parentId,
|
||||||
labelIds = currentItem.labels.map { it.id }
|
purchaseFrom = currentItem.purchaseFrom,
|
||||||
))
|
purchasePrice = currentItem.purchasePrice,
|
||||||
Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.")
|
purchaseTime = currentItem.purchaseTime,
|
||||||
val createdItem = Item(
|
serialNumber = currentItem.serialNumber,
|
||||||
id = createdItemSummary.id,
|
soldNotes = currentItem.soldNotes,
|
||||||
name = createdItemSummary.name,
|
soldPrice = currentItem.soldPrice,
|
||||||
description = null, // ItemSummary does not have description
|
soldTime = currentItem.soldTime,
|
||||||
quantity = 0, // ItemSummary does not have quantity
|
soldTo = currentItem.soldTo,
|
||||||
image = null, // ItemSummary does not have image
|
syncChildItemsLocations = currentItem.syncChildItemsLocations,
|
||||||
location = null, // ItemSummary does not have location
|
warrantyDetails = currentItem.warrantyDetails,
|
||||||
labels = emptyList(), // ItemSummary does not have labels
|
warrantyExpires = currentItem.warrantyExpires,
|
||||||
value = null, // ItemSummary does not have value
|
locationId = currentItem.location?.id,
|
||||||
createdAt = null // ItemSummary does not have createdAt
|
labelIds = currentItem.labels.map { it.id }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, item = createdItem)
|
Timber.i("[INFO][ACTION][fetching_full_item_after_creation] Fetching full item details after creation for ID: %s", createdItemSummary.id)
|
||||||
Timber.i("[INFO][ACTION][new_item_created] Successfully created new item with ID: %s", createdItem.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)
|
_saveCompleted.emit(Unit)
|
||||||
} else {
|
} else {
|
||||||
Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id)
|
Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id)
|
||||||
val updatedItemOut = updateItemUseCase(currentItem)
|
val updatedItemOut = updateItemUseCase(currentItem)
|
||||||
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
|
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping updated ItemOut to Item for UI state.")
|
||||||
val updatedItem = Item(
|
val item = itemMapper.toItem(updatedItemOut)
|
||||||
id = updatedItemOut.id,
|
_uiState.value = _uiState.value.copy(isLoading = false, item = item)
|
||||||
name = updatedItemOut.name,
|
Timber.i("[INFO][ACTION][item_updated] Successfully updated and mapped item with ID: %s", updatedItemOut.id)
|
||||||
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
|
|
||||||
)
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, item = updatedItem)
|
|
||||||
Timber.i("[INFO][ACTION][item_updated] Successfully updated item with ID: %s", updatedItem.id)
|
|
||||||
_saveCompleted.emit(Unit)
|
_saveCompleted.emit(Unit)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -209,6 +279,234 @@ class ItemEditViewModel @Inject constructor(
|
|||||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity))
|
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity))
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('updateQuantity')]
|
// [END_ENTITY: Function('updateQuantity')]
|
||||||
}
|
|
||||||
// [END_ENTITY: ViewModel('ItemEditViewModel')]
|
// [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: 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))
|
||||||
|
}
|
||||||
|
// [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: 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))
|
||||||
|
}
|
||||||
|
// [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]
|
// [END_FILE_ItemEditViewModel.kt]
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ fun LabelEditScreen(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.description.orEmpty(),
|
||||||
|
onValueChange = viewModel::onDescriptionChange,
|
||||||
|
label = { Text(stringResource(R.string.label_description)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ColorPicker(
|
ColorPicker(
|
||||||
selectedColor = uiState.color,
|
selectedColor = uiState.color,
|
||||||
onColorSelected = viewModel::onColorChange,
|
onColorSelected = viewModel::onColorChange,
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ class LabelEditViewModel @Inject constructor(
|
|||||||
uiState = uiState.copy(name = newName, nameError = null)
|
uiState = uiState.copy(name = newName, nameError = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onDescriptionChange(newDescription: String) {
|
||||||
|
uiState = uiState.copy(description = newDescription)
|
||||||
|
}
|
||||||
|
|
||||||
fun onColorChange(newColor: String) {
|
fun onColorChange(newColor: String) {
|
||||||
uiState = uiState.copy(color = newColor)
|
uiState = uiState.copy(color = newColor)
|
||||||
}
|
}
|
||||||
@@ -63,35 +67,41 @@ class LabelEditViewModel @Inject constructor(
|
|||||||
|
|
||||||
uiState = uiState.copy(isLoading = true, error = null)
|
uiState = uiState.copy(isLoading = true, error = null)
|
||||||
try {
|
try {
|
||||||
if (labelId == null) {
|
val result = if (labelId == null) {
|
||||||
// Create new label
|
// [LOG_EVENT] [EVENT_TYPE: LabelCreationAttempt] [DATA: { "labelName": "${uiState.name}" }]
|
||||||
val newLabel = LabelCreate(name = uiState.name, color = uiState.color)
|
val newLabel = LabelCreate(name = uiState.name, color = uiState.color, description = uiState.description)
|
||||||
createLabelUseCase(newLabel)
|
createLabelUseCase(newLabel)
|
||||||
} else {
|
} else {
|
||||||
// Update existing label
|
// [LOG_EVENT] [EVENT_TYPE: LabelUpdateAttempt] [DATA: { "labelId": "$labelId", "labelName": "${uiState.name}" }]
|
||||||
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color)
|
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color, description = uiState.description)
|
||||||
updateLabelUseCase(labelId, updatedLabel)
|
updateLabelUseCase(labelId, updatedLabel)
|
||||||
}
|
}
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelSaveSuccess] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
|
||||||
uiState = uiState.copy(isSaved = true)
|
uiState = uiState.copy(isSaved = true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelSaveFailure] [ERROR: "${e.message}"] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
|
||||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
uiState = uiState.copy(error = e.message, isLoading = false)
|
||||||
} finally {
|
} finally {
|
||||||
uiState = uiState.copy(isLoading = false)
|
uiState = uiState.copy(isLoading = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadLabelDetails(id: String) {
|
private fun loadLabelDetails(id: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiState = uiState.copy(isLoading = true, error = null)
|
uiState = uiState.copy(isLoading = true, error = null)
|
||||||
try {
|
try {
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchAttempt] [DATA: { "labelId": "$id" }]
|
||||||
val label = getLabelDetailsUseCase(id)
|
val label = getLabelDetailsUseCase(id)
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchSuccess] [DATA: { "labelId": "$id", "labelName": "${label.name}" }]
|
||||||
uiState = uiState.copy(
|
uiState = uiState.copy(
|
||||||
name = label.name,
|
name = label.name,
|
||||||
color = label.color,
|
color = label.color,
|
||||||
isLoading = false
|
description = label.description,
|
||||||
|
isLoading = false,
|
||||||
|
originalLabel = label
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchFailure] [ERROR: "${e.message}"] [DATA: { "labelId": "$id" }]
|
||||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
uiState = uiState.copy(error = e.message, isLoading = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +114,7 @@ class LabelEditViewModel @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
data class LabelEditUiState(
|
data class LabelEditUiState(
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
|
val description: String? = null,
|
||||||
val color: String = "#FFFFFF", // Default color
|
val color: String = "#FFFFFF", // Default color
|
||||||
val nameError: String? = null,
|
val nameError: String? = null,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
|||||||
@@ -17,24 +17,18 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.Label
|
import androidx.compose.material.icons.automirrored.filled.Label
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -53,9 +47,11 @@ import timber.log.Timber
|
|||||||
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
|
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
|
||||||
/**
|
/**
|
||||||
* @summary Отображает экран со списком всех меток.
|
* @summary Отображает экран со списком всех меток.
|
||||||
* @param navController Контроллер навигации для перемещения между экранами.
|
* @param currentRoute Текущий маршрут навигации.
|
||||||
|
* @param navigationActions Объект, содержащий действия по навигации.
|
||||||
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LabelsListScreen(
|
fun LabelsListScreen(
|
||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
@@ -90,19 +86,19 @@ fun LabelsListScreen(
|
|||||||
.padding(innerPaddingValues), // Use innerPaddingValues here
|
.padding(innerPaddingValues), // Use innerPaddingValues here
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
when (currentState) {
|
when (val state = uiState) {
|
||||||
is LabelsListUiState.Loading -> {
|
is LabelsListUiState.Loading -> {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
is LabelsListUiState.Error -> {
|
is LabelsListUiState.Error -> {
|
||||||
Text(text = currentState.message)
|
Text(text = state.message)
|
||||||
}
|
}
|
||||||
is LabelsListUiState.Success -> {
|
is LabelsListUiState.Success -> {
|
||||||
if (currentState.labels.isEmpty()) {
|
if (state.labels.isEmpty()) {
|
||||||
Text(text = stringResource(id = R.string.no_labels_found))
|
Text(text = stringResource(id = R.string.no_labels_found))
|
||||||
} else {
|
} else {
|
||||||
LabelsList(
|
LabelsList(
|
||||||
labels = currentState.labels,
|
labels = state.labels,
|
||||||
onLabelClick = { label ->
|
onLabelClick = { label ->
|
||||||
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
|
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
|
||||||
navigationActions.navigateToLabelEdit(label.id)
|
navigationActions.navigateToLabelEdit(label.id)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.ui.screen.settings
|
||||||
|
// [FILE] SettingsScreen.kt
|
||||||
|
// [SEMANTICS] ui, screen, settings
|
||||||
|
|
||||||
|
package com.homebox.lens.ui.screen.settings
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.homebox.lens.R
|
||||||
|
import com.homebox.lens.navigation.NavigationActions
|
||||||
|
import com.homebox.lens.ui.common.MainScaffold
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Function('SettingsScreen')]
|
||||||
|
// [RELATION: Function('SettingsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||||
|
/**
|
||||||
|
* @summary Composable-функция для экрана настроек.
|
||||||
|
* @param currentRoute Текущий маршрут навигации.
|
||||||
|
* @param navigationActions Объект, содержащий действия по навигации.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
currentRoute: String?,
|
||||||
|
navigationActions: NavigationActions
|
||||||
|
) {
|
||||||
|
MainScaffold(
|
||||||
|
topBarTitle = stringResource(id = R.string.screen_title_settings),
|
||||||
|
currentRoute = currentRoute,
|
||||||
|
navigationActions = navigationActions
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(text = "Settings Screen (Under Construction)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('SettingsScreen')]
|
||||||
|
// [END_FILE_SettingsScreen.kt]
|
||||||
@@ -7,17 +7,22 @@
|
|||||||
package com.homebox.lens.ui.screen.setup
|
package com.homebox.lens.ui.screen.setup
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.homebox.lens.R
|
import com.homebox.lens.R
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
@@ -83,42 +88,74 @@ private fun SetupScreenContent(
|
|||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
Image(
|
||||||
value = uiState.serverUrl,
|
imageVector = Icons.Default.Lock,
|
||||||
onValueChange = onServerUrlChange,
|
contentDescription = stringResource(id = R.string.app_name),
|
||||||
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
|
modifier = Modifier.size(128.dp)
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.username,
|
|
||||||
onValueChange = onUsernameChange,
|
|
||||||
label = { Text(stringResource(id = R.string.setup_username_label)) },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.password,
|
|
||||||
onValueChange = onPasswordChange,
|
|
||||||
label = { Text(stringResource(id = R.string.setup_password_label)) },
|
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.setup_title),
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
fontSize = 28.sp // Adjust font size as needed
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.serverUrl,
|
||||||
|
onValueChange = onServerUrlChange,
|
||||||
|
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.username,
|
||||||
|
onValueChange = onUsernameChange,
|
||||||
|
label = { Text(stringResource(id = R.string.setup_username_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.password,
|
||||||
|
onValueChange = onPasswordChange,
|
||||||
|
label = { Text(stringResource(id = R.string.setup_password_label)) },
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = onConnectClick,
|
onClick = onConnectClick,
|
||||||
enabled = !uiState.isLoading,
|
enabled = !uiState.isLoading,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp) // Make button more prominent
|
||||||
) {
|
) {
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(stringResource(id = R.string.setup_connect_button))
|
Text(stringResource(id = R.string.setup_connect_button), fontSize = 18.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uiState.error?.let {
|
uiState.error?.let {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,40 +74,61 @@ class SetupViewModel @Inject constructor(
|
|||||||
// [END_ENTITY: Function('onUsernameChange')]
|
// [END_ENTITY: Function('onUsernameChange')]
|
||||||
|
|
||||||
// [ENTITY: Function('onPasswordChange')]
|
// [ENTITY: Function('onPasswordChange')]
|
||||||
|
/**
|
||||||
|
* @summary Updates the password in the UI state.
|
||||||
|
* @param newPassword The new password.
|
||||||
|
* @sideeffect Updates the `password` in `_uiState`.
|
||||||
|
*/
|
||||||
fun onPasswordChange(newPassword: String) {
|
fun onPasswordChange(newPassword: String) {
|
||||||
_uiState.update { it.copy(password = newPassword) }
|
_uiState.update { it.copy(password = newPassword) }
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('onPasswordChange')]
|
// [END_ENTITY: Function('onPasswordChange')]
|
||||||
|
|
||||||
// [ENTITY: Function('connect')]
|
// [ENTITY: Function('areCredentialsSaved')]
|
||||||
fun connect() {
|
/**
|
||||||
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
|
* @summary Checks synchronously if credentials are saved.
|
||||||
viewModelScope.launch {
|
* @return true if credentials are saved, false otherwise.
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
* @sideeffect None.
|
||||||
|
*/
|
||||||
|
fun areCredentialsSaved(): Boolean {
|
||||||
|
Timber.d("[DEBUG][ENTRYPOINT][checking_credentials_saved] Checking if credentials are saved.")
|
||||||
|
return credentialsRepository.areCredentialsSavedSync()
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('areCredentialsSaved')]
|
||||||
|
|
||||||
val credentials = Credentials(
|
// [ENTITY: Function('connect')]
|
||||||
serverUrl = _uiState.value.serverUrl.trim(),
|
/**
|
||||||
username = _uiState.value.username.trim(),
|
* @summary Initiates the connection process, saving credentials and attempting to log in.
|
||||||
password = _uiState.value.password
|
* @sideeffect Updates `_uiState` with loading, error, and completion states.
|
||||||
)
|
*/
|
||||||
|
fun connect() {
|
||||||
|
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
|
val credentials = Credentials(
|
||||||
credentialsRepository.saveCredentials(credentials)
|
serverUrl = _uiState.value.serverUrl.trim(),
|
||||||
|
username = _uiState.value.username.trim(),
|
||||||
|
password = _uiState.value.password
|
||||||
|
)
|
||||||
|
|
||||||
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
|
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
|
||||||
loginUseCase(credentials).fold(
|
credentialsRepository.saveCredentials(credentials)
|
||||||
onSuccess = {
|
|
||||||
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
|
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
|
||||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
loginUseCase(credentials).fold(
|
||||||
},
|
onSuccess = {
|
||||||
onFailure = { exception ->
|
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
|
||||||
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
|
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||||
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
},
|
||||||
}
|
onFailure = { exception ->
|
||||||
)
|
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
|
||||||
}
|
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('connect')]
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('connect')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: ViewModel('SetupViewModel')]
|
// [END_ENTITY: ViewModel('SetupViewModel')]
|
||||||
// [END_FILE_SetupViewModel.kt]
|
// [END_FILE_SetupViewModel.kt]
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.ui.screen.splash
|
||||||
|
// [FILE] SplashScreen.kt
|
||||||
|
// [SEMANTICS] ui, screen, splash, navigation, authentication_flow
|
||||||
|
|
||||||
|
package com.homebox.lens.ui.screen.splash
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.homebox.lens.navigation.Screen
|
||||||
|
import com.homebox.lens.ui.screen.setup.SetupViewModel
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
// [ENTITY: Function('SplashScreen')]
|
||||||
|
/**
|
||||||
|
* @summary Displays a splash screen while checking if credentials are saved.
|
||||||
|
* @param navController The NavController for navigation.
|
||||||
|
* @param viewModel The SetupViewModel to check credential status.
|
||||||
|
* @sideeffect Navigates to either SetupScreen or DashboardScreen based on credential status.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SplashScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: SetupViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
Timber.d("[DEBUG][ENTRYPOINT][splash_screen_composable] SplashScreen entered.")
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = true) {
|
||||||
|
Timber.i("[INFO][ACTION][checking_credentials_on_launch] Checking if credentials are saved on launch.")
|
||||||
|
val credentialsSaved = viewModel.areCredentialsSaved()
|
||||||
|
if (credentialsSaved) {
|
||||||
|
Timber.i("[INFO][ACTION][credentials_found_navigating_dashboard] Credentials found, navigating to Dashboard.")
|
||||||
|
navController.navigate(Screen.Dashboard.route) {
|
||||||
|
popUpTo(Screen.Splash.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.i("[INFO][ACTION][no_credentials_found_navigating_setup] No credentials found, navigating to Setup.")
|
||||||
|
navController.navigate(Screen.Setup.route) {
|
||||||
|
popUpTo(Screen.Splash.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('SplashScreen')]
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
<string name="item_edit_title_create">Создать элемент</string>
|
<string name="item_edit_title_create">Создать элемент</string>
|
||||||
<string name="content_desc_save_item">Сохранить элемент</string>
|
<string name="content_desc_save_item">Сохранить элемент</string>
|
||||||
<string name="label_name">Название</string>
|
<string name="label_name">Название</string>
|
||||||
<string name="label_description">Описание</string>
|
|
||||||
|
|
||||||
<!-- Search Screen -->
|
<!-- Search Screen -->
|
||||||
<string name="placeholder_search_items">Поиск элементов...</string>
|
<string name="placeholder_search_items">Поиск элементов...</string>
|
||||||
@@ -70,6 +69,36 @@
|
|||||||
<string name="item_name">Название</string>
|
<string name="item_name">Название</string>
|
||||||
<string name="item_description">Описание</string>
|
<string name="item_description">Описание</string>
|
||||||
<string name="item_quantity">Количество</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 & 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 -->
|
<!-- Location Edit Screen -->
|
||||||
<string name="location_edit_title_create">Создать локацию</string>
|
<string name="location_edit_title_create">Создать локацию</string>
|
||||||
@@ -90,6 +119,8 @@
|
|||||||
|
|
||||||
<!-- Labels List Screen -->
|
<!-- Labels List Screen -->
|
||||||
<string name="screen_title_labels">Метки</string>
|
<string name="screen_title_labels">Метки</string>
|
||||||
|
<!-- Settings Screen -->
|
||||||
|
<string name="screen_title_settings">Настройки</string>
|
||||||
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
|
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
|
||||||
<string name="content_desc_create_label">Создать новую метку</string>
|
<string name="content_desc_create_label">Создать новую метку</string>
|
||||||
<string name="content_desc_label_icon">Иконка метки</string>
|
<string name="content_desc_label_icon">Иконка метки</string>
|
||||||
@@ -103,6 +134,7 @@
|
|||||||
<string name="label_edit_title_create">Создать метку</string>
|
<string name="label_edit_title_create">Создать метку</string>
|
||||||
<string name="label_edit_title_edit">Редактировать метку</string>
|
<string name="label_edit_title_edit">Редактировать метку</string>
|
||||||
<string name="label_name_edit">Название метки</string>
|
<string name="label_name_edit">Название метки</string>
|
||||||
|
<string name="label_description">Описание</string>
|
||||||
|
|
||||||
<!-- Common Actions -->
|
<!-- Common Actions -->
|
||||||
<string name="back">Назад</string>
|
<string name="back">Назад</string>
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// [PLUGIN] Android Application plugin
|
// [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
|
// [PLUGIN] Kotlin Android plugin
|
||||||
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
||||||
// [PLUGIN] Hilt Android plugin
|
// [PLUGIN] Hilt Android plugin
|
||||||
|
|||||||
@@ -17,17 +17,28 @@ import com.homebox.lens.domain.model.ItemCreate
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemCreateDto(
|
data class ItemCreateDto(
|
||||||
@Json(name = "name") val name: String,
|
@Json(name = "name") val name: String,
|
||||||
@Json(name = "assetId") val assetId: String?,
|
|
||||||
@Json(name = "description") val description: 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 = "quantity") val quantity: Int?,
|
||||||
@Json(name = "value") val value: Double?,
|
@Json(name = "archived") val archived: Boolean?,
|
||||||
@Json(name = "purchasePrice") val purchasePrice: Double?,
|
@Json(name = "assetId") val assetId: String?,
|
||||||
@Json(name = "purchaseDate") val purchaseDate: String?,
|
@Json(name = "insured") val insured: Boolean?,
|
||||||
@Json(name = "warrantyUntil") val warrantyUntil: String?,
|
@Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
|
||||||
@Json(name = "locationId") val locationId: String?,
|
@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 = "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>?
|
@Json(name = "labelIds") val labelIds: List<String>?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('ItemCreateDto')]
|
// [END_ENTITY: DataClass('ItemCreateDto')]
|
||||||
@@ -37,20 +48,31 @@ data class ItemCreateDto(
|
|||||||
/**
|
/**
|
||||||
* @summary Маппер из доменной модели ItemCreate в ItemCreateDto.
|
* @summary Маппер из доменной модели ItemCreate в ItemCreateDto.
|
||||||
*/
|
*/
|
||||||
fun ItemCreate.toDto(): ItemCreateDto {
|
fun ItemCreate.toItemCreateDto(): ItemCreateDto {
|
||||||
return ItemCreateDto(
|
return ItemCreateDto(
|
||||||
name = this.name,
|
name = this.name,
|
||||||
assetId = this.assetId,
|
|
||||||
description = this.description,
|
description = this.description,
|
||||||
notes = this.notes,
|
|
||||||
serialNumber = this.serialNumber,
|
|
||||||
quantity = this.quantity,
|
quantity = this.quantity,
|
||||||
value = this.value,
|
archived = this.archived,
|
||||||
purchasePrice = this.purchasePrice,
|
assetId = this.assetId,
|
||||||
purchaseDate = this.purchaseDate,
|
insured = this.insured,
|
||||||
warrantyUntil = this.warrantyUntil,
|
lifetimeWarranty = this.lifetimeWarranty,
|
||||||
locationId = this.locationId,
|
manufacturer = this.manufacturer,
|
||||||
|
modelNumber = this.modelNumber,
|
||||||
|
notes = this.notes,
|
||||||
parentId = this.parentId,
|
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
|
labelIds = this.labelIds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,20 @@ data class ItemOutDto(
|
|||||||
@Json(name = "serialNumber") val serialNumber: String?,
|
@Json(name = "serialNumber") val serialNumber: String?,
|
||||||
@Json(name = "quantity") val quantity: Int,
|
@Json(name = "quantity") val quantity: Int,
|
||||||
@Json(name = "isArchived") val isArchived: Boolean,
|
@Json(name = "isArchived") val isArchived: Boolean,
|
||||||
@Json(name = "value") val value: Double,
|
|
||||||
@Json(name = "purchasePrice") val purchasePrice: Double?,
|
@Json(name = "purchasePrice") val purchasePrice: Double?,
|
||||||
@Json(name = "purchaseDate") val purchaseDate: String?,
|
@Json(name = "purchaseTime") val purchaseTime: String?,
|
||||||
@Json(name = "warrantyUntil") val warrantyUntil: 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 = "location") val location: LocationOutDto?,
|
||||||
@Json(name = "parent") val parent: ItemSummaryDto?,
|
@Json(name = "parent") val parent: ItemSummaryDto?,
|
||||||
@Json(name = "children") val children: List<ItemSummaryDto>,
|
@Json(name = "children") val children: List<ItemSummaryDto>,
|
||||||
@@ -40,36 +50,3 @@ data class ItemOutDto(
|
|||||||
@Json(name = "updatedAt") val updatedAt: String
|
@Json(name = "updatedAt") val updatedAt: String
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('ItemOutDto')]
|
// [END_ENTITY: DataClass('ItemOutDto')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDomain')]
|
|
||||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemOut')]
|
|
||||||
/**
|
|
||||||
* @summary Маппер из ItemOutDto в доменную модель ItemOut.
|
|
||||||
*/
|
|
||||||
fun ItemOutDto.toDomain(): ItemOut {
|
|
||||||
return ItemOut(
|
|
||||||
id = this.id,
|
|
||||||
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,
|
|
||||||
location = this.location?.toDomain(),
|
|
||||||
parent = this.parent?.toDomain(),
|
|
||||||
children = this.children.map { it.toDomain() },
|
|
||||||
labels = this.labels.map { it.toDomain() },
|
|
||||||
attachments = this.attachments.map { it.toDomain() },
|
|
||||||
images = this.images.map { it.toDomain() },
|
|
||||||
fields = this.fields.map { it.toDomain() },
|
|
||||||
maintenance = this.maintenance.map { it.toDomain() },
|
|
||||||
createdAt = this.createdAt,
|
|
||||||
updatedAt = this.updatedAt
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDomain')]
|
|
||||||
@@ -28,24 +28,3 @@ data class ItemSummaryDto(
|
|||||||
@Json(name = "updatedAt") val updatedAt: String
|
@Json(name = "updatedAt") val updatedAt: String
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('ItemSummaryDto')]
|
// [END_ENTITY: DataClass('ItemSummaryDto')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDomain')]
|
|
||||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
|
||||||
/**
|
|
||||||
* @summary Маппер из ItemSummaryDto в доменную модель ItemSummary.
|
|
||||||
*/
|
|
||||||
fun ItemSummaryDto.toDomain(): ItemSummary {
|
|
||||||
return ItemSummary(
|
|
||||||
id = this.id,
|
|
||||||
name = this.name,
|
|
||||||
assetId = this.assetId,
|
|
||||||
image = this.image?.toDomain(),
|
|
||||||
isArchived = this.isArchived,
|
|
||||||
labels = this.labels.map { it.toDomain() },
|
|
||||||
location = this.location?.toDomain(),
|
|
||||||
value = this.value,
|
|
||||||
createdAt = this.createdAt,
|
|
||||||
updatedAt = this.updatedAt
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDomain')]
|
|
||||||
@@ -17,18 +17,28 @@ import com.homebox.lens.domain.model.ItemUpdate
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemUpdateDto(
|
data class ItemUpdateDto(
|
||||||
@Json(name = "name") val name: String?,
|
@Json(name = "name") val name: String?,
|
||||||
@Json(name = "assetId") val assetId: String?,
|
|
||||||
@Json(name = "description") val description: 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 = "quantity") val quantity: Int?,
|
||||||
@Json(name = "isArchived") val isArchived: Boolean?,
|
@Json(name = "archived") val archived: Boolean?,
|
||||||
@Json(name = "value") val value: Double?,
|
@Json(name = "assetId") val assetId: String?,
|
||||||
@Json(name = "purchasePrice") val purchasePrice: Double?,
|
@Json(name = "insured") val insured: Boolean?,
|
||||||
@Json(name = "purchaseDate") val purchaseDate: String?,
|
@Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
|
||||||
@Json(name = "warrantyUntil") val warrantyUntil: String?,
|
@Json(name = "manufacturer") val manufacturer: String?,
|
||||||
@Json(name = "locationId") val locationId: String?,
|
@Json(name = "modelNumber") val modelNumber: String?,
|
||||||
|
@Json(name = "notes") val notes: String?,
|
||||||
@Json(name = "parentId") val parentId: 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>?
|
@Json(name = "labelIds") val labelIds: List<String>?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('ItemUpdateDto')]
|
// [END_ENTITY: DataClass('ItemUpdateDto')]
|
||||||
@@ -38,21 +48,31 @@ data class ItemUpdateDto(
|
|||||||
/**
|
/**
|
||||||
* @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto.
|
* @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto.
|
||||||
*/
|
*/
|
||||||
fun ItemUpdate.toDto(): ItemUpdateDto {
|
fun ItemUpdate.toItemUpdateDto(): ItemUpdateDto {
|
||||||
return ItemUpdateDto(
|
return ItemUpdateDto(
|
||||||
name = this.name,
|
name = this.name,
|
||||||
assetId = this.assetId,
|
|
||||||
description = this.description,
|
description = this.description,
|
||||||
notes = this.notes,
|
|
||||||
serialNumber = this.serialNumber,
|
|
||||||
quantity = this.quantity,
|
quantity = this.quantity,
|
||||||
isArchived = this.isArchived,
|
archived = this.archived,
|
||||||
value = this.value,
|
assetId = this.assetId,
|
||||||
purchasePrice = this.purchasePrice,
|
insured = this.insured,
|
||||||
purchaseDate = this.purchaseDate,
|
lifetimeWarranty = this.lifetimeWarranty,
|
||||||
warrantyUntil = this.warrantyUntil,
|
manufacturer = this.manufacturer,
|
||||||
locationId = this.locationId,
|
modelNumber = this.modelNumber,
|
||||||
|
notes = this.notes,
|
||||||
parentId = this.parentId,
|
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
|
labelIds = this.labelIds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,20 +26,4 @@ data class LabelOutDto(
|
|||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LabelOutDto')]
|
// [END_ENTITY: DataClass('LabelOutDto')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDomain')]
|
|
||||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
|
|
||||||
/**
|
|
||||||
* @summary Маппер из LabelOutDto в доменную модель LabelOut.
|
|
||||||
*/
|
|
||||||
fun LabelOutDto.toDomain(): LabelOut {
|
|
||||||
return LabelOut(
|
|
||||||
id = this.id,
|
|
||||||
name = this.name,
|
|
||||||
color = this.color ?: "",
|
|
||||||
isArchived = this.isArchived ?: false,
|
|
||||||
createdAt = this.createdAt,
|
|
||||||
updatedAt = this.updatedAt
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDomain')]
|
|
||||||
// [END_FILE_LabelOutDto.kt]
|
// [END_FILE_LabelOutDto.kt]
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ data class LabelSummaryDto(
|
|||||||
fun LabelSummaryDto.toDomain(): LabelSummary {
|
fun LabelSummaryDto.toDomain(): LabelSummary {
|
||||||
return LabelSummary(
|
return LabelSummary(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
name = this.name
|
name = this.name,
|
||||||
|
color = this.color ?: ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('toDomain')]
|
// [END_ENTITY: Function('toDomain')]
|
||||||
|
|||||||
@@ -15,17 +15,9 @@ data class LabelUpdateDto(
|
|||||||
@Json(name = "name")
|
@Json(name = "name")
|
||||||
val name: String?,
|
val name: String?,
|
||||||
@Json(name = "color")
|
@Json(name = "color")
|
||||||
val color: String?
|
val color: String?,
|
||||||
|
@Json(name = "description")
|
||||||
|
val description: String?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LabelUpdateDto')]
|
// [END_ENTITY: DataClass('LabelUpdateDto')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDto')]
|
|
||||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
|
|
||||||
fun LabelUpdate.toDto(): LabelUpdateDto {
|
|
||||||
return LabelUpdateDto(
|
|
||||||
name = this.name,
|
|
||||||
color = this.color
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDto')]
|
|
||||||
// [END_FILE_LabelUpdateDto.kt]
|
// [END_FILE_LabelUpdateDto.kt]
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import com.squareup.moshi.JsonClass
|
|||||||
data class LocationCreateDto(
|
data class LocationCreateDto(
|
||||||
@Json(name = "name")
|
@Json(name = "name")
|
||||||
val name: String,
|
val name: String,
|
||||||
|
@Json(name = "parentId")
|
||||||
|
val parentId: String?,
|
||||||
@Json(name = "color")
|
@Json(name = "color")
|
||||||
val color: String?,
|
val color: String?,
|
||||||
@Json(name = "description")
|
@Json(name = "description")
|
||||||
val description: String? // Assuming description can be null for creation
|
val description: String?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LocationCreateDto')]
|
// [END_ENTITY: DataClass('LocationCreateDto')]
|
||||||
// [END_FILE_LocationCreateDto.kt]
|
// [END_FILE_LocationCreateDto.kt]
|
||||||
|
|||||||
@@ -27,21 +27,4 @@ data class LocationOutCountDto(
|
|||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LocationOutCountDto')]
|
// [END_ENTITY: DataClass('LocationOutCountDto')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDomain')]
|
|
||||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOutCount')]
|
|
||||||
/**
|
|
||||||
* @summary Маппер из LocationOutCountDto в доменную модель LocationOutCount.
|
|
||||||
*/
|
|
||||||
fun LocationOutCountDto.toDomain(): LocationOutCount {
|
|
||||||
return LocationOutCount(
|
|
||||||
id = this.id,
|
|
||||||
name = this.name,
|
|
||||||
color = this.color ?: "",
|
|
||||||
isArchived = this.isArchived ?: false,
|
|
||||||
itemCount = this.itemCount,
|
|
||||||
createdAt = this.createdAt,
|
|
||||||
updatedAt = this.updatedAt
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDomain')]
|
|
||||||
// [END_FILE_LocationOutCountDto.kt]
|
// [END_FILE_LocationOutCountDto.kt]
|
||||||
|
|||||||
@@ -27,17 +27,4 @@ data class LocationOutDto(
|
|||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LocationOutDto')]
|
// [END_ENTITY: DataClass('LocationOutDto')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDomain')]
|
|
||||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOut')]
|
|
||||||
fun LocationOutDto.toDomain(): LocationOut {
|
|
||||||
return LocationOut(
|
|
||||||
id = this.id,
|
|
||||||
name = this.name,
|
|
||||||
color = this.color,
|
|
||||||
isArchived = this.isArchived,
|
|
||||||
createdAt = this.createdAt,
|
|
||||||
updatedAt = this.updatedAt
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDomain')]
|
|
||||||
// [END_FILE_LocationOutDto.kt]
|
// [END_FILE_LocationOutDto.kt]
|
||||||
|
|||||||
@@ -15,17 +15,10 @@ data class LocationUpdateDto(
|
|||||||
@Json(name = "name")
|
@Json(name = "name")
|
||||||
val name: String?,
|
val name: String?,
|
||||||
@Json(name = "color")
|
@Json(name = "color")
|
||||||
val color: String?
|
val color: String?,
|
||||||
|
@Json(name = "description")
|
||||||
|
val description: String?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LocationUpdateDto')]
|
// [END_ENTITY: DataClass('LocationUpdateDto')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDto')]
|
|
||||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LocationUpdateDto')]
|
|
||||||
fun LocationUpdate.toDto(): LocationUpdateDto {
|
|
||||||
return LocationUpdateDto(
|
|
||||||
name = this.name,
|
|
||||||
color = this.color
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDto')]
|
|
||||||
// [END_FILE_LocationUpdateDto.kt]
|
// [END_FILE_LocationUpdateDto.kt]
|
||||||
|
|||||||
@@ -22,19 +22,3 @@ data class PaginationResultDto<T>(
|
|||||||
@Json(name = "total") val total: Int
|
@Json(name = "total") val total: Int
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('PaginationResultDto')]
|
// [END_ENTITY: DataClass('PaginationResultDto')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDomain')]
|
|
||||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('PaginationResult')]
|
|
||||||
/**
|
|
||||||
* @summary Маппер из PaginationResultDto в доменную модель PaginationResult.
|
|
||||||
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
|
|
||||||
*/
|
|
||||||
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> {
|
|
||||||
return PaginationResult(
|
|
||||||
items = this.items.map(transform),
|
|
||||||
page = this.page,
|
|
||||||
pageSize = this.pageSize,
|
|
||||||
total = this.total
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDomain')]
|
|
||||||
@@ -24,7 +24,7 @@ import com.homebox.lens.data.db.entity.*
|
|||||||
LocationEntity::class,
|
LocationEntity::class,
|
||||||
ItemLabelCrossRef::class
|
ItemLabelCrossRef::class
|
||||||
],
|
],
|
||||||
version = 1,
|
version = 2,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package com.homebox.lens.data.db.entity
|
|||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import java.math.BigDecimal
|
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTITY: DatabaseTable('ItemEntity')]
|
// [ENTITY: DatabaseTable('ItemEntity')]
|
||||||
@@ -18,10 +17,29 @@ data class ItemEntity(
|
|||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
|
val quantity: Int,
|
||||||
val image: String?,
|
val image: String?,
|
||||||
val locationId: String?,
|
val locationId: String?,
|
||||||
val value: BigDecimal?,
|
val purchasePrice: Double?,
|
||||||
val createdAt: String?
|
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: Double?,
|
||||||
|
val soldTime: String?,
|
||||||
|
val soldTo: String?,
|
||||||
|
val syncChildItemsLocations: Boolean,
|
||||||
|
val warrantyDetails: String?,
|
||||||
|
val warrantyExpires: String?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DatabaseTable('ItemEntity')]
|
// [END_ENTITY: DatabaseTable('ItemEntity')]
|
||||||
|
|
||||||
|
|||||||
@@ -4,46 +4,173 @@
|
|||||||
package com.homebox.lens.data.db.entity
|
package com.homebox.lens.data.db.entity
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import com.homebox.lens.domain.model.Image
|
import com.homebox.lens.data.mapper.toDomain
|
||||||
import com.homebox.lens.domain.model.ItemSummary
|
import com.homebox.lens.domain.model.*
|
||||||
import com.homebox.lens.domain.model.LabelOut
|
|
||||||
import com.homebox.lens.domain.model.LocationOut
|
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTITY: Function('toDomain')]
|
// [ENTITY: Function('ItemWithLabels.toDomainItemSummary')]
|
||||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
// [RELATION: Function('ItemWithLabels.toDomainItemSummary')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||||
/**
|
/**
|
||||||
* @summary Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
|
* @summary Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
|
||||||
*/
|
*/
|
||||||
fun ItemWithLabels.toDomain(): ItemSummary {
|
fun ItemWithLabels.toDomainItemSummary(): ItemSummary {
|
||||||
return ItemSummary(
|
return ItemSummary(
|
||||||
id = this.item.id,
|
id = this.item.id,
|
||||||
name = this.item.name,
|
name = this.item.name,
|
||||||
image = this.item.image?.let { Image(id = "", path = it, isPrimary = true) },
|
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 = "") },
|
location = this.item.locationId?.let { LocationOut(id = it, name = "", color = "", isArchived = false, createdAt = "", updatedAt = "") },
|
||||||
labels = this.labels.map { it.toDomain() },
|
labels = this.labels.map { it.toDomainLabelOut() },
|
||||||
assetId = null,
|
assetId = this.item.assetId,
|
||||||
isArchived = false,
|
isArchived = this.item.archived,
|
||||||
value = this.item.value?.toDouble() ?: 0.0,
|
value = this.item.purchasePrice ?: 0.0,
|
||||||
createdAt = this.item.createdAt ?: "",
|
createdAt = this.item.createdAt ?: "",
|
||||||
updatedAt = ""
|
updatedAt = "" // ItemEntity does not have updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('toDomain')]
|
// [END_ENTITY: Function('ItemWithLabels.toDomainItemSummary')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDomain')]
|
// [ENTITY: Function('ItemEntity.toDomainItem')]
|
||||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
|
// [RELATION: Function('ItemEntity.toDomainItem')] -> [RETURNS] -> [DataClass('Item')]
|
||||||
/**
|
/**
|
||||||
* @summary Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
|
* @summary Преобразует [ItemEntity] (сущность БД) в [Item] (доменную модель).
|
||||||
*/
|
*/
|
||||||
fun LabelEntity.toDomain(): LabelOut {
|
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('ItemOut.toItemEntity')]
|
||||||
|
// [RELATION: Function('ItemOut.toItemEntity')] -> [RETURNS] -> [DataClass('ItemEntity')]
|
||||||
|
fun ItemOut.toItemEntity(): ItemEntity {
|
||||||
|
return ItemEntity(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
description = this.description,
|
||||||
|
quantity = this.quantity,
|
||||||
|
image = this.images.firstOrNull()?.path,
|
||||||
|
locationId = this.location?.id,
|
||||||
|
purchasePrice = this.purchasePrice,
|
||||||
|
createdAt = this.createdAt,
|
||||||
|
archived = this.isArchived,
|
||||||
|
assetId = this.assetId,
|
||||||
|
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,
|
||||||
|
soldTime = this.soldTime,
|
||||||
|
soldTo = this.soldTo,
|
||||||
|
syncChildItemsLocations = this.syncChildItemsLocations ?: false,
|
||||||
|
warrantyDetails = this.warrantyDetails,
|
||||||
|
warrantyExpires = this.warrantyExpires
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('ItemOut.toItemEntity')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('LabelEntity.toDomain')]
|
||||||
|
// [RELATION: Function('LabelEntity.toDomain')] -> [RETURNS] -> [DataClass('Label')]
|
||||||
|
fun LabelEntity.toDomain(): Label {
|
||||||
|
return Label(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('LabelEntity.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('LabelEntity.toDomainLabelOut')]
|
||||||
|
// [RELATION: Function('LabelEntity.toDomainLabelOut')] -> [RETURNS] -> [DataClass('LabelOut')]
|
||||||
|
fun LabelEntity.toDomainLabelOut(): LabelOut {
|
||||||
return LabelOut(
|
return LabelOut(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
color = "#CCCCCC",
|
description = null, // Not available in LabelEntity
|
||||||
isArchived = false,
|
color = "", // Not available in LabelEntity
|
||||||
createdAt = "",
|
isArchived = false, // Not available in LabelEntity
|
||||||
updatedAt = ""
|
createdAt = "", // Not available in LabelEntity
|
||||||
|
updatedAt = "" // Not available in LabelEntity
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('toDomain')]
|
// [END_ENTITY: Function('LabelEntity.toDomainLabelOut')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('Label.toEntity')]
|
||||||
|
// [RELATION: Function('Label.toEntity')] -> [RETURNS] -> [DataClass('LabelEntity')]
|
||||||
|
fun Label.toEntity(): LabelEntity {
|
||||||
|
return LabelEntity(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('Label.toEntity')]
|
||||||
|
// [END_FILE_Mapper.kt]
|
||||||
@@ -34,7 +34,7 @@ object DatabaseModule {
|
|||||||
context,
|
context,
|
||||||
HomeboxDatabase::class.java,
|
HomeboxDatabase::class.java,
|
||||||
HomeboxDatabase.DATABASE_NAME
|
HomeboxDatabase.DATABASE_NAME
|
||||||
).build()
|
).fallbackToDestructiveMigration().build()
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('provideHomeboxDatabase')]
|
// [END_ENTITY: Function('provideHomeboxDatabase')]
|
||||||
|
|
||||||
|
|||||||
130
data/src/main/java/com/homebox/lens/data/mapper/DomainToDto.kt
Normal file
130
data/src/main/java/com/homebox/lens/data/mapper/DomainToDto.kt
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.data.mapper
|
||||||
|
// [FILE] DomainToDto.kt
|
||||||
|
// [SEMANTICS] data, mapper, domain, dto
|
||||||
|
package com.homebox.lens.data.mapper
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
|
import com.homebox.lens.data.api.dto.ItemCreateDto
|
||||||
|
import com.homebox.lens.data.api.dto.ItemUpdateDto
|
||||||
|
import com.homebox.lens.data.api.dto.LabelCreateDto
|
||||||
|
import com.homebox.lens.data.api.dto.LabelUpdateDto
|
||||||
|
import com.homebox.lens.data.api.dto.LocationCreateDto
|
||||||
|
import com.homebox.lens.data.api.dto.LocationUpdateDto
|
||||||
|
import com.homebox.lens.domain.model.ItemCreate as DomainItemCreate
|
||||||
|
import com.homebox.lens.domain.model.ItemUpdate as DomainItemUpdate
|
||||||
|
import com.homebox.lens.domain.model.LabelCreate as DomainLabelCreate
|
||||||
|
import com.homebox.lens.domain.model.LabelUpdate as DomainLabelUpdate
|
||||||
|
import com.homebox.lens.domain.model.LocationCreate as DomainLocationCreate
|
||||||
|
import com.homebox.lens.domain.model.LocationUpdate as DomainLocationUpdate
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Function('DomainItemCreate.toDto')]
|
||||||
|
// [RELATION: Function('DomainItemCreate.toDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')]
|
||||||
|
fun DomainItemCreate.toDto(): ItemCreateDto {
|
||||||
|
return ItemCreateDto(
|
||||||
|
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('ItemCreate.toDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('DomainItemUpdate.toDto')]
|
||||||
|
// [RELATION: Function('DomainItemUpdate.toDto')] -> [RETURNS] -> [DataClass('ItemUpdateDto')]
|
||||||
|
fun DomainItemUpdate.toDto(): 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.toDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('DomainLabelCreate.toDto')]
|
||||||
|
// [RELATION: Function('DomainLabelCreate.toDto')] -> [RETURNS] -> [DataClass('LabelCreateDto')]
|
||||||
|
fun DomainLabelCreate.toDto(): LabelCreateDto {
|
||||||
|
return LabelCreateDto(
|
||||||
|
name = this.name,
|
||||||
|
color = this.color,
|
||||||
|
description = this.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('LabelCreate.toDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('DomainLabelUpdate.toDto')]
|
||||||
|
// [RELATION: Function('DomainLabelUpdate.toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
|
||||||
|
fun DomainLabelUpdate.toDto(): LabelUpdateDto {
|
||||||
|
return LabelUpdateDto(
|
||||||
|
name = this.name,
|
||||||
|
color = this.color,
|
||||||
|
description = this.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('DomainLabelUpdate.toDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('DomainLocationCreate.toDto')]
|
||||||
|
// [RELATION: Function('DomainLocationCreate.toDto')] -> [RETURNS] -> [DataClass('LocationCreateDto')]
|
||||||
|
fun DomainLocationCreate.toDto(): LocationCreateDto {
|
||||||
|
return LocationCreateDto(
|
||||||
|
name = this.name,
|
||||||
|
parentId = this.parentId,
|
||||||
|
color = null,
|
||||||
|
description = this.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('DomainLocationCreate.toDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('DomainLocationUpdate.toDto')]
|
||||||
|
// [RELATION: Function('DomainLocationUpdate.toDto')] -> [RETURNS] -> [DataClass('LocationUpdateDto')]
|
||||||
|
fun DomainLocationUpdate.toDto(): LocationUpdateDto {
|
||||||
|
return LocationUpdateDto(
|
||||||
|
name = this.name,
|
||||||
|
color = this.color,
|
||||||
|
description = this.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('DomainLocationUpdate.toDto')]
|
||||||
|
|
||||||
|
// [END_FILE_DomainToDto.kt]
|
||||||
261
data/src/main/java/com/homebox/lens/data/mapper/DtoToDomain.kt
Normal file
261
data/src/main/java/com/homebox/lens/data/mapper/DtoToDomain.kt
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.data.mapper
|
||||||
|
// [FILE] DtoToDomain.kt
|
||||||
|
// [SEMANTICS] data, mapper, dto, domain
|
||||||
|
package com.homebox.lens.data.mapper
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
|
import com.homebox.lens.data.api.dto.*
|
||||||
|
import com.homebox.lens.domain.model.CustomField as DomainCustomField
|
||||||
|
import com.homebox.lens.domain.model.GroupStatistics as DomainGroupStatistics
|
||||||
|
import com.homebox.lens.domain.model.Image as DomainImage
|
||||||
|
import com.homebox.lens.domain.model.Item as DomainItem
|
||||||
|
import com.homebox.lens.domain.model.ItemAttachment as DomainItemAttachment
|
||||||
|
import com.homebox.lens.domain.model.ItemOut as DomainItemOut
|
||||||
|
import com.homebox.lens.domain.model.ItemSummary as DomainItemSummary
|
||||||
|
import com.homebox.lens.domain.model.Label as DomainLabel
|
||||||
|
import com.homebox.lens.domain.model.LabelOut as DomainLabelOut
|
||||||
|
import com.homebox.lens.domain.model.LabelSummary as DomainLabelSummary
|
||||||
|
import com.homebox.lens.domain.model.Location as DomainLocation
|
||||||
|
import com.homebox.lens.domain.model.LocationOut as DomainLocationOut
|
||||||
|
import com.homebox.lens.domain.model.LocationOutCount as DomainLocationOutCount
|
||||||
|
import com.homebox.lens.domain.model.MaintenanceEntry as DomainMaintenanceEntry
|
||||||
|
import com.homebox.lens.domain.model.PaginationResult as DomainPaginationResult
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Function('ItemOutDto.toDomain')]
|
||||||
|
// [RELATION: Function('ItemOutDto.toDomain')] -> [RETURNS] -> [DataClass('DomainItemOut')]
|
||||||
|
fun ItemOutDto.toDomain(): DomainItemOut {
|
||||||
|
return DomainItemOut(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
assetId = this.assetId,
|
||||||
|
description = this.description,
|
||||||
|
notes = this.notes,
|
||||||
|
serialNumber = this.serialNumber,
|
||||||
|
quantity = this.quantity,
|
||||||
|
isArchived = this.isArchived,
|
||||||
|
purchasePrice = this.purchasePrice,
|
||||||
|
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() },
|
||||||
|
labels = this.labels.map { it.toDomain() },
|
||||||
|
attachments = this.attachments.map { it.toDomain() },
|
||||||
|
images = this.images.map { it.toDomain() },
|
||||||
|
fields = this.fields.map { it.toDomain() },
|
||||||
|
maintenance = this.maintenance.map { it.toDomain() },
|
||||||
|
createdAt = this.createdAt,
|
||||||
|
updatedAt = this.updatedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ItemOutDto.toDomainItem(): DomainItem {
|
||||||
|
return DomainItem(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
description = this.description,
|
||||||
|
quantity = this.quantity,
|
||||||
|
image = this.images.firstOrNull { it.isPrimary }?.path,
|
||||||
|
location = this.location?.toDomainLocation(),
|
||||||
|
labels = this.labels.map { it.toDomainLabel() },
|
||||||
|
purchasePrice = this.purchasePrice,
|
||||||
|
createdAt = this.createdAt,
|
||||||
|
archived = this.isArchived,
|
||||||
|
assetId = this.assetId,
|
||||||
|
fields = this.fields.map { it.toDomain() },
|
||||||
|
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,
|
||||||
|
soldTime = this.soldTime,
|
||||||
|
soldTo = this.soldTo,
|
||||||
|
syncChildItemsLocations = this.syncChildItemsLocations ?: false,
|
||||||
|
warrantyDetails = this.warrantyDetails,
|
||||||
|
warrantyExpires = this.warrantyExpires
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('ItemOutDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('ItemSummaryDto.toDomain')]
|
||||||
|
// [RELATION: Function('ItemSummaryDto.toDomain')] -> [RETURNS] -> [DataClass('DomainItemSummary')]
|
||||||
|
fun ItemSummaryDto.toDomain(): DomainItemSummary {
|
||||||
|
return DomainItemSummary(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
assetId = this.assetId,
|
||||||
|
image = this.image?.toDomain(),
|
||||||
|
isArchived = this.isArchived,
|
||||||
|
labels = this.labels.map { it.toDomain() },
|
||||||
|
location = this.location?.toDomain(),
|
||||||
|
value = this.value,
|
||||||
|
createdAt = this.createdAt,
|
||||||
|
updatedAt = this.updatedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('ItemSummaryDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('LabelOutDto.toDomain')]
|
||||||
|
// [RELATION: Function('LabelOutDto.toDomain')] -> [RETURNS] -> [DataClass('DomainLabelOut')]
|
||||||
|
fun LabelOutDto.toDomain(): DomainLabelOut {
|
||||||
|
return DomainLabelOut(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
description = this.description,
|
||||||
|
color = this.color ?: "",
|
||||||
|
isArchived = this.isArchived ?: false,
|
||||||
|
createdAt = this.createdAt,
|
||||||
|
updatedAt = this.updatedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LabelOutDto.toDomainLabel(): DomainLabel {
|
||||||
|
return DomainLabel(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('LabelOutDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('LocationOutDto.toDomain')]
|
||||||
|
// [RELATION: Function('LocationOutDto.toDomain')] -> [RETURNS] -> [DataClass('DomainLocationOut')]
|
||||||
|
fun LocationOutDto.toDomain(): DomainLocationOut {
|
||||||
|
return DomainLocationOut(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
color = this.color,
|
||||||
|
isArchived = this.isArchived,
|
||||||
|
createdAt = this.createdAt,
|
||||||
|
updatedAt = this.updatedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LocationOutDto.toDomainLocation(): DomainLocation {
|
||||||
|
return DomainLocation(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('LocationOutDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('LocationOutCountDto.toDomain')]
|
||||||
|
// [RELATION: Function('LocationOutCountDto.toDomain')] -> [RETURNS] -> [DataClass('DomainLocationOutCount')]
|
||||||
|
fun LocationOutCountDto.toDomain(): DomainLocationOutCount {
|
||||||
|
return DomainLocationOutCount(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
color = this.color ?: "",
|
||||||
|
isArchived = this.isArchived ?: false,
|
||||||
|
itemCount = this.itemCount,
|
||||||
|
createdAt = this.createdAt,
|
||||||
|
updatedAt = this.updatedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('LocationOutCountDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('PaginationResultDto.toDomain')]
|
||||||
|
// [RELATION: Function('PaginationResultDto.toDomain')] -> [RETURNS] -> [DataClass('DomainPaginationResult')]
|
||||||
|
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): DomainPaginationResult<R> {
|
||||||
|
return DomainPaginationResult(
|
||||||
|
items = this.items.map(transform),
|
||||||
|
page = this.page,
|
||||||
|
pageSize = this.pageSize,
|
||||||
|
total = this.total
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('PaginationResultDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('ImageDto.toDomain')]
|
||||||
|
// [RELATION: Function('ImageDto.toDomain')] -> [RETURNS] -> [DataClass('DomainImage')]
|
||||||
|
fun ImageDto.toDomain(): DomainImage {
|
||||||
|
return DomainImage(
|
||||||
|
id = this.id,
|
||||||
|
path = this.path,
|
||||||
|
isPrimary = this.isPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('ImageDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('CustomFieldDto.toDomain')]
|
||||||
|
// [RELATION: Function('CustomFieldDto.toDomain')] -> [RETURNS] -> [DataClass('DomainCustomField')]
|
||||||
|
fun CustomFieldDto.toDomain(): DomainCustomField {
|
||||||
|
return DomainCustomField(
|
||||||
|
name = this.name,
|
||||||
|
value = this.value,
|
||||||
|
type = this.type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('CustomFieldDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('ItemAttachmentDto.toDomain')]
|
||||||
|
// [RELATION: Function('ItemAttachmentDto.toDomain')] -> [RETURNS] -> [DataClass('DomainItemAttachment')]
|
||||||
|
fun ItemAttachmentDto.toDomain(): DomainItemAttachment {
|
||||||
|
return DomainItemAttachment(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
path = this.path,
|
||||||
|
type = this.type,
|
||||||
|
createdAt = this.createdAt,
|
||||||
|
updatedAt = this.updatedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('ItemAttachmentDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('MaintenanceEntryDto.toDomain')]
|
||||||
|
// [RELATION: Function('MaintenanceEntryDto.toDomain')] -> [RETURNS] -> [DataClass('DomainMaintenanceEntry')]
|
||||||
|
fun MaintenanceEntryDto.toDomain(): DomainMaintenanceEntry {
|
||||||
|
return DomainMaintenanceEntry(
|
||||||
|
id = this.id,
|
||||||
|
itemId = this.itemId,
|
||||||
|
title = this.title,
|
||||||
|
details = this.details,
|
||||||
|
dueAt = this.dueAt,
|
||||||
|
completedAt = this.completedAt,
|
||||||
|
createdAt = this.createdAt,
|
||||||
|
updatedAt = this.updatedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('MaintenanceEntryDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('GroupStatisticsDto.toDomain')]
|
||||||
|
// [RELATION: Function('GroupStatisticsDto.toDomain')] -> [RETURNS] -> [DataClass('DomainGroupStatistics')]
|
||||||
|
fun GroupStatisticsDto.toDomain(): DomainGroupStatistics {
|
||||||
|
return DomainGroupStatistics(
|
||||||
|
items = this.totalItems,
|
||||||
|
labels = this.totalLabels,
|
||||||
|
locations = this.totalLocations,
|
||||||
|
totalValue = this.totalItemPrice
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('GroupStatisticsDto.toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('LabelSummaryDto.toDomain')]
|
||||||
|
// [RELATION: Function('LabelSummaryDto.toDomain')] -> [RETURNS] -> [DataClass('DomainLabelSummary')]
|
||||||
|
fun LabelSummaryDto.toDomain(): DomainLabelSummary {
|
||||||
|
return DomainLabelSummary(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
color = this.color ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('LabelSummaryDto.toDomain')]
|
||||||
|
|
||||||
|
// [END_FILE_DtoToDomain.kt]
|
||||||
@@ -98,11 +98,46 @@ class CredentialsRepositoryImpl @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
override suspend fun getToken(): String? {
|
override suspend fun getToken(): String? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
|
val token = encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
||||||
encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
if (token != null) {
|
||||||
|
Timber.i("[INFO][ACTION][token_retrieved] Auth token retrieved successfully.")
|
||||||
|
} else {
|
||||||
|
Timber.w("[WARN][FALLBACK][no_token_found] No auth token found.")
|
||||||
|
}
|
||||||
|
token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('getToken')]
|
// [END_ENTITY: Function('getToken')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('clearAllCredentials')]
|
||||||
|
/**
|
||||||
|
* @summary Очищает все сохраненные учетные данные и токены.
|
||||||
|
* @sideeffect Удаляет все записи, связанные с учетными данными, из SharedPreferences.
|
||||||
|
*/
|
||||||
|
override suspend fun clearAllCredentials() {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Timber.i("[INFO][ACTION][clearing_all_credentials] Clearing all saved credentials and tokens.")
|
||||||
|
encryptedPrefs.edit()
|
||||||
|
.remove(KEY_SERVER_URL)
|
||||||
|
.remove(KEY_USERNAME)
|
||||||
|
.remove(KEY_PASSWORD)
|
||||||
|
.remove(KEY_AUTH_TOKEN)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('clearAllCredentials')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('areCredentialsSavedSync')]
|
||||||
|
/**
|
||||||
|
* @summary Synchronously checks if user credentials are saved.
|
||||||
|
* @return true if all essential credentials (URL, username, password) are present, false otherwise.
|
||||||
|
*/
|
||||||
|
override fun areCredentialsSavedSync(): Boolean {
|
||||||
|
return encryptedPrefs.contains(KEY_SERVER_URL) &&
|
||||||
|
encryptedPrefs.contains(KEY_USERNAME) &&
|
||||||
|
encryptedPrefs.contains(KEY_PASSWORD)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('areCredentialsSavedSync')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Class('CredentialsRepositoryImpl')]
|
// [END_ENTITY: Class('CredentialsRepositoryImpl')]
|
||||||
// [END_FILE_CredentialsRepositoryImpl.kt]
|
// [END_FILE_CredentialsRepositoryImpl.kt]
|
||||||
|
|||||||
@@ -5,15 +5,10 @@ package com.homebox.lens.data.repository
|
|||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import com.homebox.lens.data.api.HomeboxApiService
|
import com.homebox.lens.data.api.HomeboxApiService
|
||||||
import com.homebox.lens.data.api.dto.LabelCreateDto
|
|
||||||
import com.homebox.lens.data.api.dto.toDomain
|
|
||||||
import com.homebox.lens.data.api.dto.toDto
|
|
||||||
import com.homebox.lens.data.api.dto.LocationCreateDto
|
|
||||||
import com.homebox.lens.data.api.dto.LocationUpdateDto
|
|
||||||
import com.homebox.lens.data.api.dto.LabelUpdateDto
|
|
||||||
import com.homebox.lens.data.api.dto.LocationOutDto
|
|
||||||
import com.homebox.lens.data.db.dao.ItemDao
|
import com.homebox.lens.data.db.dao.ItemDao
|
||||||
import com.homebox.lens.data.db.entity.toDomain
|
import com.homebox.lens.data.db.entity.toDomainItemSummary
|
||||||
|
import com.homebox.lens.data.mapper.toDomain
|
||||||
|
import com.homebox.lens.data.mapper.toDto
|
||||||
import com.homebox.lens.domain.model.*
|
import com.homebox.lens.domain.model.*
|
||||||
import com.homebox.lens.domain.repository.ItemRepository
|
import com.homebox.lens.domain.repository.ItemRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -151,43 +146,11 @@ class ItemRepositoryImpl @Inject constructor(
|
|||||||
// [RELATION: Function('getRecentlyAddedItems')] -> [RETURNS] -> [DataStructure('Flow<List<ItemSummary>>')]
|
// [RELATION: Function('getRecentlyAddedItems')] -> [RETURNS] -> [DataStructure('Flow<List<ItemSummary>>')]
|
||||||
override fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> {
|
override fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> {
|
||||||
return itemDao.getRecentlyAddedItems(limit).map { entities ->
|
return itemDao.getRecentlyAddedItems(limit).map { entities ->
|
||||||
entities.map { it.toDomain() }
|
entities.map { it.toDomainItemSummary() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('getRecentlyAddedItems')]
|
// [END_ENTITY: Function('getRecentlyAddedItems')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Repository('ItemRepositoryImpl')]
|
// [END_ENTITY: Repository('ItemRepositoryImpl')]
|
||||||
|
|
||||||
// [ENTITY: Function('toDto')]
|
|
||||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelCreateDto')]
|
|
||||||
private fun LabelCreate.toDto(): LabelCreateDto {
|
|
||||||
return LabelCreateDto(
|
|
||||||
name = this.name,
|
|
||||||
color = this.color,
|
|
||||||
description = null // Description is not part of the domain model for creation.
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDto')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('toDto')]
|
|
||||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LocationCreateDto')]
|
|
||||||
private fun LocationCreate.toDto(): LocationCreateDto {
|
|
||||||
return LocationCreateDto(
|
|
||||||
name = this.name,
|
|
||||||
color = this.color,
|
|
||||||
description = null // Description is not part of the domain model for creation.
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDto')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('toDto')]
|
|
||||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
|
|
||||||
private fun LabelUpdate.toDto(): LabelUpdateDto {
|
|
||||||
return LabelUpdateDto(
|
|
||||||
name = this.name,
|
|
||||||
color = this.color
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('toDto')]
|
|
||||||
|
|
||||||
// [END_FILE_ItemRepositoryImpl.kt]
|
// [END_FILE_ItemRepositoryImpl.kt]
|
||||||
@@ -5,6 +5,8 @@ package com.homebox.lens.domain.model
|
|||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
import com.homebox.lens.domain.model.CustomField
|
||||||
|
import com.homebox.lens.domain.model.Image
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTITY: DataClass('Item')]
|
// [ENTITY: DataClass('Item')]
|
||||||
@@ -18,8 +20,27 @@ import java.math.BigDecimal
|
|||||||
* @param image Url изображения.
|
* @param image Url изображения.
|
||||||
* @param location Местоположение вещи.
|
* @param location Местоположение вещи.
|
||||||
* @param labels Список меток, присвоенных вещи.
|
* @param labels Список меток, присвоенных вещи.
|
||||||
* @param value Стоимость вещи.
|
* @param purchasePrice Цена покупки вещи.
|
||||||
* @param createdAt Дата создания.
|
* @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(
|
data class Item(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -29,8 +50,27 @@ data class Item(
|
|||||||
val image: String?,
|
val image: String?,
|
||||||
val location: Location?,
|
val location: Location?,
|
||||||
val labels: List<Label>,
|
val labels: List<Label>,
|
||||||
val value: BigDecimal?,
|
val purchasePrice: Double?,
|
||||||
val createdAt: String?
|
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: Double? = 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')]
|
// [END_ENTITY: DataClass('Item')]
|
||||||
|
|
||||||
|
|||||||
@@ -22,17 +22,28 @@ package com.homebox.lens.domain.model
|
|||||||
*/
|
*/
|
||||||
data class ItemCreate(
|
data class ItemCreate(
|
||||||
val name: String,
|
val name: String,
|
||||||
val assetId: String?,
|
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val notes: String?,
|
|
||||||
val serialNumber: String?,
|
|
||||||
val quantity: Int?,
|
val quantity: Int?,
|
||||||
val value: Double?,
|
val archived: Boolean?,
|
||||||
val purchasePrice: Double?,
|
val assetId: String?,
|
||||||
val purchaseDate: String?,
|
val insured: Boolean?,
|
||||||
val warrantyUntil: String?,
|
val lifetimeWarranty: Boolean?,
|
||||||
val locationId: String?,
|
val manufacturer: String?,
|
||||||
|
val modelNumber: String?,
|
||||||
|
val notes: String?,
|
||||||
val parentId: String?,
|
val parentId: String?,
|
||||||
|
val purchaseFrom: String?,
|
||||||
|
val purchasePrice: Double?,
|
||||||
|
val purchaseTime: String?,
|
||||||
|
val serialNumber: String?,
|
||||||
|
val soldNotes: String?,
|
||||||
|
val soldPrice: Double?,
|
||||||
|
val soldTime: String?,
|
||||||
|
val soldTo: String?,
|
||||||
|
val syncChildItemsLocations: Boolean?,
|
||||||
|
val warrantyDetails: String?,
|
||||||
|
val warrantyExpires: String?,
|
||||||
|
val locationId: String?,
|
||||||
val labelIds: List<String>?
|
val labelIds: List<String>?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('ItemCreate')]
|
// [END_ENTITY: DataClass('ItemCreate')]
|
||||||
|
|||||||
@@ -14,10 +14,20 @@ package com.homebox.lens.domain.model
|
|||||||
* @param serialNumber Серийный номер.
|
* @param serialNumber Серийный номер.
|
||||||
* @param quantity Количество.
|
* @param quantity Количество.
|
||||||
* @param isArchived Флаг архивации.
|
* @param isArchived Флаг архивации.
|
||||||
* @param value Стоимость.
|
|
||||||
* @param purchasePrice Цена покупки.
|
* @param purchasePrice Цена покупки.
|
||||||
* @param purchaseDate Дата покупки.
|
* @param purchaseTime Время покупки.
|
||||||
* @param warrantyUntil Гарантия до.
|
* @param purchaseFrom Место покупки.
|
||||||
|
* @param warrantyExpires Дата окончания гарантии.
|
||||||
|
* @param warrantyDetails Детали гарантии.
|
||||||
|
* @param lifetimeWarranty Пожизненная гарантия.
|
||||||
|
* @param insured Застрахована ли вещь.
|
||||||
|
* @param manufacturer Производитель.
|
||||||
|
* @param modelNumber Номер модели.
|
||||||
|
* @param soldPrice Цена продажи.
|
||||||
|
* @param soldTime Время продажи.
|
||||||
|
* @param soldTo Кому продано.
|
||||||
|
* @param soldNotes Заметки о продаже.
|
||||||
|
* @param syncChildItemsLocations Синхронизировать местоположения дочерних элементов.
|
||||||
* @param location Местоположение.
|
* @param location Местоположение.
|
||||||
* @param parent Родительская вещь (если есть).
|
* @param parent Родительская вещь (если есть).
|
||||||
* @param children Дочерние вещи.
|
* @param children Дочерние вещи.
|
||||||
@@ -38,10 +48,20 @@ data class ItemOut(
|
|||||||
val serialNumber: String?,
|
val serialNumber: String?,
|
||||||
val quantity: Int,
|
val quantity: Int,
|
||||||
val isArchived: Boolean,
|
val isArchived: Boolean,
|
||||||
val value: Double,
|
|
||||||
val purchasePrice: Double?,
|
val purchasePrice: Double?,
|
||||||
val purchaseDate: String?,
|
val purchaseTime: String?,
|
||||||
val warrantyUntil: String?,
|
val purchaseFrom: String?,
|
||||||
|
val warrantyExpires: String?,
|
||||||
|
val warrantyDetails: String?,
|
||||||
|
val lifetimeWarranty: Boolean?,
|
||||||
|
val insured: Boolean?,
|
||||||
|
val manufacturer: String?,
|
||||||
|
val modelNumber: String?,
|
||||||
|
val soldPrice: Double?,
|
||||||
|
val soldTime: String?,
|
||||||
|
val soldTo: String?,
|
||||||
|
val soldNotes: String?,
|
||||||
|
val syncChildItemsLocations: Boolean?,
|
||||||
val location: LocationOut?,
|
val location: LocationOut?,
|
||||||
val parent: ItemSummary?,
|
val parent: ItemSummary?,
|
||||||
val children: List<ItemSummary>,
|
val children: List<ItemSummary>,
|
||||||
|
|||||||
@@ -22,19 +22,30 @@ package com.homebox.lens.domain.model
|
|||||||
* @param labelIds Список ID меток для полной замены.
|
* @param labelIds Список ID меток для полной замены.
|
||||||
*/
|
*/
|
||||||
data class ItemUpdate(
|
data class ItemUpdate(
|
||||||
|
val id: String,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val assetId: String?,
|
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val notes: String?,
|
|
||||||
val serialNumber: String?,
|
|
||||||
val quantity: Int?,
|
val quantity: Int?,
|
||||||
val isArchived: Boolean?,
|
val archived: Boolean?,
|
||||||
val value: Double?,
|
val assetId: String?,
|
||||||
val purchasePrice: Double?,
|
val insured: Boolean?,
|
||||||
val purchaseDate: String?,
|
val lifetimeWarranty: Boolean?,
|
||||||
val warrantyUntil: String?,
|
val manufacturer: String?,
|
||||||
val locationId: String?,
|
val modelNumber: String?,
|
||||||
|
val notes: String?,
|
||||||
val parentId: String?,
|
val parentId: String?,
|
||||||
|
val purchaseFrom: String?,
|
||||||
|
val purchasePrice: Double?,
|
||||||
|
val purchaseTime: String?,
|
||||||
|
val serialNumber: String?,
|
||||||
|
val soldNotes: String?,
|
||||||
|
val soldPrice: Double?,
|
||||||
|
val soldTime: String?,
|
||||||
|
val soldTo: String?,
|
||||||
|
val syncChildItemsLocations: Boolean?,
|
||||||
|
val warrantyDetails: String?,
|
||||||
|
val warrantyExpires: String?,
|
||||||
|
val locationId: String?,
|
||||||
val labelIds: List<String>?
|
val labelIds: List<String>?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('ItemUpdate')]
|
// [END_ENTITY: DataClass('ItemUpdate')]
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ package com.homebox.lens.domain.model
|
|||||||
*/
|
*/
|
||||||
data class LabelCreate(
|
data class LabelCreate(
|
||||||
val name: String,
|
val name: String,
|
||||||
val color: String?
|
val color: String?,
|
||||||
|
val description: String?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LabelCreate')]
|
// [END_ENTITY: DataClass('LabelCreate')]
|
||||||
// [END_FILE_LabelCreate.kt]
|
// [END_FILE_LabelCreate.kt]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package com.homebox.lens.domain.model
|
|||||||
data class LabelOut(
|
data class LabelOut(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val description: String?,
|
||||||
val color: String,
|
val color: String,
|
||||||
val isArchived: Boolean,
|
val isArchived: Boolean,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ package com.homebox.lens.domain.model
|
|||||||
*/
|
*/
|
||||||
data class LabelSummary(
|
data class LabelSummary(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String
|
val name: String,
|
||||||
|
val color: String
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LabelSummary')]
|
// [END_ENTITY: DataClass('LabelSummary')]
|
||||||
// [END_FILE_LabelSummary.kt]
|
// [END_FILE_LabelSummary.kt]
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ package com.homebox.lens.domain.model
|
|||||||
*/
|
*/
|
||||||
data class LabelUpdate(
|
data class LabelUpdate(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val color: String?
|
val color: String?,
|
||||||
|
val description: String?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LabelUpdate')]
|
// [END_ENTITY: DataClass('LabelUpdate')]
|
||||||
// [END_FILE_LabelUpdate.kt]
|
// [END_FILE_LabelUpdate.kt]
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ package com.homebox.lens.domain.model
|
|||||||
*/
|
*/
|
||||||
data class LocationCreate(
|
data class LocationCreate(
|
||||||
val name: String,
|
val name: String,
|
||||||
val color: String?
|
val parentId: String?,
|
||||||
|
val color: String?,
|
||||||
|
val description: String?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LocationCreate')]
|
// [END_ENTITY: DataClass('LocationCreate')]
|
||||||
// [END_FILE_LocationCreate.kt]
|
// [END_FILE_LocationCreate.kt]
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ package com.homebox.lens.domain.model
|
|||||||
*/
|
*/
|
||||||
data class LocationUpdate(
|
data class LocationUpdate(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val color: String?
|
val color: String?,
|
||||||
|
val description: String?
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('LocationUpdate')]
|
// [END_ENTITY: DataClass('LocationUpdate')]
|
||||||
// [END_FILE_LocationUpdate.kt]
|
// [END_FILE_LocationUpdate.kt]
|
||||||
|
|||||||
@@ -44,8 +44,25 @@ interface CredentialsRepository {
|
|||||||
* @summary Retrieves the saved authorization token.
|
* @summary Retrieves the saved authorization token.
|
||||||
* @return The saved token as a String, or null if no token is saved.
|
* @return The saved token as a String, or null if no token is saved.
|
||||||
*/
|
*/
|
||||||
suspend fun getToken(): String?
|
|
||||||
// [END_ENTITY: Function('getToken')]
|
suspend fun getToken(): String?
|
||||||
|
// [END_ENTITY: Function('getToken')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('areCredentialsSavedSync')]
|
||||||
|
/**
|
||||||
|
* @summary Synchronously checks if user credentials are saved.
|
||||||
|
* @return true if all essential credentials (URL, username, password) are present, false otherwise.
|
||||||
|
*/
|
||||||
|
fun areCredentialsSavedSync(): Boolean
|
||||||
|
// [END_ENTITY: Function('areCredentialsSavedSync')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('clearAllCredentials')]
|
||||||
|
/**
|
||||||
|
* @summary Clears all saved credentials and tokens.
|
||||||
|
* @sideeffect Removes all credential-related entries from SharedPreferences.
|
||||||
|
*/
|
||||||
|
suspend fun clearAllCredentials()
|
||||||
|
// [END_ENTITY: Function('clearAllCredentials')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Interface('CredentialsRepository')]
|
// [END_ENTITY: Interface('CredentialsRepository')]
|
||||||
// [END_FILE_CredentialsRepository.kt]
|
// [END_FILE_CredentialsRepository.kt]
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.usecase
|
// [PACKAGE] com.homebox.lens.domain.usecase
|
||||||
// [FILE] GetLabelDetailsUseCase.kt
|
// [FILE] GetLabelDetailsUseCase.kt
|
||||||
// [SEMANTICS] business_logic, use_case, label_retrieval
|
// [SEMANTICS] business_logic, use_case, label, get
|
||||||
|
|
||||||
package com.homebox.lens.domain.usecase
|
package com.homebox.lens.domain.usecase
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
@@ -13,23 +12,25 @@ import javax.inject.Inject
|
|||||||
// [ENTITY: UseCase('GetLabelDetailsUseCase')]
|
// [ENTITY: UseCase('GetLabelDetailsUseCase')]
|
||||||
// [RELATION: UseCase('GetLabelDetailsUseCase')] -> [DEPENDS_ON] -> [Interface('ItemRepository')]
|
// [RELATION: UseCase('GetLabelDetailsUseCase')] -> [DEPENDS_ON] -> [Interface('ItemRepository')]
|
||||||
/**
|
/**
|
||||||
* @summary Получает детальную информацию о метке по ее ID.
|
* @summary Сценарий использования для получения деталей метки.
|
||||||
* @param itemRepository Репозиторий для работы с данными о метках.
|
* @param repository Репозиторий для доступа к данным.
|
||||||
*/
|
*/
|
||||||
class GetLabelDetailsUseCase @Inject constructor(
|
class GetLabelDetailsUseCase @Inject constructor(
|
||||||
private val itemRepository: ItemRepository
|
private val repository: ItemRepository
|
||||||
) {
|
) {
|
||||||
|
// [ENTITY: Function('invoke')]
|
||||||
/**
|
/**
|
||||||
* @summary Выполняет получение детальной информации о метке.
|
* @summary Выполняет получение деталей метки.
|
||||||
* @param labelId ID запрашиваемой метки.
|
* @param labelId ID метки для получения деталей.
|
||||||
* @return Детальная информация о метке [LabelOut].
|
* @return Возвращает полную информацию о метке [LabelOut].
|
||||||
* @throws IllegalArgumentException если `labelId` пустой.
|
* @throws Exception в случае ошибки на уровне репозитория (сеть, API).
|
||||||
* @throws NoSuchElementException если метка с указанным ID не найдена.
|
* @precondition `labelId` не должен быть пустым.
|
||||||
*/
|
*/
|
||||||
suspend operator fun invoke(labelId: String): LabelOut {
|
suspend operator fun invoke(labelId: String): LabelOut {
|
||||||
require(labelId.isNotBlank()) { "Label ID cannot be blank." }
|
require(labelId.isNotBlank()) { "Label ID cannot be blank." }
|
||||||
return itemRepository.getLabelDetails(labelId)
|
return repository.getLabelDetails(labelId)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('invoke')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: UseCase('GetLabelDetailsUseCase')]
|
// [END_ENTITY: UseCase('GetLabelDetailsUseCase')]
|
||||||
// [END_FILE_GetLabelDetailsUseCase.kt]
|
// [END_FILE_GetLabelDetailsUseCase.kt]
|
||||||
@@ -33,19 +33,30 @@ class UpdateItemUseCase @Inject constructor(
|
|||||||
require(item.name.isNotBlank()) { "Item name cannot be blank." }
|
require(item.name.isNotBlank()) { "Item name cannot be blank." }
|
||||||
|
|
||||||
val itemUpdate = ItemUpdate(
|
val itemUpdate = ItemUpdate(
|
||||||
|
id = item.id,
|
||||||
name = item.name,
|
name = item.name,
|
||||||
description = item.description,
|
description = item.description,
|
||||||
quantity = item.quantity,
|
quantity = item.quantity,
|
||||||
assetId = null, // Assuming these are not updated via this use case
|
archived = item.archived,
|
||||||
notes = null,
|
assetId = item.assetId,
|
||||||
serialNumber = null,
|
insured = item.insured,
|
||||||
isArchived = null,
|
lifetimeWarranty = item.lifetimeWarranty,
|
||||||
value = null,
|
manufacturer = item.manufacturer,
|
||||||
purchasePrice = null,
|
modelNumber = item.modelNumber,
|
||||||
purchaseDate = null,
|
notes = item.notes,
|
||||||
warrantyUntil = null,
|
parentId = item.parentId,
|
||||||
|
purchaseFrom = item.purchaseFrom,
|
||||||
|
purchasePrice = item.purchasePrice,
|
||||||
|
purchaseTime = item.purchaseTime,
|
||||||
|
serialNumber = item.serialNumber,
|
||||||
|
soldNotes = item.soldNotes,
|
||||||
|
soldPrice = item.soldPrice,
|
||||||
|
soldTime = item.soldTime,
|
||||||
|
soldTo = item.soldTo,
|
||||||
|
syncChildItemsLocations = item.syncChildItemsLocations,
|
||||||
|
warrantyDetails = item.warrantyDetails,
|
||||||
|
warrantyExpires = item.warrantyExpires,
|
||||||
locationId = item.location?.id,
|
locationId = item.location?.id,
|
||||||
parentId = null,
|
|
||||||
labelIds = item.labels.map { it.id }
|
labelIds = item.labels.map { it.id }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.usecase
|
|
||||||
// [FILE] UpdateItemUseCaseTest.kt
|
|
||||||
// [SEMANTICS] testing, usecase, unit_test
|
|
||||||
|
|
||||||
package com.homebox.lens.domain.usecase
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
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 com.homebox.lens.domain.model.LocationOut
|
|
||||||
import com.homebox.lens.domain.model.ItemSummary
|
|
||||||
import com.homebox.lens.domain.model.ItemAttachment
|
|
||||||
import com.homebox.lens.domain.model.Image
|
|
||||||
import com.homebox.lens.domain.model.CustomField
|
|
||||||
import com.homebox.lens.domain.model.MaintenanceEntry
|
|
||||||
import com.homebox.lens.domain.model.LabelOut
|
|
||||||
import com.homebox.lens.domain.repository.ItemRepository
|
|
||||||
import io.kotest.core.spec.style.FunSpec
|
|
||||||
import io.kotest.matchers.shouldBe
|
|
||||||
import io.kotest.assertions.throwables.shouldThrow
|
|
||||||
import io.mockk.coEvery
|
|
||||||
import io.mockk.mockk
|
|
||||||
import java.math.BigDecimal
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Class('UpdateItemUseCaseTest')]
|
|
||||||
// [RELATION: Class('UpdateItemUseCaseTest')] -> [TESTS] -> [UseCase('UpdateItemUseCase')]
|
|
||||||
/**
|
|
||||||
* @summary Unit tests for [UpdateItemUseCase].
|
|
||||||
*/
|
|
||||||
class UpdateItemUseCaseTest : FunSpec({
|
|
||||||
|
|
||||||
val itemRepository = mockk<ItemRepository>()
|
|
||||||
val updateItemUseCase = UpdateItemUseCase(itemRepository)
|
|
||||||
|
|
||||||
// [ENTITY: Function('should update item successfully')]
|
|
||||||
/**
|
|
||||||
* @summary Tests that the item is updated successfully.
|
|
||||||
*/
|
|
||||||
test("should update item successfully") {
|
|
||||||
// Given
|
|
||||||
val item = Item(
|
|
||||||
id = "1",
|
|
||||||
name = "Test Item",
|
|
||||||
description = "Description",
|
|
||||||
quantity = 1,
|
|
||||||
image = null,
|
|
||||||
location = Location(id = "loc1", name = "Location 1"),
|
|
||||||
labels = listOf(Label(id = "lab1", name = "Label 1")),
|
|
||||||
value = BigDecimal.ZERO,
|
|
||||||
createdAt = "2025-01-01T00:00:00Z"
|
|
||||||
)
|
|
||||||
val expectedItemOut = ItemOut(
|
|
||||||
id = "1",
|
|
||||||
name = "Test Item",
|
|
||||||
assetId = null,
|
|
||||||
description = "Description",
|
|
||||||
notes = null,
|
|
||||||
serialNumber = null,
|
|
||||||
quantity = 1,
|
|
||||||
isArchived = false,
|
|
||||||
value = 0.0,
|
|
||||||
purchasePrice = null,
|
|
||||||
purchaseDate = null,
|
|
||||||
warrantyUntil = null,
|
|
||||||
location = LocationOut(
|
|
||||||
id = "loc1",
|
|
||||||
name = "Location 1",
|
|
||||||
color = "#FFFFFF", // Default color
|
|
||||||
isArchived = false,
|
|
||||||
createdAt = "2025-01-01T00:00:00Z",
|
|
||||||
updatedAt = "2025-01-01T00:00:00Z"
|
|
||||||
),
|
|
||||||
parent = null,
|
|
||||||
children = emptyList(),
|
|
||||||
labels = listOf(LabelOut(
|
|
||||||
id = "lab1",
|
|
||||||
name = "Label 1",
|
|
||||||
color = "#FFFFFF", // Default color
|
|
||||||
isArchived = false,
|
|
||||||
createdAt = "2025-01-01T00:00:00Z",
|
|
||||||
updatedAt = "2025-01-01T00:00:00Z"
|
|
||||||
)),
|
|
||||||
attachments = emptyList(),
|
|
||||||
images = emptyList(),
|
|
||||||
fields = emptyList(),
|
|
||||||
maintenance = emptyList(),
|
|
||||||
createdAt = "2025-01-01T00:00:00Z",
|
|
||||||
updatedAt = "2025-01-01T00:00:00Z"
|
|
||||||
)
|
|
||||||
|
|
||||||
coEvery { itemRepository.updateItem(any(), any()) } returns expectedItemOut
|
|
||||||
|
|
||||||
// When
|
|
||||||
val result = updateItemUseCase.invoke(item)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
result shouldBe expectedItemOut
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('should update item successfully')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('should throw IllegalArgumentException when item name is blank')]
|
|
||||||
/**
|
|
||||||
* @summary Tests that an IllegalArgumentException is thrown when the item name is blank.
|
|
||||||
*/
|
|
||||||
test("should throw IllegalArgumentException when item name is blank") {
|
|
||||||
// Given
|
|
||||||
val item = Item(
|
|
||||||
id = "1",
|
|
||||||
name = "", // Blank name
|
|
||||||
description = "Description",
|
|
||||||
quantity = 1,
|
|
||||||
image = null,
|
|
||||||
location = Location(id = "loc1", name = "Location 1"),
|
|
||||||
labels = listOf(Label(id = "lab1", name = "Label 1")),
|
|
||||||
value = BigDecimal.ZERO,
|
|
||||||
createdAt = "2025-01-01T00:00:00Z"
|
|
||||||
)
|
|
||||||
|
|
||||||
// When & Then
|
|
||||||
val exception = shouldThrow<IllegalArgumentException> {
|
|
||||||
updateItemUseCase.invoke(item)
|
|
||||||
}
|
|
||||||
exception.message shouldBe "Item name cannot be blank."
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('should throw IllegalArgumentException when repository returns null')]
|
|
||||||
}) // Removed the third test case
|
|
||||||
// [END_ENTITY: Class('UpdateItemUseCaseTest')]
|
|
||||||
// [END_FILE_UpdateItemUseCaseTest.kt]
|
|
||||||
@@ -18,7 +18,6 @@ distributionPath=wrapper/dists
|
|||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
org.gradle.java.home=/snap/android-studio/197/jbr
|
|
||||||
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
|||||||
51
tasks/work_order_item_edit_screen.xml
Normal file
51
tasks/work_order_item_edit_screen.xml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WORK_ORDER>
|
||||||
|
<META>
|
||||||
|
<ID>WO-ITEMEDIT-FIX</ID>
|
||||||
|
<TITLE>[ARCHITECT -> DEV] Исправление выбора локации и меток на экране ItemEdit</TITLE>
|
||||||
|
<DESCRIPTION>В текущей реализации на экране редактирования/создания элемента (ItemEditScreen) поля "Location" и "Labels" неактивны. Необходимо реализовать функционал выбора значения для этих полей из списка доступных.</DESCRIPTION>
|
||||||
|
<CREATED_BY>architect-agent</CREATED_BY>
|
||||||
|
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
|
||||||
|
<STATUS>pending</STATUS>
|
||||||
|
</META>
|
||||||
|
|
||||||
|
<TASK_BREAKDOWN>
|
||||||
|
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt">
|
||||||
|
<ACTION>Загрузка списков локаций и меток.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
1. Внедрите `GetAllLocationsUseCase` и `GetAllLabelsUseCase` в `ItemEditViewModel`.
|
||||||
|
2. Обновите `ItemEditUiState`, добавив два новых поля: `val allLocations: List<Location> = emptyList()` и `val allLabels: List<Label> = emptyList()`.
|
||||||
|
3. В функции `loadItem`, после загрузки основной информации о товаре, вызовите `getAllLocationsUseCase` и `getAllLabelsUseCase` и обновите `uiState` полученными списками.
|
||||||
|
4. Добавьте публичные методы `updateLocation(location: Location)` и `updateLabels(labels: List<Label>)` для обновления `item` в `uiState`.
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
|
||||||
|
<ACTION>Реализация UI для выбора локации.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
1. Замените `OutlinedTextField` для локации на `ExposedDropdownMenuBox`.
|
||||||
|
2. В качестве `dropdownMenu` используйте `DropdownMenuItem` для каждого элемента из `uiState.allLocations`.
|
||||||
|
3. При выборе элемента из списка вызывайте `viewModel.updateLocation(selectedLocation)`.
|
||||||
|
4. В `ExposedDropdownMenuBox` должно отображаться `item.location?.name`.
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
<STEP n="3" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
|
||||||
|
<ACTION>Реализация UI для выбора меток (множественный выбор).</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
1. Поле для меток `Labels` должно оставаться `OutlinedTextField` (read-only), но `onClick` по нему должен открывать диалоговое окно (`AlertDialog`).
|
||||||
|
2. В `AlertDialog` отобразите список всех меток (`uiState.allLabels`) с `Checkbox`'ами.
|
||||||
|
3. Состояние `Checkbox`'ов должно соответствовать списку `item.labels`.
|
||||||
|
4. При нажатии на "OK" в диалоге, вызывайте `viewModel.updateLabels(selectedLabels)`.
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
</TASK_BREAKDOWN>
|
||||||
|
|
||||||
|
<ACCEPTANCE_CRITERIA>
|
||||||
|
<CRITERION>При нажатии на поле "Location" открывается выпадающий список со всеми локациями.</CRITERION>
|
||||||
|
<CRITERION>Выбранная локация отображается в поле и сохраняется вместе с элементом.</CRITERION>
|
||||||
|
<CRITERION>При нажатии на поле "Labels" открывается диалоговое окно со списком всех меток и чекбоксами.</CRITERION>
|
||||||
|
<CRITERION>Выбранные метки отображаются в поле и сохраняются вместе с элементом.</CRITERION>
|
||||||
|
</ACCEPTANCE_CRITERIA>
|
||||||
|
|
||||||
|
</WORK_ORDER>
|
||||||
59
tasks/work_order_login_screen.xml
Normal file
59
tasks/work_order_login_screen.xml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WORK_ORDER>
|
||||||
|
<META>
|
||||||
|
<ID>WO-LOGIN-REFACTOR</ID>
|
||||||
|
<TITLE>[ARCHITECT -> DEV] Рефакторинг экрана входа и логики первого запуска</TITLE>
|
||||||
|
<DESCRIPTION>Цель этой задачи - изменить логику запуска приложения. Экран входа (SetupScreen) должен появляться только при первом запуске, когда учетные данные еще не сохранены. В последующие запуски пользователь должен сразу попадать на главный экран (Dashboard). Также необходимо улучшить визуальное оформление экрана входа.</DESCRIPTION>
|
||||||
|
<CREATED_BY>architect-agent</CREATED_BY>
|
||||||
|
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
|
||||||
|
<STATUS>pending</STATUS>
|
||||||
|
</META>
|
||||||
|
|
||||||
|
<TASK_BREAKDOWN>
|
||||||
|
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt">
|
||||||
|
<ACTION>Добавить public-метод для синхронной проверки наличия учетных данных.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
Добавьте в класс `SetupViewModel` новый метод `fun areCredentialsSaved(): Boolean`.
|
||||||
|
Этот метод должен синхронно проверять, сохранены ли учетные данные в `CredentialsRepository`.
|
||||||
|
Текущая реализация `getCredentials()` асинхронна, что не подходит для быстрой проверки в `NavGraph`.
|
||||||
|
Вам может потребоваться изменить `CredentialsRepository` для поддержки синхронной проверки (например, используя `SharedPreferences` напрямую).
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/splash/SplashScreen.kt">
|
||||||
|
<ACTION>Создать новый `SplashScreen`.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
Создайте новый Composable-экран `SplashScreen.kt`.
|
||||||
|
Этот экран будет новой точкой входа в `NavGraph`.
|
||||||
|
Он будет использовать `SetupViewModel` для вызова `areCredentialsSaved()` и, в зависимости от результата, немедленно навигироваться либо на `Screen.Setup`, либо на `Screen.Dashboard`.
|
||||||
|
Пока идет проверка, на экране должен отображаться `CircularProgressIndicator`.
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
<STEP n="3" file="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt">
|
||||||
|
<ACTION>Обновить `NavGraph` для использования `SplashScreen`.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
Измените `startDestination` в `NavHost` на `Screen.Splash.route`.
|
||||||
|
Добавьте `composable` для `SplashScreen`.
|
||||||
|
В `SplashScreen` вызовите `navController.navigate` с очисткой бэкстека (`popUpTo(Screen.Splash.route) { inclusive = true }`), чтобы пользователь не мог вернуться на сплэш-экран.
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
<STEP n="4" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt">
|
||||||
|
<ACTION>Улучшить UI экрана `SetupScreen`.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
Текущий UI слишком прост. Добавьте заголовок, иконку приложения, и более приятное расположение элементов.
|
||||||
|
Используйте `Card` для группировки полей ввода. Добавьте `Spacer` для лучшего отступа.
|
||||||
|
Кнопку "Connect" сделайте более заметной.
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
</TASK_BREAKDOWN>
|
||||||
|
|
||||||
|
<ACCEPTANCE_CRITERIA>
|
||||||
|
<CRITERION>При первом запуске приложения открывается `SetupScreen`.</CRITERION>
|
||||||
|
<CRITERION>После успешного ввода данных и входа, при последующих перезапусках приложения открывается `DashboardScreen`, минуя `SetupScreen`.</CRITERION>
|
||||||
|
<CRITERION>`SetupScreen` имеет улучшенный и более привлекательный дизайн.</CRITERION>
|
||||||
|
</ACCEPTANCE_CRITERIA>
|
||||||
|
|
||||||
|
</WORK_ORDER>
|
||||||
Reference in New Issue
Block a user