feat: Refactor login screen - fix compilation error
This commit is contained in:
@@ -25,11 +25,14 @@ import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
|
||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||
import com.homebox.lens.ui.screen.settings.SettingsScreen
|
||||
import com.homebox.lens.ui.screen.splash.SplashScreen
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('NavGraph')]
|
||||
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
||||
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('NavGraph')] -> [USES] -> [Screen('SplashScreen')]
|
||||
/**
|
||||
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
||||
* @param navController Контроллер навигации.
|
||||
@@ -47,11 +50,13 @@ fun NavGraph(
|
||||
val navigationActions = remember(navController) {
|
||||
NavigationActions(navController)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Setup.route
|
||||
startDestination = Screen.Splash.route
|
||||
) {
|
||||
composable(route = Screen.Splash.route) {
|
||||
SplashScreen(navController = navController)
|
||||
}
|
||||
composable(route = Screen.Setup.route) {
|
||||
SetupScreen(onSetupComplete = {
|
||||
navController.navigate(Screen.Dashboard.route) {
|
||||
@@ -137,6 +142,12 @@ fun NavGraph(
|
||||
navigationActions = navigationActions
|
||||
)
|
||||
}
|
||||
composable(route = Screen.Settings.route) {
|
||||
SettingsScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('NavGraph')]
|
||||
|
||||
@@ -10,6 +10,10 @@ package com.homebox.lens.navigation
|
||||
* @param route Строковый идентификатор маршрута.
|
||||
*/
|
||||
sealed class Screen(val route: String) {
|
||||
// [ENTITY: Object('Splash')]
|
||||
data object Splash : Screen("splash_screen")
|
||||
// [END_ENTITY: Object('Splash')]
|
||||
|
||||
// [ENTITY: Object('Setup')]
|
||||
data object Setup : Screen("setup_screen")
|
||||
// [END_ENTITY: Object('Setup')]
|
||||
@@ -118,6 +122,10 @@ 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')]
|
||||
}
|
||||
// [END_ENTITY: SealedClass('Screen')]
|
||||
// [END_FILE_Screen.kt]
|
||||
|
||||
@@ -310,10 +310,10 @@ fun DashboardContentSuccessPreview() {
|
||||
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 = "")
|
||||
LabelOut(id="1", name="electronics", description = null, color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
|
||||
LabelOut(id="2", name="important", description = null, color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
|
||||
LabelOut(id="3", name="seasonal", description = null, color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
|
||||
LabelOut(id="4", name="hobby", description = null, color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
|
||||
),
|
||||
recentlyAddedItems = emptyList()
|
||||
)
|
||||
|
||||
@@ -7,8 +7,14 @@ package com.homebox.lens.ui.screen.itemedit
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.model.*
|
||||
import com.homebox.lens.domain.model.Item
|
||||
import com.homebox.lens.domain.model.ItemCreate
|
||||
import com.homebox.lens.domain.model.ItemUpdate
|
||||
import com.homebox.lens.domain.model.Location
|
||||
import com.homebox.lens.domain.model.Label
|
||||
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
||||
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
||||
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
||||
import com.homebox.lens.ui.mapper.ItemMapper
|
||||
@@ -30,11 +36,15 @@ import javax.inject.Inject
|
||||
* @param item The item being edited, or null if creating a new item.
|
||||
* @param isLoading Whether data is currently being loaded or saved.
|
||||
* @param error An error message if an operation failed.
|
||||
* @param allLocations A list of all available locations.
|
||||
* @param allLabels A list of all available labels.
|
||||
*/
|
||||
data class ItemEditUiState(
|
||||
val item: Item? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null
|
||||
val error: String? = null,
|
||||
val allLocations: List<Location> = emptyList(),
|
||||
val allLabels: List<Label> = emptyList()
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemEditUiState')]
|
||||
|
||||
@@ -56,6 +66,8 @@ class ItemEditViewModel @Inject constructor(
|
||||
private val createItemUseCase: CreateItemUseCase,
|
||||
private val updateItemUseCase: UpdateItemUseCase,
|
||||
private val getItemDetailsUseCase: GetItemDetailsUseCase,
|
||||
private val getAllLocationsUseCase: GetAllLocationsUseCase,
|
||||
private val getAllLabelsUseCase: GetAllLabelsUseCase,
|
||||
private val itemMapper: ItemMapper
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -123,10 +135,47 @@ class ItemEditViewModel @Inject constructor(
|
||||
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Load all locations and labels
|
||||
try {
|
||||
Timber.i("[INFO][ACTION][fetching_all_locations] Fetching all locations.")
|
||||
val allLocations = getAllLocationsUseCase().map { Location(it.id, it.name) }
|
||||
Timber.i("[INFO][ACTION][fetching_all_labels] Fetching all labels.")
|
||||
val allLabels = getAllLabelsUseCase().map { Label(it.id, it.name) }
|
||||
_uiState.value = _uiState.value.copy(allLocations = allLocations, allLabels = allLabels)
|
||||
Timber.i("[INFO][ACTION][all_locations_labels_fetched] Successfully fetched all locations and labels.")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "[ERROR][FALLBACK][locations_labels_load_failed] Failed to load locations or labels.")
|
||||
_uiState.value = _uiState.value.copy(error = e.localizedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('loadItem')]
|
||||
|
||||
// [ENTITY: Function('updateLocation')]
|
||||
/**
|
||||
* @summary Updates the location of the item in the UI state.
|
||||
* @param location The new location for the item.
|
||||
* @sideeffect Updates the `item` in `_uiState`.
|
||||
*/
|
||||
fun updateLocation(location: Location) {
|
||||
Timber.d("[DEBUG][ACTION][updating_item_location] Updating item location to: %s", location.name)
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(location = location))
|
||||
}
|
||||
// [END_ENTITY: Function('updateLocation')]
|
||||
|
||||
// [ENTITY: Function('updateLabels')]
|
||||
/**
|
||||
* @summary Updates the labels of the item in the UI state.
|
||||
* @param labels The new list of labels for the item.
|
||||
* @sideeffect Updates the `item` in `_uiState`.
|
||||
*/
|
||||
fun updateLabels(labels: List<Label>) {
|
||||
Timber.d("[DEBUG][ACTION][updating_item_labels] Updating item labels to: %s", labels.map { it.name }.joinToString())
|
||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = labels))
|
||||
}
|
||||
// [END_ENTITY: Function('updateLabels')]
|
||||
|
||||
// [ENTITY: Function('saveItem')]
|
||||
/**
|
||||
* @summary Saves the current item, either creating a new one or updating an existing one.
|
||||
|
||||
@@ -97,6 +97,13 @@ fun LabelEditScreen(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.description.orEmpty(),
|
||||
onValueChange = viewModel::onDescriptionChange,
|
||||
label = { Text(stringResource(R.string.label_description)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ColorPicker(
|
||||
selectedColor = uiState.color,
|
||||
onColorSelected = viewModel::onColorChange,
|
||||
|
||||
@@ -50,6 +50,10 @@ class LabelEditViewModel @Inject constructor(
|
||||
uiState = uiState.copy(name = newName, nameError = null)
|
||||
}
|
||||
|
||||
fun onDescriptionChange(newDescription: String) {
|
||||
uiState = uiState.copy(description = newDescription)
|
||||
}
|
||||
|
||||
fun onColorChange(newColor: String) {
|
||||
uiState = uiState.copy(color = newColor)
|
||||
}
|
||||
@@ -63,35 +67,41 @@ class LabelEditViewModel @Inject constructor(
|
||||
|
||||
uiState = uiState.copy(isLoading = true, error = null)
|
||||
try {
|
||||
if (labelId == null) {
|
||||
// Create new label
|
||||
val newLabel = LabelCreate(name = uiState.name, color = uiState.color, description = null)
|
||||
val result = if (labelId == null) {
|
||||
// [LOG_EVENT] [EVENT_TYPE: LabelCreationAttempt] [DATA: { "labelName": "${uiState.name}" }]
|
||||
val newLabel = LabelCreate(name = uiState.name, color = uiState.color, description = uiState.description)
|
||||
createLabelUseCase(newLabel)
|
||||
} else {
|
||||
// Update existing label
|
||||
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color, description = null)
|
||||
// [LOG_EVENT] [EVENT_TYPE: LabelUpdateAttempt] [DATA: { "labelId": "$labelId", "labelName": "${uiState.name}" }]
|
||||
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color, description = uiState.description)
|
||||
updateLabelUseCase(labelId, updatedLabel)
|
||||
}
|
||||
// [LOG_EVENT] [EVENT_TYPE: LabelSaveSuccess] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
|
||||
uiState = uiState.copy(isSaved = true)
|
||||
} catch (e: Exception) {
|
||||
// [LOG_EVENT] [EVENT_TYPE: LabelSaveFailure] [ERROR: "${e.message}"] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
|
||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
||||
} finally {
|
||||
uiState = uiState.copy(isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLabelDetails(id: String) {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, error = null)
|
||||
try {
|
||||
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchAttempt] [DATA: { "labelId": "$id" }]
|
||||
val label = getLabelDetailsUseCase(id)
|
||||
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchSuccess] [DATA: { "labelId": "$id", "labelName": "${label.name}" }]
|
||||
uiState = uiState.copy(
|
||||
name = label.name,
|
||||
color = label.color,
|
||||
isLoading = false
|
||||
description = label.description,
|
||||
isLoading = false,
|
||||
originalLabel = label
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchFailure] [ERROR: "${e.message}"] [DATA: { "labelId": "$id" }]
|
||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
||||
}
|
||||
}
|
||||
@@ -104,6 +114,7 @@ class LabelEditViewModel @Inject constructor(
|
||||
*/
|
||||
data class LabelEditUiState(
|
||||
val name: String = "",
|
||||
val description: String? = null,
|
||||
val color: String = "#FFFFFF", // Default color
|
||||
val nameError: String? = null,
|
||||
val isLoading: Boolean = false,
|
||||
|
||||
@@ -17,24 +17,18 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Label
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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
|
||||
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
|
||||
@@ -53,9 +47,11 @@ import timber.log.Timber
|
||||
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
|
||||
/**
|
||||
* @summary Отображает экран со списком всех меток.
|
||||
* @param navController Контроллер навигации для перемещения между экранами.
|
||||
* @param currentRoute Текущий маршрут навигации.
|
||||
* @param navigationActions Объект, содержащий действия по навигации.
|
||||
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LabelsListScreen(
|
||||
currentRoute: String?,
|
||||
@@ -90,19 +86,19 @@ fun LabelsListScreen(
|
||||
.padding(innerPaddingValues), // Use innerPaddingValues here
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (currentState) {
|
||||
when (val state = uiState) {
|
||||
is LabelsListUiState.Loading -> {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
is LabelsListUiState.Error -> {
|
||||
Text(text = currentState.message)
|
||||
Text(text = state.message)
|
||||
}
|
||||
is LabelsListUiState.Success -> {
|
||||
if (currentState.labels.isEmpty()) {
|
||||
if (state.labels.isEmpty()) {
|
||||
Text(text = stringResource(id = R.string.no_labels_found))
|
||||
} else {
|
||||
LabelsList(
|
||||
labels = currentState.labels,
|
||||
labels = state.labels,
|
||||
onLabelClick = { label ->
|
||||
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
|
||||
navigationActions.navigateToLabelEdit(label.id)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.settings
|
||||
// [FILE] SettingsScreen.kt
|
||||
// [SEMANTICS] ui, screen, settings
|
||||
|
||||
package com.homebox.lens.ui.screen.settings
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('SettingsScreen')]
|
||||
// [RELATION: Function('SettingsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
/**
|
||||
* @summary Composable-функция для экрана настроек.
|
||||
* @param currentRoute Текущий маршрут навигации.
|
||||
* @param navigationActions Объект, содержащий действия по навигации.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions
|
||||
) {
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.screen_title_settings),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "Settings Screen (Under Construction)")
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('SettingsScreen')]
|
||||
// [END_FILE_SettingsScreen.kt]
|
||||
@@ -7,17 +7,22 @@
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.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.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.homebox.lens.R
|
||||
// [END_IMPORTS]
|
||||
@@ -83,42 +88,74 @@ private fun SetupScreenContent(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = uiState.serverUrl,
|
||||
onValueChange = onServerUrlChange,
|
||||
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.username,
|
||||
onValueChange = onUsernameChange,
|
||||
label = { Text(stringResource(id = R.string.setup_username_label)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.password,
|
||||
onValueChange = onPasswordChange,
|
||||
label = { Text(stringResource(id = R.string.setup_password_label)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
Image(
|
||||
imageVector = Icons.Default.Lock,
|
||||
contentDescription = stringResource(id = R.string.app_name),
|
||||
modifier = Modifier.size(128.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.setup_title),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontSize = 28.sp // Adjust font size as needed
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = uiState.serverUrl,
|
||||
onValueChange = onServerUrlChange,
|
||||
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.username,
|
||||
onValueChange = onUsernameChange,
|
||||
label = { Text(stringResource(id = R.string.setup_username_label)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.password,
|
||||
onValueChange = onPasswordChange,
|
||||
label = { Text(stringResource(id = R.string.setup_password_label)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(
|
||||
onClick = onConnectClick,
|
||||
enabled = !uiState.isLoading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp) // Make button more prominent
|
||||
) {
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(id = R.string.setup_connect_button))
|
||||
Text(stringResource(id = R.string.setup_connect_button), fontSize = 18.sp)
|
||||
}
|
||||
}
|
||||
uiState.error?.let {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,40 +74,61 @@ class SetupViewModel @Inject constructor(
|
||||
// [END_ENTITY: Function('onUsernameChange')]
|
||||
|
||||
// [ENTITY: Function('onPasswordChange')]
|
||||
/**
|
||||
* @summary Updates the password in the UI state.
|
||||
* @param newPassword The new password.
|
||||
* @sideeffect Updates the `password` in `_uiState`.
|
||||
*/
|
||||
fun onPasswordChange(newPassword: String) {
|
||||
_uiState.update { it.copy(password = newPassword) }
|
||||
}
|
||||
// [END_ENTITY: Function('onPasswordChange')]
|
||||
|
||||
// [ENTITY: Function('connect')]
|
||||
fun connect() {
|
||||
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
// [ENTITY: Function('areCredentialsSaved')]
|
||||
/**
|
||||
* @summary Checks synchronously if credentials are saved.
|
||||
* @return true if credentials are saved, false otherwise.
|
||||
* @sideeffect None.
|
||||
*/
|
||||
fun areCredentialsSaved(): Boolean {
|
||||
Timber.d("[DEBUG][ENTRYPOINT][checking_credentials_saved] Checking if credentials are saved.")
|
||||
return credentialsRepository.areCredentialsSavedSync()
|
||||
}
|
||||
// [END_ENTITY: Function('areCredentialsSaved')]
|
||||
|
||||
val credentials = Credentials(
|
||||
serverUrl = _uiState.value.serverUrl.trim(),
|
||||
username = _uiState.value.username.trim(),
|
||||
password = _uiState.value.password
|
||||
)
|
||||
// [ENTITY: Function('connect')]
|
||||
/**
|
||||
* @summary Initiates the connection process, saving credentials and attempting to log in.
|
||||
* @sideeffect Updates `_uiState` with loading, error, and completion states.
|
||||
*/
|
||||
fun connect() {
|
||||
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
|
||||
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
|
||||
credentialsRepository.saveCredentials(credentials)
|
||||
val credentials = Credentials(
|
||||
serverUrl = _uiState.value.serverUrl.trim(),
|
||||
username = _uiState.value.username.trim(),
|
||||
password = _uiState.value.password
|
||||
)
|
||||
|
||||
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
|
||||
loginUseCase(credentials).fold(
|
||||
onSuccess = {
|
||||
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
|
||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||
},
|
||||
onFailure = { exception ->
|
||||
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
|
||||
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('connect')]
|
||||
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
|
||||
credentialsRepository.saveCredentials(credentials)
|
||||
|
||||
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
|
||||
loginUseCase(credentials).fold(
|
||||
onSuccess = {
|
||||
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
|
||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||
},
|
||||
onFailure = { exception ->
|
||||
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
|
||||
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('connect')]
|
||||
}
|
||||
// [END_ENTITY: ViewModel('SetupViewModel')]
|
||||
// [END_FILE_SetupViewModel.kt]
|
||||
@@ -0,0 +1,57 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.splash
|
||||
// [FILE] SplashScreen.kt
|
||||
// [SEMANTICS] ui, screen, splash, navigation, authentication_flow
|
||||
|
||||
package com.homebox.lens.ui.screen.splash
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.homebox.lens.navigation.Screen
|
||||
import com.homebox.lens.ui.screen.setup.SetupViewModel
|
||||
import timber.log.Timber
|
||||
|
||||
// [ENTITY: Function('SplashScreen')]
|
||||
/**
|
||||
* @summary Displays a splash screen while checking if credentials are saved.
|
||||
* @param navController The NavController for navigation.
|
||||
* @param viewModel The SetupViewModel to check credential status.
|
||||
* @sideeffect Navigates to either SetupScreen or DashboardScreen based on credential status.
|
||||
*/
|
||||
@Composable
|
||||
fun SplashScreen(
|
||||
navController: NavController,
|
||||
viewModel: SetupViewModel = hiltViewModel()
|
||||
) {
|
||||
Timber.d("[DEBUG][ENTRYPOINT][splash_screen_composable] SplashScreen entered.")
|
||||
|
||||
LaunchedEffect(key1 = true) {
|
||||
Timber.i("[INFO][ACTION][checking_credentials_on_launch] Checking if credentials are saved on launch.")
|
||||
val credentialsSaved = viewModel.areCredentialsSaved()
|
||||
if (credentialsSaved) {
|
||||
Timber.i("[INFO][ACTION][credentials_found_navigating_dashboard] Credentials found, navigating to Dashboard.")
|
||||
navController.navigate(Screen.Dashboard.route) {
|
||||
popUpTo(Screen.Splash.route) { inclusive = true }
|
||||
}
|
||||
} else {
|
||||
Timber.i("[INFO][ACTION][no_credentials_found_navigating_setup] No credentials found, navigating to Setup.")
|
||||
navController.navigate(Screen.Setup.route) {
|
||||
popUpTo(Screen.Splash.route) { inclusive = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('SplashScreen')]
|
||||
@@ -35,7 +35,6 @@
|
||||
<string name="item_edit_title_create">Создать элемент</string>
|
||||
<string name="content_desc_save_item">Сохранить элемент</string>
|
||||
<string name="label_name">Название</string>
|
||||
<string name="label_description">Описание</string>
|
||||
|
||||
<!-- Search Screen -->
|
||||
<string name="placeholder_search_items">Поиск элементов...</string>
|
||||
@@ -120,6 +119,8 @@
|
||||
|
||||
<!-- Labels List Screen -->
|
||||
<string name="screen_title_labels">Метки</string>
|
||||
<!-- Settings Screen -->
|
||||
<string name="screen_title_settings">Настройки</string>
|
||||
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
|
||||
<string name="content_desc_create_label">Создать новую метку</string>
|
||||
<string name="content_desc_label_icon">Иконка метки</string>
|
||||
@@ -133,6 +134,7 @@
|
||||
<string name="label_edit_title_create">Создать метку</string>
|
||||
<string name="label_edit_title_edit">Редактировать метку</string>
|
||||
<string name="label_name_edit">Название метки</string>
|
||||
<string name="label_description">Описание</string>
|
||||
|
||||
<!-- Common Actions -->
|
||||
<string name="back">Назад</string>
|
||||
|
||||
@@ -155,6 +155,7 @@ fun LabelEntity.toDomainLabelOut(): LabelOut {
|
||||
return LabelOut(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
description = null, // Not available in LabelEntity
|
||||
color = "", // Not available in LabelEntity
|
||||
isArchived = false, // Not available in LabelEntity
|
||||
createdAt = "", // Not available in LabelEntity
|
||||
|
||||
@@ -119,6 +119,7 @@ fun LabelOutDto.toDomain(): DomainLabelOut {
|
||||
return DomainLabelOut(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
description = this.description,
|
||||
color = this.color ?: "",
|
||||
isArchived = this.isArchived ?: false,
|
||||
createdAt = this.createdAt,
|
||||
|
||||
@@ -98,11 +98,46 @@ class CredentialsRepositoryImpl @Inject constructor(
|
||||
*/
|
||||
override suspend fun getToken(): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
|
||||
encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
||||
val token = encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
||||
if (token != null) {
|
||||
Timber.i("[INFO][ACTION][token_retrieved] Auth token retrieved successfully.")
|
||||
} else {
|
||||
Timber.w("[WARN][FALLBACK][no_token_found] No auth token found.")
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('getToken')]
|
||||
|
||||
// [ENTITY: Function('clearAllCredentials')]
|
||||
/**
|
||||
* @summary Очищает все сохраненные учетные данные и токены.
|
||||
* @sideeffect Удаляет все записи, связанные с учетными данными, из SharedPreferences.
|
||||
*/
|
||||
override suspend fun clearAllCredentials() {
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.i("[INFO][ACTION][clearing_all_credentials] Clearing all saved credentials and tokens.")
|
||||
encryptedPrefs.edit()
|
||||
.remove(KEY_SERVER_URL)
|
||||
.remove(KEY_USERNAME)
|
||||
.remove(KEY_PASSWORD)
|
||||
.remove(KEY_AUTH_TOKEN)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('clearAllCredentials')]
|
||||
|
||||
// [ENTITY: Function('areCredentialsSavedSync')]
|
||||
/**
|
||||
* @summary Synchronously checks if user credentials are saved.
|
||||
* @return true if all essential credentials (URL, username, password) are present, false otherwise.
|
||||
*/
|
||||
override fun areCredentialsSavedSync(): Boolean {
|
||||
return encryptedPrefs.contains(KEY_SERVER_URL) &&
|
||||
encryptedPrefs.contains(KEY_USERNAME) &&
|
||||
encryptedPrefs.contains(KEY_PASSWORD)
|
||||
}
|
||||
// [END_ENTITY: Function('areCredentialsSavedSync')]
|
||||
}
|
||||
// [END_ENTITY: Class('CredentialsRepositoryImpl')]
|
||||
// [END_FILE_CredentialsRepositoryImpl.kt]
|
||||
|
||||
@@ -16,6 +16,7 @@ package com.homebox.lens.domain.model
|
||||
data class LabelOut(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val color: String,
|
||||
val isArchived: Boolean,
|
||||
val createdAt: String,
|
||||
|
||||
@@ -44,8 +44,25 @@ interface CredentialsRepository {
|
||||
* @summary Retrieves the saved authorization token.
|
||||
* @return The saved token as a String, or null if no token is saved.
|
||||
*/
|
||||
suspend fun getToken(): String?
|
||||
// [END_ENTITY: Function('getToken')]
|
||||
|
||||
suspend fun getToken(): String?
|
||||
// [END_ENTITY: Function('getToken')]
|
||||
|
||||
// [ENTITY: Function('areCredentialsSavedSync')]
|
||||
/**
|
||||
* @summary Synchronously checks if user credentials are saved.
|
||||
* @return true if all essential credentials (URL, username, password) are present, false otherwise.
|
||||
*/
|
||||
fun areCredentialsSavedSync(): Boolean
|
||||
// [END_ENTITY: Function('areCredentialsSavedSync')]
|
||||
|
||||
// [ENTITY: Function('clearAllCredentials')]
|
||||
/**
|
||||
* @summary Clears all saved credentials and tokens.
|
||||
* @sideeffect Removes all credential-related entries from SharedPreferences.
|
||||
*/
|
||||
suspend fun clearAllCredentials()
|
||||
// [END_ENTITY: Function('clearAllCredentials')]
|
||||
}
|
||||
// [END_ENTITY: Interface('CredentialsRepository')]
|
||||
// [END_FILE_CredentialsRepository.kt]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.usecase
|
||||
// [FILE] GetLabelDetailsUseCase.kt
|
||||
// [SEMANTICS] business_logic, use_case, label_retrieval
|
||||
|
||||
// [SEMANTICS] business_logic, use_case, label, get
|
||||
package com.homebox.lens.domain.usecase
|
||||
|
||||
// [IMPORTS]
|
||||
@@ -13,23 +12,25 @@ import javax.inject.Inject
|
||||
// [ENTITY: UseCase('GetLabelDetailsUseCase')]
|
||||
// [RELATION: UseCase('GetLabelDetailsUseCase')] -> [DEPENDS_ON] -> [Interface('ItemRepository')]
|
||||
/**
|
||||
* @summary Получает детальную информацию о метке по ее ID.
|
||||
* @param itemRepository Репозиторий для работы с данными о метках.
|
||||
* @summary Сценарий использования для получения деталей метки.
|
||||
* @param repository Репозиторий для доступа к данным.
|
||||
*/
|
||||
class GetLabelDetailsUseCase @Inject constructor(
|
||||
private val itemRepository: ItemRepository
|
||||
private val repository: ItemRepository
|
||||
) {
|
||||
// [ENTITY: Function('invoke')]
|
||||
/**
|
||||
* @summary Выполняет получение детальной информации о метке.
|
||||
* @param labelId ID запрашиваемой метки.
|
||||
* @return Детальная информация о метке [LabelOut].
|
||||
* @throws IllegalArgumentException если `labelId` пустой.
|
||||
* @throws NoSuchElementException если метка с указанным ID не найдена.
|
||||
* @summary Выполняет получение деталей метки.
|
||||
* @param labelId ID метки для получения деталей.
|
||||
* @return Возвращает полную информацию о метке [LabelOut].
|
||||
* @throws Exception в случае ошибки на уровне репозитория (сеть, API).
|
||||
* @precondition `labelId` не должен быть пустым.
|
||||
*/
|
||||
suspend operator fun invoke(labelId: String): LabelOut {
|
||||
require(labelId.isNotBlank()) { "Label ID cannot be blank." }
|
||||
return itemRepository.getLabelDetails(labelId)
|
||||
return repository.getLabelDetails(labelId)
|
||||
}
|
||||
// [END_ENTITY: Function('invoke')]
|
||||
}
|
||||
// [END_ENTITY: UseCase('GetLabelDetailsUseCase')]
|
||||
// [END_FILE_GetLabelDetailsUseCase.kt]
|
||||
@@ -1,98 +1,51 @@
|
||||
<![CDATA[
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<WORK_ORDER>
|
||||
<META>
|
||||
<VERSION>1.0</VERSION>
|
||||
<CREATED_AT>{timestamp}</CREATED_AT>
|
||||
<TITLE>[ARCHITECT -> DEV] Refactor Item Edit Screen</TITLE>
|
||||
<DESCRIPTION>
|
||||
This work order instructs the developer agent to refactor the Item Edit screen to include all available API fields and implement a user-friendly, grouped layout.
|
||||
</DESCRIPTION>
|
||||
<ASSIGNED_TO>agent-developer</ASSIGNED_TO>
|
||||
<LABELS>type::refactoring,feature::item-edit,status::pending</LABELS>
|
||||
<ID>WO-ITEMEDIT-FIX</ID>
|
||||
<TITLE>[ARCHITECT -> DEV] Исправление выбора локации и меток на экране ItemEdit</TITLE>
|
||||
<DESCRIPTION>В текущей реализации на экране редактирования/создания элемента (ItemEditScreen) поля "Location" и "Labels" неактивны. Необходимо реализовать функционал выбора значения для этих полей из списка доступных.</DESCRIPTION>
|
||||
<CREATED_BY>architect-agent</CREATED_BY>
|
||||
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
|
||||
<STATUS>pending</STATUS>
|
||||
</META>
|
||||
|
||||
<SPECIFICATION>
|
||||
<GOAL>
|
||||
The primary goal is to refactor the `ItemEditScreen` to include all available fields from the Homebox API for creating and updating items, and to present them in a user-friendly manner.
|
||||
</GOAL>
|
||||
|
||||
<CONTEXT>
|
||||
<FILE_LIST>
|
||||
<FILE path="domain/src/main/java/com/homebox/lens/domain/model/Item.kt" />
|
||||
<FILE path="data/src/main/java/com/homebox/lens/data/db/entity/ItemEntity.kt" />
|
||||
<FILE path="data/src/main/java/com/homebox/lens/data/api/dto/ItemCreateDto.kt" />
|
||||
<FILE path="data/src/main/java/com/homebox/lens/data/api/dto/ItemUpdateDto.kt" />
|
||||
<FILE path="data/src/main/java/com/homebox/lens/data/api/dto/ItemOutDto.kt" />
|
||||
<FILE path="data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt" />
|
||||
<FILE path="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt" />
|
||||
<FILE path="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" />
|
||||
</FILE_LIST>
|
||||
</CONTEXT>
|
||||
</SPECIFICATION>
|
||||
|
||||
<EXECUTION_PLAN>
|
||||
<STEP id="1" name="Update Domain Model">
|
||||
<ACTION>Modify `domain/src/main/java/com/homebox/lens/domain/model/Item.kt`.</ACTION>
|
||||
<TASK_BREAKDOWN>
|
||||
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt">
|
||||
<ACTION>Загрузка списков локаций и меток.</ACTION>
|
||||
<DETAILS>
|
||||
- Add the following fields to the `Item` data class:
|
||||
- `archived: Boolean`
|
||||
- `assetId: String?`
|
||||
- `fields: List<CustomField>`
|
||||
- `insured: Boolean`
|
||||
- `lifetimeWarranty: Boolean`
|
||||
- `manufacturer: String?`
|
||||
- `modelNumber: String?`
|
||||
- `notes: String?`
|
||||
- `parentId: String?`
|
||||
- `purchaseFrom: String?`
|
||||
- `purchaseTime: String?`
|
||||
- `serialNumber: String?`
|
||||
- `soldNotes: String?`
|
||||
- `soldPrice: Double?`
|
||||
- `soldTime: String?`
|
||||
- `soldTo: String?`
|
||||
- `syncChildItemsLocations: Boolean`
|
||||
- `warrantyDetails: String?`
|
||||
- `warrantyExpires: String?`
|
||||
- Rename the existing `value: BigDecimal?` field to `purchasePrice: Double?`.
|
||||
1. Внедрите `GetAllLocationsUseCase` и `GetAllLabelsUseCase` в `ItemEditViewModel`.
|
||||
2. Обновите `ItemEditUiState`, добавив два новых поля: `val allLocations: List<Location> = emptyList()` и `val allLabels: List<Label> = emptyList()`.
|
||||
3. В функции `loadItem`, после загрузки основной информации о товаре, вызовите `getAllLocationsUseCase` и `getAllLabelsUseCase` и обновите `uiState` полученными списками.
|
||||
4. Добавьте публичные методы `updateLocation(location: Location)` и `updateLabels(labels: List<Label>)` для обновления `item` в `uiState`.
|
||||
</DETAILS>
|
||||
</STEP>
|
||||
|
||||
<STEP id="2" name="Update Data Layer">
|
||||
<ACTION>Update database entity, DTOs, and mappers.</ACTION>
|
||||
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
|
||||
<ACTION>Реализация UI для выбора локации.</ACTION>
|
||||
<DETAILS>
|
||||
- Modify `data/src/main/java/com/homebox/lens/data/db/entity/ItemEntity.kt` to reflect the new `Item` model.
|
||||
- Ensure `ItemCreateDto`, `ItemUpdateDto`, and `ItemOutDto` contain all necessary fields according to the API spec.
|
||||
- Update the mapping functions in `data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt` to handle all new fields correctly across all layers.
|
||||
1. Замените `OutlinedTextField` для локации на `ExposedDropdownMenuBox`.
|
||||
2. В качестве `dropdownMenu` используйте `DropdownMenuItem` для каждого элемента из `uiState.allLocations`.
|
||||
3. При выборе элемента из списка вызывайте `viewModel.updateLocation(selectedLocation)`.
|
||||
4. В `ExposedDropdownMenuBox` должно отображаться `item.location?.name`.
|
||||
</DETAILS>
|
||||
</STEP>
|
||||
|
||||
<STEP id="3" name="Update ViewModel">
|
||||
<ACTION>Modify `app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt`.</ACTION>
|
||||
<STEP n="3" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
|
||||
<ACTION>Реализация UI для выбора меток (множественный выбор).</ACTION>
|
||||
<DETAILS>
|
||||
- Update `loadItem` and `saveItem` to handle the state for all new fields.
|
||||
- Add public functions to update the state for each new field (e.g., `updateManufacturer(String)`, `toggleInsured(Boolean)`).
|
||||
1. Поле для меток `Labels` должно оставаться `OutlinedTextField` (read-only), но `onClick` по нему должен открывать диалоговое окно (`AlertDialog`).
|
||||
2. В `AlertDialog` отобразите список всех меток (`uiState.allLabels`) с `Checkbox`'ами.
|
||||
3. Состояние `Checkbox`'ов должно соответствовать списку `item.labels`.
|
||||
4. При нажатии на "OK" в диалоге, вызывайте `viewModel.updateLabels(selectedLabels)`.
|
||||
</DETAILS>
|
||||
</STEP>
|
||||
</TASK_BREAKDOWN>
|
||||
|
||||
<STEP id="4" name="Implement UI">
|
||||
<ACTION>Refactor `app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt`.</ACTION>
|
||||
<DETAILS>
|
||||
- Implement a user-friendly layout using tabs or expandable cards for the following groups: General, Purchase, Warranty, Identification, Status & Notes.
|
||||
- Use appropriate controls for each field type (e.g., `Switch` for booleans, Date Picker for dates).
|
||||
- Connect all UI controls to the ViewModel.
|
||||
- Implement basic input validation.
|
||||
</DETAILS>
|
||||
</STEP>
|
||||
</EXECUTION_PLAN>
|
||||
<ACCEPTANCE_CRITERIA>
|
||||
<CRITERION>При нажатии на поле "Location" открывается выпадающий список со всеми локациями.</CRITERION>
|
||||
<CRITERION>Выбранная локация отображается в поле и сохраняется вместе с элементом.</CRITERION>
|
||||
<CRITERION>При нажатии на поле "Labels" открывается диалоговое окно со списком всех меток и чекбоксами.</CRITERION>
|
||||
<CRITERION>Выбранные метки отображаются в поле и сохраняются вместе с элементом.</CRITERION>
|
||||
</ACCEPTANCE_CRITERIA>
|
||||
|
||||
<VERIFICATION>
|
||||
<CHECK>The application compiles successfully.</CHECK>
|
||||
<CHECK>The Item Edit screen displays all new fields, grouped logically.</CHECK>
|
||||
<CHECK>Creating a new item with all fields populated works correctly.</CHECK>
|
||||
<CHECK>Editing an existing item and modifying the new fields works correctly.</CHECK>
|
||||
<CHECK>Data is persisted correctly and is visible in the Item Details screen after saving.</CHECK>
|
||||
</VERIFICATION>
|
||||
|
||||
</WORK_ORDER>
|
||||
]]>
|
||||
</WORK_ORDER>
|
||||
59
tasks/work_order_login_screen.xml
Normal file
59
tasks/work_order_login_screen.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<WORK_ORDER>
|
||||
<META>
|
||||
<ID>WO-LOGIN-REFACTOR</ID>
|
||||
<TITLE>[ARCHITECT -> DEV] Рефакторинг экрана входа и логики первого запуска</TITLE>
|
||||
<DESCRIPTION>Цель этой задачи - изменить логику запуска приложения. Экран входа (SetupScreen) должен появляться только при первом запуске, когда учетные данные еще не сохранены. В последующие запуски пользователь должен сразу попадать на главный экран (Dashboard). Также необходимо улучшить визуальное оформление экрана входа.</DESCRIPTION>
|
||||
<CREATED_BY>architect-agent</CREATED_BY>
|
||||
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
|
||||
<STATUS>pending</STATUS>
|
||||
</META>
|
||||
|
||||
<TASK_BREAKDOWN>
|
||||
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt">
|
||||
<ACTION>Добавить public-метод для синхронной проверки наличия учетных данных.</ACTION>
|
||||
<DETAILS>
|
||||
Добавьте в класс `SetupViewModel` новый метод `fun areCredentialsSaved(): Boolean`.
|
||||
Этот метод должен синхронно проверять, сохранены ли учетные данные в `CredentialsRepository`.
|
||||
Текущая реализация `getCredentials()` асинхронна, что не подходит для быстрой проверки в `NavGraph`.
|
||||
Вам может потребоваться изменить `CredentialsRepository` для поддержки синхронной проверки (например, используя `SharedPreferences` напрямую).
|
||||
</DETAILS>
|
||||
</STEP>
|
||||
|
||||
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/splash/SplashScreen.kt">
|
||||
<ACTION>Создать новый `SplashScreen`.</ACTION>
|
||||
<DETAILS>
|
||||
Создайте новый Composable-экран `SplashScreen.kt`.
|
||||
Этот экран будет новой точкой входа в `NavGraph`.
|
||||
Он будет использовать `SetupViewModel` для вызова `areCredentialsSaved()` и, в зависимости от результата, немедленно навигироваться либо на `Screen.Setup`, либо на `Screen.Dashboard`.
|
||||
Пока идет проверка, на экране должен отображаться `CircularProgressIndicator`.
|
||||
</DETAILS>
|
||||
</STEP>
|
||||
|
||||
<STEP n="3" file="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt">
|
||||
<ACTION>Обновить `NavGraph` для использования `SplashScreen`.</ACTION>
|
||||
<DETAILS>
|
||||
Измените `startDestination` в `NavHost` на `Screen.Splash.route`.
|
||||
Добавьте `composable` для `SplashScreen`.
|
||||
В `SplashScreen` вызовите `navController.navigate` с очисткой бэкстека (`popUpTo(Screen.Splash.route) { inclusive = true }`), чтобы пользователь не мог вернуться на сплэш-экран.
|
||||
</DETAILS>
|
||||
</STEP>
|
||||
|
||||
<STEP n="4" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt">
|
||||
<ACTION>Улучшить UI экрана `SetupScreen`.</ACTION>
|
||||
<DETAILS>
|
||||
Текущий UI слишком прост. Добавьте заголовок, иконку приложения, и более приятное расположение элементов.
|
||||
Используйте `Card` для группировки полей ввода. Добавьте `Spacer` для лучшего отступа.
|
||||
Кнопку "Connect" сделайте более заметной.
|
||||
</DETAILS>
|
||||
</STEP>
|
||||
|
||||
</TASK_BREAKDOWN>
|
||||
|
||||
<ACCEPTANCE_CRITERIA>
|
||||
<CRITERION>При первом запуске приложения открывается `SetupScreen`.</CRITERION>
|
||||
<CRITERION>После успешного ввода данных и входа, при последующих перезапусках приложения открывается `DashboardScreen`, минуя `SetupScreen`.</CRITERION>
|
||||
<CRITERION>`SetupScreen` имеет улучшенный и более привлекательный дизайн.</CRITERION>
|
||||
</ACCEPTANCE_CRITERIA>
|
||||
|
||||
</WORK_ORDER>
|
||||
Reference in New Issue
Block a user