Add start dashboard

This commit is contained in:
2025-08-09 11:34:40 +03:00
parent 07a8d82a4d
commit 8db12a7599
5 changed files with 549 additions and 73 deletions

View File

@@ -36,7 +36,16 @@ fun NavGraph() {
}) })
} }
composable(route = Screen.Dashboard.route) { composable(route = Screen.Dashboard.route) {
DashboardScreen() 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 }
}
}
)
} }
composable(route = Screen.InventoryList.route) { composable(route = Screen.InventoryList.route) {
InventoryListScreen() InventoryListScreen()

View File

@@ -1,100 +1,385 @@
// [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/DashboardScreen.kt // [FILE] DashboardScreen.kt
// [SEMANTICS] ui, screen, dashboard, compose
// [IMPORTS]
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.Scaffold import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import timber.log.Timber import com.homebox.lens.R
import com.homebox.lens.domain.model.*
import com.homebox.lens.ui.theme.HomeboxLensTheme
import kotlinx.coroutines.launch
// [CORE-LOGIC] // [ANCHOR] Главная точка входа для экрана Dashboard
/** @OptIn(ExperimentalMaterial3Api::class)
* [CONTRACT]
* Главный Composable для экрана "Дэшборд".
* @param viewModel ViewModel для этого экрана, предоставляемая Hilt.
*/
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel() viewModel: DashboardViewModel = hiltViewModel(),
onNavigateToLocations: () -> Unit,
onNavigateToSearch: () -> Unit,
onNavigateToCreateItem: () -> Unit,
onLogout: () -> Unit
) { ) {
// [ACTION] Собираем состояние из ViewModel
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
Scaffold { paddingValues -> // [ANCHOR] Определяем навигационное меню
Box( ModalNavigationDrawer(
modifier = Modifier drawerState = drawerState,
.fillMaxSize() drawerContent = {
.padding(paddingValues) DrawerContent(
) { onNavigateToLocations = onNavigateToLocations,
when (val state = uiState) { onNavigateToSearch = onNavigateToSearch,
is DashboardUiState.Loading -> { onNavigateToCreateItem = onNavigateToCreateItem,
// [UI-ACTION] Показываем индикатор загрузки onLogout = onLogout,
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) 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))
}
}
)
}
) { paddingValues ->
// [ANCHOR] Основной контент экрана
DashboardContent(
modifier = Modifier.padding(paddingValues),
uiState = uiState,
onLocationClick = { /* TODO */ },
onLabelClick = { /* TODO */ }
)
}
}
}
// [ANCHOR] Компонент основного контента
@Composable
private fun DashboardContent(
modifier: Modifier = Modifier,
uiState: DashboardUiState,
onLocationClick: (LocationOutCount) -> Unit,
onLabelClick: (LabelOut) -> Unit
) {
// [FIX] Based on the UiState, we decide what to show
when (uiState) {
is DashboardUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is DashboardUiState.Error -> {
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
Text(
text = uiState.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
)
}
}
is DashboardUiState.Success -> {
LazyColumn(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
// [ANCHOR] Секция "Быстрая статистика"
item {
StatisticsSection(statistics = uiState.statistics)
} }
is DashboardUiState.Error -> {
// [UI-ACTION] Показываем сообщение об ошибке // [ANCHOR] Секция "Недавно добавлено"
val errorMessage = "Error: ${state.message}" item {
Text( // TODO: Add recently added items to UiState and display them here
text = errorMessage, // RecentlyAddedSection(items = uiState.recentlyAddedItems)
modifier = Modifier.align(Alignment.Center)
)
Timber.w("[UI-STATE] Displaying Error: $errorMessage")
} }
is DashboardUiState.Success -> {
// [UI-ACTION] Отображаем основной контент // [ANCHOR] Секция "Места хранения"
Timber.d("[UI-STATE] Displaying Success") item {
DashboardContent(state) LocationsSection(locations = uiState.locations, onLocationClick = onLocationClick)
}
// [ANCHOR] Секция "Метки"
item {
LabelsSection(labels = uiState.labels, onLabelClick = onLabelClick)
}
item { Spacer(modifier = Modifier.height(16.dp)) }
}
}
}
}
// [ANCHOR] Секция статистики
@Composable
private fun StatisticsSection(statistics: GroupStatistics) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_quick_stats),
style = MaterialTheme.typography.titleMedium
)
Card {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.height(120.dp).fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) }
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) }
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) }
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) }
}
}
}
}
@Composable
private fun StatisticCard(title: String, value: String) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center)
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
}
}
// [ANCHOR] Секция недавно добавленных
@Composable
private fun RecentlyAddedSection(items: List<ItemSummary>) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_recently_added),
style = MaterialTheme.typography.titleMedium
)
if (items.isEmpty()) {
Text(
text = stringResource(id = R.string.items_not_found),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
textAlign = TextAlign.Center
)
} else {
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
items(items) { item ->
ItemCard(item = item)
} }
} }
} }
} }
} }
/**
* [CONTRACT]
* Composable для отображения успешного состояния дэшборда.
* @param state Состояние UI с данными.
*/
@Composable @Composable
fun DashboardContent(state: DashboardUiState.Success) { private fun ItemCard(item: ItemSummary) {
Column( Card(modifier = Modifier.width(150.dp)) {
modifier = Modifier Column(modifier = Modifier.padding(8.dp)) {
.fillMaxSize() // TODO: Add image here from item.image
.padding(16.dp), Spacer(modifier = Modifier.height(80.dp).fillMaxWidth().background(MaterialTheme.colorScheme.secondaryContainer))
verticalArrangement = Arrangement.spacedBy(16.dp) Spacer(modifier = Modifier.height(8.dp))
) { Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
// [UI-COMPONENT] Статистика Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1)
Text(text = "Statistics:")
Text(text = " Items: ${state.statistics.items}")
Text(text = " Locations: ${state.statistics.locations}")
Text(text = " Labels: ${state.statistics.labels}")
Text(text = " Total Value: ${state.statistics.totalValue}")
// [UI-COMPONENT] Локации
Text(text = "Locations:")
state.locations.forEach { location ->
Text(text = " - ${location.name} (${location.itemCount})")
}
// [UI-COMPONENT] Метки
Text(text = "Labels:")
state.labels.forEach { label ->
Text(text = " - ${label.name}")
} }
} }
} }
// [END_FILE_DashboardScreen.kt]
// [ANCHOR] Секция местоположений
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick: (LocationOutCount) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_locations),
style = MaterialTheme.typography.titleMedium
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
locations.forEach { location ->
SuggestionChip(
onClick = { onLocationClick(location) },
label = { Text("${location.name} (${location.itemCount})") }
)
}
}
}
}
// [ANCHOR] Секция меток
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_labels),
style = MaterialTheme.typography.titleMedium
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
labels.forEach { label ->
SuggestionChip(
onClick = { onLabelClick(label) },
label = { Text(label.name) }
)
}
}
}
}
// [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(showBackground = true, name = "Dashboard Success State")
@Composable
fun DashboardContentSuccessPreview() {
val previewState = DashboardUiState.Success(
statistics = GroupStatistics(
items = 123,
totalValue = 9999.99,
locations = 5,
labels = 8
),
locations = listOf(
LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""),
LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""),
LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""),
LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""),
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
),
labels = listOf(
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
)
)
HomeboxLensTheme {
DashboardContent(
uiState = previewState,
onLocationClick = {},
onLabelClick = {}
)
}
}
@Preview(showBackground = true, name = "Dashboard Loading State")
@Composable
fun DashboardContentLoadingPreview() {
HomeboxLensTheme {
DashboardContent(
uiState = DashboardUiState.Loading,
onLocationClick = {},
onLabelClick = {}
)
}
}
@Preview(showBackground = true, name = "Dashboard Error State")
@Composable
fun DashboardContentErrorPreview() {
HomeboxLensTheme {
DashboardContent(
uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)),
onLocationClick = {},
onLabelClick = {}
)
}
}

