5 Commits

Author SHA1 Message Date
07a8d82a4d Login to dashboard worked 2025-08-09 10:36:45 +03:00
d9fc689185 docs: Update UI component statuses after review
- Set Dashboard and Setup screens to 'реализовано' (implemented).
- Set all other screens to 'заглушка' (stub) to reflect their actual state.
2025-08-09 10:19:43 +03:00
2874c3dd67 docs(i18n): Translate project specifications to Russian
Translated the content of tech_spec.txt and project_structure.txt to Russian, including descriptions and statuses, while keeping the tag structure intact as per the guidelines.
2025-08-09 10:12:40 +03:00
258deb93d9 docs: Update project specs and align statuses
- Add Timber to technical decisions in tech_spec.txt
- Update feature statuses to 'backend_implemented' in tech_spec.txt
- Update UI component statuses to 'needs_review' in project_structure.txt
2025-08-09 10:07:59 +03:00
2853b5a47e feat: Implement setup screen and login logic
- Add SetupScreen with UI for server URL, username, and password input.
- Make SetupScreen the initial screen in the navigation graph.
- Implement secure credential storage using EncryptedSharedPreferences.
- Create CredentialsRepository and AuthRepository to manage credentials and auth tokens.
- Add LoginUseCase to handle the business logic for logging in.
- Implement a temporary Retrofit client in ItemRepository to handle login against a user-provided URL.
- Integrate login logic into SetupViewModel.
- Update all relevant project documentation and DI modules.
2025-08-08 20:17:50 +03:00
34 changed files with 1460 additions and 284 deletions

View File

@@ -8,6 +8,13 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.homebox.lens.ui.screen.dashboard.DashboardScreen import com.homebox.lens.ui.screen.dashboard.DashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.search.SearchScreen
import com.homebox.lens.ui.screen.setup.SetupScreen
// [CORE-LOGIC] // [CORE-LOGIC]
/** /**
@@ -19,12 +26,36 @@ fun NavGraph() {
val navController = rememberNavController() val navController = rememberNavController()
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.Dashboard.route startDestination = Screen.Setup.route
) { ) {
composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Setup.route) { inclusive = true }
}
})
}
composable(route = Screen.Dashboard.route) { composable(route = Screen.Dashboard.route) {
DashboardScreen() DashboardScreen()
} }
// TODO: Добавить остальные экраны в граф навигации composable(route = Screen.InventoryList.route) {
InventoryListScreen()
}
composable(route = Screen.ItemDetails.route) {
ItemDetailsScreen()
}
composable(route = Screen.ItemEdit.route) {
ItemEditScreen()
}
composable(route = Screen.LabelsList.route) {
LabelsListScreen()
}
composable(route = Screen.LocationsList.route) {
LocationsListScreen()
}
composable(route = Screen.Search.route) {
SearchScreen()
}
} }
} }
// [END_FILE_NavGraph.kt] // [END_FILE_NavGraph.kt]

View File

@@ -11,14 +11,17 @@ package com.homebox.lens.navigation
* @property route Строковый идентификатор маршрута. * @property route Строковый идентификатор маршрута.
*/ */
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
/** data object Setup : Screen("setup_screen")
* [CONTRACT]
* Представляет экран "Дэшборд".
*/
data object Dashboard : Screen("dashboard_screen") data object Dashboard : Screen("dashboard_screen")
data object InventoryList : Screen("inventory_list_screen")
// TODO: Добавить объекты для остальных экранов: data object ItemDetails : Screen("item_details_screen/{itemId}") {
// data object ItemDetails : Screen("item_details_screen") fun createRoute(itemId: String) = "item_details_screen/$itemId"
// data object Search : Screen("search_screen") }
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
fun createRoute(itemId: String) = "item_edit_screen/$itemId"
}
data object LabelsList : Screen("labels_list_screen")
data object LocationsList : Screen("locations_list_screen")
data object Search : Screen("search_screen")
} }
// [END_FILE_Screen.kt] // [END_FILE_Screen.kt]

View File

@@ -1,25 +1,32 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard // [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt // [FILE] DashboardViewModel.kt
// [SEMANTICS] ui, viewmodel, dashboard, hilt // [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
// [IMPORTS]
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS]
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import com.homebox.lens.domain.usecase.GetStatisticsUseCase import com.homebox.lens.domain.usecase.GetStatisticsUseCase
import com.homebox.lens.ui.screen.dashboard.DashboardUiState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber // [FIX] Логирование происходит здесь import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// [CORE-LOGIC] // [VIEWMODEL]
// [ENTITY: ViewModel('DashboardViewModel')]
/**
* [CONTRACT]
* @summary ViewModel для главного экрана (Dashboard).
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
*/
@HiltViewModel @HiltViewModel
class DashboardViewModel @Inject constructor( class DashboardViewModel @Inject constructor(
private val getStatisticsUseCase: GetStatisticsUseCase, private val getStatisticsUseCase: GetStatisticsUseCase,
@@ -27,51 +34,72 @@ class DashboardViewModel @Inject constructor(
private val getAllLabelsUseCase: GetAllLabelsUseCase private val getAllLabelsUseCase: GetAllLabelsUseCase
) : ViewModel() { ) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading) private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
val uiState: StateFlow<DashboardUiState> = _uiState.asStateFlow() // [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
// [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
// должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
val uiState = _uiState.asStateFlow()
// [LIFECYCLE_HANDLER]
init { init {
loadDashboardData() loadDashboardData()
} }
private fun loadDashboardData() { /**
Timber.i("[ACTION] Starting dashboard data load.") * [CONTRACT]
_uiState.value = DashboardUiState.Loading * @summary Загружает все необходимые данные для экрана Dashboard.
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
*/
fun loadDashboardData() {
// [ENTRYPOINT]
viewModelScope.launch { viewModelScope.launch {
try { _uiState.value = DashboardUiState.Loading
// Параллельно запрашиваем все данные // [FIX] Используем Timber для логирования.
val statsDeferred = async { getStatisticsUseCase() } Timber.i("[ACTION] Starting parallel dashboard data load. State -> Loading.")
val locationsDeferred = async { getAllLocationsUseCase() }
val labelsDeferred = async { getAllLabelsUseCase() }
val stats = statsDeferred.await() // [CORE-LOGIC: PARALLEL_FETCH]
val locations = locationsDeferred.await() val result = runCatching {
val labels = labelsDeferred.await() coroutineScope {
val statsDeferred = async { getStatisticsUseCase() }
val locationsDeferred = async { getAllLocationsUseCase() }
val labelsDeferred = async { getAllLabelsUseCase() }
// [ACTION] Логируем результат здесь, во ViewModel val stats = statsDeferred.await()
if (stats != null && locations != null && labels != null) { val locations = locationsDeferred.await()
val labels = labelsDeferred.await()
// [POSTCONDITION_CHECK]
check(stats != null && locations != null && labels != null) {
"[POSTCONDITION_FAILED] One or more dashboard data sources returned null."
}
Triple(stats, locations, labels)
}
}
// [RESULT_HANDLER]
result.fold(
onSuccess = { (stats, locations, labels) ->
// [FIX] Используем Timber для логирования.
Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
_uiState.value = DashboardUiState.Success( _uiState.value = DashboardUiState.Success(
statistics = stats, statistics = stats,
locations = locations, locations = locations,
labels = labels labels = labels
) )
Timber.i("[COHERENCE_CHECK_PASSED] Dashboard data loaded successfully.") },
} else { onFailure = { exception ->
// Одна из операций вернула null // [FIX] Используем Timber для логирования ошибок с передачей исключения.
val errorMessage = "Failed to load dashboard data: " + Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
"stats is ${if(stats==null) "null" else "ok"}, " + _uiState.value = DashboardUiState.Error(
"locations is ${if(locations==null) "null" else "ok"}, " + message = exception.message ?: "Could not load dashboard data."
"labels is ${if(labels==null) "null" else "ok"}" )
Timber.e(errorMessage)
_uiState.value = DashboardUiState.Error("Could not load all dashboard data.")
} }
} catch (e: Exception) { )
// [ERROR_HANDLER] Эта ошибка будет отловлена, если сама корутина `launch` упадет
Timber.e(e, "[ERROR] Critical failure in loadDashboardData coroutine.")
_uiState.value = DashboardUiState.Error(e.message ?: "An unknown critical error occurred")
}
} }
} }
// [END_CLASS_DashboardViewModel]
} }
// [END_FILE_DashboardViewModel.kt] // [END_FILE_DashboardViewModel.kt]

View File

@@ -0,0 +1,120 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt
package com.homebox.lens.ui.screen.setup
import androidx.compose.foundation.layout.*
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.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
// [FIX] Opt-in for experimental Material 3 APIs
@OptIn(ExperimentalMaterial3Api::class)
// [ENTRYPOINT]
@Composable
fun SetupScreen(
viewModel: SetupViewModel = hiltViewModel(),
onSetupComplete: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
if (uiState.isSetupComplete) {
onSetupComplete()
}
SetupScreenContent(
uiState = uiState,
onServerUrlChange = viewModel::onServerUrlChange,
onUsernameChange = viewModel::onUsernameChange,
onPasswordChange = viewModel::onPasswordChange,
onConnectClick = viewModel::connect
)
}
// [FIX] Opt-in for experimental Material 3 APIs
@OptIn(ExperimentalMaterial3Api::class)
// [CONTENT]
@Composable
private fun SetupScreenContent(
uiState: SetupUiState,
onServerUrlChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onConnectClick: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(title = { Text("Server Setup") })
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = uiState.serverUrl,
onValueChange = onServerUrlChange,
label = { Text("Server URL") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.username,
onValueChange = onUsernameChange,
label = { Text("Username") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.password,
onValueChange = onPasswordChange,
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onConnectClick,
enabled = !uiState.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Text("Connect")
}
}
uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
}
}
}
// [FIX] Opt-in for experimental Material 3 APIs
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
fun SetupScreenPreview() {
SetupScreenContent(
uiState = SetupUiState(error = "Failed to connect"),
onServerUrlChange = {},
onUsernameChange = {},
onPasswordChange = {},
onConnectClick = {}
)
}
// [END_FILE_SetupScreen.kt]

View File

@@ -0,0 +1,27 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupUiState.kt
// [SEMANTICS] ui_state, data_model, immutable
package com.homebox.lens.ui.screen.setup
/**
* [ENTITY: DataClass('SetupUiState')]
* [CONTRACT]
* Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
* Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
* @property serverUrl URL-адрес сервера Homebox.
* @property username Имя пользователя для входа.
* @property password Пароль пользователя.
* @property isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
* @property error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
* @property isSetupComplete Флаг, указывающий на успешное завершение настройки и входа.
*/
data class SetupUiState(
val serverUrl: String = "",
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null,
val isSetupComplete: Boolean = false
)
// [END_FILE_SetupUiState.kt]

