CREATE_OR_UPDATE_FILE app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt [DEPENDS_ON] -> [Class('LabelsListViewModel')] // [RELATION: Class('LabelsListScreen')] -> [CREATES_INSTANCE_OF] -> [Class('Scaffold')] /** * [MAIN-CONTRACT] * Экран для отображения списка всех меток. * * Этот Composable является точкой входа для UI, определенного в спецификации `screen_labels_list`. * Он получает состояние от [LabelsListViewModel] и отображает его, делегируя обработку * пользовательских событий в ViewModel. * * @param onLabelClick Функция обратного вызова для обработки нажатия на метку. * @param onNavigateBack Функция обратного вызова для навигации назад. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @ENTRYPOINT fun LabelsListScreen( viewModel: LabelsListViewModel = hiltViewModel(), onLabelClick: (Label) -> Unit, onNavigateBack: () -> Unit ) { // [STATE] val labels by viewModel.labels.collectAsState() // [ACTION] Scaffold( topBar = { TopAppBar( title = { Text(stringResource(id = R.string.screen_title_labels)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back) ) } } ) }, floatingActionButton = { FloatingActionButton(onClick = { /* TODO: Implement create new label dialog/screen */ }) { Icon( imageVector = Icons.Filled.Add, contentDescription = stringResource(id = R.string.content_desc_add_label) ) } } ) { innerPadding -> // [DELEGATES] Box(modifier = Modifier.padding(innerPadding)) { LabelsListContent( labels = labels, onLabelClick = onLabelClick ) } } } // [ENTITY: Function('LabelsListContent')] // [RELATION: Function('LabelsListContent')] -> [CALLS] -> [Function('LabelListItem')] /** * [CONTRACT] * Отображает основной контент экрана: список меток. * * @param labels Список меток для отображения. * @param onLabelClick Обработчик нажатия на элемент списка. * @sideeffect Отсутствуют. */ @Composable @HELPER private fun LabelsListContent( labels: List CREATE_OR_UPDATE_FILE app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt [DEPENDS_ON] -> [Class('InventoryListViewModel')] /** * [MAIN-CONTRACT] * Экран для отображения списка инвентарных позиций. * * Реализует спецификацию `screen_inventory_list`. Позволяет просматривать, * искать и синхронизировать инвентарь. * * @param onItemClick Обработчик нажатия на элемент инвентаря. * @param onNavigateBack Обработчик для возврата на предыдущий экран. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @ENTRYPOINT fun InventoryListScreen( viewModel: InventoryListViewModel = hiltViewModel(), onItemClick: (Item) -> Unit, onNavigateBack: () -> Unit ) { // [STATE] val uiState by viewModel.uiState.collectAsState() // [ACTION] Scaffold( topBar = { TopAppBar( title = { Text(stringResource(id = R.string.screen_title_inventory)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back) ) } } ) }, floatingActionButton = { FloatingActionButton(onClick = { Timber.i("[INFO][ACTION][ui_interaction] Sync inventory triggered.") viewModel.onSyncClicked() }) { Icon( imageVector = Icons.Filled.Refresh, contentDescription = stringResource(id = R.string.content_desc_sync_inventory) ) } } ) { innerPadding -> // [DELEGATES] Column(modifier = Modifier.padding(innerPadding)) { SearchBar( query = uiState.searchQuery, onQueryChange = viewModel::onSearchQueryChanged ) InventoryListContent( isLoading = uiState.isLoading, items = uiState.items, onItemClick = onItemClick ) } } } /** * [CONTRACT] * Поле для ввода поискового запроса. */ @Composable @HELPER private fun SearchBar(query: String, onQueryChange: (String) -> Unit) { TextField( value = query, onValueChange = onQueryChange, modifier = Modifier .fillMaxWidth() .padding(8.dp), placeholder = { Text(stringResource(id = R.string.placeholder_search)) }, leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) } ) } /** * [CONTRACT] * Основной контент: индикатор загрузки или список предметов. */ @Composable @HELPER private fun InventoryListContent( isLoading: Boolean, items: List, onItemClick: (Item) -> Unit ) { Box(modifier = Modifier.fillMaxSize()) { if (isLoading) { // [STATE] CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } else if (items.isEmpty()) { // [FALLBACK] Text( text = stringResource(id = R.string.message_no_items_found), modifier = Modifier.align(Alignment.Center) ) } else { // [CORE-LOGIC] LazyColumn { items(items, key = { it.id }) { item -> ItemCard(item = item, onClick = { Timber.i("[INFO][ACTION][ui_interaction] Item clicked: ${item.name}") onItemClick(item) }) } } } } } /** * [CONTRACT] * Карточка для отображения одного элемента инвентаря. */ @Composable @HELPER private fun ItemCard( item: Item, onClick: () -> Unit ) { // [PRECONDITION] require(item.name.isNotBlank()) { "Item name cannot be blank." } // [CORE-LOGIC] Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 4.dp) .clickable(onClick = onClick) ) { Column(modifier = Modifier.padding(16.dp)) { Text(text = item.name, style = androidx.compose.material3.MaterialTheme.typography.titleMedium) Text(text = "Quantity: ${item.quantity}", style = androidx.compose.material3.MaterialTheme.typography.bodySmall) item.location?.let { Text(text = "Location: ${it.name}", style = androidx.compose.material3.MaterialTheme.typography.bodySmall) } } } } ]]> CREATE_OR_UPDATE_FILE app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt [DEPENDS_ON] -> [Class('ItemDetailsViewModel')] /** * [MAIN-CONTRACT] * Экран для отображения детальной информации о товаре. * * Реализует спецификацию `screen_item_details`. * * @param onNavigateBack Обработчик для возврата на предыдущий экран. * @param onEditClick Обработчик нажатия на кнопку редактирования. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @ENTRYPOINT fun ItemDetailsScreen( viewModel: ItemDetailsViewModel = hiltViewModel(), onNavigateBack: () -> Unit, onEditClick: (Int) -> Unit ) { // [STATE] val uiState by viewModel.uiState.collectAsState() Scaffold( topBar = { TopAppBar( title = { Text(uiState.item?.name ?: stringResource(id = R.string.screen_title_item_details)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back)) } }, actions = { IconButton(onClick = { uiState.item?.id?.let { Timber.i("[INFO][ACTION][ui_interaction] Edit item clicked: id=$it") onEditClick(it) } }) { Icon(Icons.Default.Edit, contentDescription = stringResource(id = R.string.content_desc_edit_item)) } IconButton(onClick = { Timber.w("[WARN][ACTION][ui_interaction] Delete item clicked: id=${uiState.item?.id}") viewModel.deleteItem() // После удаления нужно навигироваться назад onNavigateBack() }) { Icon(Icons.Default.Delete, contentDescription = stringResource(id = R.string.content_desc_delete_item)) } } ) } ) { innerPadding -> ItemDetailsContent( modifier = Modifier.padding(innerPadding), isLoading = uiState.isLoading, item = uiState.item ) } } /** * [CONTRACT] * Отображает контент экрана: индикатор загрузки или детали товара. */ @Composable @HELPER private fun ItemDetailsContent( modifier: Modifier = Modifier, isLoading: Boolean, item: Item? ) { Box(modifier = modifier.fillMaxSize()) { when { isLoading -> { // [STATE] CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } item == null -> { // [FALLBACK] Text(stringResource(id = R.string.message_item_not_found), modifier = Modifier.align(Alignment.Center)) } else -> { // [CORE-LOGIC] Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { // TODO: ImageCarousel // Text("Image Carousel Placeholder") DetailsSection(title = stringResource(id = R.string.section_title_description)) { Text(text = item.description ?: stringResource(id = R.string.placeholder_no_description)) } DetailsSection(title = stringResource(id = R.string.section_title_details)) { InfoRow(label = stringResource(id = R.string.label_quantity), value = item.quantity.toString()) item.location?.let { InfoRow(label = stringResource(id = R.string.label_location), value = it.name) } } if (item.labels.isNotEmpty()) { DetailsSection(title = stringResource(id = R.string.section_title_labels)) { // TODO: Use FlowRow for better layout Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { item.labels.forEach { label -> AssistChip(onClick = { /* No-op */ }, label = { Text(label.name) }) } } } } // TODO: CustomFieldsGrid } } } } } /** * [CONTRACT] * Секция с заголовком и контентом. */ @Composable @HELPER private fun DetailsSection(title: String, content: @Composable ColumnScope.() -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text(text = title, style = MaterialTheme.typography.titleMedium) Divider() content() } } /** * [CONTRACT] * Строка для отображения пары "метка: значение". */ @Composable @HELPER private fun InfoRow(label: String, value: String) { Row { Text(text = "$label: ", style = MaterialTheme.typography.bodyLarge) Text(text = value, style = MaterialTheme.typography.bodyLarge) } } ]]> CREATE_OR_UPDATE_FILE app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt [DEPENDS_ON] -> [Class('ItemEditViewModel')] /** * [MAIN-CONTRACT] * Экран для создания или редактирования товара. * * Реализует спецификацию `screen_item_edit`. * * @param onNavigateBack Обработчик для возврата на предыдущий экран после сохранения или отмены. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @ENTRYPOINT fun ItemEditScreen( viewModel: ItemEditViewModel = hiltViewModel(), onNavigateBack: () -> Unit ) { // [STATE] val uiState by viewModel.uiState.collectAsState() // [SIDE-EFFECT] LaunchedEffect(uiState.isSaved) { if (uiState.isSaved) { Timber.i("[INFO][SIDE_EFFECT][navigation] Item saved, navigating back.") onNavigateBack() } } Scaffold( topBar = { TopAppBar( title = { Text(stringResource(id = if (uiState.isEditing) R.string.screen_title_edit_item else R.string.screen_title_create_item)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back)) } }, actions = { IconButton(onClick = { Timber.i("[INFO][ACTION][ui_interaction] Save item clicked.") viewModel.saveItem() }) { Icon(Icons.Default.Done, contentDescription = stringResource(id = R.string.content_desc_save_item)) } } ) } ) { innerPadding -> ItemEditContent( modifier = Modifier.padding(innerPadding), state = uiState, onNameChange = { viewModel.onNameChange(it) }, onDescriptionChange = { viewModel.onDescriptionChange(it) }, onQuantityChange = { viewModel.onQuantityChange(it) } ) } } /** * [CONTRACT] * Отображает форму для редактирования данных товара. */ @Composable @HELPER private fun ItemEditContent( modifier: Modifier = Modifier, state: ItemEditUiState, onNameChange: (String) -> Unit, onDescriptionChange: (String) -> Unit, onQuantityChange: (String) -> Unit ) { // [CORE-LOGIC] Column( modifier = modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { OutlinedTextField( value = state.name, onValueChange = onNameChange, label = { Text(stringResource(id = R.string.label_name)) }, modifier = Modifier.fillMaxWidth(), isError = state.nameError != null ) state.nameError?.let { Text(text = stringResource(id = it), color = MaterialTheme.colorScheme.error) } OutlinedTextField( value = state.description, onValueChange = onDescriptionChange, label = { Text(stringResource(id = R.string.label_description)) }, modifier = Modifier.fillMaxWidth(), minLines = 3 ) OutlinedTextField( value = state.quantity, onValueChange = onQuantityChange, label = { Text(stringResource(id = R.string.label_quantity)) }, modifier = Modifier.fillMaxWidth(), isError = state.quantityError != null ) state.quantityError?.let { Text(text = stringResource(id = it), color = MaterialTheme.colorScheme.error) } // TODO: Location Dropdown // TODO: Labels ChipGroup // TODO: ImagePicker } } ]]> CREATE_OR_UPDATE_FILE app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt [DEPENDS_ON] -> [Class('SearchViewModel')] /** * [MAIN-CONTRACT] * Специализированный экран для поиска товаров. * * Реализует спецификацию `screen_search`. * * @param onNavigateBack Обработчик для возврата на предыдущий экран. * @param onItemClick Обработчик нажатия на найденный товар. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @ENTRYPOINT fun SearchScreen( viewModel: SearchViewModel = hiltViewModel(), onNavigateBack: () -> Unit, onItemClick: (Item) -> Unit ) { // [STATE] val uiState by viewModel.uiState.collectAsState() Scaffold( topBar = { TopAppBar( title = { TextField( value = uiState.searchQuery, onValueChange = viewModel::onSearchQueryChanged, placeholder = { Text(stringResource(R.string.placeholder_search_items)) }, modifier = Modifier.fillMaxWidth() ) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.content_desc_navigate_back)) } } ) } ) { innerPadding -> SearchContent( modifier = Modifier.padding(innerPadding), isLoading = uiState.isLoading, results = uiState.results, onItemClick = onItemClick ) } } /** * [CONTRACT] * Отображает основной контент экрана: фильтры и результаты поиска. */ @Composable @HELPER private fun SearchContent( modifier: Modifier = Modifier, isLoading: Boolean, results: List, onItemClick: (Item) -> Unit ) { Column(modifier = modifier.fillMaxSize()) { // [SECTION] FILTERS // TODO: Implement FilterSection with chips for locations/labels // Spacer(modifier = Modifier.height(8.dp)) // [SECTION] RESULTS Box(modifier = Modifier.weight(1f)) { if (isLoading) { // [STATE] CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } else { // [CORE-LOGIC] LazyColumn { items(results, key = { it.id }) { item -> ListItem( headlineContent = { Text(item.name) }, supportingContent = { Text(item.location?.name ?: "") }, modifier = Modifier.clickable { onItemClick(item) } ) } } } } } } ]]> CREATE_OR_UPDATE_FILE app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt [DEPENDS_ON] -> [Class('SetupViewModel')] /** * [MAIN-CONTRACT] * Экран для начальной настройки соединения с сервером Homebox. * * @param onSetupComplete Обработчик, вызываемый после успешной настройки и входа. */ @Composable @ENTRYPOINT fun SetupScreen( viewModel: SetupViewModel = hiltViewModel(), onSetupComplete: () -> Unit ) { // [STATE] val uiState by viewModel.uiState.collectAsState() // [SIDE-EFFECT] LaunchedEffect(uiState.isLoginSuccessful) { if (uiState.isLoginSuccessful) { Timber.i("[INFO][SIDE_EFFECT][navigation] Setup complete, navigating to main screen.") onSetupComplete() } } // [CORE-LOGIC] Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier .fillMaxWidth() .padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text(text = stringResource(id = R.string.screen_title_setup), style = MaterialTheme.typography.headlineMedium) OutlinedTextField( value = uiState.serverUrl, onValueChange = viewModel::onServerUrlChange, label = { Text(stringResource(id = R.string.label_server_url)) }, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), isError = uiState.error != null ) OutlinedTextField( value = uiState.apiKey, onValueChange = viewModel::onApiKeyChange, label = { Text(stringResource(id = R.string.label_api_key)) }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation(), isError = uiState.error != null ) if (uiState.isLoading) { // [STATE] CircularProgressIndicator() } else { // [ACTION] Button( onClick = { Timber.i("[INFO][ACTION][ui_interaction] Login button clicked.") viewModel.login() }, modifier = Modifier.fillMaxWidth() ) { Text(text = stringResource(id = R.string.button_connect)) } } uiState.error?.let { // [FALLBACK] Text( text = it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodyMedium ) } } } } ]]> CREATE_OR_UPDATE_FILE PROJECT_SPECIFICATION.xml Homebox Lens Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox. Библиотека логирования В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования. Пример корректного использования Timber Интернационализация (Мультиязычность) Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории. Реализация будет основана на стандартном механизме ресурсов Android. - Все строки, видимые пользователю, должны быть вынесены в файл `app/src/main/res/values/strings.xml`. Использование жестко закодированных строк в коде запрещено. - Язык по умолчанию - русский (ru). Файл `strings.xml` будет содержать русские строки. - Для поддержки других языков (например, английского - en) будут создаваться соответствующие каталоги ресурсов (например, `app/src/main/res/values-en/strings.xml`). - В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`). UI Framework Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных. Внедрение зависимостей (Dependency Injection) Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях. Навигация Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation. Асинхронные операции Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока. Сетевое взаимодействие Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON. Локальное хранилище Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных. Спецификация безопасности проекта. Все сетевые взаимодействия должны быть защищены HTTPS. Аутентификация пользователя хранится в EncryptedSharedPreferences. Обработка ошибок аутентификации должна включать logout и редирект на экран логина. Использовать JWT или API-ключ для авторизации запросов. При истечении токена автоматически обновлять. Локальные данные (credentials) шифровать с помощью Android KeyStore. Спецификация обработки ошибок. Все потенциальные ошибки (сеть, БД, валидация) должны быть обработаны с использованием sealed classes для ошибок (e.g., NetworkError, ValidationError) и отображаться пользователю через Snackbar или Dialog. При сетевых ошибках показывать сообщение "No internet connection" и предлагать retry. Для HTTP 4xx/5xx отображать user-friendly сообщение на основе response body. Использовать require/check для контрактов, логировать и показывать toast. Модель инвентарного товара. Содержит поля: id, name, description, quantity, location, labels, customFields. Модель метки. Содержит поля: id, name, color. Модель местоположения. Содержит поля: id, name, parentLocation. Модель статистики инвентаря. Содержит поля: totalItems, totalValue, locationsCount, labelsCount. Экран панели управления Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам. Получение и отображение статистики Получает общую статистику по инвентарю с сервера. Пользователь аутентифицирован; сеть доступна. Возвращает объект Statistics; данные кэшированы локально. Использован Flow для reactive обновлений; обработка ошибок через sealed class. Получение и отображение недавно добавленных товаров Получает список последних N добавленных товаров из локальной базы данных. Пользователь аутентифицирован. Возвращает Flow со списком ItemSummary; список отсортирован по дате создания. Данные берутся из локального кэша (Room) для быстрого отображения. Экран списка инвентаря Отображает список всех инвентарных позиций с возможностью поиска и фильтрации. Поиск и фильтрация товаров Ищет товары по строке запроса и фильтрам. Результаты разбиты на страницы. Запрос не пустой; параметры пагинации валидны (page >= 1). Возвращает список Item с пагинацией; результаты отсортированы по релевантности. Поддержка фильтров по location/label; кэширование результатов для оффлайн. Синхронизация инвентаря Выполняет полную синхронизацию локального кэша инвентаря с сервером. Сеть доступна; пользователь аутентифицирован. Локальная БД обновлена; возвращает success/failure. Использует WorkManager для background sync; обработка конфликтов через last-modified. Экран сведений о товаре Показывает все сведения о конкретном инвентарном товаре, включая его название, описание, изображения, вложения и настраиваемые поля. Получение сведений о товаре Получает полные сведения о конкретном товаре из репозитория. Item ID валиден и существует. Возвращает полный объект Item с attachments. Загрузка изображений через Coil; оффлайн-поддержка из Room. Создание/редактирование/удаление товаров Позволяет пользователям создавать новые товары, обновлять существующие и удалять их. Создать товар Создает новый инвентарный товар на сервере. Все обязательные поля (name, quantity) заполнены; данные валидны. Новый Item сохранен на сервере; ID возвращен. Валидация через require; sync с локальной БД. Обновить товар Обновляет существующий инвентарный товар на сервере. Item ID существует; изменения валидны. Item обновлен; версия инкрементирована. Partial update через PATCH; обработка concurrency. Удалить товар Удаляет инвентарный товар с сервера. Item ID существует; пользователь имеет права. Item удален; связанные ресурсы (attachments) очищены. Soft delete для восстановления; sync с локальной БД. Управление метками и местоположениями Позволяет пользователям просматривать списки всех доступных меток и местоположений. Получить все метки Получает список всех меток из репозитория. Сеть доступна или кэш существует. Возвращает список Label; отсортирован по name. Кэширование в Room; reactive обновления. Получить все местоположения Получает список всех местоположений из репозитория. Сеть доступна или кэш существует. Возвращает список Location; иерархическая структура сохранена. Поддержка nested locations; кэширование. Экран поиска Предоставляет специальный пользовательский интерфейс для поиска товаров. Поиск со специального экрана Использует ту же функцию поиска, но со специального экрана. Запрос не пустой. Возвращает результаты поиска; UI обновлен. Интеграция с SearchView; debounce для запросов. Главный экран "Панель управления" Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox. Верхняя панель приложения. Содержит иконку навигационного меню (гамбургер), название/логотип приложения и иконку для запуска сканера (например, QR-кода). Боковое навигационное меню. Открывается по нажатию на иконку в TopAppBar. Содержит основные разделы: Главная, Локации, Поиск, Профиль, Инструменты, а также кнопку "Выйти". Основная область контента. Содержит несколько информационных блоков. Сетка из 2x2 карточек, отображающих ключевые метрики. Горизонтально прокручиваемый список карточек недавно добавленных предметов. Если предметов нет, отображается сообщение "Элементы не найдены". Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими местоположения. Нажатие на чип ведет к списку предметов в этом местоположении. Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими метки. Нажатие на чип ведет к списку предметов с этой меткой. Вместо плавающей кнопки (FAB), в референсе используется заметная кнопка "Создать" в навигационном меню. Мы будем придерживаться этого подхода для консистентности. Эта кнопка инициирует процесс создания нового предмета. Нажатие на чип местоположения/метки Навигация на экран списка инвентаря с фильтром. Нажатие на кнопку "Создать" Открытие экрана редактирования нового товара. Экран "Локации" Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer). Общая верхняя панель приложения, аналогичная экрану "Панель управления". Общее боковое меню навигации. Основная область контента, занимающая все доступное пространство под TopAppBar. Заголовок экрана, расположенный вверху основной области контента. Вертикальный, прокручиваемый список (LazyColumn) всех местоположений. Элемент списка, представляющий одно местоположение. Состоит из иконки (например, 'place') и названия местоположения. Весь элемент является кликабельным и ведет на экран со списком предметов в данной локации. Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новое местоположение. В веб-версии для этого используются иконки в углу, но FAB является более нативным паттерном для Android. Нажатие на элемент списка локаций Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной локации. Нажатие на FloatingActionButton Открывается диалоговое окно или новый экран для создания нового местоположения. Экран "Метки" Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения. Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад". Основная область контента, занимающая все доступное пространство под TopAppBar. Вертикальный, прокручиваемый список (LazyColumn) всех меток. Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой. Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку. Нажатие на элемент списка меток Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке. Нажатие на FloatingActionButton Открывается диалоговое окно или новый экран для создания новой метки. Экран "Список инвентаря" Отображает список всех инвентарных позиций с возможностью поиска, фильтрации и пагинации. Интегрирован в навигацию. Верхняя панель с поиском и фильтрами. Прокручиваемый список товаров. LazyColumn с карточками товаров (name, quantity, location). Кликабельная карточка товара, ведущая на details. Кнопка для синхронизации инвентаря. Ввод в поиск Обновление списка с debounce. Нажатие на товар Навигация на screen_item_details. Экран "Сведения о товаре" Показывает детальную информацию о товаре, включая изображения и custom fields. С кнопками edit/delete. Карусель изображений. Текст description. Сетка custom полей. Нажатие edit Навигация на screen_item_edit. Нажатие delete Подтверждение и вызов func_delete_item. Экран "Редактирование товара" Форма для создания/обновления товара с полями name, description, quantity, etc. С кнопкой save. Поле ввода имени. Выбор местоположения. Выбор меток. Добавление изображений. Нажатие save Валидация и вызов func_create_item или func_update_item. Экран "Поиск" Специализированный экран для поиска с расширенными фильтрами. С поисковой строкой. Чипы для фильтров (location, label). LazyColumn результатов. Изменение запроса/фильтров Обновление результатов. Руководство по использованию иконок Этот раздел определяет стандартный набор иконок 'androidx.compose.material.icons.Icons.Filled' для использования в приложении. Для устаревших иконок указаны актуальные замены. ]]>