View File

@@ -0,0 +1,32 @@
<resources>
<string name="app_name">Homebox Lens</string>
<!-- Common -->
<string name="create">Create</string>
<string name="search">Search</string>
<string name="logout">Logout</string>
<string name="no_location">No location</string>
<string name="items_not_found">Items not found</string>
<string name="error_loading_failed">Failed to load data. Please try again.</string>
<!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
<string name="cd_scan_qr_code">Scan QR code</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string>
<string name="dashboard_section_quick_stats">Quick Stats</string>
<string name="dashboard_section_recently_added">Recently Added</string>
<string name="dashboard_section_locations">Locations</string>
<string name="dashboard_section_labels">Labels</string>
<!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Total Items</string>
<string name="dashboard_stat_total_value">Total Value</string>
<string name="dashboard_stat_total_labels">Total Labels</string>
<string name="dashboard_stat_total_locations">Total Locations</string>
<!-- Navigation -->
<string name="nav_locations">Locations</string>
</resources>

View File

@@ -1,3 +1,32 @@
<resources> <resources>
<string name="app_name">Homebox Lens</string> <string name="app_name">Homebox Lens</string>
<!-- Common -->
<string name="create">Создать</string>
<string name="search">Поиск</string>
<string name="logout">Выйти</string>
<string name="no_location">Нет локации</string>
<string name="items_not_found">Элементы не найдены</string>
<string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string>
<!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Открыть боковое меню</string>
<string name="cd_scan_qr_code">Сканировать QR-код</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Главная</string>
<string name="dashboard_section_quick_stats">Быстрая статистика</string>
<string name="dashboard_section_recently_added">Недавно добавлено</string>
<string name="dashboard_section_locations">Места хранения</string>
<string name="dashboard_section_labels">Метки</string>
<!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Всего вещей</string>
<string name="dashboard_stat_total_value">Общая стоимость</string>
<string name="dashboard_stat_total_labels">Всего меток</string>
<string name="dashboard_stat_total_locations">Всего локаций</string>
<!-- Navigation -->
<string name="nav_locations">Локации</string>
</resources> </resources>