View File

@@ -0,0 +1,143 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupViewModel.kt
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
package com.homebox.lens.ui.screen.setup
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.usecase.LoginUseCase
import com.homebox.lens.ui.screen.setup.SetupUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
// [VIEWMODEL]
// [ENTITY: ViewModel('SetupViewModel')]
/**
* [CONTRACT]
* ViewModel для экрана первоначальной настройки (Setup).
* Отвечает за:
* 1. Загрузку и сохранение учетных данных (URL сервера, логин, пароль).
* 2. Управление состоянием UI экрана (`SetupUiState`).
* 3. Инициацию процесса входа в систему через `LoginUseCase`.
* @property credentialsRepository Репозиторий для операций с учетными данными.
* @property loginUseCase Use case для выполнения логики входа.
* @invariant Состояние `uiState` всегда является единственным источником истины для UI.
*/
@HiltViewModel
class SetupViewModel @Inject constructor(
private val credentialsRepository: CredentialsRepository,
private val loginUseCase: LoginUseCase
) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow(SetupUiState())
val uiState = _uiState.asStateFlow()
// [LIFECYCLE_HANDLER]
init {
// [ACTION] Загружаем учетные данные при создании ViewModel.
loadCredentials()
}
/**
* [CONTRACT]
* [HELPER] Загружает учетные данные из репозитория при инициализации.
* @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными.
*/
private fun loadCredentials() {
// [ENTRYPOINT]
viewModelScope.launch {
// [CORE-LOGIC] Подписываемся на поток учетных данных.
credentialsRepository.getCredentials().collect { credentials ->
// [ACTION] Обновляем состояние, если учетные данные существуют.
if (credentials != null) {
_uiState.update {
it.copy(
serverUrl = credentials.serverUrl,
username = credentials.username,
password = credentials.password
)
}
}
}
}
}
/**
* [CONTRACT]
* [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя.
* @param newUrl Новое значение URL.
* @sideeffect Обновляет поле `serverUrl` в `_uiState`.
*/
fun onServerUrlChange(newUrl: String) {
_uiState.update { it.copy(serverUrl = newUrl) }
}
/**
* [CONTRACT]
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
* @param newUsername Новое значение имени пользователя.
* @sideeffect Обновляет поле `username` в `_uiState`.
*/
fun onUsernameChange(newUsername: String) {
_uiState.update { it.copy(username = newUsername) }
}
/**
* [CONTRACT]
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
* @param newPassword Новое значение пароля.
* @sideeffect Обновляет поле `password` в `_uiState`.
*/
fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) }
}
/**
* [CONTRACT]
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
* Выполняет две основные операции:
* 1. Сохраняет введенные учетные данные для последующих сессий.
* 2. Выполняет вход в систему с использованием этих данных.
* @sideeffect Обновляет `_uiState`, управляя флагами `isLoading`, `error` и `isSetupComplete`.
* @sideeffect Вызывает `credentialsRepository.saveCredentials`, изменяя сохраненные данные.
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
*/
fun connect() {
// [ENTRYPOINT]
viewModelScope.launch {
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
_uiState.update { it.copy(isLoading = true, error = null) }
// [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов.
val credentials = Credentials(
serverUrl = _uiState.value.serverUrl.trim(),
username = _uiState.value.username.trim(),
password = _uiState.value.password
)
// [ACTION] Сохраняем учетные данные для будущего использования.
credentialsRepository.saveCredentials(credentials)
// [CORE-LOGIC] Выполняем UseCase и обрабатываем результат.
loginUseCase(credentials).fold(
onSuccess = {
// [ACTION] Обработка успеха: обновляем UI, отмечая завершение настройки.
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
},
onFailure = { exception ->
// [ERROR_HANDLER] Обработка ошибки: обновляем UI с сообщением об ошибке.
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
}
)
}
}
// [END_CLASS_SetupViewModel]
}
// [END_FILE_SetupViewModel.kt]

View File

@@ -94,6 +94,7 @@ object Libs {
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4" const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4"
const val composeUiTooling = "androidx.compose.ui:ui-tooling" const val composeUiTooling = "androidx.compose.ui:ui-tooling"
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest" const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
} }
// [END_FILE_Dependencies.kt] // [END_FILE_Dependencies.kt]

View File

