211
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("kotlin-kapt")
|
||||
// id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
|
||||
@@ -46,9 +47,7 @@ android {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = Versions.composeCompiler
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
@@ -61,6 +60,8 @@ dependencies {
|
||||
implementation(project(":data"))
|
||||
// [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity)
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":feature:scan"))
|
||||
implementation(project(":feature:dashboard"))
|
||||
|
||||
// [DEPENDENCY] AndroidX
|
||||
implementation(Libs.coreKtx)
|
||||
@@ -68,7 +69,7 @@ dependencies {
|
||||
implementation(Libs.activityCompose)
|
||||
|
||||
// [DEPENDENCY] Compose
|
||||
implementation(platform(Libs.composeBom))
|
||||
|
||||
implementation(Libs.composeUi)
|
||||
implementation(Libs.composeUiGraphics)
|
||||
implementation(Libs.composeUiToolingPreview)
|
||||
@@ -93,7 +94,7 @@ dependencies {
|
||||
testImplementation("app.cash.turbine:turbine:1.1.0")
|
||||
androidTestImplementation(Libs.extJunit)
|
||||
androidTestImplementation(Libs.espressoCore)
|
||||
androidTestImplementation(platform(Libs.composeBom))
|
||||
|
||||
androidTestImplementation(Libs.composeUiTestJunit4)
|
||||
debugImplementation(Libs.composeUiTooling)
|
||||
debugImplementation(Libs.composeUiTestManifest)
|
||||
|
||||
@@ -15,7 +15,7 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
|
||||
import com.homebox.lens.feature.dashboard.addDashboardScreen
|
||||
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
|
||||
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
|
||||
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
|
||||
@@ -25,6 +25,10 @@ import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
|
||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||
import com.homebox.lens.feature.scan.ScanScreen
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
// import com.homebox.lens.ui.screen.settings.SettingsScreen
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('NavGraph')]
|
||||
@@ -59,12 +63,24 @@ fun NavGraph(
|
||||
}
|
||||
})
|
||||
}
|
||||
composable(route = Screen.Dashboard.route) {
|
||||
DashboardScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
)
|
||||
}
|
||||
addDashboardScreen(
|
||||
route = Screen.Dashboard.route,
|
||||
currentRoute = currentRoute,
|
||||
navigateToScan = navigationActions::navigateToScan,
|
||||
navigateToSearch = navigationActions::navigateToSearch,
|
||||
navigateToInventoryListWithLocation = navigationActions::navigateToInventoryListWithLocation,
|
||||
navigateToInventoryListWithLabel = navigationActions::navigateToInventoryListWithLabel,
|
||||
MainScaffoldContent = { topBarTitle, currentRoute, topBarActions, content ->
|
||||
MainScaffold(
|
||||
topBarTitle = topBarTitle,
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
topBarActions = topBarActions,
|
||||
content = content
|
||||
)
|
||||
},
|
||||
HomeboxLensTheme = { content -> HomeboxLensTheme(content = content) }
|
||||
)
|
||||
composable(route = Screen.InventoryList.route) {
|
||||
InventoryListScreen(
|
||||
currentRoute = currentRoute,
|
||||
@@ -137,6 +153,20 @@ fun NavGraph(
|
||||
navigationActions = navigationActions
|
||||
)
|
||||
}
|
||||
composable(Screen.Settings.route) {
|
||||
com.homebox.lens.ui.screen.settings.SettingsScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
onNavigateUp = { navController.navigateUp() }
|
||||
)
|
||||
}
|
||||
composable(Screen.Scan.route) { backStackEntry ->
|
||||
ScanScreen(onBarcodeResult = { barcode ->
|
||||
val previousBackStackEntry = navController.previousBackStackEntry
|
||||
previousBackStackEntry?.savedStateHandle?.set("barcodeResult", barcode)
|
||||
navController.popBackStack()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('NavGraph')]
|
||||
|
||||
@@ -15,7 +15,7 @@ import timber.log.Timber
|
||||
* @param navController Контроллер Jetpack Navigation.
|
||||
* @invariant Все навигационные действия должны использовать предоставленный navController.
|
||||
*/
|
||||
class NavigationActions(private val navController: NavHostController) {
|
||||
class NavigationActions(val navController: NavHostController) {
|
||||
|
||||
// [ENTITY: Function('navigateToDashboard')]
|
||||
/**
|
||||
@@ -65,6 +65,30 @@ class NavigationActions(private val navController: NavHostController) {
|
||||
}
|
||||
// [END_ENTITY: Function('navigateToSearch')]
|
||||
|
||||
// [ENTITY: Function('navigateToScan')]
|
||||
/**
|
||||
* @summary Навигация на экран сканирования QR/штрих-кодов.
|
||||
*/
|
||||
fun navigateToScan() {
|
||||
Timber.i("[INFO][ACTION][navigate_to_scan] Navigating to Scan screen.")
|
||||
navController.navigate(Screen.Scan.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('navigateToScan')]
|
||||
|
||||
// [ENTITY: Function('navigateToSettings')]
|
||||
/**
|
||||
* @summary Навигация на экран настроек.
|
||||
*/
|
||||
fun navigateToSettings() {
|
||||
Timber.i("[INFO][ACTION][navigate_to_settings] Navigating to Settings.")
|
||||
navController.navigate(Screen.Settings.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('navigateToSettings')]
|
||||
|
||||
// [ENTITY: Function('navigateToInventoryListWithLabel')]
|
||||
fun navigateToInventoryListWithLabel(labelId: String) {
|
||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Navigating to Inventory with label: %s", labelId)
|
||||
|
||||
@@ -118,6 +118,14 @@ sealed class Screen(val route: String) {
|
||||
// [ENTITY: Object('Search')]
|
||||
data object Search : Screen("search_screen")
|
||||
// [END_ENTITY: Object('Search')]
|
||||
|
||||
// [ENTITY: Object('Settings')]
|
||||
data object Settings : Screen("settings_screen")
|
||||
// [END_ENTITY: Object('Settings')]
|
||||
|
||||
// [ENTITY: Object('Scan')]
|
||||
data object Scan : Screen("scan_screen")
|
||||
// [END_ENTITY: Object('Scan')]
|
||||
}
|
||||
// [END_ENTITY: SealedClass('Screen')]
|
||||
// [END_FILE_Screen.kt]
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -90,6 +91,15 @@ internal fun AppDrawerContent(
|
||||
onCloseDrawer()
|
||||
}
|
||||
)
|
||||
NavigationDrawerItem(
|
||||
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
|
||||
label = { Text("Настройки") },
|
||||
selected = false,
|
||||
onClick = {
|
||||
navigationActions.navigateToSettings()
|
||||
onCloseDrawer()
|
||||
}
|
||||
)
|
||||
// [AI_NOTE]: Add Profile and Tools items
|
||||
Divider()
|
||||
NavigationDrawerItem(
|
||||
|
||||
@@ -8,6 +8,7 @@ package com.homebox.lens.ui.common
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -36,7 +37,10 @@ fun MainScaffold(
|
||||
topBarTitle: String,
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions,
|
||||
onNavigateUp: (() -> Unit)? = null,
|
||||
topBarActions: @Composable () -> Unit = {},
|
||||
snackbarHost: @Composable () -> Unit = {},
|
||||
floatingActionButton: @Composable () -> Unit = {},
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
@@ -57,16 +61,27 @@ fun MainScaffold(
|
||||
TopAppBar(
|
||||
title = { Text(topBarTitle) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
||||
Icon(
|
||||
Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.cd_open_navigation_drawer)
|
||||
)
|
||||
if (onNavigateUp != null) {
|
||||
IconButton(onClick = onNavigateUp) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.cd_navigate_up)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
||||
Icon(
|
||||
Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.cd_open_navigation_drawer)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = { topBarActions() }
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = snackbarHost,
|
||||
floatingActionButton = floatingActionButton
|
||||
) { paddingValues ->
|
||||
content(paddingValues)
|
||||
}
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
||||
// [FILE] DashboardScreen.kt
|
||||
// [SEMANTICS] ui, screen, dashboard, compose, navigation
|
||||
package com.homebox.lens.ui.screen.dashboard
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.domain.model.*
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('DashboardScreen')]
|
||||
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [ViewModel('DashboardViewModel')]
|
||||
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
||||
/**
|
||||
* @summary Главная Composable-функция для экрана "Панель управления".
|
||||
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
|
||||
*/
|
||||
@Composable
|
||||
fun DashboardScreen(
|
||||
viewModel: DashboardViewModel = hiltViewModel(),
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.dashboard_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
topBarActions = {
|
||||
IconButton(onClick = { navigationActions.navigateToSearch() }) {
|
||||
Icon(
|
||||
Icons.Default.Search,
|
||||
contentDescription = stringResource(id = R.string.cd_scan_qr_code) // [AI_NOTE]: Rename string resource
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
DashboardContent(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
uiState = uiState,
|
||||
onLocationClick = { location ->
|
||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Location chip clicked: ${location.id}. Navigating...")
|
||||
navigationActions.navigateToInventoryListWithLocation(location.id)
|
||||
},
|
||||
onLabelClick = { label ->
|
||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Label chip clicked: ${label.id}. Navigating...")
|
||||
navigationActions.navigateToInventoryListWithLabel(label.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardScreen')]
|
||||
|
||||
// [ENTITY: Function('DashboardContent')]
|
||||
// [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
* @summary Отображает основной контент экрана в зависимости от uiState.
|
||||
* @param modifier Модификатор для стилизации.
|
||||
* @param uiState Текущее состояние UI экрана.
|
||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
*/
|
||||
@Composable
|
||||
private fun DashboardContent(
|
||||
modifier: Modifier = Modifier,
|
||||
uiState: DashboardUiState,
|
||||
onLocationClick: (LocationOutCount) -> Unit,
|
||||
onLabelClick: (LabelOut) -> Unit
|
||||
) {
|
||||
when (uiState) {
|
||||
is DashboardUiState.Loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
is DashboardUiState.Error -> {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = uiState.message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
is DashboardUiState.Success -> {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||
item { StatisticsSection(statistics = uiState.statistics) }
|
||||
item { RecentlyAddedSection(items = uiState.recentlyAddedItems) }
|
||||
item { LocationsSection(locations = uiState.locations, onLocationClick = onLocationClick) }
|
||||
item { LabelsSection(labels = uiState.labels, onLabelClick = onLabelClick) }
|
||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardContent')]
|
||||
|
||||
// [ENTITY: Function('StatisticsSection')]
|
||||
// [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
|
||||
/**
|
||||
* @summary Секция для отображения общей статистики.
|
||||
* @param statistics Объект со статистическими данными.
|
||||
*/
|
||||
@Composable
|
||||
private fun StatisticsSection(statistics: GroupStatistics) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.dashboard_section_quick_stats),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Card {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier
|
||||
.height(120.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) }
|
||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) }
|
||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) }
|
||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('StatisticsSection')]
|
||||
|
||||
// [ENTITY: Function('StatisticCard')]
|
||||
/**
|
||||
* @summary Карточка для отображения одного статистического показателя.
|
||||
* @param title Название показателя.
|
||||
* @param value Значение показателя.
|
||||
*/
|
||||
@Composable
|
||||
private fun StatisticCard(title: String, value: String) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
||||
Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center)
|
||||
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('StatisticCard')]
|
||||
|
||||
// [ENTITY: Function('RecentlyAddedSection')]
|
||||
// [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* @summary Секция для отображения недавно добавленных элементов.
|
||||
* @param items Список элементов для отображения.
|
||||
*/
|
||||
@Composable
|
||||
private fun RecentlyAddedSection(items: List<ItemSummary>) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.dashboard_section_recently_added),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (items.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.items_not_found),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
} else {
|
||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
items(items) { item ->
|
||||
ItemCard(item = item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('RecentlyAddedSection')]
|
||||
|
||||
// [ENTITY: Function('ItemCard')]
|
||||
// [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* @summary Карточка для отображения краткой информации об элементе.
|
||||
* @param item Элемент для отображения.
|
||||
*/
|
||||
@Composable
|
||||
private fun ItemCard(item: ItemSummary) {
|
||||
Card(modifier = Modifier.width(150.dp)) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
// [AI_NOTE]: Add image here from item.image
|
||||
Spacer(modifier = Modifier
|
||||
.height(80.dp)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
|
||||
Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('ItemCard')]
|
||||
|
||||
// [ENTITY: Function('LocationsSection')]
|
||||
// [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
||||
/**
|
||||
* @summary Секция для отображения местоположений в виде чипсов.
|
||||
* @param locations Список местоположений.
|
||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick: (LocationOutCount) -> Unit) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.dashboard_section_locations),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
locations.forEach { location ->
|
||||
SuggestionChip(
|
||||
onClick = { onLocationClick(location) },
|
||||
label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LocationsSection')]
|
||||
|
||||
// [ENTITY: Function('LabelsSection')]
|
||||
// [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
|
||||
/**
|
||||
* @summary Секция для отображения меток в виде чипсов.
|
||||
* @param labels Список меток.
|
||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Unit) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.dashboard_section_labels),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
labels.forEach { label ->
|
||||
SuggestionChip(
|
||||
onClick = { onLabelClick(label) },
|
||||
label = { Text(label.name) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LabelsSection')]
|
||||
|
||||
// [ENTITY: Function('DashboardContentSuccessPreview')]
|
||||
@Preview(showBackground = true, name = "Dashboard Success State")
|
||||
@Composable
|
||||
fun DashboardContentSuccessPreview() {
|
||||
val previewState = DashboardUiState.Success(
|
||||
statistics = GroupStatistics(
|
||||
items = 123,
|
||||
totalValue = 9999.99,
|
||||
locations = 5,
|
||||
labels = 8
|
||||
),
|
||||
locations = listOf(
|
||||
LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""),
|
||||
LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""),
|
||||
LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""),
|
||||
LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""),
|
||||
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
|
||||
),
|
||||
labels = listOf(
|
||||
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
|
||||
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
|
||||
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
|
||||
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
|
||||
),
|
||||
recentlyAddedItems = emptyList()
|
||||
)
|
||||
HomeboxLensTheme {
|
||||
DashboardContent(
|
||||
uiState = previewState,
|
||||
onLocationClick = {},
|
||||
onLabelClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardContentSuccessPreview')]
|
||||
|
||||
// [ENTITY: Function('DashboardContentLoadingPreview')]
|
||||
@Preview(showBackground = true, name = "Dashboard Loading State")
|
||||
@Composable
|
||||
fun DashboardContentLoadingPreview() {
|
||||
HomeboxLensTheme {
|
||||
DashboardContent(
|
||||
uiState = DashboardUiState.Loading,
|
||||
onLocationClick = {},
|
||||
onLabelClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardContentLoadingPreview')]
|
||||
|
||||
// [ENTITY: Function('DashboardContentErrorPreview')]
|
||||
@Preview(showBackground = true, name = "Dashboard Error State")
|
||||
@Composable
|
||||
fun DashboardContentErrorPreview() {
|
||||
HomeboxLensTheme {
|
||||
DashboardContent(
|
||||
uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)),
|
||||
onLocationClick = {},
|
||||
onLabelClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardContentErrorPreview')]
|
||||
// [END_FILE_DashboardScreen.kt]
|
||||
@@ -1,55 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
||||
// [FILE] DashboardUiState.kt
|
||||
// [SEMANTICS] ui, state, dashboard
|
||||
package com.homebox.lens.ui.screen.dashboard
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.GroupStatistics
|
||||
import com.homebox.lens.domain.model.ItemSummary
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
* @summary Определяет все возможные состояния для экрана "Дэшборд".
|
||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
||||
*/
|
||||
sealed interface DashboardUiState {
|
||||
// [ENTITY: DataClass('Success')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* @summary Состояние успешной загрузки данных.
|
||||
* @param statistics Статистика по инвентарю.
|
||||
* @param locations Список локаций со счетчиками.
|
||||
* @param labels Список всех меток.
|
||||
* @param recentlyAddedItems Список недавно добавленных товаров.
|
||||
*/
|
||||
data class Success(
|
||||
val statistics: GroupStatistics,
|
||||
val locations: List<LocationOutCount>,
|
||||
val labels: List<LabelOut>,
|
||||
val recentlyAddedItems: List<ItemSummary>
|
||||
) : DashboardUiState
|
||||
// [END_ENTITY: DataClass('Success')]
|
||||
|
||||
// [ENTITY: DataClass('Error')]
|
||||
/**
|
||||
* @summary Состояние ошибки во время загрузки данных.
|
||||
* @param message Человекочитаемое сообщение об ошибке.
|
||||
*/
|
||||
data class Error(val message: String) : DashboardUiState
|
||||
// [END_ENTITY: DataClass('Error')]
|
||||
|
||||
// [ENTITY: Object('Loading')]
|
||||
/**
|
||||
* @summary Состояние, когда данные для экрана загружаются.
|
||||
*/
|
||||
data object Loading : DashboardUiState
|
||||
// [END_ENTITY: Object('Loading')]
|
||||
}
|
||||
// [END_ENTITY: SealedInterface('DashboardUiState')]
|
||||
// [END_FILE_DashboardUiState.kt]
|
||||
@@ -1,85 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
||||
// [FILE] DashboardViewModel.kt
|
||||
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
|
||||
package com.homebox.lens.ui.screen.dashboard
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetRecentlyAddedItemsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: ViewModel('DashboardViewModel')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetStatisticsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
* @summary ViewModel для главного экрана (Dashboard).
|
||||
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
|
||||
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
|
||||
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class DashboardViewModel @Inject constructor(
|
||||
private val getStatisticsUseCase: GetStatisticsUseCase,
|
||||
private val getAllLocationsUseCase: GetAllLocationsUseCase,
|
||||
private val getAllLabelsUseCase: GetAllLabelsUseCase,
|
||||
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadDashboardData()
|
||||
}
|
||||
|
||||
// [ENTITY: Function('loadDashboardData')]
|
||||
/**
|
||||
* @summary Загружает все необходимые данные для экрана Dashboard.
|
||||
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
|
||||
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
|
||||
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
|
||||
*/
|
||||
fun loadDashboardData() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = DashboardUiState.Loading
|
||||
Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.")
|
||||
|
||||
val statsFlow = flow { emit(getStatisticsUseCase()) }
|
||||
val locationsFlow = flow { emit(getAllLocationsUseCase()) }
|
||||
val labelsFlow = flow { emit(getAllLabelsUseCase()) }
|
||||
val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
|
||||
|
||||
combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
|
||||
DashboardUiState.Success(
|
||||
statistics = stats,
|
||||
locations = locations,
|
||||
labels = labels,
|
||||
recentlyAddedItems = recentItems
|
||||
)
|
||||
}.catch { exception ->
|
||||
Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.")
|
||||
_uiState.value = DashboardUiState.Error(
|
||||
message = exception.message ?: "Could not load dashboard data."
|
||||
)
|
||||
}.collect { successState ->
|
||||
Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.")
|
||||
_uiState.value = successState
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('loadDashboardData')]
|
||||
}
|
||||
// [END_ENTITY: ViewModel('DashboardViewModel')]
|
||||
// [END_FILE_DashboardViewModel.kt]
|
||||
@@ -5,28 +5,50 @@
|
||||
package com.homebox.lens.ui.screen.itemedit
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberDatePickerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
@@ -35,7 +57,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('ItemEditScreen')]
|
||||
@@ -51,22 +79,33 @@ import timber.log.Timber
|
||||
* @param viewModel ViewModel для управления состоянием экрана.
|
||||
* @param onSaveSuccess Callback, вызываемый после успешного сохранения товара.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ItemEditScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions,
|
||||
itemId: String?,
|
||||
viewModel: ItemEditViewModel = hiltViewModel(),
|
||||
onSaveSuccess: () -> Unit
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions,
|
||||
itemId: String?,
|
||||
viewModel: ItemEditViewModel = hiltViewModel(),
|
||||
onSaveSuccess: () -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val navBackStackEntry = navigationActions.navController.currentBackStackEntry
|
||||
|
||||
LaunchedEffect(itemId) {
|
||||
Timber.i("[INFO][ENTRYPOINT][item_edit_screen_init] Initializing ItemEditScreen for item ID: %s", itemId)
|
||||
viewModel.loadItem(itemId)
|
||||
}
|
||||
|
||||
LaunchedEffect(navBackStackEntry) {
|
||||
navBackStackEntry?.savedStateHandle?.get<String>("barcodeResult")?.let { barcode ->
|
||||
viewModel.updateAssetId(barcode)
|
||||
navBackStackEntry.savedStateHandle?.remove<String>("barcodeResult")
|
||||
Timber.i("[INFO][ACTION][barcode_received] Received barcode: %s", barcode)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.error) {
|
||||
uiState.error?.let {
|
||||
snackbarHostState.showSnackbar(it)
|
||||
@@ -75,7 +114,7 @@ fun ItemEditScreen(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.saveCompleted.collect {
|
||||
viewModel.saveCompleted.collect {
|
||||
Timber.i("[INFO][ACTION][save_completed_callback] Item save completed. Triggering onSaveSuccess.")
|
||||
onSaveSuccess()
|
||||
}
|
||||
@@ -84,52 +123,310 @@ fun ItemEditScreen(
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.item_edit_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
) {
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
Timber.i("[INFO][ACTION][save_button_click] Save button clicked.")
|
||||
viewModel.saveItem()
|
||||
}) {
|
||||
Icon(Icons.Filled.Save, contentDescription = stringResource(R.string.save_item))
|
||||
}
|
||||
navigationActions = navigationActions,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
Timber.i("[INFO][ACTION][save_button_click] Save button clicked.")
|
||||
viewModel.saveItem()
|
||||
}) {
|
||||
Icon(Icons.Filled.Save, contentDescription = stringResource(R.string.save_item))
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
} else {
|
||||
uiState.item?.let { item ->
|
||||
OutlinedTextField(
|
||||
value = item.name,
|
||||
onValueChange = { viewModel.updateName(it) },
|
||||
label = { Text(stringResource(R.string.item_name)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = item.description ?: "",
|
||||
onValueChange = { viewModel.updateDescription(it) },
|
||||
label = { Text(stringResource(R.string.item_description)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = item.quantity.toString(),
|
||||
onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) },
|
||||
label = { Text(stringResource(R.string.item_quantity)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
// Add more fields as needed
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
} else {
|
||||
uiState.item?.let { item ->
|
||||
OutlinedTextField(
|
||||
value = item.name,
|
||||
onValueChange = { viewModel.updateName(it) },
|
||||
label = { Text(stringResource(R.string.item_name)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = item.description ?: "",
|
||||
onValueChange = { viewModel.updateDescription(it) },
|
||||
label = { Text(stringResource(R.string.item_description)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = item.quantity.toString(),
|
||||
onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) },
|
||||
label = { Text(stringResource(R.string.item_quantity)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
// Asset ID
|
||||
OutlinedTextField(
|
||||
value = item.assetId ?: "",
|
||||
onValueChange = { viewModel.updateAssetId(it) },
|
||||
label = { Text(stringResource(R.string.item_asset_id)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
Timber.d("[DEBUG][ACTION][scan_qr_code_click] Scan QR code button clicked.")
|
||||
navigationActions.navigateToScan()
|
||||
}) {
|
||||
Icon(Icons.Filled.QrCodeScanner, contentDescription = stringResource(R.string.scan_qr_code))
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Notes
|
||||
OutlinedTextField(
|
||||
value = item.notes ?: "",
|
||||
onValueChange = { viewModel.updateNotes(it) },
|
||||
label = { Text(stringResource(R.string.item_notes)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Serial Number
|
||||
OutlinedTextField(
|
||||
value = item.serialNumber ?: "",
|
||||
onValueChange = { viewModel.updateSerialNumber(it) },
|
||||
label = { Text(stringResource(R.string.item_serial_number)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Purchase Price
|
||||
OutlinedTextField(
|
||||
value = item.purchasePrice?.toString() ?: "",
|
||||
onValueChange = { viewModel.updatePurchasePrice(it.toDoubleOrNull()) },
|
||||
label = { Text(stringResource(R.string.item_purchase_price)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Purchase Date
|
||||
var showPurchaseDatePicker by remember { mutableStateOf(false) }
|
||||
val purchaseDatePickerState = rememberDatePickerState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
OutlinedTextField(
|
||||
value = item.purchaseDate ?: "",
|
||||
onValueChange = { }, // Read-only
|
||||
label = { Text(stringResource(R.string.item_purchase_date)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
readOnly = true,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
.also { interactionSource ->
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect {
|
||||
coroutineScope.launch {
|
||||
showPurchaseDatePicker = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (showPurchaseDatePicker) {
|
||||
DatePickerDialog(
|
||||
onDismissRequest = { showPurchaseDatePicker = false },
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
purchaseDatePickerState.selectedDateMillis?.let { millis ->
|
||||
val selectedDate = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
viewModel.updatePurchaseDate(selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE))
|
||||
}
|
||||
showPurchaseDatePicker = false
|
||||
}) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { showPurchaseDatePicker = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
) {
|
||||
DatePicker(state = purchaseDatePickerState)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Warranty Until
|
||||
var showWarrantyDatePicker by remember { mutableStateOf(false) }
|
||||
val warrantyDatePickerState = rememberDatePickerState()
|
||||
OutlinedTextField(
|
||||
value = item.warrantyUntil ?: "",
|
||||
onValueChange = { }, // Read-only
|
||||
label = { Text(stringResource(R.string.item_warranty_until)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
readOnly = true,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
.also { interactionSource ->
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect {
|
||||
coroutineScope.launch {
|
||||
showWarrantyDatePicker = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (showWarrantyDatePicker) {
|
||||
DatePickerDialog(
|
||||
onDismissRequest = { showWarrantyDatePicker = false },
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
warrantyDatePickerState.selectedDateMillis?.let { millis ->
|
||||
val selectedDate = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
viewModel.updateWarrantyUntil(selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE))
|
||||
}
|
||||
showWarrantyDatePicker = false
|
||||
}) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { showWarrantyDatePicker = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
) {
|
||||
DatePicker(state = warrantyDatePickerState)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Parent ID (simplified for now, ideally a picker)
|
||||
OutlinedTextField(
|
||||
value = item.parentId ?: "",
|
||||
onValueChange = { viewModel.updateParentId(it) },
|
||||
label = { Text(stringResource(R.string.item_parent_id)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Checkboxes
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(stringResource(R.string.item_is_archived))
|
||||
Checkbox(
|
||||
checked = item.isArchived ?: false,
|
||||
onCheckedChange = { viewModel.updateIsArchived(it) }
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(stringResource(R.string.item_insured))
|
||||
Checkbox(
|
||||
checked = item.insured ?: false,
|
||||
onCheckedChange = { viewModel.updateInsured(it) }
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(stringResource(R.string.item_lifetime_warranty))
|
||||
Checkbox(
|
||||
checked = item.lifetimeWarranty ?: false,
|
||||
onCheckedChange = { viewModel.updateLifetimeWarranty(it) }
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(stringResource(R.string.item_sync_child_items_locations))
|
||||
Checkbox(
|
||||
checked = item.syncChildItemsLocations ?: false,
|
||||
onCheckedChange = { viewModel.updateSyncChildItemsLocations(it) }
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Manufacturer
|
||||
OutlinedTextField(
|
||||
value = item.manufacturer ?: "",
|
||||
onValueChange = { viewModel.updateManufacturer(it) },
|
||||
label = { Text(stringResource(R.string.item_manufacturer)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Model Number
|
||||
OutlinedTextField(
|
||||
value = item.modelNumber ?: "",
|
||||
onValueChange = { viewModel.updateModelNumber(it) },
|
||||
label = { Text(stringResource(R.string.item_model_number)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Purchase From
|
||||
OutlinedTextField(
|
||||
value = item.purchaseFrom ?: "",
|
||||
onValueChange = { viewModel.updatePurchaseFrom(it) },
|
||||
label = { Text(stringResource(R.string.item_purchase_from)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Warranty Details
|
||||
OutlinedTextField(
|
||||
value = item.warrantyDetails ?: "",
|
||||
onValueChange = { viewModel.updateWarrantyDetails(it) },
|
||||
label = { Text(stringResource(R.string.item_warranty_details)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Sold Details (simplified for now)
|
||||
OutlinedTextField(
|
||||
value = item.soldNotes ?: "",
|
||||
onValueChange = { viewModel.updateSoldNotes(it) },
|
||||
label = { Text(stringResource(R.string.item_sold_notes)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = item.soldPrice?.toString() ?: "",
|
||||
onValueChange = { viewModel.updateSoldPrice(it.toDoubleOrNull()) },
|
||||
label = { Text(stringResource(R.string.item_sold_price)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = item.soldTime ?: "",
|
||||
onValueChange = { viewModel.updateSoldTime(it) },
|
||||
label = { Text(stringResource(R.string.item_sold_time)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = item.soldTo ?: "",
|
||||
onValueChange = { viewModel.updateSoldTo(it) },
|
||||
label = { Text(stringResource(R.string.item_sold_to)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,14 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.model.Item
|
||||
import com.homebox.lens.domain.model.ItemCreate
|
||||
import com.homebox.lens.domain.model.ItemOut
|
||||
import com.homebox.lens.domain.model.ItemSummary
|
||||
import com.homebox.lens.domain.model.ItemUpdate
|
||||
import com.homebox.lens.domain.model.Label
|
||||
import com.homebox.lens.domain.model.Location
|
||||
import com.homebox.lens.domain.model.LocationOut
|
||||
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
||||
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -23,6 +28,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
@@ -35,6 +41,8 @@ import javax.inject.Inject
|
||||
*/
|
||||
data class ItemEditUiState(
|
||||
val item: Item? = null,
|
||||
val locations: List<LocationOut> = emptyList(),
|
||||
val selectedLocationId: String? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null
|
||||
)
|
||||
@@ -52,7 +60,8 @@ data class ItemEditUiState(
|
||||
class ItemEditViewModel @Inject constructor(
|
||||
private val createItemUseCase: CreateItemUseCase,
|
||||
private val updateItemUseCase: UpdateItemUseCase,
|
||||
private val getItemDetailsUseCase: GetItemDetailsUseCase
|
||||
private val getItemDetailsUseCase: GetItemDetailsUseCase,
|
||||
private val getAllLocationsUseCase: GetAllLocationsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(ItemEditUiState())
|
||||
@@ -71,9 +80,41 @@ class ItemEditViewModel @Inject constructor(
|
||||
Timber.i("[INFO][ENTRYPOINT][loading_item] Attempting to load item with ID: %s", itemId)
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
||||
loadLocations()
|
||||
if (itemId == null) {
|
||||
Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.")
|
||||
_uiState.value = _uiState.value.copy(isLoading = false, item = Item(id = "", name = "", description = null, quantity = 0, image = null, location = null, labels = emptyList(), value = null, createdAt = null))
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false, item = Item(
|
||||
id = "",
|
||||
name = "",
|
||||
description = null,
|
||||
quantity = 0,
|
||||
image = null,
|
||||
location = null,
|
||||
labels = emptyList(),
|
||||
value = null,
|
||||
createdAt = null,
|
||||
assetId = null,
|
||||
notes = null,
|
||||
serialNumber = null,
|
||||
purchasePrice = null,
|
||||
purchaseDate = null,
|
||||
warrantyUntil = null,
|
||||
parentId = null,
|
||||
isArchived = null,
|
||||
insured = null,
|
||||
lifetimeWarranty = null,
|
||||
manufacturer = null,
|
||||
modelNumber = null,
|
||||
purchaseFrom = null,
|
||||
soldNotes = null,
|
||||
soldPrice = null,
|
||||
soldTime = null,
|
||||
soldTo = null,
|
||||
syncChildItemsLocations = null,
|
||||
warrantyDetails = null
|
||||
)
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId)
|
||||
@@ -84,13 +125,36 @@ class ItemEditViewModel @Inject constructor(
|
||||
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
|
||||
image = itemOut.images.firstOrNull()?.path,
|
||||
location = itemOut.location?.let { Location(it.id, it.name) },
|
||||
labels = itemOut.labels.map { Label(it.id, it.name) },
|
||||
value = itemOut.value,
|
||||
createdAt = itemOut.createdAt,
|
||||
assetId = itemOut.assetId,
|
||||
notes = itemOut.notes,
|
||||
serialNumber = itemOut.serialNumber,
|
||||
purchasePrice = itemOut.purchasePrice,
|
||||
purchaseDate = itemOut.purchaseDate,
|
||||
warrantyUntil = itemOut.warrantyUntil,
|
||||
parentId = itemOut.parent?.id,
|
||||
isArchived = itemOut.isArchived,
|
||||
insured = itemOut.insured,
|
||||
lifetimeWarranty = itemOut.lifetimeWarranty,
|
||||
manufacturer = itemOut.manufacturer,
|
||||
modelNumber = itemOut.modelNumber,
|
||||
purchaseFrom = itemOut.purchaseFrom,
|
||||
soldNotes = itemOut.soldNotes,
|
||||
soldPrice = itemOut.soldPrice,
|
||||
soldTime = itemOut.soldTime,
|
||||
soldTo = itemOut.soldTo,
|
||||
syncChildItemsLocations = itemOut.syncChildItemsLocations,
|
||||
warrantyDetails = itemOut.warrantyDetails
|
||||
)
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
item = item,
|
||||
selectedLocationId = item.location?.id
|
||||
)
|
||||
_uiState.value = _uiState.value.copy(isLoading = false, item = item)
|
||||
Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched item details for ID: %s", itemId)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId)
|
||||
@@ -107,43 +171,61 @@ class ItemEditViewModel @Inject constructor(
|
||||
* @sideeffect Updates `_uiState` with loading, success, or error states. Calls `createItemUseCase` or `updateItemUseCase`.
|
||||
* @throws IllegalStateException if `uiState.value.item` is null when attempting to save.
|
||||
*/
|
||||
private fun loadLocations() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val locations = getAllLocationsUseCase()
|
||||
_uiState.value = _uiState.value.copy(locations = locations.map { LocationOut(it.id, it.name, it.color, it.isArchived, it.createdAt, it.updatedAt) })
|
||||
Timber.i("[INFO][ACTION][locations_loaded] Loaded %d locations", locations.size)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "[ERROR][FALLBACK][locations_load_failed] Failed to load locations")
|
||||
_uiState.value = _uiState.value.copy(error = e.localizedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSelectedLocationId(locationId: String?) {
|
||||
Timber.d("[DEBUG][ACTION][updating_selected_location] Selected location ID: %s", locationId)
|
||||
val location = _uiState.value.locations.find { it.id == locationId }
|
||||
_uiState.value = _uiState.value.copy(
|
||||
selectedLocationId = locationId,
|
||||
item = _uiState.value.item?.copy(location = location?.let { Location(it.id, it.name) })
|
||||
)
|
||||
}
|
||||
|
||||
fun saveItem() {
|
||||
Timber.i("[INFO][ENTRYPOINT][saving_item] Attempting to save item.")
|
||||
viewModelScope.launch {
|
||||
val currentItem = _uiState.value.item
|
||||
val selectedLocationId = _uiState.value.selectedLocationId
|
||||
require(currentItem != null) { "[CONTRACT_VIOLATION][PRECONDITION][item_not_present] Cannot save a null item." }
|
||||
if (currentItem.id.isBlank() && selectedLocationId == null) {
|
||||
throw IllegalStateException("Location is required for creating a new item.")
|
||||
}
|
||||
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
||||
try {
|
||||
if (currentItem.id.isBlank()) {
|
||||
Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name)
|
||||
val createdItemSummary = createItemUseCase(ItemCreate(
|
||||
name = currentItem.name,
|
||||
description = currentItem.description,
|
||||
quantity = currentItem.quantity,
|
||||
assetId = null, // Item does not have assetId
|
||||
notes = null, // Item does not have notes
|
||||
serialNumber = null, // Item does not have serialNumber
|
||||
value = currentItem.value?.toDouble(), // Convert BigDecimal to Double
|
||||
purchasePrice = null, // Item does not have purchasePrice
|
||||
purchaseDate = null, // Item does not have purchaseDate
|
||||
warrantyUntil = null, // Item does not have warrantyUntil
|
||||
locationId = currentItem.location?.id,
|
||||
parentId = null, // Item does not have parentId
|
||||
labelIds = currentItem.labels.map { it.id }
|
||||
))
|
||||
Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.")
|
||||
val createdItem = Item(
|
||||
id = createdItemSummary.id,
|
||||
name = createdItemSummary.name,
|
||||
description = null, // ItemSummary does not have description
|
||||
quantity = 0, // ItemSummary does not have quantity
|
||||
image = null, // ItemSummary does not have image
|
||||
location = null, // ItemSummary does not have location
|
||||
labels = emptyList(), // ItemSummary does not have labels
|
||||
value = null, // ItemSummary does not have value
|
||||
createdAt = null // ItemSummary does not have createdAt
|
||||
val createdItemSummary = createItemUseCase(
|
||||
ItemCreate(
|
||||
name = currentItem.name,
|
||||
description = currentItem.description,
|
||||
quantity = currentItem.quantity,
|
||||
assetId = currentItem.assetId,
|
||||
notes = currentItem.notes,
|
||||
serialNumber = currentItem.serialNumber,
|
||||
value = currentItem.value,
|
||||
purchasePrice = currentItem.purchasePrice,
|
||||
purchaseDate = currentItem.purchaseDate,
|
||||
warrantyUntil = currentItem.warrantyUntil,
|
||||
locationId = selectedLocationId,
|
||||
parentId = currentItem.parentId,
|
||||
labelIds = currentItem.labels.map { it.id }
|
||||
)
|
||||
)
|
||||
Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.")
|
||||
val createdItem = currentItem.copy(id = createdItemSummary.id, name = createdItemSummary.name)
|
||||
_uiState.value = _uiState.value.copy(isLoading = false, item = createdItem)
|
||||
Timber.i("[INFO][ACTION][new_item_created] Successfully created new item with ID: %s", createdItem.id)
|
||||
_saveCompleted.emit(Unit)
|
||||
@@ -151,7 +233,7 @@ class ItemEditViewModel @Inject constructor(
|
||||
Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id)
|
||||
val updatedItemOut = updateItemUseCase(currentItem)
|
||||
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
|
||||
val updatedItem = Item(
|
||||
val updatedItem = currentItem.copy(
|
||||
id = updatedItemOut.id,
|
||||
name = updatedItemOut.name,
|
||||
description = updatedItemOut.description,
|
||||
@@ -159,10 +241,33 @@ class ItemEditViewModel @Inject constructor(
|
||||
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
|
||||
value = updatedItemOut.value,
|
||||
createdAt = updatedItemOut.createdAt,
|
||||
assetId = updatedItemOut.assetId,
|
||||
notes = updatedItemOut.notes,
|
||||
serialNumber = updatedItemOut.serialNumber,
|
||||
purchasePrice = updatedItemOut.purchasePrice,
|
||||
purchaseDate = updatedItemOut.purchaseDate,
|
||||
warrantyUntil = updatedItemOut.warrantyUntil,
|
||||
parentId = updatedItemOut.parent?.id,
|
||||
isArchived = updatedItemOut.isArchived,
|
||||
insured = updatedItemOut.insured,
|
||||
lifetimeWarranty = updatedItemOut.lifetimeWarranty,
|
||||
manufacturer = updatedItemOut.manufacturer,
|
||||
modelNumber = updatedItemOut.modelNumber,
|
||||
purchaseFrom = updatedItemOut.purchaseFrom,
|
||||
soldNotes = updatedItemOut.soldNotes,
|
||||
soldPrice = updatedItemOut.soldPrice,
|
||||
soldTime = updatedItemOut.soldTime,
|
||||
soldTo = updatedItemOut.soldTo,
|
||||
syncChildItemsLocations = updatedItemOut.syncChildItemsLocations,
|
||||
warrantyDetails = updatedItemOut.warrantyDetails
|
||||
)
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
item = updatedItem,
|
||||
selectedLocationId = updatedItem.location?.id
|
||||
)
|
||||
_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)
|
||||
}
|
||||
@@ -209,6 +314,270 @@ class ItemEditViewModel @Inject constructor(
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity))
|
||||
}
|
||||
// [END_ENTITY: Function('updateQuantity')]
|
||||
|
||||
// [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 assetId to: %s", newAssetId)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(assetId = newAssetId))
|
||||
}
|
||||
// [END_ENTITY: Function('updateAssetId')]
|
||||
|
||||
// [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('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_serialNumber] Updating item serialNumber to: %s", newSerialNumber)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(serialNumber = newSerialNumber))
|
||||
}
|
||||
// [END_ENTITY: Function('updateSerialNumber')]
|
||||
|
||||
// [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_purchasePrice] Updating item purchasePrice to: %f", newPurchasePrice)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchasePrice = newPurchasePrice))
|
||||
}
|
||||
// [END_ENTITY: Function('updatePurchasePrice')]
|
||||
|
||||
// [ENTITY: Function('updatePurchaseDate')]
|
||||
/**
|
||||
* @summary Updates the purchase date of the item in the UI state.
|
||||
* @param newPurchaseDate The new purchase date for the item.
|
||||
* @sideeffect Updates the `item` in `_uiState`.
|
||||
*/
|
||||
fun updatePurchaseDate(newPurchaseDate: String?) {
|
||||
Timber.d("[DEBUG][ACTION][updating_item_purchaseDate] Updating item purchaseDate to: %s", newPurchaseDate)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseDate = newPurchaseDate))
|
||||
}
|
||||
// [END_ENTITY: Function('updatePurchaseDate')]
|
||||
|
||||
// [ENTITY: Function('updateWarrantyUntil')]
|
||||
/**
|
||||
* @summary Updates the warranty until date of the item in the UI state.
|
||||
* @param newWarrantyUntil The new warranty until date for the item.
|
||||
* @sideeffect Updates the `item` in `_uiState`.
|
||||
*/
|
||||
fun updateWarrantyUntil(newWarrantyUntil: String?) {
|
||||
Timber.d("[DEBUG][ACTION][updating_item_warrantyUntil] Updating item warrantyUntil to: %s", newWarrantyUntil)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyUntil = newWarrantyUntil))
|
||||
}
|
||||
// [END_ENTITY: Function('updateWarrantyUntil')]
|
||||
|
||||
// [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_parentId] Updating item parentId to: %s", newParentId)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(parentId = newParentId))
|
||||
}
|
||||
// [END_ENTITY: Function('updateParentId')]
|
||||
|
||||
// [ENTITY: Function('updateIsArchived')]
|
||||
/**
|
||||
* @summary Updates the archived status of the item in the UI state.
|
||||
* @param newIsArchived The new archived status for the item.
|
||||
* @sideeffect Updates the `item` in `_uiState`.
|
||||
*/
|
||||
fun updateIsArchived(newIsArchived: Boolean?) {
|
||||
Timber.d("[DEBUG][ACTION][updating_item_isArchived] Updating item isArchived to: %b", newIsArchived)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(isArchived = newIsArchived))
|
||||
}
|
||||
// [END_ENTITY: Function('updateIsArchived')]
|
||||
|
||||
// [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 to: %b", 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_lifetimeWarranty] Updating item lifetimeWarranty to: %b", 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_modelNumber] Updating item modelNumber to: %s", newModelNumber)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(modelNumber = newModelNumber))
|
||||
}
|
||||
// [END_ENTITY: Function('updateModelNumber')]
|
||||
|
||||
// [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_purchaseFrom] Updating item purchaseFrom to: %s", newPurchaseFrom)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseFrom = newPurchaseFrom))
|
||||
}
|
||||
// [END_ENTITY: Function('updatePurchaseFrom')]
|
||||
|
||||
// [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_soldNotes] Updating item soldNotes 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_soldPrice] Updating item soldPrice to: %f", 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_soldTime] Updating item soldTime 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 field for the item.
|
||||
* @sideeffect Updates the `item` in `_uiState`.
|
||||
*/
|
||||
fun updateSoldTo(newSoldTo: String?) {
|
||||
Timber.d("[DEBUG][ACTION][updating_item_soldTo] Updating item soldTo 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_syncChildItemsLocations] Updating item syncChildItemsLocations to: %b", 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_warrantyDetails] Updating item warrantyDetails to: %s", newWarrantyDetails)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyDetails = newWarrantyDetails))
|
||||
}
|
||||
// [END_ENTITY: Function('updateWarrantyDetails')]
|
||||
|
||||
// [ENTITY: Function('updateLocation')]
|
||||
/**
|
||||
* @summary Updates the location of the item in the UI state.
|
||||
* @param newLocation The new location for the item.
|
||||
* @sideeffect Updates the `item` in `_uiState`.
|
||||
*/
|
||||
fun updateLocation(newLocation: Location?) {
|
||||
Timber.d("[DEBUG][ACTION][updating_item_location] Updating item location to: %s", newLocation?.name)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(location = newLocation))
|
||||
}
|
||||
// [END_ENTITY: Function('updateLocation')]
|
||||
|
||||
// [ENTITY: Function('addLabel')]
|
||||
/**
|
||||
* @summary Adds a label to the item in the UI state.
|
||||
* @param label The label to add.
|
||||
* @sideeffect Updates the `item` in `_uiState`.
|
||||
*/
|
||||
fun addLabel(label: Label) {
|
||||
Timber.d("[DEBUG][ACTION][adding_label_to_item] Adding label: %s", label.name)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = _uiState.value.item?.labels.orEmpty() + label))
|
||||
}
|
||||
// [END_ENTITY: Function('addLabel')]
|
||||
|
||||
// [ENTITY: Function('removeLabel')]
|
||||
/**
|
||||
* @summary Removes a label from the item in the UI state.
|
||||
* @param labelId The ID of the label to remove.
|
||||
* @sideeffect Updates the `item` in `_uiState`.
|
||||
*/
|
||||
fun removeLabel(labelId: String) {
|
||||
Timber.d("[DEBUG][ACTION][removing_label_from_item] Removing label with ID: %s", labelId)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = _uiState.value.item?.labels.orEmpty().filter { it.id != labelId }))
|
||||
}
|
||||
// [END_ENTITY: Function('removeLabel')]
|
||||
}
|
||||
// [END_ENTITY: ViewModel('ItemEditViewModel')]
|
||||
// [END_FILE_ItemEditViewModel.kt]
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Label
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -24,7 +25,6 @@ import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -32,9 +32,6 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -106,6 +103,45 @@ fun LabelsListScreen(
|
||||
onLabelClick = { label ->
|
||||
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
|
||||
navigationActions.navigateToLabelEdit(label.id)
|
||||
},
|
||||
onDeleteClick = { label ->
|
||||
viewModel.onShowDeleteDialog(label)
|
||||
},
|
||||
isShowingDeleteDialog = currentState.isShowingDeleteDialog,
|
||||
labelToDelete = currentState.labelToDelete,
|
||||
onConfirmDelete = {
|
||||
currentState.labelToDelete?.let { label ->
|
||||
viewModel.deleteLabel(label.id)
|
||||
}
|
||||
},
|
||||
onDismissDeleteDialog = {
|
||||
viewModel.onDismissDeleteDialog()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Delete confirmation dialog
|
||||
if (currentState is LabelsListUiState.Success && currentState.isShowingDeleteDialog && currentState.labelToDelete != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.onDismissDeleteDialog() },
|
||||
title = { Text("Delete Label") },
|
||||
text = { Text("Are you sure you want to delete the label '${currentState.labelToDelete!!.name}'? This action cannot be undone.") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.deleteLabel(currentState.labelToDelete!!.id)
|
||||
viewModel.onDismissDeleteDialog()
|
||||
}
|
||||
) {
|
||||
Text("Delete")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { viewModel.onDismissDeleteDialog() }
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -129,6 +165,11 @@ fun LabelsListScreen(
|
||||
private fun LabelsList(
|
||||
labels: List<Label>,
|
||||
onLabelClick: (Label) -> Unit,
|
||||
onDeleteClick: (Label) -> Unit,
|
||||
isShowingDeleteDialog: Boolean,
|
||||
labelToDelete: Label?,
|
||||
onConfirmDelete: () -> Unit,
|
||||
onDismissDeleteDialog: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyColumn(
|
||||
@@ -139,7 +180,8 @@ private fun LabelsList(
|
||||
items(labels, key = { it.id }) { label ->
|
||||
LabelListItem(
|
||||
label = label,
|
||||
onClick = { onLabelClick(label) }
|
||||
onClick = { onLabelClick(label) },
|
||||
onDeleteClick = { onDeleteClick(label) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -156,7 +198,8 @@ private fun LabelsList(
|
||||
@Composable
|
||||
private fun LabelListItem(
|
||||
label: Label,
|
||||
onClick: () -> Unit
|
||||
onClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(text = label.name) },
|
||||
@@ -166,6 +209,14 @@ private fun LabelListItem(
|
||||
contentDescription = stringResource(id = R.string.content_desc_label_icon)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(onClick = onDeleteClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(id = R.string.content_desc_delete_label)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ sealed interface LabelsListUiState {
|
||||
*/
|
||||
data class Success(
|
||||
val labels: List<Label>,
|
||||
val isShowingCreateDialog: Boolean = false
|
||||
val isShowingCreateDialog: Boolean = false,
|
||||
val isShowingDeleteDialog: Boolean = false,
|
||||
val labelToDelete: Label? = null
|
||||
) : LabelsListUiState
|
||||
// [END_ENTITY: DataClass('Success')]
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ package com.homebox.lens.ui.screen.labelslist
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.model.Label
|
||||
import com.homebox.lens.domain.usecase.DeleteLabelUseCase
|
||||
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -27,7 +28,8 @@ import javax.inject.Inject
|
||||
*/
|
||||
@HiltViewModel
|
||||
class LabelsListViewModel @Inject constructor(
|
||||
private val getAllLabelsUseCase: GetAllLabelsUseCase
|
||||
private val getAllLabelsUseCase: GetAllLabelsUseCase,
|
||||
private val deleteLabelUseCase: DeleteLabelUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
|
||||
@@ -75,57 +77,73 @@ class LabelsListViewModel @Inject constructor(
|
||||
}
|
||||
// [END_ENTITY: Function('loadLabels')]
|
||||
|
||||
// [ENTITY: Function('onShowCreateDialog')]
|
||||
// [ENTITY: Function('onShowDeleteDialog')]
|
||||
/**
|
||||
* @summary Инициирует отображение диалога для создания метки.
|
||||
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`.
|
||||
* @sideeffect Обновляет `_uiState`.
|
||||
* @summary Показывает диалог подтверждения удаления метки.
|
||||
* @param label Метка для удаления.
|
||||
* @sideeffect Обновляет состояние для показа диалога удаления.
|
||||
*/
|
||||
fun onShowCreateDialog() {
|
||||
Timber.i("[INFO][ACTION][show_create_dialog] Show create label dialog requested.")
|
||||
fun onShowDeleteDialog(label: Label) {
|
||||
Timber.i("[INFO][ACTION][show_delete_dialog] Show delete label dialog for: ${label.id}")
|
||||
if (_uiState.value is LabelsListUiState.Success) {
|
||||
_uiState.update {
|
||||
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = true)
|
||||
_uiState.update { currentState ->
|
||||
(currentState as LabelsListUiState.Success).copy(
|
||||
isShowingDeleteDialog = true,
|
||||
labelToDelete = label
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('onShowCreateDialog')]
|
||||
// [END_ENTITY: Function('onShowDeleteDialog')]
|
||||
|
||||
// [ENTITY: Function('onDismissCreateDialog')]
|
||||
// [ENTITY: Function('onDismissDeleteDialog')]
|
||||
/**
|
||||
* @summary Скрывает диалог создания метки.
|
||||
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`.
|
||||
* @sideeffect Обновляет `_uiState`.
|
||||
* @summary Скрывает диалог подтверждения удаления метки.
|
||||
* @sideeffect Обновляет состояние для скрытия диалога удаления.
|
||||
*/
|
||||
fun onDismissCreateDialog() {
|
||||
Timber.i("[INFO][ACTION][dismiss_create_dialog] Dismiss create label dialog requested.")
|
||||
fun onDismissDeleteDialog() {
|
||||
Timber.i("[INFO][ACTION][dismiss_delete_dialog] Dismiss delete label dialog")
|
||||
if (_uiState.value is LabelsListUiState.Success) {
|
||||
_uiState.update {
|
||||
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = false)
|
||||
_uiState.update { currentState ->
|
||||
(currentState as LabelsListUiState.Success).copy(
|
||||
isShowingDeleteDialog = false,
|
||||
labelToDelete = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('onDismissCreateDialog')]
|
||||
// [END_ENTITY: Function('onDismissDeleteDialog')]
|
||||
|
||||
// [ENTITY: Function('createLabel')]
|
||||
// [ENTITY: Function('deleteLabel')]
|
||||
/**
|
||||
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
|
||||
* @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие
|
||||
* и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе.
|
||||
* @param name Название новой метки.
|
||||
* @precondition `name` не должен быть пустым.
|
||||
* @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог.
|
||||
* @summary Удаляет выбранную метку.
|
||||
* @param labelId ID метки для удаления.
|
||||
* @sideeffect Выполняет удаление через UseCase, обновляет состояние UI.
|
||||
*/
|
||||
fun createLabel(name: String) {
|
||||
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }
|
||||
fun deleteLabel(labelId: String) {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = LabelsListUiState.Loading
|
||||
Timber.i("[INFO][ENTRYPOINT][deleting_label] Starting label deletion for ID: $labelId. State -> Loading.")
|
||||
|
||||
Timber.i("[INFO][ACTION][create_label] Create label called with name: '$name'. [STUBBED]")
|
||||
val result = runCatching {
|
||||
deleteLabelUseCase(labelId)
|
||||
}
|
||||
|
||||
// [AI_NOTE]: Здесь будет вызов CreateLabelUseCase.
|
||||
|
||||
onDismissCreateDialog()
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
Timber.i("[INFO][SUCCESS][label_deleted] Label deleted successfully. Reloading labels.")
|
||||
loadLabels() // Refresh the list
|
||||
},
|
||||
onFailure = { exception ->
|
||||
Timber.e(exception, "[ERROR][EXCEPTION][deletion_failed] Failed to delete label. State -> Error.")
|
||||
_uiState.value = LabelsListUiState.Error(
|
||||
message = exception.message ?: "Could not delete label."
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('createLabel')]
|
||||
// [END_ENTITY: Function('deleteLabel')]
|
||||
}
|
||||
// [END_ENTITY: ViewModel('LabelsListViewModel')]
|
||||
// [END_FILE_LabelsListViewModel.kt]
|
||||
@@ -0,0 +1,104 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.settings
|
||||
// [FILE] SettingsScreen.kt
|
||||
// [SEMANTICS] ui, screen, settings, compose
|
||||
|
||||
package com.homebox.lens.ui.screen.settings
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.homebox.lens.navigation.Screen
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.screen.settings.SettingsUiState
|
||||
import com.homebox.lens.ui.screen.settings.SettingsViewModel
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('SettingsScreen')]
|
||||
/**
|
||||
* @summary Composable function for the Settings screen.
|
||||
* @param viewModel The ViewModel for the Settings screen.
|
||||
* @param onNavigateUp Callback to navigate up in the navigation stack.
|
||||
* @sideeffect Collects UI state from ViewModel.
|
||||
*/
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions,
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
onNavigateUp: () -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
MainScaffold(
|
||||
topBarTitle = "Настройки",
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
onNavigateUp = onNavigateUp
|
||||
) { paddingValues ->
|
||||
SettingsContent(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
uiState = uiState,
|
||||
onServerUrlChange = viewModel::onServerUrlChange,
|
||||
onSaveClick = viewModel::saveSettings
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('SettingsScreen')]
|
||||
|
||||
// [ENTITY: Function('SettingsContent')]
|
||||
/**
|
||||
* @summary Composable function for the content of the Settings screen.
|
||||
* @param modifier Modifier for the layout.
|
||||
* @param uiState The current UI state of the settings.
|
||||
* @param onServerUrlChange Callback for server URL changes.
|
||||
* @param onSaveClick Callback for save button clicks.
|
||||
* @sideeffect Displays UI elements based on uiState.
|
||||
*/
|
||||
@Composable
|
||||
fun SettingsContent(
|
||||
modifier: Modifier = Modifier,
|
||||
uiState: SettingsUiState,
|
||||
onServerUrlChange: (String) -> Unit,
|
||||
onSaveClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = uiState.serverUrl,
|
||||
onValueChange = onServerUrlChange,
|
||||
label = { Text("URL Сервера") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = onSaveClick,
|
||||
enabled = !uiState.isLoading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
} else {
|
||||
Text("Сохранить")
|
||||
}
|
||||
}
|
||||
if (uiState.isSaved) {
|
||||
Text("Настройки сохранены!", color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
if (uiState.error != null) {
|
||||
Text(uiState.error, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('SettingsContent')]
|
||||
|
||||
// [END_FILE_SettingsScreen.kt]
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.homebox.lens.ui.screen.settings
|
||||
|
||||
data class SettingsUiState(
|
||||
val serverUrl: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isSaved: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.homebox.lens.ui.screen.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val credentialsRepository: CredentialsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SettingsUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadCurrentSettings()
|
||||
}
|
||||
|
||||
private fun loadCurrentSettings() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
val credentials = credentialsRepository.getCredentials().first()
|
||||
_uiState.value = _uiState.value.copy(
|
||||
serverUrl = credentials?.serverUrl ?: "",
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onServerUrlChange(newUrl: String) {
|
||||
_uiState.value = _uiState.value.copy(serverUrl = newUrl, isSaved = false)
|
||||
}
|
||||
|
||||
fun saveSettings() {
|
||||
Timber.i("[INFO][ACTION][settings_save] Attempting to save settings.")
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
val currentCredentials = credentialsRepository.getCredentials().first()
|
||||
val updatedCredentials = currentCredentials?.copy(serverUrl = _uiState.value.serverUrl)
|
||||
?: Credentials(serverUrl = _uiState.value.serverUrl, username = "", password = "") // Create new if no existing credentials
|
||||
|
||||
credentialsRepository.saveCredentials(updatedCredentials)
|
||||
_uiState.value = _uiState.value.copy(isLoading = false, isSaved = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
<!-- Content Descriptions -->
|
||||
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
|
||||
<string name="cd_scan_qr_code">Scan QR code</string>
|
||||
<string name="cd_search">Search</string>
|
||||
<string name="cd_navigate_back">Navigate back</string>
|
||||
<string name="cd_navigate_up">Go back</string>
|
||||
<string name="cd_add_new_location">Add new location</string>
|
||||
<string name="content_desc_add_label">Add new label</string>
|
||||
|
||||
@@ -72,6 +74,7 @@
|
||||
<string name="content_desc_navigate_back">Navigate back</string>
|
||||
<string name="content_desc_create_label">Create new label</string>
|
||||
<string name="content_desc_label_icon">Label icon</string>
|
||||
<string name="content_desc_delete_label">Delete label</string>
|
||||
<string name="no_labels_found">No labels found.</string>
|
||||
<string name="dialog_title_create_label">Create Label</string>
|
||||
<string name="dialog_field_label_name">Label Name</string>
|
||||
@@ -118,4 +121,26 @@
|
||||
<string name="label_color">Color</string>
|
||||
<string name="label_hex_color">HEX color code</string>
|
||||
|
||||
<string name="item_asset_id">Asset ID</string>
|
||||
<string name="item_notes">Notes</string>
|
||||
<string name="item_serial_number">Serial Number</string>
|
||||
<string name="item_purchase_price">Purchase Price</string>
|
||||
<string name="item_purchase_date">Purchase Date</string>
|
||||
<string name="item_warranty_until">Warranty Until</string>
|
||||
<string name="item_parent_id">Parent ID</string>
|
||||
<string name="item_is_archived">Is Archived</string>
|
||||
<string name="item_insured">Insured</string>
|
||||
<string name="item_lifetime_warranty">Lifetime Warranty</string>
|
||||
<string name="item_sync_child_items_locations">Sync Child Items Locations</string>
|
||||
<string name="item_manufacturer">Manufacturer</string>
|
||||
<string name="item_model_number">Model Number</string>
|
||||
<string name="item_purchase_from">Purchase From</string>
|
||||
<string name="item_warranty_details">Warranty Details</string>
|
||||
<string name="item_sold_notes">Sold Notes</string>
|
||||
<string name="item_sold_price">Sold Price</string>
|
||||
<string name="item_sold_time">Sold Time</string>
|
||||
<string name="item_sold_to">Sold To</string>
|
||||
<string name="scan_qr_code">Scan QR Code</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
</resources>
|
||||
@@ -13,8 +13,10 @@
|
||||
|
||||
<!-- Content Descriptions -->
|
||||
<string name="cd_open_navigation_drawer">Открыть боковое меню</string>
|
||||
<string name="cd_scan_qr_code">Сканировать QR-код</string>
|
||||
<string name="cd_scan_qr_code">Сканировать QR/штрих-код</string>
|
||||
<string name="cd_search">Поиск</string>
|
||||
<string name="cd_navigate_back">Вернуться назад</string>
|
||||
<string name="cd_navigate_up">Вернуться</string>
|
||||
<string name="cd_add_new_location">Добавить новую локацию</string>
|
||||
<string name="content_desc_add_label">Добавить новую метку</string>
|
||||
|
||||
@@ -93,6 +95,7 @@
|
||||
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
|
||||
<string name="content_desc_create_label">Создать новую метку</string>
|
||||
<string name="content_desc_label_icon">Иконка метки</string>
|
||||
<string name="content_desc_delete_label">Удалить метку</string>
|
||||
<string name="no_labels_found">Метки не найдены.</string>
|
||||
<string name="dialog_title_create_label">Создать метку</string>
|
||||
<string name="dialog_field_label_name">Название метки</string>
|
||||
@@ -112,4 +115,26 @@
|
||||
<!-- Color Picker -->
|
||||
<string name="label_color">Цвет</string>
|
||||
<string name="label_hex_color">HEX-код цвета</string>
|
||||
<string name="item_asset_id">Идентификатор актива</string>
|
||||
<string name="item_notes">Заметки</string>
|
||||
<string name="item_serial_number">Серийный номер</string>
|
||||
<string name="item_purchase_price">Цена покупки</string>
|
||||
<string name="item_purchase_date">Дата покупки</string>
|
||||
<string name="item_warranty_until">Гарантия до</string>
|
||||
<string name="item_parent_id">Родительский ID</string>
|
||||
<string name="item_is_archived">Архивировано</string>
|
||||
<string name="item_insured">Застраховано</string>
|
||||
<string name="item_lifetime_warranty">Пожизненная гарантия</string>
|
||||
<string name="item_sync_child_items_locations">Синхронизировать дочерние элементы</string>
|
||||
<string name="item_manufacturer">Производитель</string>
|
||||
<string name="item_model_number">Номер модели</string>
|
||||
<string name="item_purchase_from">Куплено у</string>
|
||||
<string name="item_warranty_details">Детали гарантии</string>
|
||||
<string name="item_sold_notes">Примечания о продаже</string>
|
||||
<string name="item_sold_price">Цена продажи</string>
|
||||
<string name="item_sold_time">Время продажи</string>
|
||||
<string name="item_sold_to">Продано кому</string>
|
||||
<string name="scan_qr_code">Сканировать QR-код</string>
|
||||
<string name="ok">ОК</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,8 +9,10 @@ import com.homebox.lens.domain.model.Item
|
||||
import com.homebox.lens.domain.model.ItemCreate
|
||||
import com.homebox.lens.domain.model.ItemOut
|
||||
import com.homebox.lens.domain.model.ItemSummary
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
||||
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
@@ -37,6 +39,7 @@ class ItemEditViewModelTest {
|
||||
private lateinit var createItemUseCase: CreateItemUseCase
|
||||
private lateinit var updateItemUseCase: UpdateItemUseCase
|
||||
private lateinit var getItemDetailsUseCase: GetItemDetailsUseCase
|
||||
private lateinit var getAllLocationsUseCase: GetAllLocationsUseCase
|
||||
private lateinit var viewModel: ItemEditViewModel
|
||||
|
||||
@Before
|
||||
@@ -45,7 +48,11 @@ class ItemEditViewModelTest {
|
||||
createItemUseCase = mockk()
|
||||
updateItemUseCase = mockk()
|
||||
getItemDetailsUseCase = mockk()
|
||||
viewModel = ItemEditViewModel(createItemUseCase, updateItemUseCase, getItemDetailsUseCase)
|
||||
getAllLocationsUseCase = mockk<GetAllLocationsUseCase>()
|
||||
coEvery { getAllLocationsUseCase() } returns listOf(
|
||||
LocationOutCount("1", "Test Location", "#000000", false, 0, "2025-08-28T12:00:00Z", "2025-08-28T12:00:00Z")
|
||||
)
|
||||
viewModel = ItemEditViewModel(createItemUseCase, updateItemUseCase, getItemDetailsUseCase, getAllLocationsUseCase)
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -56,7 +63,41 @@ class ItemEditViewModelTest {
|
||||
@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")
|
||||
val itemOut = ItemOut(
|
||||
id = itemId,
|
||||
name = "Test Item",
|
||||
assetId = null,
|
||||
description = "Description",
|
||||
notes = null,
|
||||
serialNumber = null,
|
||||
quantity = 1,
|
||||
isArchived = false,
|
||||
value = 10.0,
|
||||
purchasePrice = null,
|
||||
purchaseDate = null,
|
||||
warrantyUntil = null,
|
||||
location = null,
|
||||
parent = null,
|
||||
children = emptyList(),
|
||||
labels = emptyList(),
|
||||
attachments = emptyList(),
|
||||
images = emptyList(),
|
||||
fields = emptyList(),
|
||||
maintenance = emptyList(),
|
||||
createdAt = "2025-08-28T12:00:00Z",
|
||||
updatedAt = "2025-08-28T12:00:00Z",
|
||||
insured = null,
|
||||
lifetimeWarranty = null,
|
||||
manufacturer = null,
|
||||
modelNumber = null,
|
||||
purchaseFrom = null,
|
||||
soldNotes = null,
|
||||
soldPrice = null,
|
||||
soldTime = null,
|
||||
soldTo = null,
|
||||
syncChildItemsLocations = null,
|
||||
warrantyDetails = null
|
||||
)
|
||||
coEvery { getItemDetailsUseCase(itemId) } returns itemOut
|
||||
|
||||
viewModel.loadItem(itemId)
|
||||
@@ -91,6 +132,7 @@ class ItemEditViewModelTest {
|
||||
viewModel.updateName("New Item")
|
||||
viewModel.updateDescription("New Description")
|
||||
viewModel.updateQuantity(2)
|
||||
viewModel.updateSelectedLocationId("1")
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.saveItem()
|
||||
@@ -105,8 +147,76 @@ class ItemEditViewModelTest {
|
||||
@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")
|
||||
val updatedItemOut = ItemOut(
|
||||
id = itemId,
|
||||
name = "Updated Item",
|
||||
assetId = null,
|
||||
description = "Updated Description",
|
||||
notes = null,
|
||||
serialNumber = null,
|
||||
quantity = 4,
|
||||
isArchived = false,
|
||||
value = 12.0,
|
||||
purchasePrice = null,
|
||||
purchaseDate = null,
|
||||
warrantyUntil = null,
|
||||
location = null,
|
||||
parent = null,
|
||||
children = emptyList(),
|
||||
labels = emptyList(),
|
||||
attachments = emptyList(),
|
||||
images = emptyList(),
|
||||
fields = emptyList(),
|
||||
maintenance = emptyList(),
|
||||
createdAt = "2025-08-28T12:00:00Z",
|
||||
updatedAt = "2025-08-28T12:00:00Z",
|
||||
insured = null,
|
||||
lifetimeWarranty = null,
|
||||
manufacturer = null,
|
||||
modelNumber = null,
|
||||
purchaseFrom = null,
|
||||
soldNotes = null,
|
||||
soldPrice = null,
|
||||
soldTime = null,
|
||||
soldTo = null,
|
||||
syncChildItemsLocations = null,
|
||||
warrantyDetails = null
|
||||
)
|
||||
coEvery { getItemDetailsUseCase(itemId) } returns ItemOut(
|
||||
id = itemId,
|
||||
name = "Existing Item",
|
||||
assetId = null,
|
||||
description = "Existing Description",
|
||||
notes = null,
|
||||
serialNumber = null,
|
||||
quantity = 3,
|
||||
isArchived = false,
|
||||
value = 10.0,
|
||||
purchasePrice = null,
|
||||
purchaseDate = null,
|
||||
warrantyUntil = null,
|
||||
location = null,
|
||||
parent = null,
|
||||
children = emptyList(),
|
||||
labels = emptyList(),
|
||||
attachments = emptyList(),
|
||||
images = emptyList(),
|
||||
fields = emptyList(),
|
||||
maintenance = emptyList(),
|
||||
createdAt = "2025-08-28T12:00:00Z",
|
||||
updatedAt = "2025-08-28T12:00:00Z",
|
||||
insured = null,
|
||||
lifetimeWarranty = null,
|
||||
manufacturer = null,
|
||||
modelNumber = null,
|
||||
purchaseFrom = null,
|
||||
soldNotes = null,
|
||||
soldPrice = null,
|
||||
soldTime = null,
|
||||
soldTo = null,
|
||||
syncChildItemsLocations = null,
|
||||
warrantyDetails = null
|
||||
)
|
||||
coEvery { updateItemUseCase(any()) } returns updatedItemOut
|
||||
|
||||
viewModel.loadItem(itemId)
|
||||
|
||||
Reference in New Issue
Block a user