View File

@@ -10,6 +10,41 @@
<summary>Библиотека логирования</summary> <summary>Библиотека логирования</summary>
<description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description> <description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description>
</DECISION> </DECISION>
<DECISION id="tech_i18n" status="defined">
<summary>Интернационализация (Мультиязычность)</summary>
<description>
Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории.
Реализация будет основана на стандартном механизме ресурсов Android.
- Все строки, видимые пользователю, должны быть вынесены в файл `app/src/main/res/values/strings.xml`. Использование жестко закодированных строк в коде запрещено.
- Язык по умолчанию - русский (ru). Файл `strings.xml` будет содержать русские строки.
- Для поддержки других языков (например, английского - en) будут создаваться соответствующие каталоги ресурсов (например, `app/src/main/res/values-en/strings.xml`).
- В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`).
</description>
</DECISION>
<DECISION id="tech_ui_framework" status="defined">
<summary>UI Framework</summary>
<description>Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных.</description>
</DECISION>
<DECISION id="tech_di" status="defined">
<summary>Внедрение зависимостей (Dependency Injection)</summary>
<description>Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях.</description>
</DECISION>
<DECISION id="tech_navigation" status="defined">
<summary>Навигация</summary>
<description>Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation.</description>
</DECISION>
<DECISION id="tech_async" status="defined">
<summary>Асинхронные операции</summary>
<description>Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока.</description>
</DECISION>
<DECISION id="tech_networking" status="defined">
<summary>Сетевое взаимодействие</summary>
<description>Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON.</description>
</DECISION>
<DECISION id="tech_database" status="defined">
<summary>Локальное хранилище</summary>
<description>Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных.</description>
</DECISION>
</TECHNICAL_DECISIONS> </TECHNICAL_DECISIONS>
<FEATURES> <FEATURES>
@@ -113,6 +148,92 @@
</FEATURE> </FEATURE>
</FEATURES> </FEATURES>
<UI_SPECIFICATIONS>
<SCREEN id="screen_dashboard" status="defined">
<summary>Главный экран "Панель управления"</summary>
<description>
Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Верхняя панель приложения. Содержит иконку навигационного меню (гамбургер), название/логотип приложения и иконку для запуска сканера (например, QR-кода).</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Боковое навигационное меню. Открывается по нажатию на иконку в TopAppBar. Содержит основные разделы: Главная, Локации, Поиск, Профиль, Инструменты, а также кнопку "Выйти".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<description>Основная область контента. Содержит несколько информационных блоков.</description>
<SUB_COMPONENT type="Section" title="Быстрая статистика">
<description>Сетка из 2x2 карточек, отображающих ключевые метрики.</description>
<ELEMENT type="Card" name="Общая стоимость" />
<ELEMENT type="Card" name="Всего вещей" />
<ELEMENT type="Card" name="Общее количество местоположений" />
<ELEMENT type="Card" name="Всего меток" />
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Недавно добавлено">
<description>Горизонтально прокручиваемый список карточек недавно добавленных предметов. Если предметов нет, отображается сообщение "Элементы не найдены".</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Места хранения">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими местоположения. Нажатие на чип ведет к списку предметов в этом местоположении.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Метки">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими метки. Нажатие на чип ведет к списку предметов с этой меткой.</description>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton_or_PrimaryButton" icon="add">
<description>
Вместо плавающей кнопки (FAB), в референсе используется заметная кнопка "Создать" в навигационном меню. Мы будем придерживаться этого подхода для консистентности. Эта кнопка инициирует процесс создания нового предмета.
</description>
</COMPONENT>
</LAYOUT>
</SCREEN>
<!-- [ЯКОРЬ] Начало спецификации UI для экрана Локаций. Добавлено на основе референса HomeBox. -->
<SCREEN id="screen_locations_list" status="defined">
<summary>Экран "Локации"</summary>
<description>
Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer).
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения, аналогичная экрану "Панель управления".</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Общее боковое меню навигации.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="Header" title="Локации">
<description>Заголовок экрана, расположенный вверху основной области контента.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="List" name="LocationsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех местоположений.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одно местоположение. Состоит из иконки (например, 'place') и названия местоположения. Весь элемент является кликабельным и ведет на экран со списком предметов в данной локации.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новое местоположение. В веб-версии для этого используются иконки в углу, но FAB является более нативным паттерном для Android.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка локаций</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной локации.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания нового местоположения.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<!-- [ЯКОРЬ] Конец спецификации UI для экрана Локаций. -->
</UI_SPECIFICATIONS>
<IMPLEMENTATION_MAP> <IMPLEMENTATION_MAP>
<!-- Use Cases --> <!-- Use Cases -->
<USE_CASE id="uc_get_stats" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" /> <USE_CASE id="uc_get_stats" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" />