diff --git a/GEMINI.md b/GEMINI.md
index b0ea732..e47b6b0 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -60,6 +60,11 @@
Добавление обработки исключений, граничных условий и альтернативных сценариев, описанных в контрактах.
Рефакторинг с сохранением всех контрактных гарантий.
+
+ Принцип "Сначала Анализ" для предотвращения ошибок, связанных с некорректными предположениями о структурах данных.
+ Перед написанием или изменением любого кода, который зависит от других классов (например, мапперы, use case'ы, view model'и), я ОБЯЗАН сначала прочитать определения всех задействованных классов (моделей, DTO, сущностей БД). Я не должен делать никаких предположений об их полях или типах.
+ При реализации интерфейсов или переопределении методов я ОБЯЗАН сначала прочитать определение базового интерфейса или класса, чтобы убедиться, что сигнатура метода (включая `suspend`) полностью совпадает.
+
Принципы для обеспечения компилируемости и совместимости генерируемого кода в Android/Gradle/Kotlin проектах.
@@ -104,6 +109,10 @@
Поддерживать поток чтения "сверху вниз": KDoc-контракт -> `require` -> `логика` -> `check` -> `return`.
Использовать явные типы, четкие имена. DbC усиливает этот принцип.
Активно использовать идиомы Kotlin (`data class`, `when`, `require`, `check`, scope-функции).
+
+ Функции, возвращающие `Flow`, не должны быть `suspend`. `Flow` сам по себе является асинхронным. `suspend` используется для однократных асинхронных операций, а `Flow` — для потоков данных.
+
+
Использовать семантические разметки (КОНТРАКТЫ, ЯКОРЯ) как основу архитектуры.
diff --git a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt
index ed43c9c..6a5486d 100644
--- a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt
+++ b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt
@@ -1,11 +1,17 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] NavGraph.kt
// [SEMANTICS] navigation, compose, nav_host
+
package com.homebox.lens.navigation
+
// [IMPORTS]
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
@@ -19,15 +25,31 @@ import com.homebox.lens.ui.screen.setup.SetupScreen
// [CORE-LOGIC]
/**
* [CONTRACT]
- * Определяет граф навигации для приложения.
+ * Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
+ * @param navController Контроллер навигации.
+ * @see Screen
+ * @sideeffect Регистрирует все экраны и управляет состоянием навигации.
+ * @invariant Стартовый экран - `Screen.Setup`.
*/
@Composable
-fun NavGraph() {
- val navController = rememberNavController()
+fun NavGraph(
+ navController: NavHostController = rememberNavController()
+) {
+ // [STATE]
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentRoute = navBackStackEntry?.destination?.route
+
+ // [HELPER]
+ val navigationActions = remember(navController) {
+ NavigationActions(navController)
+ }
+
+ // [ACTION]
NavHost(
navController = navController,
startDestination = Screen.Setup.route
) {
+ // [COMPOSABLE_SETUP]
composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) {
@@ -35,30 +57,39 @@ fun NavGraph() {
}
})
}
+ // [COMPOSABLE_DASHBOARD]
composable(route = Screen.Dashboard.route) {
DashboardScreen(
- onNavigateToLocations = { navController.navigate(Screen.LocationsList.route) },
- onNavigateToSearch = { navController.navigate(Screen.Search.route) },
- onNavigateToCreateItem = { navController.navigate(Screen.ItemEdit.createRoute("new")) },
- onLogout = {
- navController.navigate(Screen.Setup.route) {
- popUpTo(Screen.Dashboard.route) { inclusive = true }
- }
- }
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
)
}
+ // [COMPOSABLE_INVENTORY_LIST]
composable(route = Screen.InventoryList.route) {
- InventoryListScreen()
+ InventoryListScreen(
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
+ )
}
+ // [COMPOSABLE_ITEM_DETAILS]
composable(route = Screen.ItemDetails.route) {
- ItemDetailsScreen()
+ ItemDetailsScreen(
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
+ )
}
+ // [COMPOSABLE_ITEM_EDIT]
composable(route = Screen.ItemEdit.route) {
- ItemEditScreen()
+ ItemEditScreen(
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
+ )
}
+ // [COMPOSABLE_LABELS_LIST]
composable(route = Screen.LabelsList.route) {
LabelsListScreen(
- onNavigateBack = { navController.popBackStack() },
+ currentRoute = currentRoute,
+ navigationActions = navigationActions,
onLabelClick = { labelId ->
// TODO: Navigate to a pre-filtered inventory list screen
navController.navigate(Screen.InventoryList.route)
@@ -68,9 +99,11 @@ fun NavGraph() {
}
)
}
+ // [COMPOSABLE_LOCATIONS_LIST]
composable(route = Screen.LocationsList.route) {
LocationsListScreen(
- onNavigateBack = { navController.popBackStack() },
+ currentRoute = currentRoute,
+ navigationActions = navigationActions,
onLocationClick = { locationId ->
// TODO: Navigate to a pre-filtered inventory list screen
navController.navigate(Screen.InventoryList.route)
@@ -80,9 +113,14 @@ fun NavGraph() {
}
)
}
+ // [COMPOSABLE_SEARCH]
composable(route = Screen.Search.route) {
- SearchScreen()
+ SearchScreen(
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
+ )
}
}
+ // [END_FUNCTION_NavGraph]
}
-// [END_FILE_NavGraph.kt]
\ No newline at end of file
+// [END_FILE_NavGraph.kt]
diff --git a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt
new file mode 100644
index 0000000..21bc293
--- /dev/null
+++ b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt
@@ -0,0 +1,64 @@
+// [PACKAGE] com.homebox.lens.navigation
+// [FILE] NavigationActions.kt
+// [SEMANTICS] navigation, controller, actions
+
+package com.homebox.lens.navigation
+
+import androidx.navigation.NavHostController
+
+// [CORE-LOGIC]
+/**
+ * [CONTRACT]
+ * @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
+ * @param navController Контроллер Jetpack Navigation.
+ * @invariant Все навигационные действия должны использовать предоставленный navController.
+ */
+class NavigationActions(private val navController: NavHostController) {
+
+ // [ACTION]
+ /**
+ * [CONTRACT]
+ * @summary Навигация на главный экран.
+ * @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
+ */
+ fun navigateToDashboard() {
+ navController.navigate(Screen.Dashboard.route) {
+ // Используем popUpTo для удаления всех экранов до dashboard из back stack
+ // Это предотвращает создание большой стопки экранов при навигации через drawer
+ popUpTo(navController.graph.startDestinationId)
+ launchSingleTop = true
+ }
+ }
+
+ // [ACTION]
+ fun navigateToLocations() {
+ navController.navigate(Screen.LocationsList.route) {
+ launchSingleTop = true
+ }
+ }
+
+ // [ACTION]
+ fun navigateToSearch() {
+ navController.navigate(Screen.Search.route) {
+ launchSingleTop = true
+ }
+ }
+
+ // [ACTION]
+ fun navigateToCreateItem() {
+ navController.navigate(Screen.ItemEdit.createRoute("new"))
+ }
+
+ // [ACTION]
+ fun navigateToLogout() {
+ navController.navigate(Screen.Setup.route) {
+ popUpTo(Screen.Dashboard.route) { inclusive = true }
+ }
+ }
+
+ // [ACTION]
+ fun navigateBack() {
+ navController.popBackStack()
+ }
+}
+// [END_FILE_NavigationActions.kt]
diff --git a/app/src/main/java/com/homebox/lens/navigation/Screen.kt b/app/src/main/java/com/homebox/lens/navigation/Screen.kt
index 9f6e8d4..0d4cda8 100644
--- a/app/src/main/java/com/homebox/lens/navigation/Screen.kt
+++ b/app/src/main/java/com/homebox/lens/navigation/Screen.kt
@@ -1,9 +1,10 @@
// [PACKAGE] com.homebox.lens.navigation
-// [FILE] app/src/main/java/com/homebox/lens/navigation/Screen.kt
+// [FILE] Screen.kt
// [SEMANTICS] navigation, routes, sealed_class
package com.homebox.lens.navigation
+// [CORE-LOGIC]
/**
* [CONTRACT]
* Запечатанный класс для определения маршрутов навигации в приложении.
@@ -11,17 +12,50 @@ package com.homebox.lens.navigation
* @property route Строковый идентификатор маршрута.
*/
sealed class Screen(val route: String) {
+ // [STATE]
data object Setup : Screen("setup_screen")
data object Dashboard : Screen("dashboard_screen")
data object InventoryList : Screen("inventory_list_screen")
data object ItemDetails : Screen("item_details_screen/{itemId}") {
- fun createRoute(itemId: String) = "item_details_screen/$itemId"
+ /**
+ * [CONTRACT]
+ * Создает маршрут для экрана деталей элемента с указанным ID.
+ * @param itemId ID элемента для отображения.
+ * @return Строку полного маршрута.
+ * @throws IllegalArgumentException если itemId пустой.
+ */
+ // [HELPER]
+ fun createRoute(itemId: String): String {
+ // [PRECONDITION]
+ require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
+ // [ACTION]
+ val route = "item_details_screen/$itemId"
+ // [POSTCONDITION]
+ check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
+ return route
+ }
}
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
- fun createRoute(itemId: String) = "item_edit_screen/$itemId"
+ /**
+ * [CONTRACT]
+ * Создает маршрут для экрана редактирования элемента с указанным ID.
+ * @param itemId ID элемента для редактирования.
+ * @return Строку полного маршрута.
+ * @throws IllegalArgumentException если itemId пустой.
+ */
+ // [HELPER]
+ fun createRoute(itemId: String): String {
+ // [PRECONDITION]
+ require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
+ // [ACTION]
+ val route = "item_edit_screen/$itemId"
+ // [POSTCONDITION]
+ check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
+ return route
+ }
}
data object LabelsList : Screen("labels_list_screen")
data object LocationsList : Screen("locations_list_screen")
data object Search : Screen("search_screen")
}
-// [END_FILE_Screen.kt]
\ No newline at end of file
+// [END_FILE_Screen.kt]
diff --git a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt
new file mode 100644
index 0000000..134fa36
--- /dev/null
+++ b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt
@@ -0,0 +1,92 @@
+// [PACKAGE] com.homebox.lens.ui.common
+// [FILE] AppDrawer.kt
+package com.homebox.lens.ui.common
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material3.Button
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ModalDrawerSheet
+import androidx.compose.material3.NavigationDrawerItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.homebox.lens.R
+import com.homebox.lens.navigation.NavigationActions
+import com.homebox.lens.navigation.Screen
+
+/**
+ * [CONTRACT]
+ * @summary Контент для бокового навигационного меню (Drawer).
+ * @param currentRoute Текущий маршрут для подсветки активного элемента.
+ * @param navigationActions Объект с навигационными действиями.
+ * @param onCloseDrawer Лямбда для закрытия бокового меню.
+ */
+@Composable
+internal fun AppDrawerContent(
+ currentRoute: String?,
+ navigationActions: NavigationActions,
+ onCloseDrawer: () -> Unit
+) {
+ ModalDrawerSheet {
+ Spacer(Modifier.height(12.dp))
+ Button(
+ onClick = {
+ navigationActions.navigateToCreateItem()
+ onCloseDrawer()
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ ) {
+ Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
+ Spacer(Modifier.width(8.dp))
+ Text(stringResource(id = R.string.create))
+ }
+ Spacer(Modifier.height(12.dp))
+ Divider()
+ NavigationDrawerItem(
+ label = { Text(stringResource(id = R.string.dashboard_title)) },
+ selected = currentRoute == Screen.Dashboard.route,
+ onClick = {
+ navigationActions.navigateToDashboard()
+ onCloseDrawer()
+ }
+ )
+ NavigationDrawerItem(
+ label = { Text(stringResource(id = R.string.nav_locations)) },
+ selected = currentRoute == Screen.LocationsList.route,
+ onClick = {
+ navigationActions.navigateToLocations()
+ onCloseDrawer()
+ }
+ )
+ NavigationDrawerItem(
+ label = { Text(stringResource(id = R.string.search)) },
+ selected = currentRoute == Screen.Search.route,
+ onClick = {
+ navigationActions.navigateToSearch()
+ onCloseDrawer()
+ }
+ )
+ // TODO: Add Profile and Tools items
+ Divider()
+ NavigationDrawerItem(
+ label = { Text(stringResource(id = R.string.logout)) },
+ selected = false,
+ onClick = {
+ navigationActions.navigateToLogout()
+ onCloseDrawer()
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt b/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt
new file mode 100644
index 0000000..b366974
--- /dev/null
+++ b/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt
@@ -0,0 +1,77 @@
+// [PACKAGE] com.homebox.lens.ui.common
+// [FILE] MainScaffold.kt
+// [SEMANTICS] ui, common, scaffold, navigation_drawer
+
+package com.homebox.lens.ui.common
+
+// [IMPORTS]
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.res.stringResource
+import com.homebox.lens.R
+import com.homebox.lens.navigation.NavigationActions
+import kotlinx.coroutines.launch
+
+// [UI_COMPONENT]
+/**
+ * [CONTRACT]
+ * @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
+ * @param topBarTitle Заголовок для TopAppBar.
+ * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
+ * @param navigationActions Объект с навигационными действиями.
+ * @param topBarActions Composable-функция для отображения действий (иконок) в TopAppBar.
+ * @param content Основное содержимое экрана, которое будет отображено внутри Scaffold.
+ * @sideeffect Управляет состоянием (открыто/закрыто) бокового меню (ModalNavigationDrawer).
+ * @invariant TopAppBar всегда отображается с иконкой меню.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainScaffold(
+ topBarTitle: String,
+ currentRoute: String?,
+ navigationActions: NavigationActions,
+ topBarActions: @Composable () -> Unit = {},
+ content: @Composable (PaddingValues) -> Unit
+) {
+ // [STATE]
+ val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
+ val scope = rememberCoroutineScope()
+
+ // [CORE-LOGIC]
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ AppDrawerContent(
+ currentRoute = currentRoute,
+ navigationActions = navigationActions,
+ onCloseDrawer = { scope.launch { drawerState.close() } }
+ )
+ }
+ ) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(topBarTitle) },
+ navigationIcon = {
+ IconButton(onClick = { scope.launch { drawerState.open() } }) {
+ Icon(
+ Icons.Default.Menu,
+ contentDescription = stringResource(id = R.string.cd_open_navigation_drawer)
+ )
+ }
+ },
+ actions = { topBarActions() }
+ )
+ }
+ ) { paddingValues ->
+ // [ACTION]
+ content(paddingValues)
+ }
+ }
+ // [END_FUNCTION_MainScaffold]
+}
+// [END_FILE_MainScaffold.kt]
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt
index 7798ada..43e0bf0 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt
@@ -1,8 +1,10 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardScreen.kt
+// [SEMANTICS] ui, screen, dashboard, compose
package com.homebox.lens.ui.screen.dashboard
+// [IMPORTS]
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -11,14 +13,11 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -28,67 +27,61 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.domain.model.*
+import com.homebox.lens.navigation.NavigationActions
+import com.homebox.lens.ui.common.MainScaffold
import com.homebox.lens.ui.theme.HomeboxLensTheme
-import kotlinx.coroutines.launch
-// [ANCHOR] Главная точка входа для экрана Dashboard
-@OptIn(ExperimentalMaterial3Api::class)
+// [ENTRYPOINT]
+/**
+ * [CONTRACT]
+ * @summary Главная Composable-функция для экрана "Панель управления".
+ * @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
+ * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
+ * @param navigationActions Объект с навигационными действиями.
+ * @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
+ */
@Composable
fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel(),
- onNavigateToLocations: () -> Unit,
- onNavigateToSearch: () -> Unit,
- onNavigateToCreateItem: () -> Unit,
- onLogout: () -> Unit
+ currentRoute: String?,
+ navigationActions: NavigationActions
) {
- // [ACTION] Собираем состояние из ViewModel
+ // [STATE]
val uiState by viewModel.uiState.collectAsState()
- val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
- val scope = rememberCoroutineScope()
- // [ANCHOR] Определяем навигационное меню
- ModalNavigationDrawer(
- drawerState = drawerState,
- drawerContent = {
- DrawerContent(
- onNavigateToLocations = onNavigateToLocations,
- onNavigateToSearch = onNavigateToSearch,
- onNavigateToCreateItem = onNavigateToCreateItem,
- onLogout = onLogout,
- onCloseDrawer = { scope.launch { drawerState.close() } }
- )
- }
- ) {
- // [ANCHOR] Основной Scaffold экрана
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text(stringResource(id = R.string.dashboard_title)) },
- navigationIcon = {
- IconButton(onClick = { scope.launch { drawerState.open() } }) {
- Icon(Icons.Default.Menu, contentDescription = stringResource(id = R.string.cd_open_navigation_drawer))
- }
- },
- actions = {
- IconButton(onClick = { /* TODO: Handle scanner click */ }) {
- Icon(Icons.Default.Search, contentDescription = stringResource(id = R.string.cd_scan_qr_code))
- }
- }
+ // [UI_COMPONENT]
+ MainScaffold(
+ topBarTitle = stringResource(id = R.string.dashboard_title),
+ currentRoute = currentRoute,
+ navigationActions = navigationActions,
+ topBarActions = {
+ IconButton(onClick = { /* TODO: Handle scanner click */ }) {
+ Icon(
+ Icons.Default.Search,
+ contentDescription = stringResource(id = R.string.cd_scan_qr_code)
)
}
- ) { paddingValues ->
- // [ANCHOR] Основной контент экрана
- DashboardContent(
- modifier = Modifier.padding(paddingValues),
- uiState = uiState,
- onLocationClick = { /* TODO */ },
- onLabelClick = { /* TODO */ }
- )
}
+ ) { paddingValues ->
+ DashboardContent(
+ modifier = Modifier.padding(paddingValues),
+ uiState = uiState,
+ onLocationClick = { /* TODO */ },
+ onLabelClick = { /* TODO */ }
+ )
}
+ // [END_FUNCTION_DashboardScreen]
}
-// [ANCHOR] Компонент основного контента
+// [HELPER]
+/**
+ * [CONTRACT]
+ * @summary Отображает основной контент экрана в зависимости от `uiState`.
+ * @param modifier Модификатор для стилизации.
+ * @param uiState Текущее состояние UI экрана.
+ * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
+ * @param onLabelClick Лямбда-обработчик нажатия на метку.
+ */
@Composable
private fun DashboardContent(
modifier: Modifier = Modifier,
@@ -96,7 +89,7 @@ private fun DashboardContent(
onLocationClick: (LocationOutCount) -> Unit,
onLabelClick: (LabelOut) -> Unit
) {
- // [FIX] Based on the UiState, we decide what to show
+ // [CORE-LOGIC]
when (uiState) {
is DashboardUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -120,35 +113,23 @@ private fun DashboardContent(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
-
- // [ANCHOR] Секция "Быстрая статистика"
- item {
- StatisticsSection(statistics = uiState.statistics)
- }
-
- // [ANCHOR] Секция "Недавно добавлено"
- item {
- // TODO: Add recently added items to UiState and display them here
- // RecentlyAddedSection(items = uiState.recentlyAddedItems)
- }
-
- // [ANCHOR] Секция "Места хранения"
- item {
- LocationsSection(locations = uiState.locations, onLocationClick = onLocationClick)
- }
-
- // [ANCHOR] Секция "Метки"
- item {
- LabelsSection(labels = uiState.labels, onLabelClick = onLabelClick)
- }
-
+ item { StatisticsSection(statistics = uiState.statistics) }
+ item { RecentlyAddedSection(items = uiState.recentlyAddedItems) }
+ item { LocationsSection(locations = uiState.locations, onLocationClick = onLocationClick) }
+ item { LabelsSection(labels = uiState.labels, onLabelClick = onLabelClick) }
item { Spacer(modifier = Modifier.height(16.dp)) }
}
}
}
+ // [END_FUNCTION_DashboardContent]
}
-// [ANCHOR] Секция статистики
+// [UI_COMPONENT]
+/**
+ * [CONTRACT]
+ * @summary Секция для отображения общей статистики.
+ * @param statistics Объект со статистическими данными.
+ */
@Composable
private fun StatisticsSection(statistics: GroupStatistics) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
@@ -159,7 +140,10 @@ private fun StatisticsSection(statistics: GroupStatistics) {
Card {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
- modifier = Modifier.height(120.dp).fillMaxWidth().padding(16.dp),
+ modifier = Modifier
+ .height(120.dp)
+ .fillMaxWidth()
+ .padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -172,6 +156,13 @@ private fun StatisticsSection(statistics: GroupStatistics) {
}
}
+// [UI_COMPONENT]
+/**
+ * [CONTRACT]
+ * @summary Карточка для отображения одного статистического показателя.
+ * @param title Название показателя.
+ * @param value Значение показателя.
+ */
@Composable
private fun StatisticCard(title: String, value: String) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
@@ -180,7 +171,12 @@ private fun StatisticCard(title: String, value: String) {
}
}
-// [ANCHOR] Секция недавно добавленных
+// [UI_COMPONENT]
+/**
+ * [CONTRACT]
+ * @summary Секция для отображения недавно добавленных элементов.
+ * @param items Список элементов для отображения.
+ */
@Composable
private fun RecentlyAddedSection(items: List) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
@@ -207,12 +203,21 @@ private fun RecentlyAddedSection(items: List) {
}
}
+// [UI_COMPONENT]
+/**
+ * [CONTRACT]
+ * @summary Карточка для отображения краткой информации об элементе.
+ * @param item Элемент для отображения.
+ */
@Composable
private fun ItemCard(item: ItemSummary) {
Card(modifier = Modifier.width(150.dp)) {
Column(modifier = Modifier.padding(8.dp)) {
// TODO: Add image here from item.image
- Spacer(modifier = Modifier.height(80.dp).fillMaxWidth().background(MaterialTheme.colorScheme.secondaryContainer))
+ Spacer(modifier = Modifier
+ .height(80.dp)
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.secondaryContainer))
Spacer(modifier = Modifier.height(8.dp))
Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1)
@@ -221,7 +226,13 @@ private fun ItemCard(item: ItemSummary) {
}
-// [ANCHOR] Секция местоположений
+// [UI_COMPONENT]
+/**
+ * [CONTRACT]
+ * @summary Секция для отображения местоположений в виде чипсов.
+ * @param locations Список местоположений.
+ * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
+ */
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LocationsSection(locations: List, onLocationClick: (LocationOutCount) -> Unit) {
@@ -243,7 +254,13 @@ private fun LocationsSection(locations: List, onLocationClick:
}
}
-// [ANCHOR] Секция меток
+// [UI_COMPONENT]
+/**
+ * [CONTRACT]
+ * @summary Секция для отображения меток в виде чипсов.
+ * @param labels Список меток.
+ * @param onLabelClick Лямбда-обработчик нажатия на метку.
+ */
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LabelsSection(labels: List, onLabelClick: (LabelOut) -> Unit) {
@@ -266,67 +283,7 @@ private fun LabelsSection(labels: List, onLabelClick: (LabelOut) -> Un
}
-// [ANCHOR] Контент бокового меню
-@Composable
-private fun DrawerContent(
- onNavigateToLocations: () -> Unit,
- onNavigateToSearch: () -> Unit,
- onNavigateToCreateItem: () -> Unit,
- onLogout: () -> Unit,
- onCloseDrawer: () -> Unit
-) {
- ModalDrawerSheet {
- Spacer(Modifier.height(12.dp))
- Button(
- onClick = {
- onNavigateToCreateItem()
- onCloseDrawer()
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp)
- ) {
- Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
- Spacer(Modifier.width(8.dp))
- Text(stringResource(id = R.string.create))
- }
- Spacer(Modifier.height(12.dp))
- Divider()
- NavigationDrawerItem(
- label = { Text(stringResource(id = R.string.dashboard_title)) },
- selected = true,
- onClick = { onCloseDrawer() }
- )
- NavigationDrawerItem(
- label = { Text(stringResource(id = R.string.nav_locations)) },
- selected = false,
- onClick = {
- onNavigateToLocations()
- onCloseDrawer()
- }
- )
- NavigationDrawerItem(
- label = { Text(stringResource(id = R.string.search)) },
- selected = false,
- onClick = {
- onNavigateToSearch()
- onCloseDrawer()
- }
- )
- // TODO: Add Profile and Tools items
- Divider()
- NavigationDrawerItem(
- label = { Text(stringResource(id = R.string.logout)) },
- selected = false,
- onClick = {
- onLogout()
- onCloseDrawer()
- }
- )
- }
-}
-
-// [ANCHOR] Preview для DashboardContent
+// [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Success State")
@Composable
fun DashboardContentSuccessPreview() {
@@ -349,7 +306,8 @@ fun DashboardContentSuccessPreview() {
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
- )
+ ),
+ recentlyAddedItems = emptyList()
)
HomeboxLensTheme {
DashboardContent(
@@ -360,6 +318,7 @@ fun DashboardContentSuccessPreview() {
}
}
+// [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Loading State")
@Composable
fun DashboardContentLoadingPreview() {
@@ -372,6 +331,7 @@ fun DashboardContentLoadingPreview() {
}
}
+// [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Error State")
@Composable
fun DashboardContentErrorPreview() {
@@ -383,3 +343,4 @@ fun DashboardContentErrorPreview() {
)
}
}
+// [END_FILE_DashboardScreen.kt]
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
index 3c3a70d..a4fe49e 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
@@ -23,11 +23,13 @@ sealed interface DashboardUiState {
* @property statistics Статистика по инвентарю.
* @property locations Список локаций со счетчиками.
* @property labels Список всех меток.
+ * @property recentlyAddedItems Список недавно добавленных товаров.
*/
data class Success(
val statistics: GroupStatistics,
val locations: List,
- val labels: List
+ val labels: List,
+ val recentlyAddedItems: List
) : DashboardUiState
/**
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt
index b776332..2dce373 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt
@@ -7,13 +7,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
+import com.homebox.lens.domain.usecase.GetRecentlyAddedItemsUseCase
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
import com.homebox.lens.ui.screen.dashboard.DashboardUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -31,7 +31,8 @@ import javax.inject.Inject
class DashboardViewModel @Inject constructor(
private val getStatisticsUseCase: GetStatisticsUseCase,
private val getAllLocationsUseCase: GetAllLocationsUseCase,
- private val getAllLabelsUseCase: GetAllLabelsUseCase
+ private val getAllLabelsUseCase: GetAllLabelsUseCase,
+ private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase
) : ViewModel() {
// [STATE]
@@ -57,47 +58,29 @@ class DashboardViewModel @Inject constructor(
// [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = DashboardUiState.Loading
- // [FIX] Используем Timber для логирования.
- Timber.i("[ACTION] Starting parallel dashboard data load. State -> Loading.")
+ Timber.i("[ACTION] Starting dashboard data collection.")
- // [CORE-LOGIC: PARALLEL_FETCH]
- val result = runCatching {
- coroutineScope {
- val statsDeferred = async { getStatisticsUseCase() }
- val locationsDeferred = async { getAllLocationsUseCase() }
- val labelsDeferred = async { getAllLabelsUseCase() }
+ val statsFlow = flow { emit(getStatisticsUseCase()) }
+ val locationsFlow = flow { emit(getAllLocationsUseCase()) }
+ val labelsFlow = flow { emit(getAllLabelsUseCase()) }
+ val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
- val stats = statsDeferred.await()
- 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)
- }
+ combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
+ DashboardUiState.Success(
+ statistics = stats,
+ locations = locations,
+ labels = labels,
+ recentlyAddedItems = recentItems
+ )
+ }.catch { exception ->
+ Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
+ _uiState.value = DashboardUiState.Error(
+ message = exception.message ?: "Could not load dashboard data."
+ )
+ }.collect { successState ->
+ Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
+ _uiState.value = successState
}
-
- // [RESULT_HANDLER]
- result.fold(
- onSuccess = { (stats, locations, labels) ->
- // [FIX] Используем Timber для логирования.
- Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
- _uiState.value = DashboardUiState.Success(
- statistics = stats,
- locations = locations,
- labels = labels
- )
- },
- onFailure = { exception ->
- // [FIX] Используем Timber для логирования ошибок с передачей исключения.
- Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
- _uiState.value = DashboardUiState.Error(
- message = exception.message ?: "Could not load dashboard data."
- )
- }
- )
}
}
// [END_CLASS_DashboardViewModel]
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt
index 3ee5bf8..abf1924 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt
@@ -1,22 +1,37 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListScreen.kt
+// [SEMANTICS] ui, screen, inventory, list
package com.homebox.lens.ui.screen.inventorylist
+// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.res.stringResource
+import com.homebox.lens.R
+import com.homebox.lens.navigation.NavigationActions
+import com.homebox.lens.ui.common.MainScaffold
// [ENTRYPOINT]
+/**
+ * [CONTRACT]
+ * @summary Composable-функция для экрана "Список инвентаря".
+ * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
+ * @param navigationActions Объект с навигационными действиями.
+ */
@Composable
-fun InventoryListScreen() {
- // [ACTION]
- Text(text = "Inventory List Screen")
-}
-
-@Preview(showBackground = true)
-@Composable
-fun InventoryListScreenPreview() {
- InventoryListScreen()
-}
-// [END_FILE_InventoryListScreen.kt]
+fun InventoryListScreen(
+ currentRoute: String?,
+ navigationActions: NavigationActions
+) {
+ // [UI_COMPONENT]
+ MainScaffold(
+ topBarTitle = stringResource(id = R.string.inventory_list_title),
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
+ ) {
+ // [CORE-LOGIC]
+ Text(text = "TODO: Inventory List Screen")
+ }
+ // [END_FUNCTION_InventoryListScreen]
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt
index 0222537..6b78f9e 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt
@@ -1,22 +1,37 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsScreen.kt
+// [SEMANTICS] ui, screen, item, details
package com.homebox.lens.ui.screen.itemdetails
+// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.res.stringResource
+import com.homebox.lens.R
+import com.homebox.lens.navigation.NavigationActions
+import com.homebox.lens.ui.common.MainScaffold
// [ENTRYPOINT]
+/**
+ * [CONTRACT]
+ * @summary Composable-функция для экрана "Детали элемента".
+ * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
+ * @param navigationActions Объект с навигационными действиями.
+ */
@Composable
-fun ItemDetailsScreen() {
- // [ACTION]
- Text(text = "Item Details Screen")
-}
-
-@Preview(showBackground = true)
-@Composable
-fun ItemDetailsScreenPreview() {
- ItemDetailsScreen()
-}
-// [END_FILE_ItemDetailsScreen.kt]
+fun ItemDetailsScreen(
+ currentRoute: String?,
+ navigationActions: NavigationActions
+) {
+ // [UI_COMPONENT]
+ MainScaffold(
+ topBarTitle = stringResource(id = R.string.item_details_title),
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
+ ) {
+ // [CORE-LOGIC]
+ Text(text = "TODO: Item Details Screen")
+ }
+ // [END_FUNCTION_ItemDetailsScreen]
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt
index 4421b85..957024f 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt
@@ -1,22 +1,37 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditScreen.kt
+// [SEMANTICS] ui, screen, item, edit
package com.homebox.lens.ui.screen.itemedit
+// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.res.stringResource
+import com.homebox.lens.R
+import com.homebox.lens.navigation.NavigationActions
+import com.homebox.lens.ui.common.MainScaffold
// [ENTRYPOINT]
+/**
+ * [CONTRACT]
+ * @summary Composable-функция для экрана "Редактирование элемента".
+ * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
+ * @param navigationActions Объект с навигационными действиями.
+ */
@Composable
-fun ItemEditScreen() {
- // [ACTION]
- Text(text = "Item Edit Screen")
+fun ItemEditScreen(
+ currentRoute: String?,
+ navigationActions: NavigationActions
+) {
+ // [UI_COMPONENT]
+ MainScaffold(
+ topBarTitle = stringResource(id = R.string.item_edit_title),
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
+ ) {
+ // [CORE-LOGIC]
+ Text(text = "TODO: Item Edit Screen")
+ }
+ // [END_FUNCTION_ItemEditScreen]
}
-
-@Preview(showBackground = true)
-@Composable
-fun ItemEditScreenPreview() {
- ItemEditScreen()
-}
-// [END_FILE_ItemEditScreen.kt]
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
index a7d4f4d..b2fb35d 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
@@ -1,172 +1,41 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt
-// [SEMANTICS] ui, screen, labels_list, compose
+// [SEMANTICS] ui, screen, labels, list
+
package com.homebox.lens.ui.screen.labelslist
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material.icons.filled.PlayArrow
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material3.*
+// [IMPORTS]
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
-import com.homebox.lens.domain.model.LabelOut
-import com.homebox.lens.ui.theme.HomeboxLensTheme
+import com.homebox.lens.navigation.NavigationActions
+import com.homebox.lens.ui.common.MainScaffold
// [ENTRYPOINT]
/**
* [CONTRACT]
- * @summary Главная Composable-функция для экрана списка меток.
- * @param onNavigateBack Функция для навигации на предыдущий экран.
- * @param onLabelClick Функция, вызываемая при нажатии на метку.
- * @param onAddNewLabelClick Функция, вызываемая при нажатии на FAB для добавления новой метки.
+ * @summary Composable-функция для экрана "Список меток".
+ * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
+ * @param navigationActions Объект с навигационными действиями.
+ * @param onLabelClick Лямбда-обработчик нажатия на метку.
+ * @param onAddNewLabelClick Лямбда-обработчик нажатия на кнопку добавления новой метки.
*/
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LabelsListScreen(
- viewModel: LabelsListViewModel = hiltViewModel(),
- onNavigateBack: () -> Unit,
+ currentRoute: String?,
+ navigationActions: NavigationActions,
onLabelClick: (String) -> Unit,
onAddNewLabelClick: () -> Unit
) {
- // [STATE]
- val uiState by viewModel.uiState.collectAsState()
-
- // [UI]
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text(stringResource(id = R.string.nav_labels)) },
- navigationIcon = {
- IconButton(onClick = onNavigateBack) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = stringResource(id = R.string.cd_navigate_back)
- )
- }
- }
- )
- },
- floatingActionButton = {
- FloatingActionButton(onClick = onAddNewLabelClick) {
- Icon(
- imageVector = Icons.Default.Add,
- contentDescription = stringResource(id = R.string.cd_add_new_label)
- )
- }
- }
- ) { paddingValues ->
- // [ANCHOR] Основной контент в зависимости от состояния
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues)
- ) {
- when (val state = uiState) {
- is LabelsListUiState.Loading -> {
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
- }
- is LabelsListUiState.Error -> {
- Text(
- text = state.message,
- color = MaterialTheme.colorScheme.error,
- textAlign = TextAlign.Center,
- modifier = Modifier.align(Alignment.Center).padding(16.dp)
- )
- }
- is LabelsListUiState.Success -> {
- LabelsList(
- labels = state.labels,
- onLabelClick = onLabelClick
- )
- }
- }
- }
- }
-}
-
-// [HELPER]
-/**
- * [CONTRACT]
- * @summary Отображает список меток.
- */
-@Composable
-private fun LabelsList(
- labels: List,
- onLabelClick: (String) -> Unit,
- modifier: Modifier = Modifier
-) {
- LazyColumn(
- modifier = modifier.fillMaxSize(),
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ // [UI_COMPONENT]
+ MainScaffold(
+ topBarTitle = stringResource(id = R.string.labels_list_title),
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
) {
- items(labels) { label ->
- LabelListItem(
- label = label,
- onClick = { onLabelClick(label.id) }
- )
- }
+ // [CORE-LOGIC]
+ Text(text = "TODO: Labels List Screen")
}
-}
-
-// [HELPER]
-/**
- * [CONTRACT]
- * @summary Отображает один элемент списка меток.
- */
-@Composable
-private fun LabelListItem(
- label: LabelOut,
- onClick: () -> Unit
-) {
- ListItem(
- headlineContent = { Text(label.name) },
- leadingContent = {
- Icon(
- imageVector = Icons.Filled.PlayArrow,
- contentDescription = null
- )
- },
- modifier = Modifier.clickable(onClick = onClick)
- )
-}
-
-// [PREVIEW]
-@Preview(showBackground = true, name = "Labels List Success")
-@Composable
-private fun LabelsListScreenSuccessPreview() {
- val labels = listOf(
- LabelOut("1", "Electronics", "#FF0000", false, "", ""),
- LabelOut("2", "Books", "#00FF00", false, "", ""),
- LabelOut("3", "Winter Clothes", "#0000FF", false, "", "")
- )
- HomeboxLensTheme {
- LabelsList(labels = labels, onLabelClick = {})
- }
-}
-
-// [PREVIEW]
-@Preview(showBackground = true, name = "Labels List Empty")
-@Composable
-private fun LabelsListScreenEmptyPreview() {
- HomeboxLensTheme {
- LabelsList(labels = emptyList(), onLabelClick = {})
- }
-}
-// [END_FILE_LabelsListScreen.kt]
\ No newline at end of file
+ // [END_FUNCTION_LabelsListScreen]
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt
index 93d837f..b449f80 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt
@@ -1,175 +1,41 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListScreen.kt
-// [SEMANTICS] ui, screen, locations_list, compose
+// [SEMANTICS] ui, screen, locations, list
+
package com.homebox.lens.ui.screen.locationslist
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Place
-import androidx.compose.material3.*
+// [IMPORTS]
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
-import com.homebox.lens.domain.model.LocationOutCount
-import com.homebox.lens.ui.theme.HomeboxLensTheme
+import com.homebox.lens.navigation.NavigationActions
+import com.homebox.lens.ui.common.MainScaffold
// [ENTRYPOINT]
/**
* [CONTRACT]
- * @summary Главная Composable-функция для экрана списка локаций.
- * @param onNavigateBack Функция для навигации на предыдущий экран.
- * @param onLocationClick Функция, вызываемая при нажатии на локацию.
- * @param onAddNewLocationClick Функция, вызываемая при нажатии на FAB для добавления новой локации.
+ * @summary Composable-функция для экрана "Список местоположений".
+ * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
+ * @param navigationActions Объект с навигационными действиями.
+ * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
+ * @param onAddNewLocationClick Лямбда-обработчик нажатия на кнопку добавления нового местоположения.
*/
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LocationsListScreen(
- viewModel: LocationsListViewModel = hiltViewModel(),
- onNavigateBack: () -> Unit,
+ currentRoute: String?,
+ navigationActions: NavigationActions,
onLocationClick: (String) -> Unit,
onAddNewLocationClick: () -> Unit
) {
- // [STATE]
- val uiState by viewModel.uiState.collectAsState()
-
- // [UI]
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text(stringResource(id = R.string.nav_locations)) },
- navigationIcon = {
- IconButton(onClick = onNavigateBack) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- contentDescription = stringResource(id = R.string.cd_navigate_back)
- )
- }
- }
- )
- },
- floatingActionButton = {
- FloatingActionButton(onClick = onAddNewLocationClick) {
- Icon(
- imageVector = Icons.Default.Add,
- contentDescription = stringResource(id = R.string.cd_add_new_location)
- )
- }
- }
- ) { paddingValues ->
- // [ANCHOR] Основной контент в зависимости от состояния
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues)
- ) {
- when (val state = uiState) {
- is LocationsListUiState.Loading -> {
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
- }
- is LocationsListUiState.Error -> {
- Text(
- text = state.message,
- color = MaterialTheme.colorScheme.error,
- textAlign = TextAlign.Center,
- modifier = Modifier.align(Alignment.Center).padding(16.dp)
- )
- }
- is LocationsListUiState.Success -> {
- LocationsList(
- locations = state.locations,
- onLocationClick = onLocationClick
- )
- }
- }
- }
- }
-}
-
-// [HELPER]
-/**
- * [CONTRACT]
- * @summary Отображает список локаций.
- */
-@Composable
-private fun LocationsList(
- locations: List,
- onLocationClick: (String) -> Unit,
- modifier: Modifier = Modifier
-) {
- LazyColumn(
- modifier = modifier.fillMaxSize(),
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ // [UI_COMPONENT]
+ MainScaffold(
+ topBarTitle = stringResource(id = R.string.locations_list_title),
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
) {
- items(locations) { location ->
- LocationListItem(
- location = location,
- onClick = { onLocationClick(location.id) }
- )
- }
+ // [CORE-LOGIC]
+ Text(text = "TODO: Locations List Screen")
}
-}
-
-// [HELPER]
-/**
- * [CONTRACT]
- * @summary Отображает один элемент списка локаций.
- */
-@Composable
-private fun LocationListItem(
- location: LocationOutCount,
- onClick: () -> Unit
-) {
- ListItem(
- headlineContent = { Text(location.name) },
- leadingContent = {
- Icon(
- imageVector = Icons.Default.Place,
- contentDescription = null
- )
- },
- trailingContent = {
- Text(text = location.itemCount.toString())
- },
- modifier = Modifier.clickable(onClick = onClick)
- )
-}
-
-// [PREVIEW]
-@Preview(showBackground = true, name = "Locations List Success")
-@Composable
-private fun LocationsListScreenSuccessPreview() {
- val locations = listOf(
- LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""),
- LocationOutCount("2", "Kitchen", "#00FF00", false, 3, "", ""),
- LocationOutCount("3", "Office", "#0000FF", false, 25, "", "")
- )
- HomeboxLensTheme {
- LocationsList(locations = locations, onLocationClick = {})
- }
-}
-
-// [PREVIEW]
-@Preview(showBackground = true, name = "Locations List Empty")
-@Composable
-private fun LocationsListScreenEmptyPreview() {
- HomeboxLensTheme {
- LocationsList(locations = emptyList(), onLocationClick = {})
- }
-}
-// [END_FILE_LocationsListScreen.kt]
\ No newline at end of file
+ // [END_FUNCTION_LocationsListScreen]
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt
index 4d355bd..6b17a5b 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt
@@ -1,22 +1,37 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchScreen.kt
+// [SEMANTICS] ui, screen, search
package com.homebox.lens.ui.screen.search
+// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.res.stringResource
+import com.homebox.lens.R
+import com.homebox.lens.navigation.NavigationActions
+import com.homebox.lens.ui.common.MainScaffold
// [ENTRYPOINT]
+/**
+ * [CONTRACT]
+ * @summary Composable-функция для экрана "Поиск".
+ * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
+ * @param navigationActions Объект с навигационными действиями.
+ */
@Composable
-fun SearchScreen() {
- // [ACTION]
- Text(text = "Search Screen")
-}
-
-@Preview(showBackground = true)
-@Composable
-fun SearchScreenPreview() {
- SearchScreen()
-}
-// [END_FILE_SearchScreen.kt]
+fun SearchScreen(
+ currentRoute: String?,
+ navigationActions: NavigationActions
+) {
+ // [UI_COMPONENT]
+ MainScaffold(
+ topBarTitle = stringResource(id = R.string.search_title),
+ currentRoute = currentRoute,
+ navigationActions = navigationActions
+ ) {
+ // [CORE-LOGIC]
+ Text(text = "TODO: Search Screen")
+ }
+ // [END_FUNCTION_SearchScreen]
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt
index 6da987f..15a266c 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt
@@ -1,9 +1,12 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt
+// [SEMANTICS] ui, screen, setup, compose
+
+@file:OptIn(ExperimentalMaterial3Api::class)
package com.homebox.lens.ui.screen.setup
-
+// [IMPORTS]
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@@ -11,25 +14,35 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
-// [FIX] Opt-in for experimental Material 3 APIs
-@OptIn(ExperimentalMaterial3Api::class)
// [ENTRYPOINT]
+/**
+ * [CONTRACT]
+ * @summary Главная Composable-функция для экрана настройки соединения с сервером.
+ * @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
+ * @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа.
+ * @sideeffect Вызывает `onSetupComplete` при изменении `uiState.isSetupComplete`.
+ */
@Composable
fun SetupScreen(
viewModel: SetupViewModel = hiltViewModel(),
onSetupComplete: () -> Unit
) {
+ // [STATE]
val uiState by viewModel.uiState.collectAsState()
+ // [CORE-LOGIC]
if (uiState.isSetupComplete) {
onSetupComplete()
}
+ // [UI_COMPONENT]
SetupScreenContent(
uiState = uiState,
onServerUrlChange = viewModel::onServerUrlChange,
@@ -37,11 +50,19 @@ fun SetupScreen(
onPasswordChange = viewModel::onPasswordChange,
onConnectClick = viewModel::connect
)
+ // [END_FUNCTION_SetupScreen]
}
-// [FIX] Opt-in for experimental Material 3 APIs
-@OptIn(ExperimentalMaterial3Api::class)
-// [CONTENT]
+// [HELPER]
+/**
+ * [CONTRACT]
+ * @summary Отображает контент экрана настройки: поля ввода и кнопку.
+ * @param uiState Текущее состояние UI.
+ * @param onServerUrlChange Лямбда-обработчик изменения URL сервера.
+ * @param onUsernameChange Лямбда-обработчик изменения имени пользователя.
+ * @param onPasswordChange Лямбда-обработчик изменения пароля.
+ * @param onConnectClick Лямбда-обработчик нажатия на кнопку "Подключиться".
+ */
@Composable
private fun SetupScreenContent(
uiState: SetupUiState,
@@ -102,10 +123,10 @@ private fun SetupScreenContent(
}
}
}
+ // [END_FUNCTION_SetupScreenContent]
}
-// [FIX] Opt-in for experimental Material 3 APIs
-@OptIn(ExperimentalMaterial3Api::class)
+// [PREVIEW]
@Preview(showBackground = true)
@Composable
fun SetupScreenPreview() {
@@ -117,4 +138,4 @@ fun SetupScreenPreview() {
onConnectClick = {}
)
}
-// [END_FILE_SetupScreen.kt]
\ No newline at end of file
+// [END_FILE_SetupScreen.kt]
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cc19c67..ee31f54 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -34,6 +34,14 @@
Локации
Метки
+
+ Инвентарь
+ Детали
+ Редактирование
+ Метки
+ Места хранения
+ Поиск
+
Настройка сервера
URL сервера
diff --git a/build.gradle.kts b/build.gradle.kts
index 65fbedd..06a2e72 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,7 +3,7 @@
plugins {
// [PLUGIN] Android Application plugin
- id("com.android.application") version "8.11.0" apply false
+ id("com.android.application") version "8.11.1" apply false
// [PLUGIN] Kotlin Android plugin
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
// [PLUGIN] Hilt Android plugin
diff --git a/data/src/main/java/com/homebox/lens/data/db/dao/ItemDao.kt b/data/src/main/java/com/homebox/lens/data/db/dao/ItemDao.kt
index cbd01a6..25c8455 100644
--- a/data/src/main/java/com/homebox/lens/data/db/dao/ItemDao.kt
+++ b/data/src/main/java/com/homebox/lens/data/db/dao/ItemDao.kt
@@ -7,6 +7,7 @@ import androidx.room.*
import com.homebox.lens.data.db.entity.ItemEntity
import com.homebox.lens.data.db.entity.ItemLabelCrossRef
import com.homebox.lens.data.db.entity.ItemWithLabels
+import kotlinx.coroutines.flow.Flow
// [CONTRACT]
/**
@@ -16,6 +17,10 @@ import com.homebox.lens.data.db.entity.ItemWithLabels
@Dao
interface ItemDao {
+ @Transaction
+ @Query("SELECT * FROM items ORDER BY createdAt DESC LIMIT :limit")
+ fun getRecentlyAddedItems(limit: Int): Flow>
+
@Transaction
@Query("SELECT * FROM items")
suspend fun getItems(): List
diff --git a/data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt b/data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt
new file mode 100644
index 0000000..a89dcd1
--- /dev/null
+++ b/data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt
@@ -0,0 +1,53 @@
+// [PACKAGE] com.homebox.lens.data.db.entity
+// [FILE] Mapper.kt
+
+package com.homebox.lens.data.db.entity
+
+import com.homebox.lens.domain.model.Image
+import com.homebox.lens.domain.model.ItemSummary
+import com.homebox.lens.domain.model.LabelOut
+import com.homebox.lens.domain.model.LocationOut
+
+/**
+ * [CONTRACT]
+ * Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
+ *
+ * [COHERENCE_NOTE] Так как сущности БД содержат только подмножество полей доменной модели,
+ * недостающие поля заполняются значениями по умолчанию (false, 0.0, пустые строки) или null.
+ * Это компромисс для обеспечения компиляции и базовой функциональности.
+ */
+fun ItemWithLabels.toDomain(): ItemSummary {
+ return ItemSummary(
+ id = this.item.id,
+ name = this.item.name,
+ // Предполагаем, что `image` в БД - это URL. Создаем объект Image или null.
+ image = this.item.image?.let { Image(id = "", path = it, isPrimary = true) },
+ // `location` в ItemEntity - это только ID. Создаем базовый LocationOut.
+ location = this.item.locationId?.let { LocationOut(id = it, name = "", color = "", isArchived = false, createdAt = "", updatedAt = "") },
+ labels = this.labels.map { it.toDomain() },
+ // Заполняем недостающие поля значениями по умолчанию.
+ assetId = null,
+ isArchived = false,
+ value = this.item.value?.toDouble() ?: 0.0,
+ createdAt = this.item.createdAt ?: "",
+ updatedAt = ""
+ )
+}
+
+/**
+ * [CONTRACT]
+ * Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
+ *
+ * [COHERENCE_NOTE] Заполняет недостающие поля значениями по умолчанию.
+ */
+fun LabelEntity.toDomain(): LabelOut {
+ return LabelOut(
+ id = this.id,
+ name = this.name,
+ // Заполняем недостающие поля значениями по умолчанию.
+ color = "#CCCCCC", // Серый цвет по умолчанию
+ isArchived = false,
+ createdAt = "",
+ updatedAt = ""
+ )
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt b/data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt
index 18a85f1..f90eb01 100644
--- a/data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt
+++ b/data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt
@@ -8,8 +8,12 @@ package com.homebox.lens.data.repository
import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.toDomain
import com.homebox.lens.data.api.dto.toDto
+import com.homebox.lens.data.db.dao.ItemDao
+import com.homebox.lens.data.db.entity.toDomain
import com.homebox.lens.domain.model.*
import com.homebox.lens.domain.repository.ItemRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
@@ -18,12 +22,14 @@ import javax.inject.Singleton
* [CONTRACT]
* Реализация репозитория для работы с данными о вещах.
* @param apiService Сервис для взаимодействия с Homebox API.
+ * @param itemDao DAO для доступа к локальной базе данных.
* [COHERENCE_NOTE] Метод 'login' был полностью удален из этого класса, так как его ответственность
* была передана в AuthRepositoryImpl. Это устраняет ошибку компиляции "'login' overrides nothing".
*/
@Singleton
class ItemRepositoryImpl @Inject constructor(
private val apiService: HomeboxApiService,
+ private val itemDao: ItemDao
) : ItemRepository {
// [DELETED] Метод login был здесь, но теперь он удален.
@@ -100,5 +106,14 @@ class ItemRepositoryImpl @Inject constructor(
val resultDto = apiService.getItems(query = query)
return resultDto.toDomain { it.toDomain() }
}
+
+ /**
+ * [CONTRACT] @see ItemRepository.getRecentlyAddedItems
+ */
+ override fun getRecentlyAddedItems(limit: Int): Flow> {
+ return itemDao.getRecentlyAddedItems(limit).map { entities ->
+ entities.map { it.toDomain() }
+ }
+ }
}
// [END_FILE_ItemRepositoryImpl.kt]
\ No newline at end of file
diff --git a/domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt b/domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt
index e05e915..7e0b1d1 100644
--- a/domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt
+++ b/domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt
@@ -25,5 +25,6 @@ interface ItemRepository {
suspend fun getAllLocations(): List
suspend fun getAllLabels(): List
suspend fun searchItems(query: String): PaginationResult
+ fun getRecentlyAddedItems(limit: Int): kotlinx.coroutines.flow.Flow>
}
// [END_FILE_ItemRepository.kt]
\ No newline at end of file
diff --git a/domain/src/main/java/com/homebox/lens/domain/usecase/GetRecentlyAddedItemsUseCase.kt b/domain/src/main/java/com/homebox/lens/domain/usecase/GetRecentlyAddedItemsUseCase.kt
new file mode 100644
index 0000000..0464c20
--- /dev/null
+++ b/domain/src/main/java/com/homebox/lens/domain/usecase/GetRecentlyAddedItemsUseCase.kt
@@ -0,0 +1,33 @@
+// [PACKAGE] com.homebox.lens.domain.usecase
+// [FILE] GetRecentlyAddedItemsUseCase.kt
+
+package com.homebox.lens.domain.usecase
+
+import com.homebox.lens.domain.model.ItemSummary
+import com.homebox.lens.domain.repository.ItemRepository
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+/**
+ * [CONTRACT]
+ * Сценарий использования для получения списка недавно добавленных товаров.
+ *
+ * @param itemRepository Репозиторий для доступа к данным о товарах.
+ * @return Поток (Flow), содержащий список [ItemSummary].
+ * @precondition Количество запрашиваемых элементов (limit) должно быть положительным.
+ * @postcondition Возвращает Flow со списком товаров, отсортированных по дате создания в порядке убывания.
+ * Если товаров нет, возвращает пустой список.
+ */
+class GetRecentlyAddedItemsUseCase @Inject constructor(
+ private val itemRepository: ItemRepository
+) {
+ // [ACTION]
+ operator fun invoke(limit: Int): Flow> {
+ // [PRECONDITION]
+ require(limit > 0) { "[PRECONDITION_FAILED] Limit must be positive." }
+
+ // [CORE-LOGIC]
+ return itemRepository.getRecentlyAddedItems(limit)
+ }
+}
+// [END_FILE_GetRecentlyAddedItemsUseCase.kt]
diff --git a/tech_spec/project_structure.txt b/tech_spec/project_structure.txt
index 97d3d53..3ccec02 100644
--- a/tech_spec/project_structure.txt
+++ b/tech_spec/project_structure.txt
@@ -26,43 +26,44 @@
ViewModel для экрана панели управления, обрабатывает бизнес-логику.
-
+
UI для экрана списка инвентаря.
ViewModel для экрана списка инвентаря.
-
+
UI для экрана сведений о товаре.
ViewModel для экрана сведений о товаре.
-
+
UI для экрана редактирования товара.
ViewModel для экрана редактирования товара.
-
+
UI для экрана списка меток.
ViewModel для экрана списка меток.
-
+
UI для экрана списка местоположений.
+ Использует модель LocationOutCount для отображения количества элементов в каждой локации.
ViewModel для экрана списка местоположений.
-
+
UI для экрана поиска.
ViewModel для экрана поиска.
-
+
UI для экрана настройки.
@@ -131,11 +132,15 @@
Сценарий использования для получения всех меток.
- Сценарий использования для получения всех местоположений.
+ Сценарий использования для получения всех местоположений со счетчиками элементов.
+ Возвращает List, а не базовую модель Location.
Сценарий использования для получения сведений о конкретном товаре.
+
+ Сценарий использования для получения недавно добавленных товаров.
+
Сценарий использования для получения статистики по инвентарю.
diff --git a/tech_spec/tech_spec.txt b/tech_spec/tech_spec.txt
index 02b79f7..93fbabe 100644
--- a/tech_spec/tech_spec.txt
+++ b/tech_spec/tech_spec.txt
@@ -95,6 +95,14 @@
Использован Flow для reactive обновлений; обработка ошибок через sealed class.
+
+ Получение и отображение недавно добавленных товаров
+ Получает список последних N добавленных товаров из локальной базы данных.
+ Пользователь аутентифицирован.
+ Возвращает Flow со списком ItemSummary; список отсортирован по дате создания.
+
+ Данные берутся из локального кэша (Room) для быстрого отображения.
+