@@ -10,11 +10,14 @@ import com.homebox.lens.data.api.dto.ItemSummaryDto
import com.homebox.lens.data.api.dto.ItemUpdateDto import com.homebox.lens.data.api.dto.ItemUpdateDto
import com.homebox.lens.data.api.dto.LabelOutDto import com.homebox.lens.data.api.dto.LabelOutDto
import com.homebox.lens.data.api.dto.LocationOutCountDto import com.homebox.lens.data.api.dto.LocationOutCountDto
import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.dto.PaginationResultDto import com.homebox.lens.data.api.dto.PaginationResultDto
import com.homebox.lens.data.api.dto.TokenResponseDto
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.PUT import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
@@ -27,6 +30,13 @@ import retrofit2.http.Query
*/ */
interface HomeboxApiService { interface HomeboxApiService {
// [ENDPOINT] Auth
// [FIX] Явно указываем заголовок Content-Type, чтобы переопределить
// значение по умолчанию от Moshi, которое содержит "; charset=UTF-8".
@Headers("Content-Type: application/json")
@POST("v1/users/login")
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
// [ENDPOINT] Items // [ENDPOINT] Items
@GET("v1/items") @GET("v1/items")
suspend fun getItems( suspend fun getItems(

View File

@@ -13,24 +13,34 @@ import com.homebox.lens.domain.model.GroupStatistics
/** /**
* [CONTRACT] * [CONTRACT]
* DTO для статистики. * DTO для статистики.
* [COHERENCE_NOTE] Этот DTO был исправлен, чтобы точно соответствовать JSON-ответу от сервера.
* Поля `items`, `labels`, `locations`, `totalValue` были заменены на `totalItems`, `totalLabels`,
* `totalLocations`, `totalItemPrice` и т.д., чтобы устранить ошибку парсинга `JsonDataException`.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GroupStatisticsDto( data class GroupStatisticsDto(
@Json(name = "items") val items: Int, @Json(name = "totalItems") val totalItems: Int,
@Json(name = "labels") val labels: Int, @Json(name = "totalLabels") val totalLabels: Int,
@Json(name = "locations") val locations: Int, @Json(name = "totalLocations") val totalLocations: Int,
@Json(name = "totalValue") val totalValue: Double @Json(name = "totalItemPrice") val totalItemPrice: Double,
// [FIX] Добавляем недостающие поля, которые присутствуют в JSON, но отсутствовали в DTO.
// Делаем их nullable на случай, если API перестанет их присылать в будущем.
@Json(name = "totalUsers") val totalUsers: Int? = null,
@Json(name = "totalWithWarranty") val totalWithWarranty: Int? = null
) )
/** /**
* [CONTRACT] * [CONTRACT]
* Маппер из GroupStatisticsDto в доменную модель GroupStatistics. * Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
* [COHERENCE_NOTE] Маппер обновлен для использования правильных полей из исправленного DTO.
*/ */
fun GroupStatisticsDto.toDomain(): GroupStatistics { fun GroupStatisticsDto.toDomain(): GroupStatistics {
// [ACTION] Маппим данные из DTO в доменную модель.
return GroupStatistics( return GroupStatistics(
items = this.items, items = this.totalItems,
labels = this.labels, labels = this.totalLabels,
locations = this.locations, locations = this.totalLocations,
totalValue = this.totalValue totalValue = this.totalItemPrice
) )
} }
// [END_FILE_GroupStatisticsDto.kt]

View File

@@ -13,28 +13,39 @@ import com.homebox.lens.domain.model.LabelOut
/** /**
* [CONTRACT] * [CONTRACT]
* DTO для метки. * DTO для метки.
* [COHERENCE_NOTE] Поле `isArchived` сделано nullable (`Boolean?`),
* так как оно отсутствует в JSON-ответе от сервера. Это исправляет ошибку парсинга
* `JsonDataException: Required value 'isArchived' missing`.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LabelOutDto( data class LabelOutDto(
@Json(name = "id") val id: String, @Json(name = "id") val id: String,
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "color") val color: String, // [COHERENCE_NOTE] Поле `color` может быть null или отсутствовать, делаем его nullable для безопасности.
@Json(name = "isArchived") val isArchived: Boolean, @Json(name = "color") val color: String?,
// [FIX] Поле отсутствует в JSON, делаем nullable.
@Json(name = "isArchived") val isArchived: Boolean?,
@Json(name = "createdAt") val createdAt: String, @Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String @Json(name = "updatedAt") val updatedAt: String,
// [COHERENCE_NOTE] Поле `description` также может отсутствовать, добавляем его как nullable.
@Json(name = "description") val description: String?
) )
/** /**
* [CONTRACT] * [CONTRACT]
* Маппер из LabelOutDto в доменную модель LabelOut. * Маппер из LabelOutDto в доменную модель LabelOut.
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
* и предоставления non-nullable значений по умолчанию для доменной модели.
*/ */
fun LabelOutDto.toDomain(): LabelOut { fun LabelOutDto.toDomain(): LabelOut {
return LabelOut( return LabelOut(
id = this.id, id = this.id,
name = this.name, name = this.name,
color = this.color, // [FIX] Используем Elvis-оператор для предоставления значения по умолчанию.
isArchived = this.isArchived, color = this.color ?: "", // Пустая строка как дефолтный цвет
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
createdAt = this.createdAt, createdAt = this.createdAt,
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )
} }
// [END_FILE_LabelOutDto.kt]

View File

@@ -13,30 +13,42 @@ import com.homebox.lens.domain.model.LocationOutCount
/** /**
* [CONTRACT] * [CONTRACT]
* DTO для местоположения со счетчиком. * DTO для местоположения со счетчиком.
* [COHERENCE_NOTE] Поля `color` и `isArchived` сделаны nullable (`String?`, `Boolean?`),
* так как они отсутствуют в JSON-ответе от сервера. Это исправляет ошибку парсинга
* `JsonDataException: Required value '...' missing`.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationOutCountDto( data class LocationOutCountDto(
@Json(name = "id") val id: String, @Json(name = "id") val id: String,
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "color") val color: String, // [FIX] Поле отсутствует в JSON, делаем nullable.
@Json(name = "isArchived") val isArchived: Boolean, @Json(name = "color") val color: String?,
// [FIX] Поле отсутствует в JSON, делаем nullable.
@Json(name = "isArchived") val isArchived: Boolean?,
@Json(name = "itemCount") val itemCount: Int, @Json(name = "itemCount") val itemCount: Int,
@Json(name = "createdAt") val createdAt: String, @Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String @Json(name = "updatedAt") val updatedAt: String,
// [COHERENCE_NOTE] Поле `description` также может отсутствовать или быть null,
// поэтому его тоже безопасно сделать nullable.
@Json(name = "description") val description: String?
) )
/** /**
* [CONTRACT] * [CONTRACT]
* Маппер из LocationOutCountDto в доменную модель LocationOutCount. * Маппер из LocationOutCountDto в доменную модель LocationOutCount.
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
* и предоставления non-nullable значений по умолчанию для доменной модели.
*/ */
fun LocationOutCountDto.toDomain(): LocationOutCount { fun LocationOutCountDto.toDomain(): LocationOutCount {
return LocationOutCount( return LocationOutCount(
id = this.id, id = this.id,
name = this.name, name = this.name,
color = this.color, // [FIX] Используем Elvis-оператор для предоставления значения по умолчанию, если поле null.
isArchived = this.isArchived, color = this.color ?: "", // Пустая строка как дефолтный цвет
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
itemCount = this.itemCount, itemCount = this.itemCount,
createdAt = this.createdAt, createdAt = this.createdAt,
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )
} }
// [END_FILE_LocationOutCountDto.kt]

View File

@@ -0,0 +1,15 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LoginFormDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LoginFormDto(
@Json(name = "username") val username: String,
@Json(name = "password") val password: String,
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
)
// [END_FILE_LoginFormDto.kt]

View File

@@ -0,0 +1,15 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] TokenResponseDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TokenResponseDto(
@Json(name = "token") val token: String,
@Json(name = "attachmentToken") val attachmentToken: String,
@Json(name = "expiresAt") val expiresAt: String
)
// [END_FILE_TokenResponseDto.kt]

View File

@@ -0,0 +1,29 @@
// [PACKAGE] com.homebox.lens.data.api.mapper
// [FILE] TokenMapper.kt
// [SEMANTICS] mapper, data_conversion, clean_architecture
package com.homebox.lens.data.api.mapper
import com.homebox.lens.data.api.dto.TokenResponseDto
import com.homebox.lens.domain.model.TokenResponse
/**
* [CONTRACT]
* [HELPER] Преобразует DTO-объект токена в доменную модель.
* @receiver [TokenResponseDto] объект из слоя данных.
* @return [TokenResponse] объект для доменного слоя.
* @throws IllegalArgumentException если токен в DTO пустой.
*/
fun TokenResponseDto.toDomain(): TokenResponse {
// [PRECONDITION] DTO должен содержать валидные данные для маппинга.
require(this.token.isNotBlank()) { "[PRECONDITION_FAILED] DTO token is blank, cannot map to domain model." }
// [ACTION]
val domainModel = TokenResponse(token = this.token)
// [POSTCONDITION] Проверяем, что инвариант доменной модели соблюден.
check(domainModel.token.isNotBlank()) { "[POSTCONDITION_FAILED] Domain model token is blank after mapping." }
return domainModel
}
// [END_FILE_TokenMapper.kt]

View File

@@ -0,0 +1,29 @@
// [PACKAGE] com.homebox.lens.data.api.model
// [FILE] LoginRequest.kt
// [SEMANTICS] dto, network, serialization, authentication
package com.homebox.lens.data.api.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* [ENTITY: DataClass('LoginRequest')]
* [CONTRACT]
* DTO (Data Transfer Object) для запроса на аутентификацию.
* @property username Имя пользователя.
* @property password Пароль пользователя.
* @invariant Свойства не должны быть пустыми.
*/
@JsonClass(generateAdapter = true)
data class LoginRequest(
@Json(name = "username") val username: String,
@Json(name = "password") val password: String
) {
init {
// [INVARIANT_CHECK]
require(username.isNotBlank()) { "[INVARIANT_FAILED] Username cannot be blank." }
require(password.isNotBlank()) { "[INVARIANT_FAILED] Password cannot be blank." }
}
}
// [END_FILE_LoginRequest.kt]

View File

@@ -1,77 +1,135 @@
// [PACKAGE] com.homebox.lens.data.di // [PACKAGE] com.homebox.lens.data.di
// [FILE] ApiModule.kt // [FILE] ApiModule.kt
// [PURPOSE] Предоставляет синглтон-зависимости для работы с сетью, включая OkHttpClient, Retrofit и ApiService.
package com.homebox.lens.data.di package com.homebox.lens.data.di
// [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.domain.repository.CredentialsRepository
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
// [CONTRACT]
/** /**
* [MODULE: DaggerHilt('ApiModule')] * [ENTITY: Module('ApiModule')]
* [PURPOSE] Предоставляет зависимости для работы с сетью (Retrofit, OkHttp, Moshi). * [CONTRACT]
* Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
* необходимых для сетевого взаимодействия.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object ApiModule { object ApiModule {
// [HELPER] // [HELPER] Временный базовый URL для API. В будущем должен стать динамическим.
private const val BASE_URL = "https://api.homebox.app/" private const val BASE_URL = "https://homebox.bebesh.ru/api/"
// [PROVIDER] /**
* [PROVIDER]
* [CONTRACT]
* Предоставляет сконфигурированный OkHttpClient.
* @param credentialsRepositoryProvider Провайдер репозитория для доступа к токену авторизации.
* Используется Provider<T> для предотвращения циклов зависимостей.
* @return Синглтон-экземпляр OkHttpClient с настроенными перехватчиками.
*/
@Provides @Provides
@Singleton @Singleton
fun provideOkHttpClient(): OkHttpClient { fun provideOkHttpClient(
// [ACTION] Create logging interceptor credentialsRepositoryProvider: Provider<CredentialsRepository>
val logging = HttpLoggingInterceptor().apply { ): OkHttpClient {
// [ACTION] Создаем перехватчик для логирования.
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY level = HttpLoggingInterceptor.Level.BODY
} }
// [ACTION] Build OkHttpClient
// [ACTION] Создаем перехватчик для добавления заголовка 'Accept'.
val acceptHeaderInterceptor = Interceptor { chain ->
val request = chain.request().newBuilder()
.header("Accept", "application/json")
.build()
chain.proceed(request)
}
// [CORE-LOGIC] Создаем перехватчик для добавления токена авторизации.
val authInterceptor = Interceptor { chain ->
// [HELPER] Получаем токен из репозитория.
// runBlocking здесь допустим, т.к. чтение из SharedPreferences - быстрая I/O операция,
// а интерфейс Interceptor'а является синхронным.
val token = runBlocking { credentialsRepositoryProvider.get().getToken() }
val requestBuilder = chain.request().newBuilder()
// [ACTION] Если токен существует, добавляем его в заголовок.
if (token != null) {
// Сервер ожидает заголовок "Authorization: Bearer <token>"
// Предполагается, что `token` уже содержит префикс "Bearer ".
requestBuilder.addHeader("Authorization", token)
}
chain.proceed(requestBuilder.build())
}
// [ACTION] Собираем OkHttpClient с правильным порядком перехватчиков.
return OkHttpClient.Builder() return OkHttpClient.Builder()
.addInterceptor(logging) .addInterceptor(acceptHeaderInterceptor)
// [TODO] Add AuthInterceptor for Bearer token .addInterceptor(authInterceptor) // Добавляем перехватчик для токена
.addInterceptor(loggingInterceptor) // Логирование должно идти последним, чтобы видеть финальный запрос
.build() .build()
} }
// [PROVIDER] /**
* [PROVIDER]
* [CONTRACT] Предоставляет экземпляр Moshi для парсинга JSON.
*/
@Provides @Provides
@Singleton @Singleton
fun provideMoshi(): Moshi { fun provideMoshi(): Moshi {
// [ACTION] Build Moshi with Kotlin adapter
return Moshi.Builder() return Moshi.Builder()
.add(KotlinJsonAdapterFactory()) .add(KotlinJsonAdapterFactory())
.build() .build()
} }
// [PROVIDER] /**
* [PROVIDER]
* [CONTRACT] Предоставляет фабрику конвертеров для Retrofit.
*/
@Provides @Provides
@Singleton @Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit { fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
// [ACTION] Build Retrofit instance return MoshiConverterFactory.create(moshi)
}
/**
* [PROVIDER]
* [CONTRACT] Предоставляет сконфигурированный экземпляр Retrofit.
*/
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(BASE_URL) .baseUrl(BASE_URL)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi)) .addConverterFactory(moshiConverterFactory)
.build() .build()
} }
// [PROVIDER] /**
* [PROVIDER]
* [CONTRACT] Предоставляет реализацию интерфейса HomeboxApiService.
*/
@Provides @Provides
@Singleton @Singleton
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService { fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
// [ACTION] Create ApiService from Retrofit instance
return retrofit.create(HomeboxApiService::class.java) return retrofit.create(HomeboxApiService::class.java)
} }
} }
// [END_FILE_ApiModule.kt]
// [END_FILE_ApiModule.kt]

View File

@@ -1,32 +1,61 @@
// [PACKAGE] com.homebox.lens.data.di // [PACKAGE] com.homebox.lens.data.di
// [FILE] RepositoryModule.kt // [FILE] RepositoryModule.kt
// [SEMANTICS] dependency_injection, hilt, module, binding
package com.homebox.lens.data.di package com.homebox.lens.data.di
import com.homebox.lens.data.api.HomeboxApiService import com.homebox.lens.data.repository.AuthRepositoryImpl
import com.homebox.lens.data.repository.CredentialsRepositoryImpl
import com.homebox.lens.data.repository.ItemRepositoryImpl import com.homebox.lens.data.repository.ItemRepositoryImpl
import com.homebox.lens.domain.repository.AuthRepository
import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.repository.ItemRepository import com.homebox.lens.domain.repository.ItemRepository
import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
// [CONTRACT]
/** /**
* [MODULE: DaggerHilt('RepositoryModule')] * [ENTITY: Module('RepositoryModule')]
* [PURPOSE] Предоставляет реализацию для интерфейса ItemRepository. * [CONTRACT]
* Hilt-модуль для предоставления реализаций репозиториев.
* Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object RepositoryModule { abstract class RepositoryModule {
// [PROVIDER] /**
@Provides * [CONTRACT]
* Связывает интерфейс ItemRepository с его реализацией.
*/
@Binds
@Singleton @Singleton
fun provideItemRepository(apiService: HomeboxApiService): ItemRepository { abstract fun bindItemRepository(
return ItemRepositoryImpl(apiService) itemRepositoryImpl: ItemRepositoryImpl
} ): ItemRepository
}
// [END_FILE_RepositoryModule.kt] /**
* [CONTRACT]
* Связывает интерфейс CredentialsRepository с его реализацией.
*/
@Binds
@Singleton
abstract fun bindCredentialsRepository(
credentialsRepositoryImpl: CredentialsRepositoryImpl
): CredentialsRepository
/**
* [CONTRACT]
* [FIX] Связывает интерфейс AuthRepository с его реализацией.
* Это исправляет ошибку "could not be resolved", так как теперь Hilt знает,
* какую конкретную реализацию предоставить, когда запрашивается AuthRepository.
*/
@Binds
@Singleton
abstract fun bindAuthRepository(
authRepositoryImpl: AuthRepositoryImpl
): AuthRepository
}
// [END_FILE_RepositoryModule.kt]

