From 9b914b2904e9b1501ccc0416be753f6bf89e3af8 Mon Sep 17 00:00:00 2001 From: busya Date: Sun, 28 Sep 2025 10:10:01 +0300 Subject: [PATCH] REFACTOR END --- agent_promts/roles/architect.md | 5 +- agent_promts/roles/code.md | 43 +- agent_promts/roles/qa.md | 34 +- app/build.gradle.kts | 14 +- .../java/com/homebox/lens/MainActivity.kt | 60 +- .../java/com/homebox/lens/MainApplication.kt | 4 +- .../lens/navigation/NavigationActions.kt | 132 ---- .../com/homebox/lens/navigation/Screen.kt | 131 ---- .../com/homebox/lens/ui/common/AppDrawer.kt | 116 ---- .../homebox/lens/ui/common/MainScaffold.kt | 91 --- .../homebox/lens/ui/components/ColorPicker.kt | 76 --- .../lens/ui/components/LoadingOverlay.kt | 35 -- .../inventorylist/InventoryListScreen.kt | 39 -- .../inventorylist/InventoryListViewModel.kt | 21 - .../screen/itemdetails/ItemDetailsScreen.kt | 39 -- .../itemdetails/ItemDetailsViewModel.kt | 21 - .../lens/ui/screen/itemedit/ItemEditScreen.kt | 436 ------------- .../ui/screen/itemedit/ItemEditViewModel.kt | 583 ------------------ .../ui/screen/labeledit/LabelEditScreen.kt | 113 ---- .../ui/screen/labeledit/LabelEditViewModel.kt | 115 ---- .../ui/screen/labelslist/LabelsListScreen.kt | 225 ------- .../ui/screen/labelslist/LabelsListUiState.kt | 50 -- .../screen/labelslist/LabelsListViewModel.kt | 149 ----- .../screen/locationedit/LocationEditScreen.kt | 48 -- .../locationslist/LocationsListScreen.kt | 296 --------- .../locationslist/LocationsListUiState.kt | 42 -- .../locationslist/LocationsListViewModel.kt | 64 -- .../lens/ui/screen/search/SearchScreen.kt | 39 -- .../lens/ui/screen/search/SearchViewModel.kt | 21 - .../lens/ui/screen/settings/SettingsScreen.kt | 104 ---- .../ui/screen/settings/SettingsUiState.kt | 8 - .../ui/screen/settings/SettingsViewModel.kt | 54 -- .../lens/ui/screen/setup/SetupScreen.kt | 141 ----- .../lens/ui/screen/setup/SetupUiState.kt | 27 - .../lens/ui/screen/setup/SetupViewModel.kt | 113 ---- .../java/com/homebox/lens/ui/theme/Theme.kt | 74 --- .../com/homebox/lens/ui/theme/Typography.kt | 29 - .../screen/itemedit/ItemEditViewModelTest.kt | 239 ------- buildSrc/src/main/java/Dependencies.kt | 41 +- data/semantic-ktlint-rules/.gitignore | 1 - data/semantic-ktlint-rules/build.gradle.kts | 18 - data/semantic-ktlint-rules/proguard-rules.pro | 21 - .../ktlint/rules/ExampleInstrumentedTest.kt | 28 - .../src/main/AndroidManifest.xml | 12 - .../ktlint/rules/CustomRuleSetProvider.kt | 18 - .../com/busya/ktlint/rules/FileHeaderRule.kt | 35 -- .../rules/MandatoryEntityDeclarationRule.kt | 42 -- .../busya/ktlint/rules/NoStrayCommentsRule.kt | 26 - .../res/drawable/ic_launcher_background.xml | 170 ----- .../res/drawable/ic_launcher_foreground.xml | 30 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 - .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 0 bytes .../src/main/res/values-night/themes.xml | 16 - .../src/main/res/values/colors.xml | 10 - .../src/main/res/values/strings.xml | 3 - .../src/main/res/values/themes.xml | 16 - ...int.rule.engine.core.api.RuleSetProviderV3 | 1 - .../com/busya/ktlint/rules/ExampleUnitTest.kt | 45 -- extract_semantics.py | 502 --------------- extract_semantics_output.txt | 289 --------- feature/dashboard/build.gradle.kts | 37 +- .../feature/dashboard/DashboardNavigation.kt | 49 +- .../lens/feature/dashboard/DashboardScreen.kt | 435 ++++++++----- .../feature/dashboard/DashboardUiState.kt | 64 +- .../feature/dashboard/DashboardViewModel.kt | 118 ++-- .../feature/dashboard}/navigation/NavGraph.kt | 118 ++-- .../feature/dashboard/navigation/Screen.kt | 24 + .../lens/feature/dashboard}/ui/theme/Color.kt | 6 +- .../lens/feature/dashboard/ui/theme/Theme.kt | 93 +++ .../feature/dashboard/ui/theme/Typography.kt | 30 + .../dashboard/src/main/res/values/strings.xml | 4 - feature/inventorylist/build.gradle.kts | 50 ++ .../inventorylist/InventoryListScreen.kt | 20 + feature/itemdetails/build.gradle.kts | 54 ++ .../feature/itemdetails/ItemDetailsScreen.kt | 27 + feature/itemedit/build.gradle.kts | 55 ++ .../lens/feature/itemedit/ItemEditScreen.kt | 25 + feature/labeledit/build.gradle.kts | 54 ++ .../lens/feature/labeledit/LabelEditScreen.kt | 17 + feature/labelslist/build.gradle.kts | 54 ++ .../feature/labelslist/LabelsListScreen.kt | 23 + feature/locationedit/build.gradle.kts | 53 ++ .../locationedit/LocationEditScreen.kt | 15 + feature/locationslist/build.gradle.kts | 54 ++ .../locationslist/LocationsListScreen.kt | 25 + feature/search/build.gradle.kts | 54 ++ .../lens/feature/search/SearchScreen.kt | 23 + feature/settings/build.gradle.kts | 55 ++ .../lens/feature/settings/SettingsScreen.kt | 25 + feature/setup/build.gradle.kts | 54 ++ .../homebox/lens/feature/setup/SetupScreen.kt | 15 + gradle_build_noise_reduction_plan.md | 342 ++++++++++ scripts/quick-fix.sh | 135 ++++ scripts/smart-build.sh | 115 ++++ settings.gradle.kts | 12 +- tasks/temp/app_refactoring_plan.md | 42 ++ tasks/temp/work_order_app_refactoring.xml | 53 ++ tasks/temp/work_order_dashboard_fix_final.xml | 78 +++ .../work_order_feature_dashboard_refactor.xml | 0 .../work_order_item_creation_update.md | 0 tech_spec/dashboard_fix_plan.md | 49 ++ ui/common/build.gradle.kts | 79 +++ .../com/homebox/lens/ui/common/AppDrawer.kt | 127 ++++ .../homebox/lens/ui/common/MainScaffold.kt | 115 ++++ .../lens/ui/common/NavigationActions.kt | 168 +++++ validate_semantics.py | 143 ++++- work_order_gradle_noise_reduction.md | 162 +++++ 117 files changed, 3070 insertions(+), 5447 deletions(-) delete mode 100644 app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt delete mode 100644 app/src/main/java/com/homebox/lens/navigation/Screen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/components/ColorPicker.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/components/LoadingOverlay.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditViewModel.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListUiState.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/locationedit/LocationEditScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListUiState.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/settings/SettingsScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/settings/SettingsUiState.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/settings/SettingsViewModel.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/setup/SetupUiState.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/theme/Theme.kt delete mode 100644 app/src/main/java/com/homebox/lens/ui/theme/Typography.kt delete mode 100644 app/src/test/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModelTest.kt delete mode 100644 data/semantic-ktlint-rules/.gitignore delete mode 100644 data/semantic-ktlint-rules/build.gradle.kts delete mode 100644 data/semantic-ktlint-rules/proguard-rules.pro delete mode 100644 data/semantic-ktlint-rules/src/androidTest/java/com/busya/ktlint/rules/ExampleInstrumentedTest.kt delete mode 100644 data/semantic-ktlint-rules/src/main/AndroidManifest.xml delete mode 100644 data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/CustomRuleSetProvider.kt delete mode 100644 data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/FileHeaderRule.kt delete mode 100644 data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/MandatoryEntityDeclarationRule.kt delete mode 100644 data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/NoStrayCommentsRule.kt delete mode 100644 data/semantic-ktlint-rules/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 data/semantic-ktlint-rules/src/main/res/drawable/ic_launcher_foreground.xml delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp delete mode 100644 data/semantic-ktlint-rules/src/main/res/values-night/themes.xml delete mode 100644 data/semantic-ktlint-rules/src/main/res/values/colors.xml delete mode 100644 data/semantic-ktlint-rules/src/main/res/values/strings.xml delete mode 100644 data/semantic-ktlint-rules/src/main/res/values/themes.xml delete mode 100644 data/semantic-ktlint-rules/src/resourses/META-INF/services/com.pinterest.ktlint.rule.engine.core.api.RuleSetProviderV3 delete mode 100644 data/semantic-ktlint-rules/src/test/java/com/busya/ktlint/rules/ExampleUnitTest.kt delete mode 100644 extract_semantics.py delete mode 100644 extract_semantics_output.txt rename {app/src/main/java/com/homebox/lens => feature/dashboard/src/main/java/com/homebox/lens/feature/dashboard}/navigation/NavGraph.kt (56%) create mode 100644 feature/dashboard/src/main/java/com/homebox/lens/feature/dashboard/navigation/Screen.kt rename {app/src/main/java/com/homebox/lens => feature/dashboard/src/main/java/com/homebox/lens/feature/dashboard}/ui/theme/Color.kt (77%) create mode 100644 feature/dashboard/src/main/java/com/homebox/lens/feature/dashboard/ui/theme/Theme.kt create mode 100644 feature/dashboard/src/main/java/com/homebox/lens/feature/dashboard/ui/theme/Typography.kt create mode 100644 feature/inventorylist/build.gradle.kts create mode 100644 feature/inventorylist/src/main/java/com/homebox/lens/feature/inventorylist/InventoryListScreen.kt create mode 100644 feature/itemdetails/build.gradle.kts create mode 100644 feature/itemdetails/src/main/java/com/homebox/lens/feature/itemdetails/ItemDetailsScreen.kt create mode 100644 feature/itemedit/build.gradle.kts create mode 100644 feature/itemedit/src/main/java/com/homebox/lens/feature/itemedit/ItemEditScreen.kt create mode 100644 feature/labeledit/build.gradle.kts create mode 100644 feature/labeledit/src/main/java/com/homebox/lens/feature/labeledit/LabelEditScreen.kt create mode 100644 feature/labelslist/build.gradle.kts create mode 100644 feature/labelslist/src/main/java/com/homebox/lens/feature/labelslist/LabelsListScreen.kt create mode 100644 feature/locationedit/build.gradle.kts create mode 100644 feature/locationedit/src/main/java/com/homebox/lens/feature/locationedit/LocationEditScreen.kt create mode 100644 feature/locationslist/build.gradle.kts create mode 100644 feature/locationslist/src/main/java/com/homebox/lens/feature/locationslist/LocationsListScreen.kt create mode 100644 feature/search/build.gradle.kts create mode 100644 feature/search/src/main/java/com/homebox/lens/feature/search/SearchScreen.kt create mode 100644 feature/settings/build.gradle.kts create mode 100644 feature/settings/src/main/java/com/homebox/lens/feature/settings/SettingsScreen.kt create mode 100644 feature/setup/build.gradle.kts create mode 100644 feature/setup/src/main/java/com/homebox/lens/feature/setup/SetupScreen.kt create mode 100644 gradle_build_noise_reduction_plan.md create mode 100644 scripts/quick-fix.sh create mode 100644 scripts/smart-build.sh create mode 100644 tasks/temp/app_refactoring_plan.md create mode 100644 tasks/temp/work_order_app_refactoring.xml create mode 100644 tasks/temp/work_order_dashboard_fix_final.xml rename tasks/{ => temp}/work_order_feature_dashboard_refactor.xml (100%) rename tasks/{ => temp}/work_order_item_creation_update.md (100%) create mode 100644 tech_spec/dashboard_fix_plan.md create mode 100644 ui/common/build.gradle.kts create mode 100644 ui/common/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt create mode 100644 ui/common/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt create mode 100644 ui/common/src/main/java/com/homebox/lens/ui/common/NavigationActions.kt create mode 100644 work_order_gradle_noise_reduction.md diff --git a/agent_promts/roles/architect.md b/agent_promts/roles/architect.md index 5389845..dd146c1 100644 --- a/agent_promts/roles/architect.md +++ b/agent_promts/roles/architect.md @@ -64,12 +64,11 @@ ### Шаг 3: Синтез плана и WorkOrder 1. Сгенерировать детальный план в Markdown. 2. Представить план пользователю для одобрения. - 3. **Параллельно**, формализовать план как машиночитаемый `WorkOrder.xml`. + 3. **Параллельно**, формализовать план как машиночитаемый `WorkOrder.md`. ### Шаг 4: Ожидание одобрения **ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Ждать от человека явной, утверждающей команды. ### Шаг 5: Инициация разработки - 1. Обновить `tech_spec/PROJECT_MANIFEST.xml` на основе `WorkOrder`. - 2. Создать задачу для `Code` агента (например, путем создания файла `tasks/new_task.xml`). + Создать задачу для `Code` агента (например, путем создания файла `tasks/new_task.md`). Включить в задачу обновление `tech_spec/PROJECT_MANIFEST.xml` на основе `WorkOrder`. [/MASTER_WORKFLOW] \ No newline at end of file diff --git a/agent_promts/roles/code.md b/agent_promts/roles/code.md index 233e7d9..f044b12 100644 --- a/agent_promts/roles/code.md +++ b/agent_promts/roles/code.md @@ -26,32 +26,35 @@ [RULES] - [RULE] CONSTRAINT: Весь генерируемый код ДОЛЖЕН на 100% соответствовать `semantic_enrichment_protocol.md`. - [RULE] HEURISTIC: Перед коммитом всегда запускать локальные тесты и сборку. + - [RULE] CONSTRAINT: Если `validate_semantics.py` возвращает ошибку, ИСПРАВЛЕНИЕ ЭТОЙ ОШИБКИ ЯВЛЯЕТСЯ ЗАДАЧЕЙ №1. Агент ДОЛЖЕН прочитать отчет об ошибке, сравнить его с `semantic_enrichment_protocol.md` и исправить код. НИКАКИЕ ДРУГИЕ ДЕЙСТВИЯ НЕ ДОПУСКАЮТСЯ до тех пор, пока семантическая валидация не будет пройдена успешно. [/RULES] [/GRACE_FRAMEWORK] [MASTER_WORKFLOW] - ### Шаг 1: Поиск и принятие задачи - 1. Найти следующую задачу для `agent-developer` путем поиска файла в директории `tasks/` со статусом `pending`. - 2. Прочитать файл задачи (`WorkOrder`) с помощью `read_file`. - 3. Изменить статус задачи на `in-progress` с помощью `apply_diff`. + ### Шаг 1: Поиск и Принятие Задачи + 1. Найти `WorkOrder` в `tasks/` со статусом `pending`. + 2. Прочитать `WorkOrder` и изменить его статус на `in-progress`. + 3. Создать новую ветку для разработки. - ### Шаг 2: Реализация - 1. Изучить протокол `agent_promts/protocols/semantic_enrichment_protocol.md`. - 2. Создать новую ветку для разработки, используя `execute_command` (`git branch ...`). - 3. Реализовать код согласно `WorkOrder`, используя инструменты `write_to_file`, `apply_diff`, `insert_content`. - 4. **Автоматизированная семантическая валидация:** Для КАЖДОГО созданного или измененного `.kt` файла запустить скрипт валидации: `python validate_semantics.py path/to/your/file.kt`. - 5. **Цикл исправления:** Если скрипт валидации обнаруживает ошибки, НЕОБХОДИМО войти в цикл исправления: - a. Проанализировать отчет об ошибках. - b. Внести исправления в код с помощью `apply_diff`. - c. Повторно запустить валидацию (`python validate_semantics.py ...`). - d. Повторять шаги a-c, пока скрипт не выполнится без ошибок. - 6. Запустить тесты и сборку через `execute_command` (`./gradlew build`). + ### Шаг 2: Автоматизированный Цикл Разработки и Ревью (Automated Code & Review Loop) + **Этот цикл повторяется до тех пор, пока все проверки не будут пройдены.** - ### Шаг 3: Создание Pull Request и задачи для QA - 1. Закоммитить изменения (`execute_command git commit ...`). - 2. Создать Pull Request (через `execute_command`, если есть CLI для Gitea, или отметить как шаг для человека). - 3. Создать задачу для QA (написать файл `tasks/qa_task_...xml` с помощью `write_to_file`). - 4. Обновить статус основной задачи на `pending-qa` (`apply_diff`). + 1. **Реализация Кода:** Внести изменения в кодовую базу согласно `WorkOrder`. + + 2. **Семантическая Валидация:** + a. Для каждого измененного файла запустить `python validate_semantics.py `. + b. Если есть ошибки, проанализировать отчет и немедленно исправить код. **Вернуться к шагу 1.** + + 3. **Функциональное Тестирование (Reviewer Sub-Agent):** + a. Запустить полный набор тестов (`./gradlew build`). + b. Если тесты провалились, проанализировать отчет о сбое как **структурированный фидбэк от Reviewer'а**. + c. Интерпретировать отчет и попытаться исправить код. **Вернуться к шагу 1.** + + ### Шаг 3: Завершение и Передача на QA + 1. **Все проверки пройдены.** Закоммитить финальные изменения. + 2. Создать Pull Request. + 3. Создать задачу для QA агента (например, `tasks/qa_task_...xml`). + 4. Обновить статус `WorkOrder` на `pending-qa`. [/MASTER_WORKFLOW] [SELF_REFLECTION_PROTOCOL] diff --git a/agent_promts/roles/qa.md b/agent_promts/roles/qa.md index 1975f5c..a67b601 100644 --- a/agent_promts/roles/qa.md +++ b/agent_promts/roles/qa.md @@ -39,25 +39,21 @@ [/GRACE_FRAMEWORK] [MASTER_WORKFLOW] - ### Шаг 1: Поиск задачи на тестирование - 1. Найти в директории `tasks/` файл задачи со статусом `pending-qa`. - 2. Прочитать файл задачи с помощью `read_file` чтобы получить ID `WorkOrder` и имя feature-ветки. + ### Шаг 1: Поиск и Принятие Задачи + 1. Найти `WorkOrder` в `tasks/` со статусом `pending-qa`. + 2. Прочитать `WorkOrder` и информацию о Pull Request. + 3. Изменить статус задачи на `final-review`. - ### Шаг 2: Сбор контекста и подготовка - 1. Прочитать исходный `WorkOrder` (`tasks/workorder_{id}.xml`). - 2. Переключиться на feature-ветку (`execute_command git checkout ...`). - 3. Прочитать измененные файлы. - - ### Шаг 3: Статический и динамический анализ - 1. Проверить код на соответствие `semantic_enrichment_protocol.md`. - 2. Запустить тесты и сборку (`execute_command ./gradlew build`). - - ### Шаг 4: Вынесение вердикта - **ЕСЛИ** анализ на шаге 3 успешен: - 1. Обновить статус задачи на `approved` с помощью `apply_diff`. - 2. Опционально: инициировать слияние ветки (`execute_command git merge ...`). + ### Шаг 2: Финальное Утверждение + 1. **Проверка Pull Request:** Провести высокоуровневый обзор изменений в PR. Детальная проверка кода и тесты уже выполнены `Code` агентом в рамках его автоматизированного цикла. + 2. **Основная задача QA** — подтвердить, что работа в целом соответствует бизнес-требованиям, изложенным в `WorkOrder`, и что автоматизированные проверки (`validate_semantics`, `build`) в CI/CD пайплайне успешно пройдены. - **ИНАЧЕ (если есть проблемы):** - 1. Создать детальный отчет `reports/defect_report_{id}.md` с помощью `write_to_file`, описав все найденные проблемы и шаги для их воспроизведения. - 2. Обновить статус задачи на `rejected` и добавить в нее ссылку на отчет о дефекте с помощью `apply_diff`. + ### Шаг 3: Завершение + 1. **Если все в порядке:** + a. Влить (merge) Pull Request в основную ветку. + b. Обновить статус `WorkOrder` на `completed`. + c. Удалить ветку разработки. + 2. **Если обнаружены критические проблемы:** + a. Отклонить Pull Request с четким объяснением. + b. Вернуть `WorkOrder` в статус `pending` для `Code` агента. [/MASTER_WORKFLOW] \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 587647a..08c20a0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,7 +7,6 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") id("com.google.dagger.hilt.android") id("kotlin-kapt") - // id("org.jlleitschuh.gradle.ktlint") version "12.1.1" } android { @@ -62,6 +61,16 @@ dependencies { implementation(project(":domain")) implementation(project(":feature:scan")) implementation(project(":feature:dashboard")) + implementation(project(":feature:inventorylist")) + implementation(project(":feature:itemdetails")) + implementation(project(":feature:itemedit")) + implementation(project(":feature:labeledit")) + implementation(project(":feature:labelslist")) + implementation(project(":feature:locationedit")) + implementation(project(":feature:locationslist")) + implementation(project(":feature:search")) + implementation(project(":feature:settings")) + implementation(project(":feature:setup")) // [DEPENDENCY] AndroidX implementation(Libs.coreKtx) @@ -74,11 +83,10 @@ dependencies { implementation(Libs.composeUiGraphics) implementation(Libs.composeUiToolingPreview) implementation(Libs.composeMaterial3) - implementation("androidx.compose.material:material-icons-extended-android:1.6.8") + implementation(Libs.composeMaterialIconsExtended) implementation(Libs.navigationCompose) implementation(Libs.hiltNavigationCompose) - // ktlint(project(":data:semantic-ktlint-rules")) // [DEPENDENCY] DI (Hilt) implementation(Libs.hiltAndroid) kapt(Libs.hiltCompiler) diff --git a/app/src/main/java/com/homebox/lens/MainActivity.kt b/app/src/main/java/com/homebox/lens/MainActivity.kt index 0752d4c..a2e2a5a 100644 --- a/app/src/main/java/com/homebox/lens/MainActivity.kt +++ b/app/src/main/java/com/homebox/lens/MainActivity.kt @@ -1,5 +1,4 @@ -// [PACKAGE] com.homebox.lens -// [FILE] MainActivity.kt +// [FILE] app/src/main/java/com/homebox/lens/MainActivity.kt // [SEMANTICS] ui, activity, entrypoint package com.homebox.lens @@ -14,21 +13,31 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.homebox.lens.navigation.NavGraph -import com.homebox.lens.ui.theme.HomeboxLensTheme +import com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme +import com.homebox.lens.feature.dashboard.navigation.navGraph import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber // [END_IMPORTS] // [ENTITY: Activity('MainActivity')] + /** * @summary Главная и единственная Activity в приложении. */ +// [ANCHOR:MainActivity:Class] +// [CONTRACT:MainActivity] +// [PURPOSE] Главная и единственная Activity в приложении. +// [END_CONTRACT:MainActivity] @AndroidEntryPoint class MainActivity : ComponentActivity() { - // [ENTITY: Function('onCreate')] - // [RELATION: Function('onCreate')] -> [CALLS] -> [Function('HomeboxLensTheme')] - // [RELATION: Function('onCreate')] -> [CALLS] -> [Function('NavGraph')] + // [ANCHOR:onCreate:Function] + // [CONTRACT:onCreate] + // [PURPOSE] Инициализация Activity. + // [PARAM:savedInstanceState:Bundle?] Сохраненное состояние. + // [RELATION: CALLS:HomeboxLensTheme] + // [RELATION: CALLS:NavGraph] + // [RELATION: CALLS:Timber.d] + // [END_CONTRACT:onCreate] override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.") @@ -36,35 +45,48 @@ class MainActivity : ComponentActivity() { HomeboxLensTheme { Surface( modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background + color = MaterialTheme.colorScheme.background, ) { - NavGraph() + navGraph() } } } } - // [END_ENTITY: Function('onCreate')] + // [END_ANCHOR:onCreate] } -// [END_ENTITY: Activity('MainActivity')] +// [END_ANCHOR:MainActivity] // [ENTITY: Function('Greeting')] +// [ANCHOR:greeting:Function] +// [CONTRACT:greeting] +// [PURPOSE] Отображает приветствие. +// [PARAM:name:String] Имя для приветствия. +// [PARAM:modifier:Modifier] Модификатор для элемента. +// [END_CONTRACT:greeting] @Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { +fun greeting( + name: String, + modifier: Modifier = Modifier, +) { Text( text = "Hello $name!", - modifier = modifier + modifier = modifier, ) } -// [END_ENTITY: Function('Greeting')] +// [END_ANCHOR:greeting] // [ENTITY: Function('GreetingPreview')] +// [ANCHOR:greetingPreview:Function] +// [CONTRACT:greetingPreview] +// [PURPOSE] Предварительный просмотр функции greeting. +// [END_CONTRACT:greetingPreview] @Preview(showBackground = true) @Composable -fun GreetingPreview() { +fun greetingPreview() { HomeboxLensTheme { - Greeting("Android") + greeting("Android") } } -// [END_ENTITY: Function('GreetingPreview')] - -// [END_FILE_MainActivity.kt] \ No newline at end of file +// [END_ANCHOR:greetingPreview] +// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt] +// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt] diff --git a/app/src/main/java/com/homebox/lens/MainApplication.kt b/app/src/main/java/com/homebox/lens/MainApplication.kt index bdb7afb..fa08fb5 100644 --- a/app/src/main/java/com/homebox/lens/MainApplication.kt +++ b/app/src/main/java/com/homebox/lens/MainApplication.kt @@ -10,12 +10,12 @@ import timber.log.Timber // [END_IMPORTS] // [ENTITY: Application('MainApplication')] + /** * @summary Точка входа в приложение. Инициализирует Hilt и Timber. */ @HiltAndroidApp class MainApplication : Application() { - // [ENTITY: Function('onCreate')] override fun onCreate() { super.onCreate() @@ -27,4 +27,4 @@ class MainApplication : Application() { // [END_ENTITY: Function('onCreate')] } // [END_ENTITY: Application('MainApplication')] -// [END_FILE_MainApplication.kt] \ No newline at end of file +// [END_FILE_MainApplication.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 deleted file mode 100644 index d07bf74..0000000 --- a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt +++ /dev/null @@ -1,132 +0,0 @@ -// [PACKAGE] com.homebox.lens.navigation -// [FILE] NavigationActions.kt -// [SEMANTICS] navigation, controller, actions -package com.homebox.lens.navigation - -// [IMPORTS] -import androidx.navigation.NavHostController -import timber.log.Timber -// [END_IMPORTS] - -// [ENTITY: Class('NavigationActions')] -// [RELATION: Class('NavigationActions')] -> [DEPENDS_ON] -> [Framework('NavHostController')] -/** - * @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий. - * @param navController Контроллер Jetpack Navigation. - * @invariant Все навигационные действия должны использовать предоставленный navController. - */ -class NavigationActions(val navController: NavHostController) { - - // [ENTITY: Function('navigateToDashboard')] - /** - * @summary Навигация на главный экран. - * @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов. - */ - fun navigateToDashboard() { - Timber.i("[INFO][ACTION][navigate_to_dashboard] Navigating to Dashboard.") - navController.navigate(Screen.Dashboard.route) { - popUpTo(navController.graph.startDestinationId) - launchSingleTop = true - } - } - // [END_ENTITY: Function('navigateToDashboard')] - - // [ENTITY: Function('navigateToLocations')] - fun navigateToLocations() { - Timber.i("[INFO][ACTION][navigate_to_locations] Navigating to Locations.") - navController.navigate(Screen.LocationsList.route) { - launchSingleTop = true - } - } - // [END_ENTITY: Function('navigateToLocations')] - - // [ENTITY: Function('navigateToLabels')] - fun navigateToLabels() { - Timber.i("[INFO][ACTION][navigate_to_labels] Navigating to Labels.") - navController.navigate(Screen.LabelsList.route) { - launchSingleTop = true - } - } - // [END_ENTITY: Function('navigateToLabels')] - - // [ENTITY: Function('navigateToLabelEdit')] - fun navigateToLabelEdit(labelId: String? = null) { - Timber.i("[INFO][ACTION][navigate_to_label_edit] Navigating to Label Edit with ID: %s", labelId) - navController.navigate(Screen.LabelEdit.createRoute(labelId)) - } - // [END_ENTITY: Function('navigateToLabelEdit')] - - // [ENTITY: Function('navigateToSearch')] - fun navigateToSearch() { - Timber.i("[INFO][ACTION][navigate_to_search] Navigating to Search.") - navController.navigate(Screen.Search.route) { - launchSingleTop = true - } - } - // [END_ENTITY: Function('navigateToSearch')] - - // [ENTITY: Function('navigateToScan')] - /** - * @summary Навигация на экран сканирования QR/штрих-кодов. - */ - fun navigateToScan() { - Timber.i("[INFO][ACTION][navigate_to_scan] Navigating to Scan screen.") - navController.navigate(Screen.Scan.route) { - launchSingleTop = true - } - } - // [END_ENTITY: Function('navigateToScan')] - - // [ENTITY: Function('navigateToSettings')] - /** - * @summary Навигация на экран настроек. - */ - fun navigateToSettings() { - Timber.i("[INFO][ACTION][navigate_to_settings] Navigating to Settings.") - navController.navigate(Screen.Settings.route) { - launchSingleTop = true - } - } - // [END_ENTITY: Function('navigateToSettings')] - - // [ENTITY: Function('navigateToInventoryListWithLabel')] - fun navigateToInventoryListWithLabel(labelId: String) { - Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Navigating to Inventory with label: %s", labelId) - val route = Screen.InventoryList.withFilter("label", labelId) - navController.navigate(route) - } - // [END_ENTITY: Function('navigateToInventoryListWithLabel')] - - // [ENTITY: Function('navigateToInventoryListWithLocation')] - fun navigateToInventoryListWithLocation(locationId: String) { - Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Navigating to Inventory with location: %s", locationId) - val route = Screen.InventoryList.withFilter("location", locationId) - navController.navigate(route) - } - // [END_ENTITY: Function('navigateToInventoryListWithLocation')] - - // [ENTITY: Function('navigateToCreateItem')] - fun navigateToCreateItem() { - Timber.i("[INFO][ACTION][navigate_to_create_item] Navigating to Create Item.") - navController.navigate(Screen.ItemEdit.createRoute()) - } - // [END_ENTITY: Function('navigateToCreateItem')] - - // [ENTITY: Function('navigateToLogout')] - fun navigateToLogout() { - Timber.i("[INFO][ACTION][navigate_to_logout] Navigating to Logout.") - navController.navigate(Screen.Setup.route) { - popUpTo(Screen.Dashboard.route) { inclusive = true } - } - } - // [END_ENTITY: Function('navigateToLogout')] - - // [ENTITY: Function('navigateBack')] - fun navigateBack() { - Timber.i("[INFO][ACTION][navigate_back] Navigating back.") - navController.popBackStack() - } - // [END_ENTITY: Function('navigateBack')] -} -// [END_ENTITY: Class('NavigationActions')] -// [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 deleted file mode 100644 index b2c4ed6..0000000 --- a/app/src/main/java/com/homebox/lens/navigation/Screen.kt +++ /dev/null @@ -1,131 +0,0 @@ -// [PACKAGE] com.homebox.lens.navigation -// [FILE] Screen.kt -// [SEMANTICS] navigation, routes, sealed_class -package com.homebox.lens.navigation - -// [ENTITY: SealedClass('Screen')] -/** - * @summary Запечатанный класс для определения маршрутов навигации в приложении. - * @description Обеспечивает типобезопасность при навигации. - * @param route Строковый идентификатор маршрута. - */ -sealed class Screen(val route: String) { - // [ENTITY: Object('Setup')] - data object Setup : Screen("setup_screen") - // [END_ENTITY: Object('Setup')] - - // [ENTITY: Object('Dashboard')] - data object Dashboard : Screen("dashboard_screen") - // [END_ENTITY: Object('Dashboard')] - - // [ENTITY: Object('InventoryList')] - data object InventoryList : Screen("inventory_list_screen") { - // [ENTITY: Function('withFilter')] - /** - * @summary Создает маршрут для экрана списка инвентаря с параметром фильтра. - * @param key Ключ фильтра (например, "label" или "location"). - * @param value Значение фильтра (например, ID метки или местоположения). - * @return Строку полного маршрута с query-параметром. - * @throws IllegalArgumentException если ключ или значение пустые. - */ - fun withFilter(key: String, value: String): String { - require(key.isNotBlank()) { "Filter key cannot be blank." } - require(value.isNotBlank()) { "Filter value cannot be blank." } - val constructedRoute = "inventory_list_screen?$key=$value" - check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." } - return constructedRoute - } - // [END_ENTITY: Function('withFilter')] - } - // [END_ENTITY: Object('InventoryList')] - - // [ENTITY: Object('ItemDetails')] - data object ItemDetails : Screen("item_details_screen/{itemId}") { - // [ENTITY: Function('createRoute')] - /** - * @summary Создает маршрут для экрана деталей элемента с указанным ID. - * @param itemId ID элемента для отображения. - * @return Строку полного маршрута. - * @throws IllegalArgumentException если itemId пустой. - */ - fun createRoute(itemId: String): String { - require(itemId.isNotBlank()) { "itemId не может быть пустым." } - val route = "item_details_screen/$itemId" - check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." } - return route - } - // [END_ENTITY: Function('createRoute')] - } - // [END_ENTITY: Object('ItemDetails')] - - // [ENTITY: Object('ItemEdit')] - data object ItemEdit : Screen("item_edit_screen?itemId={itemId}") { - // [ENTITY: Function('createRoute')] - /** - * @summary Создает маршрут для экрана редактирования элемента с указанным ID. - * @param itemId ID элемента для редактирования. Null, если создается новый элемент. - * @return Строку полного маршрута. - */ - fun createRoute(itemId: String? = null): String { - return itemId?.let { "item_edit_screen?itemId=$it" } ?: "item_edit_screen" - } - // [END_ENTITY: Function('createRoute')] - } - // [END_ENTITY: Object('ItemEdit')] - - // [ENTITY: Object('LabelsList')] - data object LabelsList : Screen("labels_list_screen") - // [END_ENTITY: Object('LabelsList')] - - // [ENTITY: Object('LabelEdit')] - data object LabelEdit : Screen("label_edit_screen?labelId={labelId}") { - // [ENTITY: Function('createRoute')] - /** - * @summary Создает маршрут для экрана редактирования метки с указанным ID. - * @param labelId ID метки для редактирования. Null, если создается новая метка. - * @return Строку полного маршрута. - */ - fun createRoute(labelId: String? = null): String { - return labelId?.let { "label_edit_screen?labelId=$it" } ?: "label_edit_screen" - } - // [END_ENTITY: Function('createRoute')] - } - // [END_ENTITY: Object('LabelEdit')] - - // [ENTITY: Object('LocationsList')] - data object LocationsList : Screen("locations_list_screen") - // [END_ENTITY: Object('LocationsList')] - - // [ENTITY: Object('LocationEdit')] - data object LocationEdit : Screen("location_edit_screen/{locationId}") { - // [ENTITY: Function('createRoute')] - /** - * @summary Создает маршрут для экрана редактирования местоположения с указанным ID. - * @param locationId ID местоположения для редактирования. - * @return Строку полного маршрута. - * @throws IllegalArgumentException если locationId пустой. - */ - fun createRoute(locationId: String): String { - require(locationId.isNotBlank()) { "locationId не может быть пустым." } - val route = "location_edit_screen/$locationId" - check(route.endsWith(locationId)) { "Маршрут должен заканчиваться на locationId." } - return route - } - // [END_ENTITY: Function('createRoute')] - } - // [END_ENTITY: Object('LocationEdit')] - - // [ENTITY: Object('Search')] - data object Search : Screen("search_screen") - // [END_ENTITY: Object('Search')] - - // [ENTITY: Object('Settings')] - data object Settings : Screen("settings_screen") - // [END_ENTITY: Object('Settings')] - - // [ENTITY: Object('Scan')] - data object Scan : Screen("scan_screen") - // [END_ENTITY: Object('Scan')] -} -// [END_ENTITY: SealedClass('Screen')] -// [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 deleted file mode 100644 index d40c8b1..0000000 --- a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt +++ /dev/null @@ -1,116 +0,0 @@ -// [PACKAGE] com.homebox.lens.ui.common -// [FILE] AppDrawer.kt -// [SEMANTICS] ui, common, navigation_drawer -package com.homebox.lens.ui.common - -// [IMPORTS] -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.material.icons.filled.Settings -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 -// [END_IMPORTS] - -// [ENTITY: Function('AppDrawerContent')] -// [RELATION: Function('AppDrawerContent')] -> [DEPENDS_ON] -> [Class('NavigationActions')] -/** - * @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.nav_labels)) }, - selected = currentRoute == Screen.LabelsList.route, - onClick = { - navigationActions.navigateToLabels() - onCloseDrawer() - } - ) - NavigationDrawerItem( - label = { Text(stringResource(id = R.string.search)) }, - selected = currentRoute == Screen.Search.route, - onClick = { - navigationActions.navigateToSearch() - onCloseDrawer() - } - ) - NavigationDrawerItem( - icon = { Icon(Icons.Default.Settings, contentDescription = null) }, - label = { Text("Настройки") }, - selected = false, - onClick = { - navigationActions.navigateToSettings() - onCloseDrawer() - } - ) - // [AI_NOTE]: Add Profile and Tools items - Divider() - NavigationDrawerItem( - label = { Text(stringResource(id = R.string.logout)) }, - selected = false, - onClick = { - navigationActions.navigateToLogout() - onCloseDrawer() - } - ) - } -} -// [END_ENTITY: Function('AppDrawerContent')] -// [END_FILE_AppDrawer.kt] 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 deleted file mode 100644 index b9cec59..0000000 --- a/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt +++ /dev/null @@ -1,91 +0,0 @@ -// [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.material.icons.automirrored.filled.ArrowBack -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 -// [END_IMPORTS] - -// [ENTITY: Function('MainScaffold')] -// [RELATION: Function('MainScaffold')] -> [DEPENDS_ON] -> [Class('NavigationActions')] -// [RELATION: Function('MainScaffold')] -> [CALLS] -> [Function('AppDrawerContent')] -/** - * @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, - onNavigateUp: (() -> Unit)? = null, - topBarActions: @Composable () -> Unit = {}, - snackbarHost: @Composable () -> Unit = {}, - floatingActionButton: @Composable () -> Unit = {}, - content: @Composable (PaddingValues) -> Unit -) { - val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - val scope = rememberCoroutineScope() - - ModalNavigationDrawer( - drawerState = drawerState, - drawerContent = { - AppDrawerContent( - currentRoute = currentRoute, - navigationActions = navigationActions, - onCloseDrawer = { scope.launch { drawerState.close() } } - ) - } - ) { - Scaffold( - topBar = { - TopAppBar( - title = { Text(topBarTitle) }, - navigationIcon = { - if (onNavigateUp != null) { - IconButton(onClick = onNavigateUp) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(id = R.string.cd_navigate_up) - ) - } - } else { - IconButton(onClick = { scope.launch { drawerState.open() } }) { - Icon( - Icons.Default.Menu, - contentDescription = stringResource(id = R.string.cd_open_navigation_drawer) - ) - } - } - }, - actions = { topBarActions() } - ) - }, - snackbarHost = snackbarHost, - floatingActionButton = floatingActionButton - ) { paddingValues -> - content(paddingValues) - } - } -} -// [END_ENTITY: Function('MainScaffold')] -// [END_FILE_MainScaffold.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/components/ColorPicker.kt b/app/src/main/java/com/homebox/lens/ui/components/ColorPicker.kt deleted file mode 100644 index 3151ad5..0000000 --- a/app/src/main/java/com/homebox/lens/ui/components/ColorPicker.kt +++ /dev/null @@ -1,76 +0,0 @@ -// [PACKAGE] com.homebox.lens.ui.components -// [FILE] ColorPicker.kt -// [SEMANTICS] ui, component, color_selection - -package com.homebox.lens.ui.components - -// [IMPORTS] -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.homebox.lens.R -// [END_IMPORTS] - -// [ENTITY: Function('ColorPicker')] -/** - * @summary Компонент для выбора цвета. - * @param selectedColor Текущий выбранный цвет в формате HEX строки (например, "#FFFFFF"). - * @param onColorSelected Лямбда-функция, вызываемая при выборе нового цвета. - * @param modifier Модификатор для настройки внешнего вида. - */ -@Composable -fun ColorPicker( - selectedColor: String, - onColorSelected: (String) -> Unit, - modifier: Modifier = Modifier -) { - Column(modifier = modifier) { - Text(text = stringResource(R.string.label_color), style = MaterialTheme.typography.bodyLarge) - Spacer(modifier = Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Box( - modifier = Modifier - .size(48.dp) - .background( - if (selectedColor.isEmpty()) Color.Transparent else Color(android.graphics.Color.parseColor(selectedColor)), - CircleShape - ) - .border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape) - .clickable { /* TODO: Implement a more advanced color selection dialog */ } - ) - Spacer(modifier = Modifier.width(16.dp)) - OutlinedTextField( - value = selectedColor, - onValueChange = { newValue -> - // Basic validation for hex color - if (newValue.matches(Regex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"))) { - onColorSelected(newValue) - } else if (newValue.isEmpty() || newValue == "#") { - onColorSelected("#FFFFFF") // Default to white if input is cleared - } - }, - label = { Text(stringResource(R.string.label_hex_color)) }, - singleLine = true, - modifier = Modifier.weight(1f) - ) - } - } -} -// [END_ENTITY: Function('ColorPicker')] -// [END_FILE_ColorPicker.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/components/LoadingOverlay.kt b/app/src/main/java/com/homebox/lens/ui/components/LoadingOverlay.kt deleted file mode 100644 index 6c6ff3b..0000000 --- a/app/src/main/java/com/homebox/lens/ui/components/LoadingOverlay.kt +++ /dev/null @@ -1,35 +0,0 @@ -// [PACKAGE] com.homebox.lens.ui.components -// [FILE] LoadingOverlay.kt -// [SEMANTICS] ui, component, loading - -package com.homebox.lens.ui.components - -// [IMPORTS] -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -// [END_IMPORTS] - -// [ENTITY: Function('LoadingOverlay')] -/** - * @summary Полноэкранный оверлей с индикатором загрузки. - */ -@Composable -fun LoadingOverlay() { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } -} -// [END_ENTITY: Function('LoadingOverlay')] -// [END_FILE_LoadingOverlay.kt] \ No newline at end of file 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 deleted file mode 100644 index 3becc28..0000000 --- a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt +++ /dev/null @@ -1,39 +0,0 @@ -// [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.res.stringResource -import com.homebox.lens.R -import com.homebox.lens.navigation.NavigationActions -import com.homebox.lens.ui.common.MainScaffold -// [END_IMPORTS] - -// [ENTITY: Function('InventoryListScreen')] -// [RELATION: Function('InventoryListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] -// [RELATION: Function('InventoryListScreen')] -> [CALLS] -> [Function('MainScaffold')] -/** - * @summary Composable-функция для экрана "Список инвентаря". - * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. - * @param navigationActions Объект с навигационными действиями. - */ -@Composable -fun InventoryListScreen( - currentRoute: String?, - navigationActions: NavigationActions -) { - MainScaffold( - topBarTitle = stringResource(id = R.string.inventory_list_title), - currentRoute = currentRoute, - navigationActions = navigationActions - ) { - // [AI_NOTE]: Implement Inventory List Screen UI - Text(text = "Inventory List Screen") - } -} -// [END_ENTITY: Function('InventoryListScreen')] -// [END_FILE_InventoryListScreen.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt deleted file mode 100644 index 6ddcc31..0000000 --- a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt +++ /dev/null @@ -1,21 +0,0 @@ -// [PACKAGE] com.homebox.lens.ui.screen.inventorylist -// [FILE] InventoryListViewModel.kt -// [SEMANTICS] ui, viewmodel, inventory_list -package com.homebox.lens.ui.screen.inventorylist - -// [IMPORTS] -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -// [END_IMPORTS] - -// [ENTITY: ViewModel('InventoryListViewModel')] -/** - * @summary ViewModel for the inventory list screen. - */ -@HiltViewModel -class InventoryListViewModel @Inject constructor() : ViewModel() { - // [AI_NOTE]: Implement UI state -} -// [END_ENTITY: ViewModel('InventoryListViewModel')] -// [END_FILE_InventoryListViewModel.kt] \ 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 deleted file mode 100644 index 1feb48a..0000000 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt +++ /dev/null @@ -1,39 +0,0 @@ -// [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.res.stringResource -import com.homebox.lens.R -import com.homebox.lens.navigation.NavigationActions -import com.homebox.lens.ui.common.MainScaffold -// [END_IMPORTS] - -// [ENTITY: Function('ItemDetailsScreen')] -// [RELATION: Function('ItemDetailsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] -// [RELATION: Function('ItemDetailsScreen')] -> [CALLS] -> [Function('MainScaffold')] -/** - * @summary Composable-функция для экрана "Детали элемента". - * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. - * @param navigationActions Объект с навигационными действиями. - */ -@Composable -fun ItemDetailsScreen( - currentRoute: String?, - navigationActions: NavigationActions -) { - MainScaffold( - topBarTitle = stringResource(id = R.string.item_details_title), - currentRoute = currentRoute, - navigationActions = navigationActions - ) { - // [AI_NOTE]: Implement Item Details Screen UI - Text(text = "Item Details Screen") - } -} -// [END_ENTITY: Function('ItemDetailsScreen')] -// [END_FILE_ItemDetailsScreen.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt deleted file mode 100644 index 104c5c3..0000000 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt +++ /dev/null @@ -1,21 +0,0 @@ -// [PACKAGE] com.homebox.lens.ui.screen.itemdetails -// [FILE] ItemDetailsViewModel.kt -// [SEMANTICS] ui, viewmodel, item_details -package com.homebox.lens.ui.screen.itemdetails - -// [IMPORTS] -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -// [END_IMPORTS] - -// [ENTITY: ViewModel('ItemDetailsViewModel')] -/** - * @summary ViewModel for the item details screen. - */ -@HiltViewModel -class ItemDetailsViewModel @Inject constructor() : ViewModel() { - // [AI_NOTE]: Implement UI state -} -// [END_ENTITY: ViewModel('ItemDetailsViewModel')] -// [END_FILE_ItemDetailsViewModel.kt] 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 deleted file mode 100644 index 35a4e14..0000000 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt +++ /dev/null @@ -1,436 +0,0 @@ -// [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.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.QrCodeScanner -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DatePicker -import androidx.compose.material3.DatePickerDialog -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.rememberDatePickerState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.homebox.lens.R -import com.homebox.lens.navigation.NavigationActions -import com.homebox.lens.ui.common.MainScaffold -import kotlinx.coroutines.launch -import timber.log.Timber -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.util.Locale -// [END_IMPORTS] - -// [ENTITY: Function('ItemEditScreen')] -// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] -// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')] -// [RELATION: Function('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')] -// [RELATION: Function('ItemEditScreen')] -> [CALLS] -> [Function('MainScaffold')] -/** - * @summary Composable-функция для экрана "Редактирование элемента". - * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. - * @param navigationActions Объект с навигационными действиями. - * @param itemId ID элемента для редактирования. Null, если создается новый элемент. - * @param viewModel ViewModel для управления состоянием экрана. - * @param onSaveSuccess Callback, вызываемый после успешного сохранения товара. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ItemEditScreen( -currentRoute: String?, -navigationActions: NavigationActions, -itemId: String?, -viewModel: ItemEditViewModel = hiltViewModel(), -onSaveSuccess: () -> Unit -) { -val uiState by viewModel.uiState.collectAsState() -val snackbarHostState = remember { SnackbarHostState() } - - val navBackStackEntry = navigationActions.navController.currentBackStackEntry - - LaunchedEffect(itemId) { - Timber.i("[INFO][ENTRYPOINT][item_edit_screen_init] Initializing ItemEditScreen for item ID: %s", itemId) - viewModel.loadItem(itemId) - } - - LaunchedEffect(navBackStackEntry) { - navBackStackEntry?.savedStateHandle?.get("barcodeResult")?.let { barcode -> - viewModel.updateAssetId(barcode) - navBackStackEntry.savedStateHandle?.remove("barcodeResult") - Timber.i("[INFO][ACTION][barcode_received] Received barcode: %s", barcode) - } - } - - LaunchedEffect(uiState.error) { - uiState.error?.let { - snackbarHostState.showSnackbar(it) - Timber.e("[ERROR][UI_ERROR][item_edit_error] Displaying error: %s", it) - } - } - - LaunchedEffect(Unit) { - viewModel.saveCompleted.collect { - Timber.i("[INFO][ACTION][save_completed_callback] Item save completed. Triggering onSaveSuccess.") - onSaveSuccess() - } - } - - MainScaffold( - topBarTitle = stringResource(id = R.string.item_edit_title), - currentRoute = currentRoute, - navigationActions = navigationActions, - snackbarHost = { SnackbarHost(snackbarHostState) }, - floatingActionButton = { - FloatingActionButton(onClick = { - Timber.i("[INFO][ACTION][save_button_click] Save button clicked.") - viewModel.saveItem() - }) { - Icon(Icons.Filled.Save, contentDescription = stringResource(R.string.save_item)) - } - } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp) - ) { - if (uiState.isLoading) { - CircularProgressIndicator(modifier = Modifier.fillMaxWidth()) - } else { - uiState.item?.let { item -> - OutlinedTextField( - value = item.name, - onValueChange = { viewModel.updateName(it) }, - label = { Text(stringResource(R.string.item_name)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = item.description ?: "", - onValueChange = { viewModel.updateDescription(it) }, - label = { Text(stringResource(R.string.item_description)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = item.quantity.toString(), - onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) }, - label = { Text(stringResource(R.string.item_quantity)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() - ) - // Asset ID - OutlinedTextField( - value = item.assetId ?: "", - onValueChange = { viewModel.updateAssetId(it) }, - label = { Text(stringResource(R.string.item_asset_id)) }, - modifier = Modifier.fillMaxWidth(), - trailingIcon = { - IconButton(onClick = { - Timber.d("[DEBUG][ACTION][scan_qr_code_click] Scan QR code button clicked.") - navigationActions.navigateToScan() - }) { - Icon(Icons.Filled.QrCodeScanner, contentDescription = stringResource(R.string.scan_qr_code)) - } - } - ) - Spacer(modifier = Modifier.height(8.dp)) - - // Notes - OutlinedTextField( - value = item.notes ?: "", - onValueChange = { viewModel.updateNotes(it) }, - label = { Text(stringResource(R.string.item_notes)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - - // Serial Number - OutlinedTextField( - value = item.serialNumber ?: "", - onValueChange = { viewModel.updateSerialNumber(it) }, - label = { Text(stringResource(R.string.item_serial_number)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - - // Purchase Price - OutlinedTextField( - value = item.purchasePrice?.toString() ?: "", - onValueChange = { viewModel.updatePurchasePrice(it.toDoubleOrNull()) }, - label = { Text(stringResource(R.string.item_purchase_price)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - - // Purchase Date - var showPurchaseDatePicker by remember { mutableStateOf(false) } - val purchaseDatePickerState = rememberDatePickerState() - val coroutineScope = rememberCoroutineScope() - OutlinedTextField( - value = item.purchaseDate ?: "", - onValueChange = { }, // Read-only - label = { Text(stringResource(R.string.item_purchase_date)) }, - modifier = Modifier.fillMaxWidth(), - readOnly = true, - interactionSource = remember { MutableInteractionSource() } - .also { interactionSource -> - LaunchedEffect(interactionSource) { - interactionSource.interactions.collect { - coroutineScope.launch { - showPurchaseDatePicker = true - } - } - } - } - ) - if (showPurchaseDatePicker) { - DatePickerDialog( - onDismissRequest = { showPurchaseDatePicker = false }, - confirmButton = { - Button(onClick = { - purchaseDatePickerState.selectedDateMillis?.let { millis -> - val selectedDate = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate() - viewModel.updatePurchaseDate(selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE)) - } - showPurchaseDatePicker = false - }) { - Text(stringResource(R.string.ok)) - } - }, - dismissButton = { - Button(onClick = { showPurchaseDatePicker = false }) { - Text(stringResource(R.string.cancel)) - } - } - ) { - DatePicker(state = purchaseDatePickerState) - } - } - Spacer(modifier = Modifier.height(8.dp)) - - // Warranty Until - var showWarrantyDatePicker by remember { mutableStateOf(false) } - val warrantyDatePickerState = rememberDatePickerState() - OutlinedTextField( - value = item.warrantyUntil ?: "", - onValueChange = { }, // Read-only - label = { Text(stringResource(R.string.item_warranty_until)) }, - modifier = Modifier.fillMaxWidth(), - readOnly = true, - interactionSource = remember { MutableInteractionSource() } - .also { interactionSource -> - LaunchedEffect(interactionSource) { - interactionSource.interactions.collect { - coroutineScope.launch { - showWarrantyDatePicker = true - } - } - } - } - ) - if (showWarrantyDatePicker) { - DatePickerDialog( - onDismissRequest = { showWarrantyDatePicker = false }, - confirmButton = { - Button(onClick = { - warrantyDatePickerState.selectedDateMillis?.let { millis -> - val selectedDate = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate() - viewModel.updateWarrantyUntil(selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE)) - } - showWarrantyDatePicker = false - }) { - Text(stringResource(R.string.ok)) - } - }, - dismissButton = { - Button(onClick = { showWarrantyDatePicker = false }) { - Text(stringResource(R.string.cancel)) - } - } - ) { - DatePicker(state = warrantyDatePickerState) - } - } - Spacer(modifier = Modifier.height(8.dp)) - - // Parent ID (simplified for now, ideally a picker) - OutlinedTextField( - value = item.parentId ?: "", - onValueChange = { viewModel.updateParentId(it) }, - label = { Text(stringResource(R.string.item_parent_id)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - - // Checkboxes - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(R.string.item_is_archived)) - Checkbox( - checked = item.isArchived ?: false, - onCheckedChange = { viewModel.updateIsArchived(it) } - ) - } - HorizontalDivider() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(R.string.item_insured)) - Checkbox( - checked = item.insured ?: false, - onCheckedChange = { viewModel.updateInsured(it) } - ) - } - HorizontalDivider() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(R.string.item_lifetime_warranty)) - Checkbox( - checked = item.lifetimeWarranty ?: false, - onCheckedChange = { viewModel.updateLifetimeWarranty(it) } - ) - } - HorizontalDivider() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(R.string.item_sync_child_items_locations)) - Checkbox( - checked = item.syncChildItemsLocations ?: false, - onCheckedChange = { viewModel.updateSyncChildItemsLocations(it) } - ) - } - HorizontalDivider() - Spacer(modifier = Modifier.height(8.dp)) - - // Manufacturer - OutlinedTextField( - value = item.manufacturer ?: "", - onValueChange = { viewModel.updateManufacturer(it) }, - label = { Text(stringResource(R.string.item_manufacturer)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - - // Model Number - OutlinedTextField( - value = item.modelNumber ?: "", - onValueChange = { viewModel.updateModelNumber(it) }, - label = { Text(stringResource(R.string.item_model_number)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - - // Purchase From - OutlinedTextField( - value = item.purchaseFrom ?: "", - onValueChange = { viewModel.updatePurchaseFrom(it) }, - label = { Text(stringResource(R.string.item_purchase_from)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - - // Warranty Details - OutlinedTextField( - value = item.warrantyDetails ?: "", - onValueChange = { viewModel.updateWarrantyDetails(it) }, - label = { Text(stringResource(R.string.item_warranty_details)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - - // Sold Details (simplified for now) - OutlinedTextField( - value = item.soldNotes ?: "", - onValueChange = { viewModel.updateSoldNotes(it) }, - label = { Text(stringResource(R.string.item_sold_notes)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = item.soldPrice?.toString() ?: "", - onValueChange = { viewModel.updateSoldPrice(it.toDoubleOrNull()) }, - label = { Text(stringResource(R.string.item_sold_price)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = item.soldTime ?: "", - onValueChange = { viewModel.updateSoldTime(it) }, - label = { Text(stringResource(R.string.item_sold_time)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = item.soldTo ?: "", - onValueChange = { viewModel.updateSoldTo(it) }, - label = { Text(stringResource(R.string.item_sold_to)) }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - } -} -// [END_ENTITY: Function('ItemEditScreen')] -// [END_FILE_ItemEditScreen.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt deleted file mode 100644 index d9f63bb..0000000 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt +++ /dev/null @@ -1,583 +0,0 @@ -// [PACKAGE] com.homebox.lens.ui.screen.itemedit -// [FILE] ItemEditViewModel.kt -// [SEMANTICS] ui, viewmodel, item_edit - -package com.homebox.lens.ui.screen.itemedit - -// [IMPORTS] -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.homebox.lens.domain.model.Item -import com.homebox.lens.domain.model.ItemCreate -import com.homebox.lens.domain.model.ItemOut -import com.homebox.lens.domain.model.ItemSummary -import com.homebox.lens.domain.model.ItemUpdate -import com.homebox.lens.domain.model.Label -import com.homebox.lens.domain.model.Location -import com.homebox.lens.domain.model.LocationOut -import com.homebox.lens.domain.usecase.CreateItemUseCase -import com.homebox.lens.domain.usecase.GetAllLocationsUseCase -import com.homebox.lens.domain.usecase.GetItemDetailsUseCase -import com.homebox.lens.domain.usecase.UpdateItemUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import timber.log.Timber -import java.math.BigDecimal -import javax.inject.Inject -// [END_IMPORTS] - -// [ENTITY: DataClass('ItemEditUiState')] -/** - * @summary UI state for the item edit screen. - * @param item The item being edited, or null if creating a new item. - * @param isLoading Whether data is currently being loaded or saved. - * @param error An error message if an operation failed. - */ -data class ItemEditUiState( - val item: Item? = null, - val locations: List = emptyList(), - val selectedLocationId: String? = null, - val isLoading: Boolean = false, - val error: String? = null -) -// [END_ENTITY: DataClass('ItemEditUiState')] - -// [ENTITY: ViewModel('ItemEditViewModel')] -// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')] -// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')] -// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')] -// [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')] -/** - * @summary ViewModel for the item edit screen. - */ -@HiltViewModel -class ItemEditViewModel @Inject constructor( - private val createItemUseCase: CreateItemUseCase, - private val updateItemUseCase: UpdateItemUseCase, - private val getItemDetailsUseCase: GetItemDetailsUseCase, - private val getAllLocationsUseCase: GetAllLocationsUseCase -) : ViewModel() { - - private val _uiState = MutableStateFlow(ItemEditUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - private val _saveCompleted = MutableSharedFlow() - val saveCompleted: SharedFlow = _saveCompleted.asSharedFlow() - - // [ENTITY: Function('loadItem')] - /** - * @summary Loads item details for editing or prepares for new item creation. - * @param itemId The ID of the item to load. If null, a new item is being created. - * @sideeffect Updates `_uiState` with loading, success, or error states. - */ - fun loadItem(itemId: String?) { - Timber.i("[INFO][ENTRYPOINT][loading_item] Attempting to load item with ID: %s", itemId) - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoading = true, error = null) - loadLocations() - if (itemId == null) { - Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.") - _uiState.value = _uiState.value.copy( - isLoading = false, item = Item( - id = "", - name = "", - description = null, - quantity = 0, - image = null, - location = null, - labels = emptyList(), - value = null, - createdAt = null, - assetId = null, - notes = null, - serialNumber = null, - purchasePrice = null, - purchaseDate = null, - warrantyUntil = null, - parentId = null, - isArchived = null, - insured = null, - lifetimeWarranty = null, - manufacturer = null, - modelNumber = null, - purchaseFrom = null, - soldNotes = null, - soldPrice = null, - soldTime = null, - soldTo = null, - syncChildItemsLocations = null, - warrantyDetails = null - ) - ) - } else { - try { - Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId) - val itemOut = getItemDetailsUseCase(itemId) - Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.") - val item = Item( - id = itemOut.id, - name = itemOut.name, - description = itemOut.description, - quantity = itemOut.quantity, - image = itemOut.images.firstOrNull()?.path, - location = itemOut.location?.let { Location(it.id, it.name) }, - labels = itemOut.labels.map { Label(it.id, it.name) }, - value = itemOut.value, - createdAt = itemOut.createdAt, - assetId = itemOut.assetId, - notes = itemOut.notes, - serialNumber = itemOut.serialNumber, - purchasePrice = itemOut.purchasePrice, - purchaseDate = itemOut.purchaseDate, - warrantyUntil = itemOut.warrantyUntil, - parentId = itemOut.parent?.id, - isArchived = itemOut.isArchived, - insured = itemOut.insured, - lifetimeWarranty = itemOut.lifetimeWarranty, - manufacturer = itemOut.manufacturer, - modelNumber = itemOut.modelNumber, - purchaseFrom = itemOut.purchaseFrom, - soldNotes = itemOut.soldNotes, - soldPrice = itemOut.soldPrice, - soldTime = itemOut.soldTime, - soldTo = itemOut.soldTo, - syncChildItemsLocations = itemOut.syncChildItemsLocations, - warrantyDetails = itemOut.warrantyDetails - ) - _uiState.value = _uiState.value.copy( - isLoading = false, - item = item, - selectedLocationId = item.location?.id - ) - Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched item details for ID: %s", itemId) - } catch (e: Exception) { - Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId) - _uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage) - } - } - } - } - // [END_ENTITY: Function('loadItem')] - - // [ENTITY: Function('saveItem')] - /** - * @summary Saves the current item, either creating a new one or updating an existing one. - * @sideeffect Updates `_uiState` with loading, success, or error states. Calls `createItemUseCase` or `updateItemUseCase`. - * @throws IllegalStateException if `uiState.value.item` is null when attempting to save. - */ - private fun loadLocations() { - viewModelScope.launch { - try { - val locations = getAllLocationsUseCase() - _uiState.value = _uiState.value.copy(locations = locations.map { LocationOut(it.id, it.name, it.color, it.isArchived, it.createdAt, it.updatedAt) }) - Timber.i("[INFO][ACTION][locations_loaded] Loaded %d locations", locations.size) - } catch (e: Exception) { - Timber.e(e, "[ERROR][FALLBACK][locations_load_failed] Failed to load locations") - _uiState.value = _uiState.value.copy(error = e.localizedMessage) - } - } - } - - fun updateSelectedLocationId(locationId: String?) { - Timber.d("[DEBUG][ACTION][updating_selected_location] Selected location ID: %s", locationId) - val location = _uiState.value.locations.find { it.id == locationId } - _uiState.value = _uiState.value.copy( - selectedLocationId = locationId, - item = _uiState.value.item?.copy(location = location?.let { Location(it.id, it.name) }) - ) - } - - fun saveItem() { - Timber.i("[INFO][ENTRYPOINT][saving_item] Attempting to save item.") - viewModelScope.launch { - val currentItem = _uiState.value.item - val selectedLocationId = _uiState.value.selectedLocationId - require(currentItem != null) { "[CONTRACT_VIOLATION][PRECONDITION][item_not_present] Cannot save a null item." } - if (currentItem.id.isBlank() && selectedLocationId == null) { - throw IllegalStateException("Location is required for creating a new item.") - } - - _uiState.value = _uiState.value.copy(isLoading = true, error = null) - try { - if (currentItem.id.isBlank()) { - Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name) - val createdItemSummary = createItemUseCase( - ItemCreate( - name = currentItem.name, - description = currentItem.description, - quantity = currentItem.quantity, - assetId = currentItem.assetId, - notes = currentItem.notes, - serialNumber = currentItem.serialNumber, - value = currentItem.value, - purchasePrice = currentItem.purchasePrice, - purchaseDate = currentItem.purchaseDate, - warrantyUntil = currentItem.warrantyUntil, - locationId = selectedLocationId, - parentId = currentItem.parentId, - labelIds = currentItem.labels.map { it.id } - ) - ) - Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.") - val createdItem = currentItem.copy(id = createdItemSummary.id, name = createdItemSummary.name) - _uiState.value = _uiState.value.copy(isLoading = false, item = createdItem) - Timber.i("[INFO][ACTION][new_item_created] Successfully created new item with ID: %s", createdItem.id) - _saveCompleted.emit(Unit) - } else { - Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id) - val updatedItemOut = updateItemUseCase(currentItem) - Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.") - val updatedItem = currentItem.copy( - id = updatedItemOut.id, - name = updatedItemOut.name, - description = updatedItemOut.description, - quantity = updatedItemOut.quantity, - image = updatedItemOut.images.firstOrNull()?.path, - location = updatedItemOut.location?.let { Location(it.id, it.name) }, - labels = updatedItemOut.labels.map { Label(it.id, it.name) }, - value = updatedItemOut.value, - createdAt = updatedItemOut.createdAt, - assetId = updatedItemOut.assetId, - notes = updatedItemOut.notes, - serialNumber = updatedItemOut.serialNumber, - purchasePrice = updatedItemOut.purchasePrice, - purchaseDate = updatedItemOut.purchaseDate, - warrantyUntil = updatedItemOut.warrantyUntil, - parentId = updatedItemOut.parent?.id, - isArchived = updatedItemOut.isArchived, - insured = updatedItemOut.insured, - lifetimeWarranty = updatedItemOut.lifetimeWarranty, - manufacturer = updatedItemOut.manufacturer, - modelNumber = updatedItemOut.modelNumber, - purchaseFrom = updatedItemOut.purchaseFrom, - soldNotes = updatedItemOut.soldNotes, - soldPrice = updatedItemOut.soldPrice, - soldTime = updatedItemOut.soldTime, - soldTo = updatedItemOut.soldTo, - syncChildItemsLocations = updatedItemOut.syncChildItemsLocations, - warrantyDetails = updatedItemOut.warrantyDetails - ) - _uiState.value = _uiState.value.copy( - isLoading = false, - item = updatedItem, - selectedLocationId = updatedItem.location?.id - ) - Timber.i("[INFO][ACTION][item_updated] Successfully updated item with ID: %s", updatedItem.id) - _saveCompleted.emit(Unit) - } - } catch (e: Exception) { - Timber.e(e, "[ERROR][FALLBACK][item_save_failed] Failed to save item.") - _uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage) - } - } - } - // [END_ENTITY: Function('saveItem')] - - // [ENTITY: Function('updateName')] - /** - * @summary Updates the name of the item in the UI state. - * @param newName The new name for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateName(newName: String) { - Timber.d("[DEBUG][ACTION][updating_item_name] Updating item name to: %s", newName) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(name = newName)) - } - // [END_ENTITY: Function('updateName')] - - // [ENTITY: Function('updateDescription')] - /** - * @summary Updates the description of the item in the UI state. - * @param newDescription The new description for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateDescription(newDescription: String) { - Timber.d("[DEBUG][ACTION][updating_item_description] Updating item description to: %s", newDescription) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(description = newDescription)) - } - // [END_ENTITY: Function('updateDescription')] - - // [ENTITY: Function('updateQuantity')] - /** - * @summary Updates the quantity of the item in the UI state. - * @param newQuantity The new quantity for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateQuantity(newQuantity: Int) { - Timber.d("[DEBUG][ACTION][updating_item_quantity] Updating item quantity to: %d", newQuantity) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity)) - } - // [END_ENTITY: Function('updateQuantity')] - - // [ENTITY: Function('updateAssetId')] - /** - * @summary Updates the asset ID of the item in the UI state. - * @param newAssetId The new asset ID for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateAssetId(newAssetId: String?) { - Timber.d("[DEBUG][ACTION][updating_item_assetId] Updating item assetId to: %s", newAssetId) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(assetId = newAssetId)) - } - // [END_ENTITY: Function('updateAssetId')] - - // [ENTITY: Function('updateNotes')] - /** - * @summary Updates the notes of the item in the UI state. - * @param newNotes The new notes for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateNotes(newNotes: String?) { - Timber.d("[DEBUG][ACTION][updating_item_notes] Updating item notes to: %s", newNotes) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(notes = newNotes)) - } - // [END_ENTITY: Function('updateNotes')] - - // [ENTITY: Function('updateSerialNumber')] - /** - * @summary Updates the serial number of the item in the UI state. - * @param newSerialNumber The new serial number for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateSerialNumber(newSerialNumber: String?) { - Timber.d("[DEBUG][ACTION][updating_item_serialNumber] Updating item serialNumber to: %s", newSerialNumber) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(serialNumber = newSerialNumber)) - } - // [END_ENTITY: Function('updateSerialNumber')] - - // [ENTITY: Function('updatePurchasePrice')] - /** - * @summary Updates the purchase price of the item in the UI state. - * @param newPurchasePrice The new purchase price for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updatePurchasePrice(newPurchasePrice: Double?) { - Timber.d("[DEBUG][ACTION][updating_item_purchasePrice] Updating item purchasePrice to: %f", newPurchasePrice) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchasePrice = newPurchasePrice)) - } - // [END_ENTITY: Function('updatePurchasePrice')] - - // [ENTITY: Function('updatePurchaseDate')] - /** - * @summary Updates the purchase date of the item in the UI state. - * @param newPurchaseDate The new purchase date for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updatePurchaseDate(newPurchaseDate: String?) { - Timber.d("[DEBUG][ACTION][updating_item_purchaseDate] Updating item purchaseDate to: %s", newPurchaseDate) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseDate = newPurchaseDate)) - } - // [END_ENTITY: Function('updatePurchaseDate')] - - // [ENTITY: Function('updateWarrantyUntil')] - /** - * @summary Updates the warranty until date of the item in the UI state. - * @param newWarrantyUntil The new warranty until date for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateWarrantyUntil(newWarrantyUntil: String?) { - Timber.d("[DEBUG][ACTION][updating_item_warrantyUntil] Updating item warrantyUntil to: %s", newWarrantyUntil) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyUntil = newWarrantyUntil)) - } - // [END_ENTITY: Function('updateWarrantyUntil')] - - // [ENTITY: Function('updateParentId')] - /** - * @summary Updates the parent ID of the item in the UI state. - * @param newParentId The new parent ID for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateParentId(newParentId: String?) { - Timber.d("[DEBUG][ACTION][updating_item_parentId] Updating item parentId to: %s", newParentId) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(parentId = newParentId)) - } - // [END_ENTITY: Function('updateParentId')] - - // [ENTITY: Function('updateIsArchived')] - /** - * @summary Updates the archived status of the item in the UI state. - * @param newIsArchived The new archived status for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateIsArchived(newIsArchived: Boolean?) { - Timber.d("[DEBUG][ACTION][updating_item_isArchived] Updating item isArchived to: %b", newIsArchived) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(isArchived = newIsArchived)) - } - // [END_ENTITY: Function('updateIsArchived')] - - // [ENTITY: Function('updateInsured')] - /** - * @summary Updates the insured status of the item in the UI state. - * @param newInsured The new insured status for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateInsured(newInsured: Boolean?) { - Timber.d("[DEBUG][ACTION][updating_item_insured] Updating item insured to: %b", newInsured) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(insured = newInsured)) - } - // [END_ENTITY: Function('updateInsured')] - - // [ENTITY: Function('updateLifetimeWarranty')] - /** - * @summary Updates the lifetime warranty status of the item in the UI state. - * @param newLifetimeWarranty The new lifetime warranty status for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateLifetimeWarranty(newLifetimeWarranty: Boolean?) { - Timber.d("[DEBUG][ACTION][updating_item_lifetimeWarranty] Updating item lifetimeWarranty to: %b", newLifetimeWarranty) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(lifetimeWarranty = newLifetimeWarranty)) - } - // [END_ENTITY: Function('updateLifetimeWarranty')] - - // [ENTITY: Function('updateManufacturer')] - /** - * @summary Updates the manufacturer of the item in the UI state. - * @param newManufacturer The new manufacturer for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateManufacturer(newManufacturer: String?) { - Timber.d("[DEBUG][ACTION][updating_item_manufacturer] Updating item manufacturer to: %s", newManufacturer) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(manufacturer = newManufacturer)) - } - // [END_ENTITY: Function('updateManufacturer')] - - // [ENTITY: Function('updateModelNumber')] - /** - * @summary Updates the model number of the item in the UI state. - * @param newModelNumber The new model number for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateModelNumber(newModelNumber: String?) { - Timber.d("[DEBUG][ACTION][updating_item_modelNumber] Updating item modelNumber to: %s", newModelNumber) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(modelNumber = newModelNumber)) - } - // [END_ENTITY: Function('updateModelNumber')] - - // [ENTITY: Function('updatePurchaseFrom')] - /** - * @summary Updates the purchase source of the item in the UI state. - * @param newPurchaseFrom The new purchase source for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updatePurchaseFrom(newPurchaseFrom: String?) { - Timber.d("[DEBUG][ACTION][updating_item_purchaseFrom] Updating item purchaseFrom to: %s", newPurchaseFrom) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseFrom = newPurchaseFrom)) - } - // [END_ENTITY: Function('updatePurchaseFrom')] - - // [ENTITY: Function('updateSoldNotes')] - /** - * @summary Updates the sold notes of the item in the UI state. - * @param newSoldNotes The new sold notes for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateSoldNotes(newSoldNotes: String?) { - Timber.d("[DEBUG][ACTION][updating_item_soldNotes] Updating item soldNotes to: %s", newSoldNotes) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldNotes = newSoldNotes)) - } - // [END_ENTITY: Function('updateSoldNotes')] - - // [ENTITY: Function('updateSoldPrice')] - /** - * @summary Updates the sold price of the item in the UI state. - * @param newSoldPrice The new sold price for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateSoldPrice(newSoldPrice: Double?) { - Timber.d("[DEBUG][ACTION][updating_item_soldPrice] Updating item soldPrice to: %f", newSoldPrice) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldPrice = newSoldPrice)) - } - // [END_ENTITY: Function('updateSoldPrice')] - - // [ENTITY: Function('updateSoldTime')] - /** - * @summary Updates the sold time of the item in the UI state. - * @param newSoldTime The new sold time for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateSoldTime(newSoldTime: String?) { - Timber.d("[DEBUG][ACTION][updating_item_soldTime] Updating item soldTime to: %s", newSoldTime) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTime = newSoldTime)) - } - // [END_ENTITY: Function('updateSoldTime')] - - // [ENTITY: Function('updateSoldTo')] - /** - * @summary Updates the sold to field of the item in the UI state. - * @param newSoldTo The new sold to field for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateSoldTo(newSoldTo: String?) { - Timber.d("[DEBUG][ACTION][updating_item_soldTo] Updating item soldTo to: %s", newSoldTo) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTo = newSoldTo)) - } - // [END_ENTITY: Function('updateSoldTo')] - - // [ENTITY: Function('updateSyncChildItemsLocations')] - /** - * @summary Updates the sync child items locations status of the item in the UI state. - * @param newSyncChildItemsLocations The new sync child items locations status for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateSyncChildItemsLocations(newSyncChildItemsLocations: Boolean?) { - Timber.d("[DEBUG][ACTION][updating_item_syncChildItemsLocations] Updating item syncChildItemsLocations to: %b", newSyncChildItemsLocations) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(syncChildItemsLocations = newSyncChildItemsLocations)) - } - // [END_ENTITY: Function('updateSyncChildItemsLocations')] - - // [ENTITY: Function('updateWarrantyDetails')] - /** - * @summary Updates the warranty details of the item in the UI state. - * @param newWarrantyDetails The new warranty details for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateWarrantyDetails(newWarrantyDetails: String?) { - Timber.d("[DEBUG][ACTION][updating_item_warrantyDetails] Updating item warrantyDetails to: %s", newWarrantyDetails) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyDetails = newWarrantyDetails)) - } - // [END_ENTITY: Function('updateWarrantyDetails')] - - // [ENTITY: Function('updateLocation')] - /** - * @summary Updates the location of the item in the UI state. - * @param newLocation The new location for the item. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun updateLocation(newLocation: Location?) { - Timber.d("[DEBUG][ACTION][updating_item_location] Updating item location to: %s", newLocation?.name) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(location = newLocation)) - } - // [END_ENTITY: Function('updateLocation')] - - // [ENTITY: Function('addLabel')] - /** - * @summary Adds a label to the item in the UI state. - * @param label The label to add. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun addLabel(label: Label) { - Timber.d("[DEBUG][ACTION][adding_label_to_item] Adding label: %s", label.name) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = _uiState.value.item?.labels.orEmpty() + label)) - } - // [END_ENTITY: Function('addLabel')] - - // [ENTITY: Function('removeLabel')] - /** - * @summary Removes a label from the item in the UI state. - * @param labelId The ID of the label to remove. - * @sideeffect Updates the `item` in `_uiState`. - */ - fun removeLabel(labelId: String) { - Timber.d("[DEBUG][ACTION][removing_label_from_item] Removing label with ID: %s", labelId) - _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = _uiState.value.item?.labels.orEmpty().filter { it.id != labelId })) - } - // [END_ENTITY: Function('removeLabel')] -} -// [END_ENTITY: ViewModel('ItemEditViewModel')] -// [END_FILE_ItemEditViewModel.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditScreen.kt deleted file mode 100644 index 5e0ce2f..0000000 --- a/app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditScreen.kt +++ /dev/null @@ -1,113 +0,0 @@ -// [PACKAGE] com.homebox.lens.ui.screen.labeledit -// [FILE] LabelEditScreen.kt -// [SEMANTICS] ui, screen, label, edit - -package com.homebox.lens.ui.screen.labeledit - -// [IMPORTS] -import androidx.compose.foundation.layout.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.homebox.lens.R -import com.homebox.lens.ui.components.ColorPicker -import com.homebox.lens.ui.components.LoadingOverlay -// [END_IMPORTS] - -// [ENTITY: Function('LabelEditScreen')] -// [RELATION: Function('LabelEditScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelEditViewModel')] -/** - * @summary Composable-функция для экрана "Редактирование метки". - * @param labelId ID метки для редактирования или null для создания новой. - * @param onBack Навигация назад. - * @param onLabelSaved Действие после сохранения метки. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LabelEditScreen( - labelId: String?, - onBack: () -> Unit, - onLabelSaved: () -> Unit, - viewModel: LabelEditViewModel = hiltViewModel() -) { - val uiState = viewModel.uiState - val snackbarHostState = SnackbarHostState() - - LaunchedEffect(uiState.isSaved) { - if (uiState.isSaved) { - onLabelSaved() - } - } - - LaunchedEffect(uiState.error) { - uiState.error?.let { - snackbarHostState.showSnackbar( - message = it, - actionLabel = "Dismiss", - duration = SnackbarDuration.Short - ) - } - } - - Scaffold( - snackbarHost = { SnackbarHost(snackbarHostState) }, - topBar = { - TopAppBar( - title = { - Text( - text = if (labelId == null) { - stringResource(id = R.string.label_edit_title_create) - } else { - stringResource(id = R.string.label_edit_title_edit) - } - ) - }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back)) - } - }, - actions = { - IconButton(onClick = viewModel::saveLabel) { - Icon(Icons.Default.Check, contentDescription = stringResource(R.string.save)) - } - } - ) - } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp) - ) { - OutlinedTextField( - value = uiState.name, - onValueChange = viewModel::onNameChange, - label = { Text(stringResource(R.string.label_name)) }, - isError = uiState.nameError != null, - supportingText = { uiState.nameError?.let { Text(it) } }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(16.dp)) - ColorPicker( - selectedColor = uiState.color, - onColorSelected = viewModel::onColorChange, - modifier = Modifier.fillMaxWidth() - ) - } - - if (uiState.isLoading) { - LoadingOverlay() - } - } -} -// [END_ENTITY: Function('LabelEditScreen')] -// [END_FILE_LabelEditScreen.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditViewModel.kt deleted file mode 100644 index 9a19467..0000000 --- a/app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditViewModel.kt +++ /dev/null @@ -1,115 +0,0 @@ -// [PACKAGE] com.homebox.lens.ui.screen.labeledit -// [FILE] LabelEditViewModel.kt -// [SEMANTICS] ui, viewmodel, label_management - -package com.homebox.lens.ui.screen.labeledit - -// [IMPORTS] -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.homebox.lens.domain.model.LabelCreate -import com.homebox.lens.domain.model.LabelOut -import com.homebox.lens.domain.model.LabelUpdate -import com.homebox.lens.domain.usecase.CreateLabelUseCase -import com.homebox.lens.domain.usecase.GetLabelDetailsUseCase -import com.homebox.lens.domain.usecase.UpdateLabelUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject -// [END_IMPORTS] - -// [ENTITY: ViewModel('LabelEditViewModel')] -// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetLabelDetailsUseCase')] -// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateLabelUseCase')] -// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateLabelUseCase')] -// [RELATION: ViewModel('LabelEditViewModel')] -> [EMITS_STATE] -> [DataClass('LabelEditUiState')] -@HiltViewModel -class LabelEditViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, - private val getLabelDetailsUseCase: GetLabelDetailsUseCase, - private val createLabelUseCase: CreateLabelUseCase, - private val updateLabelUseCase: UpdateLabelUseCase -) : ViewModel() { - - var uiState by mutableStateOf(LabelEditUiState()) - private set - - private val labelId: String? = savedStateHandle["labelId"] - - init { - if (labelId != null) { - loadLabelDetails(labelId) - } - } - - fun onNameChange(newName: String) { - uiState = uiState.copy(name = newName, nameError = null) - } - - fun onColorChange(newColor: String) { - uiState = uiState.copy(color = newColor) - } - - fun saveLabel() { - viewModelScope.launch { - if (uiState.name.isBlank()) { - uiState = uiState.copy(nameError = "Label name cannot be empty.") - return@launch - } - - uiState = uiState.copy(isLoading = true, error = null) - try { - if (labelId == null) { - // Create new label - val newLabel = LabelCreate(name = uiState.name, color = uiState.color) - createLabelUseCase(newLabel) - } else { - // Update existing label - val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color) - updateLabelUseCase(labelId, updatedLabel) - } - uiState = uiState.copy(isSaved = true) - } catch (e: Exception) { - uiState = uiState.copy(error = e.message, isLoading = false) - } finally { - uiState = uiState.copy(isLoading = false) - } - } - } - - private fun loadLabelDetails(id: String) { - viewModelScope.launch { - uiState = uiState.copy(isLoading = true, error = null) - try { - val label = getLabelDetailsUseCase(id) - uiState = uiState.copy( - name = label.name, - color = label.color, - isLoading = false - ) - } catch (e: Exception) { - uiState = uiState.copy(error = e.message, isLoading = false) - } - } - } -} - -// [ENTITY: DataClass('LabelEditUiState')] -/** - * @summary Состояние UI для экрана редактирования метки. - */ -data class LabelEditUiState( - val name: String = "", - val color: String = "#FFFFFF", // Default color - val nameError: String? = null, - val isLoading: Boolean = false, - val error: String? = null, - val isSaved: Boolean = false, - val originalLabel: LabelOut? = null // To hold original label details if editing -) -// [END_ENTITY: DataClass('LabelEditUiState')] -// [END_FILE_LabelEditViewModel.kt] \ No newline at end of file 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 deleted file mode 100644 index 902a50d..0000000 --- a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt +++ /dev/null @@ -1,225 +0,0 @@ -// [PACKAGE] com.homebox.lens.ui.screen.labelslist -// [FILE] LabelsListScreen.kt -// [SEMANTICS] ui, labels_list, state_management, compose, dialog -package com.homebox.lens.ui.screen.labelslist - -// [IMPORTS] -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -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.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.Label -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.homebox.lens.R -import com.homebox.lens.domain.model.Label -import com.homebox.lens.navigation.NavigationActions -import com.homebox.lens.navigation.Screen -import com.homebox.lens.ui.common.MainScaffold -import timber.log.Timber -// [END_IMPORTS] - -// [ENTITY: Function('LabelsListScreen')] -// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelsListViewModel')] -// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')] -/** - * @summary Отображает экран со списком всех меток. - * @param navController Контроллер навигации для перемещения между экранами. - * @param viewModel ViewModel, предоставляющая состояние UI для экрана меток. - */ -@Composable -fun LabelsListScreen( - currentRoute: String?, - navigationActions: NavigationActions, - viewModel: LabelsListViewModel = hiltViewModel() -) { - val uiState by viewModel.uiState.collectAsState() - - MainScaffold( - topBarTitle = stringResource(id = R.string.screen_title_labels), - currentRoute = currentRoute, - navigationActions = navigationActions - ) { paddingValues -> - Scaffold( - floatingActionButton = { - FloatingActionButton(onClick = { - Timber.i("[INFO][ACTION][navigate_to_label_edit] FAB clicked: Navigate to create new label screen.") - navigationActions.navigateToLabelEdit(null) - }) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = stringResource(id = R.string.content_desc_create_label) - ) - } - } - ) { innerPaddingValues -> - val currentState = uiState - - Box( - modifier = Modifier - .fillMaxSize() - .padding(innerPaddingValues), // Use innerPaddingValues here - contentAlignment = Alignment.Center - ) { - when (currentState) { - is LabelsListUiState.Loading -> { - CircularProgressIndicator() - } - is LabelsListUiState.Error -> { - Text(text = currentState.message) - } - is LabelsListUiState.Success -> { - if (currentState.labels.isEmpty()) { - Text(text = stringResource(id = R.string.no_labels_found)) - } else { - LabelsList( - labels = currentState.labels, - onLabelClick = { label -> - Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.") - navigationActions.navigateToLabelEdit(label.id) - }, - onDeleteClick = { label -> - viewModel.onShowDeleteDialog(label) - }, - isShowingDeleteDialog = currentState.isShowingDeleteDialog, - labelToDelete = currentState.labelToDelete, - onConfirmDelete = { - currentState.labelToDelete?.let { label -> - viewModel.deleteLabel(label.id) - } - }, - onDismissDeleteDialog = { - viewModel.onDismissDeleteDialog() - } - ) - } - - // Delete confirmation dialog - if (currentState is LabelsListUiState.Success && currentState.isShowingDeleteDialog && currentState.labelToDelete != null) { - AlertDialog( - onDismissRequest = { viewModel.onDismissDeleteDialog() }, - title = { Text("Delete Label") }, - text = { Text("Are you sure you want to delete the label '${currentState.labelToDelete!!.name}'? This action cannot be undone.") }, - confirmButton = { - TextButton( - onClick = { - viewModel.deleteLabel(currentState.labelToDelete!!.id) - viewModel.onDismissDeleteDialog() - } - ) { - Text("Delete") - } - }, - dismissButton = { - TextButton( - onClick = { viewModel.onDismissDeleteDialog() } - ) { - Text("Cancel") - } - } - ) - } - } - } - } - } -} -} -// [END_ENTITY: Function('LabelsListScreen')] - -// [ENTITY: Function('LabelsList')] -// [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')] -/** - * @summary Composable-функция для отображения списка меток. - * @param labels Список объектов `Label` для отображения. - * @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка. - * @param modifier Модификатор для настройки внешнего вида. - */ -@Composable -private fun LabelsList( - labels: List