View File

@@ -0,0 +1,41 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] StorageModule.kt
package com.homebox.lens.data.di
import android.content.Context
import android.content.SharedPreferences
import com.homebox.lens.data.repository.EncryptedPreferencesWrapper
import com.homebox.lens.data.security.CryptoManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object StorageModule {
private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs" // No longer secret
// [ACTION] Provide a standard, unencrypted SharedPreferences instance.
@Provides
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
}
// [ACTION] Provide our new EncryptedPreferencesWrapper as the main entry point for secure storage.
// Hilt will automatically provide SharedPreferences and CryptoManager to its constructor.
@Provides
@Singleton
fun provideEncryptedPreferencesWrapper(
sharedPreferences: SharedPreferences,
cryptoManager: CryptoManager
): EncryptedPreferencesWrapper {
return EncryptedPreferencesWrapper(sharedPreferences, cryptoManager)
}
}
// [END_FILE_StorageModule.kt]

View File

@@ -0,0 +1,88 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] AuthRepositoryImpl.kt
// [SEMANTICS] data_implementation, authentication, repository
package com.homebox.lens.data.repository
// [IMPORTS]
import android.content.SharedPreferences
import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.mapper.toDomain
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.model.TokenResponse
import com.homebox.lens.domain.repository.AuthRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Inject
/**
* [ENTITY: Class('AuthRepositoryImpl')]
* [CONTRACT]
* Реализация репозитория для управления аутентификацией.
* @param encryptedPrefs Защищенное хранилище для токена.
* @param okHttpClient Общий OkHttp клиент для переиспользования.
* @param moshiConverterFactory Общий конвертер Moshi для переиспользования.
* [COHERENCE_NOTE] Реализация метода login теперь включает логику создания временного Retrofit-клиента
* "на лету", используя URL сервера из credentials. Эта логика была перенесена из ItemRepositoryImpl.
*/
class AuthRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences,
private val okHttpClient: OkHttpClient,
private val moshiConverterFactory: MoshiConverterFactory
) : AuthRepository {
companion object {
private const val KEY_AUTH_TOKEN = "key_auth_token"
}
/**
* [CONTRACT]
* Реализует вход пользователя. Создает временный API сервис для выполнения запроса
* на указанный пользователем URL сервера.
* @param credentials Учетные данные пользователя, включая URL сервера.
* @return [Result] с доменной моделью [TokenResponse] при успехе или [Exception] при ошибке.
*/
override suspend fun login(credentials: Credentials): Result<TokenResponse> {
// [PRECONDITION]
require(credentials.serverUrl.isNotBlank()) { "[PRECONDITION_FAILED] Server URL cannot be blank." }
// [CORE-LOGIC]
return withContext(Dispatchers.IO) {
runCatching {
// [ACTION] Создаем временный Retrofit клиент с URL, указанным пользователем.
val tempApiService = Retrofit.Builder()
.baseUrl(credentials.serverUrl)
.client(okHttpClient) // Переиспользуем существующий OkHttp клиент
.addConverterFactory(moshiConverterFactory) // и конвертер
.build()
.create(HomeboxApiService::class.java)
// [ACTION] Создаем DTO и выполняем запрос.
val loginForm = LoginFormDto(credentials.username, credentials.password)
val tokenResponseDto = tempApiService.login(loginForm)
// [ACTION] Маппим результат в доменную модель.
tokenResponseDto.toDomain()
}
}
}
override suspend fun saveToken(token: String) {
require(token.isNotBlank()) { "[PRECONDITION_FAILED] Token cannot be blank." }
withContext(Dispatchers.IO) {
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
}
}
override fun getToken(): Flow<String?> = flow {
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
}.flowOn(Dispatchers.IO)
}
// [END_FILE_AuthRepositoryImpl.kt]

View File

@@ -0,0 +1,99 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] CredentialsRepositoryImpl.kt
// [PURPOSE] Имплементация репозитория для управления учетными данными и токенами доступа.
package com.homebox.lens.data.repository
// [IMPORTS]
import android.content.SharedPreferences
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.repository.CredentialsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import javax.inject.Inject
/**
* [ENTITY: Class('CredentialsRepositoryImpl')]
* [CONTRACT]
* Реализует репозиторий для управления учетными данными пользователя.
* Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
* @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt.
* @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`.
*/
class CredentialsRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences
) : CredentialsRepository {
// [CONSTANTS_KEYS] Ключи для хранения данных в SharedPreferences.
companion object {
private const val KEY_SERVER_URL = "key_server_url"
private const val KEY_USERNAME = "key_username"
private const val KEY_PASSWORD = "key_password"
private const val KEY_AUTH_TOKEN = "key_auth_token"
}
/**
* [CONTRACT]
* Сохраняет основные учетные данные пользователя.
* @param credentials Объект с учетными данными для сохранения.
* @sideeffect Перезаписывает существующие учетные данные в SharedPreferences.
*/
override suspend fun saveCredentials(credentials: Credentials) {
// [ACTION] Выполняем запись в SharedPreferences в фоновом потоке.
withContext(Dispatchers.IO) {
encryptedPrefs.edit()
.putString(KEY_SERVER_URL, credentials.serverUrl)
.putString(KEY_USERNAME, credentials.username)
.putString(KEY_PASSWORD, credentials.password)
.apply()
}
}
/**
* [CONTRACT]
* Извлекает сохраненные учетные данные пользователя в виде потока.
* @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют.
*/
override fun getCredentials(): Flow<Credentials?> = flow {
// [CORE-LOGIC] Читаем данные из SharedPreferences.
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
val username = encryptedPrefs.getString(KEY_USERNAME, null)
val password = encryptedPrefs.getString(KEY_PASSWORD, null)
// [ACTION] Эммитим результат.
if (serverUrl != null && username != null && password != null) {
emit(Credentials(serverUrl, username, password))
} else {
emit(null)
}
}.flowOn(Dispatchers.IO) // [ACTION] Указываем, что Flow должен выполняться в фоновом потоке.
/**
* [CONTRACT]
* Сохраняет токен авторизации.
* @param token Токен для сохранения.
* @sideeffect Перезаписывает существующий токен в SharedPreferences.
*/
override suspend fun saveToken(token: String) {
// [ACTION] Выполняем запись токена в фоновом потоке.
withContext(Dispatchers.IO) {
encryptedPrefs.edit()
.putString(KEY_AUTH_TOKEN, token)
.apply()
}
}
/**
* [CONTRACT]
* Извлекает сохраненный токен авторизации.
* @return Строка с токеном или null, если он не найден.
*/
override suspend fun getToken(): String? {
// [ACTION] Выполняем чтение токена в фоновом потоке.
return withContext(Dispatchers.IO) {
encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
}
}
}
// [END_FILE_CredentialsRepositoryImpl.kt]

View File

@@ -0,0 +1,66 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] EncryptedPreferencesWrapper.kt
// [PURPOSE] A wrapper around SharedPreferences to provide on-the-fly encryption/decryption.
package com.homebox.lens.data.repository
import android.content.SharedPreferences
import com.homebox.lens.data.security.CryptoManager
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.nio.charset.Charset
import javax.inject.Inject
/**
* [CONTRACT]
* Provides a simplified and secure interface for storing and retrieving sensitive string data.
* It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance.
* @param sharedPreferences The underlying standard SharedPreferences instance to store encrypted data.
* @param cryptoManager The manager responsible for all cryptographic operations.
*/
class EncryptedPreferencesWrapper @Inject constructor(
private val sharedPreferences: SharedPreferences,
private val cryptoManager: CryptoManager
) {
/**
* [CONTRACT]
* Retrieves a decrypted string value for a given key.
* @param key The key for the preference.
* @param defaultValue The value to return if the key is not found or decryption fails.
* @return The decrypted string, or the defaultValue.
*/
fun getString(key: String, defaultValue: String?): String? {
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue
return try {
val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT)
val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes))
String(decryptedBytes, Charset.defaultCharset())
} catch (e: Exception) {
// Log the error, maybe clear the invalid preference
defaultValue
}
}
/**
* [CONTRACT]
* Encrypts and saves a string value for a given key.
* @param key The key for the preference.
* @param value The string value to encrypt and save.
* @sideeffect Modifies the underlying SharedPreferences file.
*/
fun putString(key: String, value: String) {
try {
val outputStream = ByteArrayOutputStream()
cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream)
val encryptedBytes = outputStream.toByteArray()
val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT)
sharedPreferences.edit().putString(key, encryptedValue).apply()
} catch (e: Exception) {
// Log the error
}
}
// [COHERENCE_NOTE] Add other methods like getInt, putInt etc. as needed, following the same pattern.
}
// [END_FILE_EncryptedPreferencesWrapper.kt]

View File

@@ -1,7 +1,9 @@
// [PACKAGE] com.homebox.lens.data.repository // [PACKAGE] com.homebox.lens.data.repository
// [FILE] ItemRepositoryImpl.kt // [FILE] ItemRepositoryImpl.kt
// [SEMANTICS] data_repository, implementation, network // [SEMANTICS] data_repository, implementation, items
package com.homebox.lens.data.repository package com.homebox.lens.data.repository
// [IMPORTS] // [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.toDomain import com.homebox.lens.data.api.dto.toDomain
@@ -16,17 +18,20 @@ import javax.inject.Singleton
* [CONTRACT] * [CONTRACT]
* Реализация репозитория для работы с данными о вещах. * Реализация репозитория для работы с данными о вещах.
* @param apiService Сервис для взаимодействия с Homebox API. * @param apiService Сервис для взаимодействия с Homebox API.
* [COHERENCE_NOTE] Метод 'login' был полностью удален из этого класса, так как его ответственность
* была передана в AuthRepositoryImpl. Это устраняет ошибку компиляции "'login' overrides nothing".
*/ */
@Singleton @Singleton
class ItemRepositoryImpl @Inject constructor( class ItemRepositoryImpl @Inject constructor(
private val apiService: HomeboxApiService private val apiService: HomeboxApiService,
) : ItemRepository { ) : ItemRepository {
// [DELETED] Метод login был здесь, но теперь он удален.
/** /**
* [CONTRACT] @see ItemRepository.createItem * [CONTRACT] @see ItemRepository.createItem
*/ */
override suspend fun createItem(newItemData: ItemCreate): ItemSummary { override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
// [ACTION]
val itemDto = newItemData.toDto() val itemDto = newItemData.toDto()
val resultDto = apiService.createItem(itemDto) val resultDto = apiService.createItem(itemDto)
return resultDto.toDomain() return resultDto.toDomain()
@@ -36,7 +41,6 @@ class ItemRepositoryImpl @Inject constructor(
* [CONTRACT] @see ItemRepository.getItemDetails * [CONTRACT] @see ItemRepository.getItemDetails
*/ */
override suspend fun getItemDetails(itemId: String): ItemOut { override suspend fun getItemDetails(itemId: String): ItemOut {
// [ACTION]
val resultDto = apiService.getItem(itemId) val resultDto = apiService.getItem(itemId)
return resultDto.toDomain() return resultDto.toDomain()
} }
@@ -45,7 +49,6 @@ class ItemRepositoryImpl @Inject constructor(
* [CONTRACT] @see ItemRepository.updateItem * [CONTRACT] @see ItemRepository.updateItem
*/ */
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut { override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
// [ACTION]
val itemDto = item.toDto() val itemDto = item.toDto()
val resultDto = apiService.updateItem(itemId, itemDto) val resultDto = apiService.updateItem(itemId, itemDto)
return resultDto.toDomain() return resultDto.toDomain()
@@ -55,7 +58,6 @@ class ItemRepositoryImpl @Inject constructor(
* [CONTRACT] @see ItemRepository.deleteItem * [CONTRACT] @see ItemRepository.deleteItem
*/ */
override suspend fun deleteItem(itemId: String) { override suspend fun deleteItem(itemId: String) {
// [ACTION]
apiService.deleteItem(itemId) apiService.deleteItem(itemId)
} }
@@ -63,7 +65,6 @@ class ItemRepositoryImpl @Inject constructor(
* [CONTRACT] @see ItemRepository.syncInventory * [CONTRACT] @see ItemRepository.syncInventory
*/ */
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> { override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
// [ACTION]
val resultDto = apiService.getItems(page = page, pageSize = pageSize) val resultDto = apiService.getItems(page = page, pageSize = pageSize)
return resultDto.toDomain { it.toDomain() } return resultDto.toDomain { it.toDomain() }
} }
@@ -72,7 +73,6 @@ class ItemRepositoryImpl @Inject constructor(
* [CONTRACT] @see ItemRepository.getStatistics * [CONTRACT] @see ItemRepository.getStatistics
*/ */
override suspend fun getStatistics(): GroupStatistics { override suspend fun getStatistics(): GroupStatistics {
// [ACTION]
val resultDto = apiService.getStatistics() val resultDto = apiService.getStatistics()
return resultDto.toDomain() return resultDto.toDomain()
} }
@@ -81,7 +81,6 @@ class ItemRepositoryImpl @Inject constructor(
* [CONTRACT] @see ItemRepository.getAllLocations * [CONTRACT] @see ItemRepository.getAllLocations
*/ */
override suspend fun getAllLocations(): List<LocationOutCount> { override suspend fun getAllLocations(): List<LocationOutCount> {
// [ACTION]
val resultDto = apiService.getLocations() val resultDto = apiService.getLocations()
return resultDto.map { it.toDomain() } return resultDto.map { it.toDomain() }
} }
@@ -90,7 +89,6 @@ class ItemRepositoryImpl @Inject constructor(
* [CONTRACT] @see ItemRepository.getAllLabels * [CONTRACT] @see ItemRepository.getAllLabels
*/ */
override suspend fun getAllLabels(): List<LabelOut> { override suspend fun getAllLabels(): List<LabelOut> {
// [ACTION]
val resultDto = apiService.getLabels() val resultDto = apiService.getLabels()
return resultDto.map { it.toDomain() } return resultDto.map { it.toDomain() }
} }
@@ -99,7 +97,6 @@ class ItemRepositoryImpl @Inject constructor(
* [CONTRACT] @see ItemRepository.searchItems * [CONTRACT] @see ItemRepository.searchItems
*/ */
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> { override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
// [ACTION]
val resultDto = apiService.getItems(query = query) val resultDto = apiService.getItems(query = query)
return resultDto.toDomain { it.toDomain() } return resultDto.toDomain { it.toDomain() }
} }

View File

@@ -0,0 +1,106 @@
// [PACKAGE] com.homebox.lens.data.security
// [FILE] CryptoManager.kt
// [PURPOSE] Handles all cryptographic operations using AndroidKeyStore.
package com.homebox.lens.data.security
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
import java.io.InputStream
import java.io.OutputStream
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.inject.Inject
import javax.inject.Singleton
/**
* [CONTRACT]
* A manager for handling encryption and decryption using the Android Keystore system.
* This class ensures that cryptographic keys are stored securely.
* It is designed to be a Singleton provided by Hilt.
* @invariant The underlying SecretKey must be valid within the AndroidKeyStore.
*/
@RequiresApi(Build.VERSION_CODES.M)
@Singleton
class CryptoManager @Inject constructor() {
// [ЯКОРЬ] Настройки для шифрования
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
private val encryptCipher
get() = Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.ENCRYPT_MODE, getKey())
}
private fun getDecryptCipherForIv(iv: ByteArray): Cipher {
return Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv))
}
}
// [CORE-LOGIC] Получение или создание ключа
private fun getKey(): SecretKey {
val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry
return existingKey?.secretKey ?: createKey()
}
private fun createKey(): SecretKey {
return KeyGenerator.getInstance(ALGORITHM).apply {
init(
KeyGenParameterSpec.Builder(
ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(BLOCK_MODE)
.setEncryptionPaddings(PADDING)
.setUserAuthenticationRequired(false)
.setRandomizedEncryptionRequired(true)
.build()
)
}.generateKey()
}
// [ACTION] Шифрование потока данных
fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray {
val cipher = encryptCipher
val encryptedBytes = cipher.doFinal(bytes)
outputStream.use {
it.write(cipher.iv.size)
it.write(cipher.iv)
it.write(encryptedBytes.size)
it.write(encryptedBytes)
}
return encryptedBytes
}
// [ACTION] Дешифрование потока данных
fun decrypt(inputStream: InputStream): ByteArray {
return inputStream.use {
val ivSize = it.read()
val iv = ByteArray(ivSize)
it.read(iv)
val encryptedBytesSize = it.read()
val encryptedBytes = ByteArray(encryptedBytesSize)
it.read(encryptedBytes)
getDecryptCipherForIv(iv).doFinal(encryptedBytes)
}
}
companion object {
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
private const val ALIAS = "homebox_lens_secret_key"
}
}
// [END_FILE_CryptoManager.kt]

View File

@@ -0,0 +1,18 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Credentials.kt
package com.homebox.lens.domain.model
/**
* [CONTRACT]
* Data class to hold server credentials.
* @property serverUrl The URL of the Homebox server.
* @property username The username for authentication.
* @property password The password for authentication.
*/
data class Credentials(
val serverUrl: String,
val username: String,
val password: String
)
// [END_FILE_Credentials.kt]

View File

@@ -0,0 +1,20 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] TokenResponse.kt
// [SEMANTICS] data_transfer_object, authentication, model
package com.homebox.lens.domain.model
/**
* [ENTITY: DataClass('TokenResponse')]
* [CONTRACT]
* Модель данных, представляющая ответ от сервера с токеном аутентификации.
* @property token Строка, содержащая JWT или другой токен доступа.
* @invariant `token` не должен быть пустым.
*/
data class TokenResponse(val token: String) {
init {
// [INVARIANT_CHECK]
require(token.isNotBlank()) { "[INVARIANT_FAILED] Token cannot be blank." }
}
}
// [END_FILE_TokenResponse.kt]

View File

@@ -0,0 +1,42 @@
// [PACKAGE] com.homebox.lens.domain.repository
// [FILE] AuthRepository.kt
// [SEMANTICS] authentication, data_access, repository
package com.homebox.lens.domain.repository
// [IMPORTS]
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.model.TokenResponse
import kotlinx.coroutines.flow.Flow
/**
* [CONTRACT]
* Репозиторий для управления аутентификацией.
* [COHERENCE_NOTE] Добавлен метод `login` для инкапсуляции логики входа.
*/
interface AuthRepository {
/**
* [CONTRACT]
* Выполняет вход в систему, используя предоставленные учетные данные.
* @param credentials Учетные данные пользователя (URL сервера, логин, пароль).
* @return [Result] с [TokenResponse] в случае успеха, или с [Exception] в случае ошибки.
* @throws IllegalArgumentException если `credentials` невалидны (предусловие).
*/
suspend fun login(credentials: Credentials): Result<TokenResponse>
/**
* [CONTRACT]
* Сохраняет токен аутентификации.
* @param token Токен для сохранения.
* @throws IllegalArgumentException если `token` пустой (предусловие).
*/
suspend fun saveToken(token: String)
/**
* [CONTRACT]
* Получает токен аутентификации.
* @return [Flow], который эммитит токен в виде строки, или `null`, если токен отсутствует.
*/
fun getToken(): Flow<String?>
}
// [END_FILE_AuthRepository.kt]

View File

@@ -0,0 +1,44 @@
// [PACKAGE] com.homebox.lens.domain.repository
// [FILE] CredentialsRepository.kt
package com.homebox.lens.domain.repository
import com.homebox.lens.domain.model.Credentials
import kotlinx.coroutines.flow.Flow
/**
* [CONTRACT]
* Repository for managing user credentials and session tokens.
*/
interface CredentialsRepository {
/**
* [CONTRACT]
* Saves the user's base credentials (URL, username, password) securely.
* @param credentials The credentials to save.
* @sideeffect Overwrites any existing saved credentials.
*/
suspend fun saveCredentials(credentials: Credentials)
/**
* [CONTRACT]
* Retrieves the saved user credentials.
* @return A Flow emitting the saved [Credentials], or null if none are saved.
*/
fun getCredentials(): Flow<Credentials?>
/**
* [CONTRACT]
* [ACTION] Saves the authorization token received after a successful login.
* @param token The authorization token (including "Bearer " prefix if provided by the server).
* @sideeffect Overwrites any existing saved token.
*/
suspend fun saveToken(token: String)
/**
* [CONTRACT]
* [ACTION] Retrieves the saved authorization token.
* @return The saved token as a String, or null if no token is saved.
*/
suspend fun getToken(): String?
}
// [END_FILE_CredentialsRepository.kt]

View File

@@ -1,7 +1,9 @@
// [PACKAGE] com.homebox.lens.domain.repository // [PACKAGE] com.homebox.lens.domain.repository
// [FILE] ItemRepository.kt // [FILE] ItemRepository.kt
// [SEMANTICS] data_access, abstraction, repository // [SEMANTICS] data_access, abstraction, repository
package com.homebox.lens.domain.repository package com.homebox.lens.domain.repository
// [IMPORTS] // [IMPORTS]
import com.homebox.lens.domain.model.* import com.homebox.lens.domain.model.*
@@ -10,8 +12,10 @@ import com.homebox.lens.domain.model.*
* [CONTRACT] * [CONTRACT]
* Абстракция репозитория для работы с "Вещами". * Абстракция репозитория для работы с "Вещами".
* Определяет контракт, которому должен следовать слой данных. * Определяет контракт, которому должен следовать слой данных.
* [COHERENCE_NOTE] Метод `login` был удален, так как он относится к аутентификации и перенесен в `AuthRepository`.
*/ */
interface ItemRepository { interface ItemRepository {
// [DELETED] suspend fun login(credentials: Credentials): Result<Unit>
suspend fun createItem(newItemData: ItemCreate): ItemSummary suspend fun createItem(newItemData: ItemCreate): ItemSummary
suspend fun getItemDetails(itemId: String): ItemOut suspend fun getItemDetails(itemId: String): ItemOut
suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut
@@ -22,4 +26,4 @@ interface ItemRepository {
suspend fun getAllLabels(): List<LabelOut> suspend fun getAllLabels(): List<LabelOut>
suspend fun searchItems(query: String): PaginationResult<ItemSummary> suspend fun searchItems(query: String): PaginationResult<ItemSummary>
} }
// [END_FILE_ItemRepository.kt] // [END_FILE_ItemRepository.kt]

View File

@@ -1,25 +1,21 @@
// [PACKAGE] com.homebox.lens.domain.usecase // [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt // [FILE] GetAllLabelsUseCase.kt
// [SEMANTICS] domain, usecase, label, list
// [IMPORTS]
package com.homebox.lens.domain.usecase package com.homebox.lens.domain.usecase
import com.homebox.lens.domain.model.LabelOut import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.repository.ItemRepository import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject import javax.inject.Inject
// [CORE-LOGIC] class GetAllLabelsUseCase @Inject constructor(private val repository: ItemRepository) {
class GetAllLabelsUseCase @Inject constructor( /**
private val itemRepository: ItemRepository * [CONTRACT]
) { * Получает список всех меток.
suspend operator fun invoke(): List<LabelOut>? { * @return Список [LabelOut].
return try { * @throws Exception в случае ошибки сети или API.
itemRepository.getAllLabels() */
} catch (e: Exception) { suspend operator fun invoke(): List<LabelOut> {
// [ERROR_HANDLER] Просто возвращаем null. // [FIX] Упрощено.
null return repository.getAllLabels()
}
} }
} }
// [END_FILE_GetAllLabelsUseCase.kt]

View File

@@ -1,25 +1,21 @@
// [PACKAGE] com.homebox.lens.domain.usecase // [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt // [FILE] GetAllLocationsUseCase.kt
// [SEMANTICS] domain, usecase, location, list
// [IMPORTS]
package com.homebox.lens.domain.usecase package com.homebox.lens.domain.usecase
import com.homebox.lens.domain.model.LocationOutCount import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.domain.repository.ItemRepository import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject import javax.inject.Inject
// [CORE-LOGIC] class GetAllLocationsUseCase @Inject constructor(private val repository: ItemRepository) {
class GetAllLocationsUseCase @Inject constructor( /**
private val itemRepository: ItemRepository * [CONTRACT]
) { * Получает список всех локаций.
suspend operator fun invoke(): List<LocationOutCount>? { * @return Список [LocationOutCount].
return try { * @throws Exception в случае ошибки сети или API.
itemRepository.getAllLocations() */
} catch (e: Exception) { suspend operator fun invoke(): List<LocationOutCount> {
// [ERROR_HANDLER] Просто возвращаем null. // [FIX] Упрощено.
null return repository.getAllLocations()
}
} }
} }
// [END_FILE_GetAllLocationsUseCase.kt]

View File

@@ -1,25 +1,22 @@
// [PACKAGE] com.homebox.lens.domain.usecase // [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt // [FILE] GetStatisticsUseCase.kt
// [SEMANTICS] domain, usecase, statistics
// [IMPORTS]
package com.homebox.lens.domain.usecase package com.homebox.lens.domain.usecase
import com.homebox.lens.domain.model.GroupStatistics import com.homebox.lens.domain.model.GroupStatistics
import com.homebox.lens.domain.repository.ItemRepository import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject import javax.inject.Inject
// [CORE-LOGIC] class GetStatisticsUseCase @Inject constructor(private val repository: ItemRepository) {
class GetStatisticsUseCase @Inject constructor( /**
private val itemRepository: ItemRepository * [CONTRACT]
) { * Получает статистику инвентаря.
suspend operator fun invoke(): GroupStatistics? { * @return [GroupStatistics] объект.
return try { * @throws Exception в случае ошибки сети или API.
itemRepository.getStatistics() */
} catch (e: Exception) { suspend operator fun invoke(): GroupStatistics {
// [ERROR_HANDLER] Просто возвращаем null, вызывающий слой обработает это. // [FIX] Упрощено. Просто вызываем репозиторий и возвращаем его результат.
null // Обработка ошибок делегирована вызывающей стороне (ViewModel).
} return repository.getStatistics()
} }
} }
// [END_FILE_GetStatisticsUseCase.kt]

View File

@@ -0,0 +1,52 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] LoginUseCase.kt
// [PURPOSE] Инкапсулирует бизнес-логику процесса входа пользователя в систему.
package com.homebox.lens.domain.usecase
// [IMPORTS]
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.model.TokenResponse
import com.homebox.lens.domain.repository.AuthRepository
import javax.inject.Inject
/**
* [ENTITY: Class('LoginUseCase')]
* [CONTRACT]
* Use case для выполнения входа пользователя.
* @param authRepository Репозиторий для выполнения сетевого запроса на вход и сохранения токена.
* [COHERENCE_NOTE] Удалена зависимость от CredentialsRepository для сохранения токена.
* Эту ответственность теперь несет AuthRepository.
*/
class LoginUseCase @Inject constructor(
private val authRepository: AuthRepository
) {
/**
* [CONTRACT]
* Выполняет процесс входа в систему.
* @param credentials Учетные данные пользователя.
* @return [Result] с [Unit] в случае успеха или с [Exception] в случае ошибки.
* @sideeffect В случае успеха, сохраняет токен авторизации через `authRepository`.
*/
suspend operator fun invoke(credentials: Credentials): Result<Unit> {
// [PRECONDITION]
require(credentials.serverUrl.isNotBlank() && credentials.username.isNotBlank()) {
"[PRECONDITION_FAILED] Server URL and username must not be blank."
}
// [ACTION] Выполняем вход через authRepository.
val loginResult: Result<TokenResponse> = authRepository.login(credentials)
// [CORE-LOGIC] Обрабатываем результат с помощью `fold`.
return loginResult.fold(
onSuccess = { tokenResponse ->
// [ACTION] В случае успеха, сохраняем токен через тот же репозиторий.
authRepository.saveToken(tokenResponse.token)
Result.success(Unit)
},
onFailure = { exception ->
// [ACTION] В случае ошибки, просто пробрасываем ее дальше.
Result.failure(exception)
}
)
}
}
// [END_FILE_LoginUseCase.kt]

View File

@@ -1,117 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<PROJECT_STRUCTURE> <PROJECT_STRUCTURE>
<module name="app" type="android_app"> <module name="app" type="android_app">
<purpose_summary>Main application module, contains UI and application entry points.</purpose_summary> <purpose_summary>Основной модуль приложения, содержит UI и точки входа в приложение.</purpose_summary>
<file name="app/src/main/java/com/homebox/lens/MainActivity.kt" status="implemented" ref_id="entry_point"> <file name="app/src/main/java/com/homebox/lens/MainActivity.kt" status="реализовано" ref_id="entry_point">
<purpose_summary>The main and only Activity of the application, hosts the NavHost.</purpose_summary> <purpose_summary>Главная и единственная Activity приложения, содержит NavHost.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/MainApplication.kt" status="implemented" ref_id="app_context"> <file name="app/src/main/java/com/homebox/lens/MainApplication.kt" status="реализовано" ref_id="app_context">
<purpose_summary>Application class, used for Hilt dependency injection setup.</purpose_summary> <purpose_summary>Класс Application, используется для настройки внедрения зависимостей Hilt.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/di/AppModule.kt" status="implemented" ref_id="di_app"> <file name="app/src/main/java/com/homebox/lens/di/AppModule.kt" status="реализовано" ref_id="di_app">
<purpose_summary>Hilt module for application-wide dependencies.</purpose_summary> <purpose_summary>Модуль Hilt для зависимостей уровня приложения.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt" status="implemented" ref_id="nav_graph"> <file name="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt" status="реализовано" ref_id="nav_graph">
<purpose_summary>Defines the navigation graph for the entire application using Jetpack Compose Navigation.</purpose_summary> <purpose_summary>Определяет навигационный граф для всего приложения с использованием Jetpack Compose Navigation.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/navigation/Screen.kt" status="implemented" ref_id="nav_screen"> <file name="app/src/main/java/com/homebox/lens/navigation/Screen.kt" status="реализовано" ref_id="nav_screen">
<purpose_summary>Defines the routes for all screens in the app as a sealed class.</purpose_summary> <purpose_summary>Определяет маршруты для всех экранов в приложении в виде запечатанного класса.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" status="stub" spec_ref_id="screen_dashboard"> <file name="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" status="реализовано" spec_ref_id="screen_dashboard">
<purpose_summary>UI for the Dashboard screen.</purpose_summary> <purpose_summary>UI для экрана панели управления.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt" status="stub" spec_ref_id="screen_dashboard"> <file name="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt" status="реализовано" spec_ref_id="screen_dashboard">
<purpose_summary>ViewModel for the Dashboard screen, handles business logic.</purpose_summary> <purpose_summary>ViewModel для экрана панели управления, обрабатывает бизнес-логику.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt" status="stub" spec_ref_id="screen_inventory_list"> <file name="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt" status="заглушка" spec_ref_id="screen_inventory_list">
<purpose_summary>UI for the Inventory List screen.</purpose_summary> <purpose_summary>UI для экрана списка инвентаря.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt" status="stub" spec_ref_id="screen_inventory_list"> <file name="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt" status="заглушка" spec_ref_id="screen_inventory_list">
<purpose_summary>ViewModel for the Inventory List screen.</purpose_summary> <purpose_summary>ViewModel для экрана списка инвентаря.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt" status="stub" spec_ref_id="screen_item_details"> <file name="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt" status="заглушка" spec_ref_id="screen_item_details">
<purpose_summary>UI for the Item Details screen.</purpose_summary> <purpose_summary>UI для экрана сведений о товаре.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt" status="stub" spec_ref_id="screen_item_details"> <file name="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt" status="заглушка" spec_ref_id="screen_item_details">
<purpose_summary>ViewModel for the Item Details screen.</purpose_summary> <purpose_summary>ViewModel для экрана сведений о товаре.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" status="stub" spec_ref_id="screen_item_edit"> <file name="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" status="заглушка" spec_ref_id="screen_item_edit">
<purpose_summary>UI for the Item Edit screen.</purpose_summary> <purpose_summary>UI для экрана редактирования товара.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt" status="stub" spec_ref_id="screen_item_edit"> <file name="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt" status="заглушка" spec_ref_id="screen_item_edit">
<purpose_summary>ViewModel for the Item Edit screen.</purpose_summary> <purpose_summary>ViewModel для экрана редактирования товара.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" status="stub" spec_ref_id="screen_labels_list"> <file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" status="заглушка" spec_ref_id="screen_labels_list">
<purpose_summary>UI for the Labels List screen.</purpose_summary> <purpose_summary>UI для экрана списка меток.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt" status="stub" spec_ref_id="screen_labels_list"> <file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt" status="заглушка" spec_ref_id="screen_labels_list">
<purpose_summary>ViewModel for the Labels List screen.</purpose_summary> <purpose_summary>ViewModel для экрана списка меток.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" status="stub" spec_ref_id="screen_locations_list"> <file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" status="заглушка" spec_ref_id="screen_locations_list">
<purpose_summary>UI for the Locations List screen.</purpose_summary> <purpose_summary>UI для экрана списка местоположений.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt" status="stub" spec_ref_id="screen_locations_list"> <file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt" status="заглушка" spec_ref_id="screen_locations_list">
<purpose_summary>ViewModel for the Locations List screen.</purpose_summary> <purpose_summary>ViewModel для экрана списка местоположений.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" status="stub" spec_ref_id="screen_search"> <file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" status="заглушка" spec_ref_id="screen_search">
<purpose_summary>UI for the Search screen.</purpose_summary> <purpose_summary>UI для экрана поиска.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt" status="stub" spec_ref_id="screen_search"> <file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt" status="заглушка" spec_ref_id="screen_search">
<purpose_summary>ViewModel for the Search screen.</purpose_summary> <purpose_summary>ViewModel для экрана поиска.</purpose_summary>
</file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" status="реализовано" spec_ref_id="screen_setup">
<purpose_summary>UI для экрана настройки.</purpose_summary>
</file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt" status="реализовано" spec_ref_id="screen_setup">
<purpose_summary>ViewModel для экрана настройки.</purpose_summary>
</file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupUiState.kt" status="реализовано" spec_ref_id="screen_setup">
<purpose_summary>Состояние UI для экрана настройки.</purpose_summary>
</file> </file>
</module> </module>
<module name="data" type="android_library"> <module name="data" type="android_library">
<purpose_summary>Data layer, responsible for data sources (network, local DB) and repository implementations.</purpose_summary> <purpose_summary>Слой данных, отвечающий за источники данных (сеть, локальная БД) и реализации репозиториев.</purpose_summary>
<file name="data/src/main/java/com/homebox/lens/data/api/HomeboxApiService.kt" status="implemented" ref_id="api_service"> <file name="data/src/main/java/com/homebox/lens/data/api/HomeboxApiService.kt" status="реализовано" ref_id="api_service">
<purpose_summary>Retrofit service interface for the Homebox API.</purpose_summary> <purpose_summary>Интерфейс сервиса Retrofit для Homebox API.</purpose_summary>
</file> </file>
<file name="data/src/main/java/com/homebox/lens/data/db/HomeboxDatabase.kt" status="implemented" ref_id="database"> <file name="data/src/main/java/com/homebox/lens/data/db/HomeboxDatabase.kt" status="реализовано" ref_id="database">
<purpose_summary>Room database definition for local caching.</purpose_summary> <purpose_summary>Определение базы данных Room для локального кэширования.</purpose_summary>
</file> </file>
<file name="data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt" status="implemented" ref_id="repo_impl"> <file name="data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt" status="реализовано" ref_id="repo_impl">
<purpose_summary>Implementation of the ItemRepository, coordinating data from API and local DB.</purpose_summary> <purpose_summary>Реализация ItemRepository, координирующая данные из API и локальной БД.</purpose_summary>
</file> </file>
<file name="data/src/main/java/com/homebox/lens/data/di/ApiModule.kt" status="implemented" ref_id="di_api"> <file name="data/src/main/java/com/homebox/lens/data/di/ApiModule.kt" status="реализовано" ref_id="di_api">
<purpose_summary>Hilt module for providing network-related dependencies (Retrofit, OkHttp).</purpose_summary> <purpose_summary>Модуль Hilt для предоставления зависимостей, связанных с сетью (Retrofit, OkHttp).</purpose_summary>
</file> </file>
<file name="data/src/main/java/com/homebox/lens/data/di/DatabaseModule.kt" status="implemented" ref_id="di_db"> <file name="data/src/main/java/com/homebox/lens/data/di/DatabaseModule.kt" status="реализовано" ref_id="di_db">
<purpose_summary>Hilt module for providing database-related dependencies (Room DB, DAOs).</purpose_summary> <purpose_summary>Модуль Hilt для предоставления зависимостей, связанных с базой данных (Room DB, DAO).</purpose_summary>
</file> </file>
<file name="data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt" status="implemented" ref_id="di_repo"> <file name="data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt" status="реализовано" ref_id="di_repo">
<purpose_summary>Hilt module for binding repository interfaces to their implementations.</purpose_summary> <purpose_summary>Модуль Hilt для привязки интерфейсов репозиториев к их реализациям.</purpose_summary>
</file>
<file name="data/src/main/java/com/homebox/lens/data/di/StorageModule.kt" status="реализовано" ref_id="di_storage">
<purpose_summary>Модуль Hilt для предоставления зависимостей, связанных с хранилищем (EncryptedSharedPreferences).</purpose_summary>
</file>
<file name="data/src/main/java/com/homebox/lens/data/repository/CredentialsRepositoryImpl.kt" status="реализовано" ref_id="repo_credentials_impl">
<purpose_summary>Реализация CredentialsRepository.</purpose_summary>
</file>
<file name="data/src/main/java/com/homebox/lens/data/repository/AuthRepositoryImpl.kt" status="реализовано" ref_id="repo_auth_impl">
<purpose_summary>Реализация AuthRepository.</purpose_summary>
</file> </file>
</module> </module>
<module name="domain" type="kotlin_jvm_library"> <module name="domain" type="kotlin_jvm_library">
<purpose_summary>Domain layer, contains business logic, use cases, and repository interfaces. Pure Kotlin module.</purpose_summary> <purpose_summary>Доменный слой, содержит бизнес-логику, сценарии использования и интерфейсы репозиториев. Чистый модуль Kotlin.</purpose_summary>
<file name="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt" status="implemented" ref_id="repo_interface"> <file name="domain/src/main/java/com/homebox/lens/domain/model/Credentials.kt" status="реализовано" ref_id="model_credentials">
<purpose_summary>Interface defining the contract for data operations related to items.</purpose_summary> <purpose_summary>Класс данных для хранения учетных данных пользователя.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" status="implemented" spec_ref_id="uc_create_item"> <file name="domain/src/main/java/com/homebox/lens/domain/repository/AuthRepository.kt" status="реализовано" ref_id="repo_auth_interface">
<purpose_summary>Use case for creating a new item.</purpose_summary> <purpose_summary>Интерфейс для репозитория аутентификации.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" status="implemented" spec_ref_id="uc_delete_item"> <file name="domain/src/main/java/com/homebox/lens/domain/repository/CredentialsRepository.kt" status="реализовано" ref_id="repo_credentials_interface">
<purpose_summary>Use case for deleting an item.</purpose_summary> <purpose_summary>Интерфейс для репозитория учетных данных.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" status="implemented" spec_ref_id="uc_get_all_labels"> <file name="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt" status="реализовано" ref_id="repo_interface">
<purpose_summary>Use case for getting all labels.</purpose_summary> <purpose_summary>Интерфейс, определяющий контракт для операций с данными, связанными с товарами.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" status="implemented" spec_ref_id="uc_get_all_locations"> <file name="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" status="реализовано" spec_ref_id="uc_login">
<purpose_summary>Use case for getting all locations.</purpose_summary> <purpose_summary>Сценарий использования для входа пользователя.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetItemDetailsUseCase.kt" status="implemented" spec_ref_id="uc_get_item_details"> <file name="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" status="реализовано" spec_ref_id="uc_create_item">
<purpose_summary>Use case for getting the details of a single item.</purpose_summary> <purpose_summary>Сценарий использования для создания нового товара.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" status="implemented" spec_ref_id="uc_get_stats"> <file name="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" status="реализовано" spec_ref_id="uc_delete_item">
<purpose_summary>Use case for getting inventory statistics.</purpose_summary> <purpose_summary>Сценарий использования для удаления товара.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/SearchItemsUseCase.kt" status="implemented" spec_ref_id="uc_search_items"> <file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" status="реализовано" spec_ref_id="uc_get_all_labels">
<purpose_summary>Use case for searching items.</purpose_summary> <purpose_summary>Сценарий использования для получения всех меток.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/SyncInventoryUseCase.kt" status="implemented" spec_ref_id="uc_sync_inventory"> <file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" status="реализовано" spec_ref_id="uc_get_all_locations">
<purpose_summary>Use case for syncing the local inventory with the remote server.</purpose_summary> <purpose_summary>Сценарий использования для получения всех местоположений.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/UpdateItemUseCase.kt" status="implemented" spec_ref_id="uc_update_item"> <file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetItemDetailsUseCase.kt" status="реализовано" spec_ref_id="uc_get_item_details">
<purpose_summary>Use case for updating an existing item.</purpose_summary> <purpose_summary>Сценарий использования для получения сведений о конкретном товаре.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" status="реализовано" spec_ref_id="uc_get_stats">
<purpose_summary>Сценарий использования для получения статистики по инвентарю.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/SearchItemsUseCase.kt" status="реализовано" spec_ref_id="uc_search_items">
<purpose_summary>Сценарий использования для поиска товаров.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/SyncInventoryUseCase.kt" status="реализовано" spec_ref_id="uc_sync_inventory">
<purpose_summary>Сценарий использования для синхронизации локального инвентаря с удаленным сервером.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/UpdateItemUseCase.kt" status="реализовано" spec_ref_id="uc_update_item">
<purpose_summary>Сценарий использования для обновления существующего товара.</purpose_summary>
</file> </file>
</module> </module>
</PROJECT_STRUCTURE> </PROJECT_STRUCTURE>

View File

@@ -2,104 +2,111 @@
<PROJECT_SPECIFICATION> <PROJECT_SPECIFICATION>
<PROJECT_INFO> <PROJECT_INFO>
<name>Homebox Lens</name> <name>Homebox Lens</name>
<description>An Android client for the Homebox inventory management system. It allows users to manage their inventory by interacting with a Homebox server instance.</description> <description>Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox.</description>
</PROJECT_INFO> </PROJECT_INFO>
<TECHNICAL_DECISIONS>
<DECISION id="tech_logging">
<summary>Библиотека логирования</summary>
<description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description>
</DECISION>
</TECHNICAL_DECISIONS>
<FEATURES> <FEATURES>
<FEATURE id="feat_dashboard" status="in_progress"> <FEATURE id="feat_dashboard" status="бэкенд_реализован">
<summary>Dashboard Screen</summary> <summary>Экран панели управления</summary>
<description>Displays a summary of the inventory, including statistics like total items, total value, and counts by location/label.</description> <description>Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам.</description>
<UI_COMPONENT ref_id="screen_dashboard" /> <UI_COMPONENT ref_id="screen_dashboard" />
<FUNCTIONALITY> <FUNCTIONALITY>
<FUNCTION id="func_get_stats" status="implemented"> <FUNCTION id="func_get_stats" status="реализовано">
<summary>Fetch and display statistics</summary> <summary>Получение и отображение статистики</summary>
<description>Retrieves overall inventory statistics from the server.</description> <description>Получает общую статистику по инвентарю с сервера.</description>
<implementation_ref id="uc_get_stats" /> <implementation_ref id="uc_get_stats" />
</FUNCTION> </FUNCTION>
</FUNCTIONALITY> </FUNCTIONALITY>
</FEATURE> </FEATURE>
<FEATURE id="feat_inventory_list" status="in_progress"> <FEATURE id="feat_inventory_list" status="бэкенд_реализован">
<summary>Inventory List Screen</summary> <summary>Экран списка инвентаря</summary>
<description>Displays a searchable and filterable list of all inventory items.</description> <description>Отображает список всех инвентарных позиций с возможностью поиска и фильтрации.</description>
<UI_COMPONENT ref_id="screen_inventory_list" /> <UI_COMPONENT ref_id="screen_inventory_list" />
<FUNCTIONALITY> <FUNCTIONALITY>
<FUNCTION id="func_search_items" status="implemented"> <FUNCTION id="func_search_items" status="реализовано">
<summary>Search and filter items</summary> <summary>Поиск и фильтрация товаров</summary>
<description>Searches for items based on a query string and filters. The results are paginated.</description> <description>Ищет товары по строке запроса и фильтрам. Результаты разбиты на страницы.</description>
<implementation_ref id="uc_search_items" /> <implementation_ref id="uc_search_items" />
</FUNCTION> </FUNCTION>
<FUNCTION id="func_sync_inventory" status="implemented"> <FUNCTION id="func_sync_inventory" status="реализовано">
<summary>Sync Inventory</summary> <summary>Синхронизация инвентаря</summary>
<description>Performs a full synchronization of the local inventory cache with the server.</description> <description>Выполняет полную синхронизацию локального кэша инвентаря с сервером.</description>
<implementation_ref id="uc_sync_inventory" /> <implementation_ref id="uc_sync_inventory" />
</FUNCTION> </FUNCTION>
</FUNCTIONALITY> </FUNCTIONALITY>
</FEATURE> </FEATURE>
<FEATURE id="feat_item_details" status="in_progress"> <FEATURE id="feat_item_details" status="бэкенд_реализован">
<summary>Item Details Screen</summary> <summary>Экран сведений о товаре</summary>
<description>Shows all details for a single inventory item, including its name, description, images, attachments, and custom fields.</description> <description>Показывает все сведения о конкретном инвентарном товаре, включая его название, описание, изображения, вложения и настраиваемые поля.</description>
<UI_COMPONENT ref_id="screen_item_details" /> <UI_COMPONENT ref_id="screen_item_details" />
<FUNCTIONALITY> <FUNCTIONALITY>
<FUNCTION id="func_get_item_details" status="implemented"> <FUNCTION id="func_get_item_details" status="реализовано">
<summary>Fetch Item Details</summary> <summary>Получение сведений о товаре</summary>
<description>Retrieves the full details for a specific item from the repository.</description> <description>Получает полные сведения о конкретном товаре из репозитория.</description>
<implementation_ref id="uc_get_item_details" /> <implementation_ref id="uc_get_item_details" />
</FUNCTION> </FUNCTION>
</FUNCTIONALITY> </FUNCTIONALITY>
</FEATURE> </FEATURE>
<FEATURE id="feat_item_management" status="in_progress"> <FEATURE id="feat_item_management" status="бэкенд_реализован">
<summary>Create/Edit/Delete Items</summary> <summary>Создание/редактирование/удаление товаров</summary>
<description>Allows users to create new items, update existing ones, and delete them.</description> <description>Позволяет пользователям создавать новые товары, обновлять существующие и удалять их.</description>
<UI_COMPONENT ref_id="screen_item_edit" /> <UI_COMPONENT ref_id="screen_item_edit" />
<FUNCTIONALITY> <FUNCTIONALITY>
<FUNCTION id="func_create_item" status="implemented"> <FUNCTION id="func_create_item" status="реализовано">
<summary>Create Item</summary> <summary>Создать товар</summary>
<description>Creates a new inventory item on the server.</description> <description>Создает новый инвентарный товар на сервере.</description>
<implementation_ref id="uc_create_item" /> <implementation_ref id="uc_create_item" />
</FUNCTION> </FUNCTION>
<FUNCTION id="func_update_item" status="implemented"> <FUNCTION id="func_update_item" status="реализовано">
<summary>Update Item</summary> <summary>Обновить товар</summary>
<description>Updates an existing inventory item on the server.</description> <description>Обновляет существующий инвентарный товар на сервере.</description>
<implementation_ref id="uc_update_item" /> <implementation_ref id="uc_update_item" />
</FUNCTION> </FUNCTION>
<FUNCTION id="func_delete_item" status="implemented"> <FUNCTION id="func_delete_item" status="реализовано">
<summary>Delete Item</summary> <summary>Удалить товар</summary>
<description>Deletes an inventory item from the server.</description> <description>Удаляет инвентарный товар с сервера.</description>
<implementation_ref id="uc_delete_item" /> <implementation_ref id="uc_delete_item" />
</FUNCTION> </FUNCTION>
</FUNCTIONALITY> </FUNCTIONALITY>
</FEATURE> </FEATURE>
<FEATURE id="feat_labels_locations" status="in_progress"> <FEATURE id="feat_labels_locations" status="бэкенд_реализован">
<summary>Manage Labels and Locations</summary> <summary>Управление метками и местоположениями</summary>
<description>Allows users to view lists of all available labels and locations.</description> <description>Позволяет пользователям просматривать списки всех доступных меток и местоположений.</description>
<UI_COMPONENT ref_id="screen_labels_list" /> <UI_COMPONENT ref_id="screen_labels_list" />
<UI_COMPONENT ref_id="screen_locations_list" /> <UI_COMPONENT ref_id="screen_locations_list" />
<FUNCTIONALITY> <FUNCTIONALITY>
<FUNCTION id="func_get_all_labels" status="implemented"> <FUNCTION id="func_get_all_labels" status="реализовано">
<summary>Get All Labels</summary> <summary>Получить все метки</summary>
<description>Retrieves a list of all labels from the repository.</description> <description>Получает список всех меток из репозитория.</description>
<implementation_ref id="uc_get_all_labels" /> <implementation_ref id="uc_get_all_labels" />
</FUNCTION> </FUNCTION>
<FUNCTION id="func_get_all_locations" status="implemented"> <FUNCTION id="func_get_all_locations" status="реализовано">
<summary>Get All Locations</summary> <summary>Получить все местоположения</summary>
<description>Retrieves a list of all locations from the repository.</description> <description>Получает список всех местоположений из репозитория.</description>
<implementation_ref id="uc_get_all_locations" /> <implementation_ref id="uc_get_all_locations" />
</FUNCTION> </FUNCTION>
</FUNCTIONALITY> </FUNCTIONALITY>
</FEATURE> </FEATURE>
<FEATURE id="feat_search" status="in_progress"> <FEATURE id="feat_search" status="бэкенд_реализован">
<summary>Search Screen</summary> <summary>Экран поиска</summary>
<description>Provides a dedicated UI for searching items.</description> <description>Предоставляет специальный пользовательский интерфейс для поиска товаров.</description>
<UI_COMPONENT ref_id="screen_search" /> <UI_COMPONENT ref_id="screen_search" />
<FUNCTIONALITY> <FUNCTIONALITY>
<FUNCTION id="func_search_items_dedicated" status="implemented"> <FUNCTION id="func_search_items_dedicated" status="реализовано">
<summary>Search from dedicated screen</summary> <summary>Поиск со специального экрана</summary>
<description>Uses the same search functionality but from a dedicated screen.</description> <description>Использует ту же функцию поиска, но со специального экрана.</description>
<implementation_ref id="uc_search_items" /> <implementation_ref id="uc_search_items" />
</FUNCTION> </FUNCTION>
</FUNCTIONALITY> </FUNCTIONALITY>
@@ -117,6 +124,7 @@
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" /> <USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" /> <USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" /> <USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
<USE_CASE id="uc_login" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" />
<!-- UI Screens --> <!-- UI Screens -->
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" /> <UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
@@ -126,5 +134,6 @@
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" /> <UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" /> <UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" /> <UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
<UI_SCREEN id="screen_setup" file_ref="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" />
</IMPLEMENTATION_MAP> </IMPLEMENTATION_MAP>
</PROJECT_SPECIFICATION> </PROJECT_SPECIFICATION>