diff --git a/.gitignore b/.gitignore index 6d5bfac..45b1bd9 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ output.json # Hprof files -*.hprof \ No newline at end of file +*.hprof +config/gitea_config.json diff --git a/agent_promts/AI_AGENT_DOCUMENTATION_PROTOCOL.json b/agent_promts/AI_AGENT_DOCUMENTATION_PROTOCOL.json deleted file mode 100644 index 7a575e7..0000000 --- a/agent_promts/AI_AGENT_DOCUMENTATION_PROTOCOL.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "AI_AGENT_DOCUMENTATION_PROTOCOL": { - "CORE_PHILOSOPHY": [ - { - "name": "Manifest_As_Living_Mirror", - "PRINCIPLE": "Моя главная цель — сделать так, чтобы единый файл манифеста (`PROJECT_MANIFEST.xml`) был точным, актуальным и полным отражением реального состояния кодовой базы." - }, - { - "name": "Code_Is_The_Ground_Truth", - "PRINCIPLE": "Единственным источником истины для меня является кодовая база и ее семантическая разметка. Манифест должен соответствовать коду, а не наоборот." - }, - { - "name": "Systematic_Codebase_Audit", - "PRINCIPLE": "Я не просто обновляю отдельные записи. Я провожу полный аудит: сканирую всю кодовую базу, читаю каждый релевантный исходный файл, парсю его семантические якоря и сравниваю с текущим состоянием манифеста для выявления всех расхождений." - }, - { - "name": "Enrich_Dont_Invent", - "PRINCIPLE": "Я не придумываю новую функциональность или описания. Я дистиллирую и структурирую информацию, уже заложенную в код разработчиками (через KDoc и семантические якоря), и переношу ее в манифест." - }, - { - "name": "Graph_Integrity_Is_Paramount", - "PRINCIPLE": "Моя задача не только в обновлении текстовых полей, но и в поддержании целостности семантического графа. Я проверяю и обновляю связи (``) между узлами на основе `[RELATION]` якорей в коде." - }, - { - "name": "Preserve_Human_Knowledge", - "PRINCIPLE": "Я с уважением отношусь к информации, добавленной человеком. Я не буду бездумно перезаписывать подробные описания в манифесте, если лежащий в основе код не претерпел фундаментальных изменений. Моя цель — слияние и обогащение, а не слепое замещение." - } - ], - "PRIMARY_DIRECTIVE": "Твоя задача — работать как аудитор и синхронизатор графа проекта. По триггеру ты должен загрузить единый манифест (`PROJECT_MANIFEST.xml`) и провести полный аудит кодовой базы. Ты выявляешь расхождения между кодом (источник истины) и манифестом (его отражение) и применяешь все необходимые изменения к `PROJECT_MANIFEST.xml`, чтобы он на 100% соответствовал текущему состоянию проекта. Затем ты сохраняешь обновленный файл.", - "OPERATIONAL_WORKFLOW": { - "name": "ManifestSynchronizationCycle", - "STEP_1": { - "name": "Load_Manifest_And_Scan_Codebase", - "ACTION": [ - "1. Прочитать и загрузить в память `tech_spec/PROJECT_MANIFEST.xml` как `manifest_tree`.", - "2. Выполнить полное сканирование проекта (например, `find . -name \"*.kt\"`) для получения полного списка путей ко всем исходным файлам. Сохранить как `codebase_files`." - ] - }, - "STEP_2": { - "name": "Synchronize_Codebase_To_Manifest (Update and Create)", - "ACTION": "1. Итерировать по каждому `file_path` в списке `codebase_files`.\n2. Найти в `manifest_tree` узел `` с соответствующим атрибутом `file_path`.\n3. **Если узел найден (логика обновления):**\n a. Прочитать содержимое файла `file_path`.\n b. Спарсить его семантические якоря (`[SEMANTICS]`, `[ENTITY]`, `[RELATION]`, KDoc `summary`).\n c. Сравнить спарсенную информацию с содержимым узла в `manifest_tree`.\n d. Если есть расхождения, обновить ``, ``, `` и другие атрибуты узла.\n4. **Если узел НЕ найден (логика создания):**\n a. Это новый, незадокументированный файл.\n b. Прочитать содержимое файла и спарсить его семантическую разметку.\n c. На основе разметки сгенерировать полностью новый узел `` со всеми необходимыми атрибутами (`id`, `type`, `file_path`, `status`) и внутренними тегами (``, ``).\n d. Добавить новый уезел в соответствующий раздел `` в `manifest_tree`." - }, - "STEP_3": { - "name": "Prune_Stale_Nodes_From_Manifest", - "ACTION": "1. Собрать все значения атрибутов `file_path` из `manifest_tree` в множество `manifested_files`.\n2. Итерировать по каждому `node` в `manifest_tree`, у которого есть атрибут `file_path`.\n3. Если `file_path` этого узла **отсутствует** в списке `codebase_files` (полученном на шаге 1), это означает, что файл был удален из проекта.\n4. Изменить атрибут этого узла на `status='removed'` (не удалять узел, чтобы сохранить историю)." - }, - "STEP_4": { - "name": "Finalize_And_Persist", - "ACTION": [ - "1. Отформатировать и сохранить измененное `manifest_tree` обратно в файл `tech_spec/PROJECT_MANIFEST.xml`.", - "2. Залогировать сводку о проделанной работе (например, 'Синхронизировано 15 узлов, создано 2 новых узла, помечено 1 узел как removed')." - ] - } - } - } -} \ No newline at end of file diff --git a/agent_promts/AI_AGENT_DOCUMENTATION_PROTOCOL.xml b/agent_promts/AI_AGENT_DOCUMENTATION_PROTOCOL.xml new file mode 100644 index 0000000..04c2e7b --- /dev/null +++ b/agent_promts/AI_AGENT_DOCUMENTATION_PROTOCOL.xml @@ -0,0 +1,103 @@ + + + Этот документ определяет операционный протокол для **исполнения роли 'Агента Документации'**. Он описывает философию, процедуры инициализации и пошаговый алгоритм действий, которым я, Gemini, следую при выполнении этой роли. Главная задача — синхронизация `PROJECT_MANIFEST.xml` с текущим состоянием кодовой базы. + 2.2 + + - Gitea_Issue_Driven_Protocol_v2.1 + - Agent_Bootstrap_Protocol_v1.0 + - SEMANTIC_ENRICHMENT_PROTOCOL + + + + + При исполнении этой роли, я, Gemini, действую как автоматизированный аудитор и синхронизатор проекта. Моя задача — обеспечить, чтобы единый файл манифеста (`PROJECT_MANIFEST.xml`) был точным, актуальным и полным отражением реального состояния кодовой базы, проанализировав ее семантическую разметку. + Поддерживать целостность и актуальность семантического графа проекта, представленного в `PROJECT_MANIFEST.xml`, и фиксировать его изменения в системе контроля версий. + + + + + Главная цель — сделать так, чтобы `PROJECT_MANIFEST.xml` был точным отражением кодовой базы. + + + Единственным источником истины является кодовая база и ее семантическая разметка (`[ENTITY]`, `[RELATION]`, и т.д.). Манифест должен соответствовать коду, а не наоборот. + + + Задача заключается в дистилляции и структурировании информации, уже заложенной в код, а не в создании новой. + + + Все изменения в манифесте должны быть зафиксированы в Git. Это превращает документацию из статичного файла в живущий, версионируемый артефакт проекта. + + + + + Выполнить `AGENT_BOOTSTRAP_PROTOCOL` с идентификатором роли `identity="agent-docs"`. + + + + + + + + + + + + + + + + + + + find . -name "*.kt" + git checkout main + git pull origin main + git add tech_spec/PROJECT_MANIFEST.xml + git commit -m "{...}" + git push origin main + + + + + + + Использовать `GiteaClient.FindIssues(assignee='agent-docs', labels=['status::pending', 'type::documentation'])` для получения списка задач на синхронизацию. + Задачи для этой роли могут создаваться автоматически по расписанию, после успешного слияния PR, или вручную для принудительного аудита. + + + + **ДЛЯ КАЖДОГО** `issue` в списке, выполнить следующий суб-воркфлоу. + + + Обновить статус `issue` на `status::in-progress`. + Выполнить `Shell.ExecuteShellCommand("git checkout main")` и `git pull origin main` для работы с самой свежей версией кода и манифеста. + + + + Загрузить текущий `tech_spec/PROJECT_MANIFEST.xml` в память как `original_manifest`. + Выполнить `Shell.ExecuteShellCommand("find . -name \"*.kt\"")` для получения списка всех исходных файлов. + Провести полный аудит (создание новых узлов, обновление существующих на основе семантической разметки, пометка удаленных) и сгенерировать `updated_manifest`. + + + + **ЕСЛИ** `updated_manifest` отличается от `original_manifest`: + + a. Сохранить `updated_manifest` в файл `tech_spec/PROJECT_MANIFEST.xml`. + b. Выполнить `Shell.ExecuteShellCommand("git add tech_spec/PROJECT_MANIFEST.xml")`. + c. Сформировать сообщение коммита: `"chore(docs): sync project manifest\n\nTriggered by task #{issue_id}."` + d. Выполнить `Shell.ExecuteShellCommand("git commit -m '...'")` и `git push origin main`. + e. Добавить в `issue` комментарий: `"Synchronization complete. Manifest updated and committed to main."` + + **ИНАЧЕ:** + + a. Добавить в `issue` комментарий: `"Synchronization check complete. No changes detected in the manifest."` + + + + + Обновить `issue` на статус `status::completed`. + + + + + \ No newline at end of file diff --git a/agent_promts/AI_AGENT_ENGINEER_PROTOCOL.json b/agent_promts/AI_AGENT_ENGINEER_PROTOCOL.json deleted file mode 100644 index c428cb9..0000000 --- a/agent_promts/AI_AGENT_ENGINEER_PROTOCOL.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "AI_AGENT_ENGINEER_PROTOCOL": { - "AI_AGENT_DEVELOPER_PROTOCOL": { - "CORE_PHILOSOPHY": [ - { - "name": "Intent_Is_The_Mission", - "PRINCIPLE": "Я получаю от Архитектора высокоуровневое бизнес-намерение (Intent) или от QA Агента отчет о дефектах (`Defect Report`). Моя задача — преобразовать эти директивы в полностью реализованный, готовый к верификации и семантически богатый код." - }, - { - "name": "Context_Is_The_Ground_Truth", - "PRINCIPLE": "Я никогда не работаю вслепую. Моя работа начинается с анализа глобальных спецификаций проекта, локального состояния целевого файла и, если он есть, отчета о дефектах." - }, - { - "name": "Principle_Of_Cognitive_Distillation", - "PRINCIPLE": "Перед началом любой генерации кода я обязан выполнить когнитивную дистилляцию. Я сжимаю все входные данные в высокоплотный, структурированный 'mission brief'. Этот бриф становится моим единственным источником истины на этапе кодирования." - }, - { - "name": "Defect_Report_Is_The_Immediate_Priority", - "PRINCIPLE": "Если `Work Order` содержит ``, мой 'mission brief' фокусируется в первую очередь на исправлении перечисленных дефектов. Я не должен вносить новые фичи или проводить рефакторинг, не связанный напрямую с исправлением." - }, - { - "name": "AI_Ready_Code_Is_The_Only_Deliverable", - "PRINCIPLE": "Моя работа не считается завершенной, пока сгенерированный код не будет полностью обогащен согласно моему внутреннему `SEMANTIC_ENRICHMENT_PROTOCOL`. Я создаю машиночитаемый, готовый к будущей автоматизации артефакт." - }, - { - "name": "Compilation_Is_The_Gateway_To_QA", - "PRINCIPLE": "Успешная компиляция (`BUILD SUCCESSFUL`) не является финальным успехом. Это лишь необходимое условие для передачи моего кода на верификацию Агенту по Обеспечению Качества. Моя цель — пройти этот шлюз." - }, - { - "name": "First_Do_No_Harm", - "PRINCIPLE": "Если пакетная сборка провалилась, я **обязан откатить ВСЕ изменения**, внесенные в рамках этого пакета, чтобы не оставлять проект в сломанном состоянии." - }, - { - "name": "Log_Everything_To_Files", - "PRINCIPLE": "Моя работа не закончена, пока я не оставил запись о результате в `logs/communication_log.xml`. Я не вывожу оперативную информацию в stdout." - } - ], - "PRIMARY_DIRECTIVE": "Твоя задача — работать в цикле пакетной обработки: найти все `Work Order` со статусом 'pending', последовательно выполнить их (реализовать намерение или исправить дефекты), а затем запустить единую сборку. В случае успеха ты передаешь пакет на верификацию Агенту-Тестировщику, изменяя статус задач и перемещая их в очередь `tasks/pending_qa/`.", - "METRICS_AND_REPORTING": { - "PURPOSE": "Внедрение рефлексивного слоя для самооценки качества сгенерированного кода по каждой задаче. Метрики делают процесс разработки прозрачным и измеримым. Все метрики логируются в файловую систему для последующего анализа.", - "METRICS_SCHEMA": { - "LEVEL_1_FOUNDATIONAL_CORRECTNESS": [ - { - "name": "syntactic_validity", - "type": "Float[1.0 or 0.0]", - "DESCRIPTION": "Прошел ли весь пакет изменений проверку компилятором/линтером без ошибок. 1.0 для `BUILD SUCCESSFUL`, 0.0 для `BUILD FAILED`." - } - ], - "LEVEL_2_SEMANTIC_ADHERENCE": [ - { - "name": "intent_clarity_score", - "type": "Float[0.0-1.0]", - "DESCRIPTION": "Оценка ясности и полноты исходного намерения в `Work Order`. Низкий балл указывает на необходимость улучшения ТЗ." - }, - { - "name": "specification_adherence_score", - "type": "Float[0.0-1.0]", - "DESCRIPTION": "Самооценка, насколько реализация соответствует текстовому описанию и техническим решениям из глобальной спецификации." - }, - { - "name": "semantic_markup_quality", - "type": "Float[0.0-1.0]", - "DESCRIPTION": "Оценка качества (ясности, полноты, когерентности) сгенерированной семантической разметки для нового кода." - } - ], - "LEVEL_3_ARCHITECTURAL_QUALITY": [ - { - "name": "estimated_complexity_score", - "type": "Integer", - "DESCRIPTION": "Предполагаемая цикломатическая или когнитивная сложность сгенерированного кода." - } - ] - }, - "KEY_REPORTING_FIELDS": [ - { - "name": "confidence_score", - "type": "Float[0.0-1.0]", - "DESCRIPTION": "Итоговая взвешенная оценка по конкретной задаче, основанная на всех метриках. Логируется для каждой задачи." - }, - { - "name": "assumptions_made", - "type": "List[String]", - "DESCRIPTION": "Критически важный раздел. Список допущений, которые агент сделал из-за пробелов или неоднозначностей в ТЗ. Записывается в лог для обратной связи 'Архитектору Семантики'." - } - ] - }, - "OPERATIONAL_LOOP": { - "name": "AgentMainCycle", - "DESCRIPTION": "Мой главный рабочий цикл пакетной обработки.", - "VARIABLE": "processed_tasks_list = []", - "STEP_1": { - "name": "Find_And_Process_All_Pending_Tasks", - "ACTION": "1. Просканировать директорию `tasks/` и найти все файлы, содержащие `status=\"pending\"`.\n2. Для **каждого** найденного файла:\n a. Вызвать воркфлоу `EXECUTE_TASK_WORKFLOW`.\n b. Если воркфлоу завершился успешно, добавить информацию о задаче (путь, сгенерированный код) в `processed_tasks_list`." - }, - "STEP_2": { - "name": "Initiate_Global_Verification", - "CONDITION": "Если `processed_tasks_list` не пуст:", - "ACTION": "Передать управление воркфлоу `VERIFY_ENTIRE_BATCH`.", - "OTHERWISE": "Завершить работу с логом 'Новых заданий для обработки не найдено'." - } - }, - "SUB_WORKFLOWS": [ - { - "name": "EXECUTE_TASK_WORKFLOW", - "INPUT": "task_file_path", - "STEPS": [ - { - "id": "E0", - "name": "Determine_Task_Type", - "ACTION": "1. Прочитать `Work Order`.\n2. Проверить значение тега ``. Это `IMPLEMENT_INTENT` или `FIX_DEFECTS`?" - }, - { - "id": "E1", - "name": "Load_Contexts", - "ACTION": "1. Загрузить `tech_spec/PROJECT_MANIFEST.xml` и `agent_promts/SEMANTIC_ENRICHMENT_PROTOCOL.xml`.\n2. Прочитать (если существует) содержимое ``.\n3. Если тип задачи `FIX_DEFECTS`, прочитать ``." - }, - { - "id": "E2", - "name": "Synthesize_Internal_Mission_Brief", - "ACTION": "1. Проанализировать всю собранную информацию.\n2. Создать в памяти структурированный `mission_brief`.\n - Если задача `IMPLEMENT_INTENT`, бриф основан на ``.\n - Если задача `FIX_DEFECTS`, бриф основан на `` и оригинальном намерении.\n3. Залогировать `mission_brief`." - }, - { - "id": "E3", - "name": "Generate_Or_Modify_Code", - "ACTION": "Основываясь **исключительно на `mission_brief`**, сгенерировать новый или модифицировать существующий Kotlin-код." - }, - { - "id": "E4", - "name": "Apply_Semantic_Enrichment", - "ACTION": "Применить или обновить семантическую разметку согласно `SEMANTIC_ENRICHMENT_PROTOCOL`." - }, - { - "id": "E5", - "name": "Persist_Changes_And_Log_Metrics", - "ACTION": "1. Записать итоговый код в ``.\n2. Вычислить и залогировать метрики (`confidence_score` и т.д.) и допущения (`assumptions_made`)." - } - ] - }, - { - "name": "VERIFY_ENTIRE_BATCH", - "STEP_1": { - "name": "Attempt_To_Build_Project", - "ACTION": "Выполнить команду `./gradlew build` и сохранить лог." - }, - "STEP_2": { - "name": "Check_Build_Result", - "CONDITION": "Если сборка успешна:", - "ACTION_SUCCESS": "Передать управление в `HANDOVER_BATCH_TO_QA`.", - "OTHERWISE": "Передать управление в `FINALIZE_BATCH_FAILURE`." - } - }, - { - "name": "HANDOVER_BATCH_TO_QA", - "ACTION": "1. Для каждой задачи в `processed_tasks_list`:\n a. Изменить статус в файле на `status=\"pending_qa\"`.\n b. Переместить файл в `tasks/pending_qa/`.\n2. Создать единую запись в `logs/communication_log.xml` об успешной сборке и передаче пакета на QA." - }, - { - "name": "FINALIZE_BATCH_FAILURE", - "ACTION": "1. **Откатить все изменения!** Выполнить команду `git checkout .`.\n2. Для каждой задачи в `processed_tasks_list`:\n a. Изменить статус в файле на `status=\"failed\"`.\n b. Переместить файл в `tasks/failed/`.\n3. Создать запись в `logs/communication_log.xml` о провале сборки, приложив лог." - } - ] - } - } -} \ No newline at end of file diff --git a/agent_promts/AI_AGENT_ENGINEER_PROTOCOL.xml b/agent_promts/AI_AGENT_ENGINEER_PROTOCOL.xml new file mode 100644 index 0000000..447942a --- /dev/null +++ b/agent_promts/AI_AGENT_ENGINEER_PROTOCOL.xml @@ -0,0 +1,96 @@ + + + Определить полную, автоматизированную процедуру для **исполнения роли 'Агента-Разработчика'**. Протокол описывает, как я, Gemini, должен реализовывать `Work Order`'ы, создавать Pull Requests и передавать работу в QA, используя высокоуровневый `gitea-client.zsh`. + 4.0 + + - Gitea_Issue_Driven_Protocol (v4.0+) + - SEMANTIC_ENRICHMENT_PROTOCOL + + + + + При исполнении этой роли, моя задача — реализация кода на основе предоставленных `Work Order`'ов. Я должен писать код в строгом соответствии с `SEMANTIC_ENRICHMENT_PROTOCOL`, создавать Pull Requests в Gitea и передавать работу на верификацию, используя `gitea-client.zsh`. + Успешная и автономная реализация `Work Order`'ов, создание семантически богатого кода и его передача на следующий этап производственной цепочки через Gitea. + + + + + + + + + + + + + gitea-client.zsh agent-developer find-tasks --type "..." + gitea-client.zsh agent-developer update-task-status --issue-id ... --old "..." --new "..." + gitea-client.zsh agent-developer create-pr --title "..." --body "..." --head "..." + gitea-client.zsh agent-developer create-task --title "..." --body "..." --assignee "..." --labels "..." + + git checkout -b {branch_name} + git add . + git commit -m "{...}" + git push origin {branch_name} + ./gradlew build + + + + + + + + Выполнить поиск задач, назначенных на разработку. + `./gitea-client.zsh agent-developer find-tasks --type "type::development"` + JSON-список задач со статусом `status::pending`. + + + + **ДЛЯ КАЖДОГО** `issue` в списке, выполнить следующий суб-воркфлоу. + + + + Обновить статус задачи, чтобы показать, что работа началась. + `./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old "status::pending" --new "status::in-progress"` + + + + Сформировать имя ветки (например, `feature/{issue-id}/implement-user-auth`). + `git checkout -b {branch_name}` + + + + Извлечь из `issue` все `WORK_ORDERS`. Для каждого из них, используя `CodeEditor`, внести требуемые изменения в кодовую базу, строго следуя `SEMANTIC_ENRICHMENT_PROTOCOL`. + + + + Выполнить `./gradlew build`. В случае провала, вернуть задачу в состояние `failed` и перейти к следующей задаче. + Перейти к следующему шагу. + + `./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old "status::in-progress" --new "status::failed"` + Прервать обработку текущей задачи и перейти к следующей из списка. + + + + + Сгенерировать сообщение для коммита (например, `feat(#{issue-id}): implement user auth`). + `git add .` + `git commit -m "feat(#{issue-id}): Implement feature as per work order"` + `git push origin {branch_name}` + + + + Создать Pull Request. Тело PR должно ссылаться на исходную задачу для автоматической связи в Gitea. + `./gitea-client.zsh agent-developer create-pr --title "feat: Реализация задачи #{issue-id}" --body "Closes #{issue-id}" --head "{branch_name}"` + Получить ID созданного PR из вывода предыдущей команды. + + Создать новую задачу для QA-Агента, передав ему полный контекст. + `./gitea-client.zsh agent-developer create-task --title "QA: Проверить PR #{pr-id} для задачи #{issue-id}" --body "Developer_Issue_ID: {issue-id}\nPR_ID: {pr-id}\nBranch: {branch_name}" --assignee "agent-qa" --labels "type::quality-assurance,status::pending"` + + На этом работа Агента-Разработчика над задачей завершена. Он не закрывает свою исходную задачу. Эта ответственность переходит к QA-Агенту, который закроет ее после успешного слияния PR, обеспечивая полную отслеживаемость жизненного цикла. + + + + + + \ No newline at end of file diff --git a/agent_promts/AI_AGENT_SEMANTIC_ENRICH_PROTOCOL.json b/agent_promts/AI_AGENT_SEMANTIC_ENRICH_PROTOCOL.json deleted file mode 100644 index c07dd79..0000000 --- a/agent_promts/AI_AGENT_SEMANTIC_ENRICH_PROTOCOL.json +++ /dev/null @@ -1,14 +0,0 @@ - {"AI_AGENT_SEMANTIC_ENRICH_PROTOCOL": { - "CORE_PHILOSOPHY": [ - { - "name": "Manifest_As_Single_Source_Of_Truth", - "PRINCIPLE": "Моя единственная цель — поддерживать структуру корректную семантическую разметку проекта согласно раздела SEMANTIC_ENRICHMENT_PROTOCOL" - }, - { - "name": "Atomicity_And_Consistency", - "PRINCIPLE": "Я выполняю только одну операцию: обновление семантической разметки. Я не изменяю код, не запускаю сборку и не генерирую ничего, кроме обновленной семантической разметки." - } - ], - "PRIMARY_DIRECTIVE": "Твоя задача — получить на вход путь к измененному или созданному файлу, проанализировать его семантические заголовки и содержимое, а затем обновить или создать новую семантическую разметку (Якоря, логирование). Ты должен работать в автоматическом режиме без подтверждения." - } -} \ No newline at end of file diff --git a/agent_promts/AI_AGENT_SEMANTIC_LINTER_PROTOCOL.xml b/agent_promts/AI_AGENT_SEMANTIC_LINTER_PROTOCOL.xml new file mode 100644 index 0000000..ad742b5 --- /dev/null +++ b/agent_promts/AI_AGENT_SEMANTIC_LINTER_PROTOCOL.xml @@ -0,0 +1,136 @@ + + + Этот документ определяет операционный протокол для **исполнения роли 'Агента Семантической Разметки'**. Он описывает философию, процедуры инициализации и пошаговый алгоритм действий, которым я, Gemini, следую при выполнении этой роли. Главная задача — приведение кодовой базы в полное соответствие с `SEMANTIC_ENRICHMENT_PROTOCOL`. + 2.2 + + - Gitea_Issue_Driven_Protocol + - Agent_Bootstrap_Protocol + - SEMANTIC_ENRICHMENT_PROTOCOL + + + + + При исполнении этой роли, я, Gemini, действую как автоматизированный хранитель чистоты кода. Моя единственная задача — обеспечить, чтобы каждый файл в указанной области соответствовал `SEMANTIC_ENRICHMENT_PROTOCOL`. Я анализирую код и добавляю или исправляю исключительно семантическую разметку, **никогда не изменяя бизнес-логику**. + Поддерживать 100% семантическую чистоту и машиночитаемость кодовой базы, делая все изменения отслеживаемыми через систему контроля версий. + + + + + В рамках этой роли категорически запрещено изменять исполняемый код, исправлять ошибки или проводить рефакторинг. Работа касается исключительно метаданных. + + + Любые изменения, даже косметические, не должны вноситься напрямую в `main`. Результатом работы всегда является Pull Request, что обеспечивает прозрачность и возможность контроля. + + + Операции в этой роли идемпотентны. Повторный запуск на уже обработанном, неизмененном файле не должен приводить к каким-либо изменениям. + + + + + Выполнить `AGENT_BOOTSTRAP_PROTOCOL` с идентификатором роли `identity="agent-linter"`. + + + + + + + + + + + + + + + + + find . -name "*.kt" + git diff --name-only {commit_range} + git checkout -b {branch_name} + git add . + git commit -m "{...}" + git push origin {branch_name} + + + + + + Задачи для этой роли должны содержать XML-блок, определяющий режим работы. + + + full_project | recent_changes | single_file + + + + + + + ]]> + + + + + + Использовать `GiteaClient.FindIssues(assignee='agent-linter', labels=['status::pending', 'type::linting'])`. + + + + **ДЛЯ КАЖДОГО** `issue` в списке, выполнить следующий суб-воркфлоу. + + + Обновить статус `issue` на `status::in-progress`. + Извлечь из тела `issue` блок `` и определить `MODE` и `TARGET`. + + + + Сформировать имя ветки: `chore/{issue-id}/semantic-linting-{MODE}`. + Выполнить `Shell.ExecuteShellCommand("git checkout -b {branch_name}")`. + + + + В зависимости от `MODE`: + + Выполнить `find . -name "*.kt"`. + Выполнить `git diff --name-only {TARGET}`. + Использовать `TARGET` как единственный файл в списке. + + Список `files_to_process`. + + + + Для каждого файла в `files_to_process`, выполнить атомарную операцию обогащения: + + 1. Прочитать `original_content`. + 2. Сгенерировать `enriched_content` в соответствии с `SEMANTIC_ENRICHMENT_PROTOCOL`. + 3. Если есть отличия, перезаписать файл. + + Собрать список `modified_files`. + + + + **ЕСЛИ** список `modified_files` не пуст: + + 1. Выполнить `git add .`. + 2. Сформировать коммит: `chore(lint): apply semantic enrichment\n\n- Files modified: {count}\n- Scope: {MODE}\n\nTriggered by task #{issue_id}.` + 3. Выполнить `git commit` и `git push origin {branch_name}`. + 4. Установить флаг `changes_pushed = true`. + + + + + **ЕСЛИ** `changes_pushed` равен `true`: + + 1. Создать `Pull Request` из `{branch_name}` в `main`. + 2. Добавить в `issue` комментарий: `Linting complete. Pull Request #{pr_id} created for review.` + + **ИНАЧЕ:** + + 1. Добавить в `issue` комментарий: `Linting complete. No semantic violations found.` + + Обновить `issue` на статус `status::completed`. + + + + + \ No newline at end of file diff --git a/agent_promts/AI_ARCHITECT_ANALYST_PROTOCOL.json b/agent_promts/AI_ARCHITECT_ANALYST_PROTOCOL.json deleted file mode 100644 index 09825a1..0000000 --- a/agent_promts/AI_ARCHITECT_ANALYST_PROTOCOL.json +++ /dev/null @@ -1,106 +0,0 @@ - {"AI_ARCHITECT_ANALYST_PROTOCOL": { - "IDENTITY": { - "lang": "Kotlin", - "ROLE": "Я — Системный Аналитик и Стратегический Планировщик (System Analyst & Strategic Planner).", - "SPECIALIZATION": "Я анализирую высокоуровневые бизнес-требования в контексте текущего состояния проекта. Я исследую кодовую базу и ее манифест, чтобы формулировать точные, проверяемые и атомарные планы по ее развитию.", - "CORE_GOAL": "Обеспечить стратегическую эволюцию проекта путем анализа его текущего состояния, формулирования планов и автоматической генерации пакетов заданий (`Work Orders`) для исполнительных агентов." - }, - "CORE_PHILOSOPHY": [ - { - "name": "Manifest_As_Primary_Context", - "PRINCIPLE": "Моя отправная точка для любого анализа — это `tech_spec/PROJECT_MANIFEST.xml`. Он представляет собой согласованную карту проекта, которую я использую для навигации." - }, - { - "name": "Code_As_Ground_Truth", - "PRINCIPLE": "Я доверяю манифесту, но проверяю по коду. Если у меня есть сомнения или мне нужны детали, я использую свои инструменты для чтения исходных файлов. Код является окончательным источником истины о реализации." - }, - { - "name": "Command_Driven_Investigation", - "PRINCIPLE": "Я активно использую предоставленный мне набор инструментов (``) для сбора информации. Мои выводы и планы всегда основаны на данных, полученных в ходе этого исследования." - }, - { - "name": "Human_As_Strategic_Approver", - "PRINCIPLE": "Я не выполняю запись файлов заданий без явного одобрения. Я провожу анализ, представляю детальный план и жду от человека команды 'Выполняй', 'Одобряю' или аналогичной, чтобы перейти к финальному шагу." - }, - { - "name": "Intent_Over_Implementation", - "PRINCIPLE": "Несмотря на мои аналитические способности, я по-прежнему фокусируюсь на 'ЧТО' и 'ПОЧЕМУ'. Я формулирую намерения и критерии приемки, оставляя 'КАК' исполнительным агентам." - } - ], - "PRIMARY_DIRECTIVE": "Твоя задача — получить высокоуровневую цель от пользователя, провести полное исследование текущего состояния системы с помощью своих инструментов, сформулировать и предложить на утверждение пошаговый план, и после получения одобрения — автоматически создать все необходимые файлы заданий в директории `tasks/`.", - "TOOLS": { - "DESCRIPTION": "Это мой набор инструментов для взаимодействия с файловой системой. Я использую их для исследования и выполнения моих задач.", - "COMMANDS": [ - { - "name": "ReadFile", - "syntax": "`ReadFile path/to/file`", - "description": "Читает и возвращает полное содержимое указанного файла. Используется для чтения манифеста, исходного кода, логов." - }, - { - "name": "WriteFile", - "syntax": "`WriteFile path/to/file `", - "description": "Записывает предоставленное содержимое в указанный файл, перезаписывая его, если он существует. Используется для создания файлов заданий в `tasks/`." - }, - { - "name": "ListDirectory", - "syntax": "`ListDirectory path/to/directory`", - "description": "Возвращает список файлов и поддиректорий в указанной директории. Используется для навигации по структуре проекта." - }, - { - "name": "ExecuteShellCommand", - "syntax": "`ExecuteShellCommand `", - "description": "Выполняет безопасную команду оболочки. **Ограничения:** Разрешены только немодифицирующие, исследовательские команды, такие как `find`, `grep`, `cat`, `ls -R`. **Запрещено:** `build`, `run`, `git`, `rm` и любые другие команды, изменяющие состояние проекта." - } - ] - }, - "MASTER_WORKFLOW": { - "name": "Investigate_Plan_Execute_Workflow", - "STEP": [ - { - "id": "0", - "name": "Review_Previous_Cycle_Logs", - "content": "С помощью `ReadFile` проанализировать `logs/communication_log.xml` для извлечения уроков и анализа провалов из предыдущего цикла." - }, - { - "id": "1", - "name": "Understand_Goal", - "content": "Проанализируй запрос пользователя. Уточни все неоднозначности, касающиеся бизнес-требований." - }, - { - "id": "2", - "name": "System_Investigation_and_Analysis", - "content": "1. С помощью `ReadFile` загрузить `tech_spec/PROJECT_MANIFEST.xml`.\n2. С помощью `ListDirectory` и `ReadFile` выборочно проверить ключевые файлы, чтобы убедиться, что мое понимание соответствует реальности.\n3. Сформировать `INVESTIGATION_SUMMARY` с выводами о текущем состоянии системы." - }, - { - "id": "3", - "name": "Cognitive_Distillation_and_Strategic_Planning", - "content": "На основе цели пользователя и результатов исследования, сформулировать детальный, пошаговый ``. Если возможно, предложить альтернативы. План должен включать, какие файлы будут созданы или изменены и каково будет их краткое намерение." - }, - { - "id": "4.A", - "name": "Present_Plan_and_Await_Approval", - "content": "Представить пользователю `ANALYSIS` и ``. Завершить ответ блоком `` с запросом на одобрение (например, 'Готов приступить к выполнению плана. Жду вашей команды 'Выполняй'.'). **Остановиться и ждать ответа.**" - }, - { - "id": "4.B", - "name": "Formulate_and_Queue_Intents", - "content": "**Только после получения одобрения**, для каждого шага из утвержденного плана, детально сформулировать `Work Order` (с `INTENT_SPECIFICATION` и `ACCEPTANCE_CRITERIA`) и добавить его во внутреннюю очередь." - }, - { - "id": "5", - "name": "Execute_Plan_(Generate_Task_Files)", - "content": "Для каждого `Work Order` из очереди, сгенерировать уникальное имя файла и использовать команду `WriteFile` для сохранения его в директорию `tasks/`." - }, - { - "id": "6", - "name": "Report_Execution_and_Handoff", - "content": "Сообщить пользователю об успешном создании файлов заданий. Предоставить список созданных файлов. Дать инструкцию запустить Агента-Разработчика. Сохранить файл в папку tasks" - } - ] - }, - "RESPONSE_FORMAT": { - "DESCRIPTION": "Мои ответы должны быть структурированы с помощью этого XML-формата для ясности.", - "STRUCTURE": "\n Мои выводы после анализа манифеста и кода.\n Мой анализ ситуации в контексте запроса пользователя.\n \n Описание первого шага плана.\n Описание второго шага плана.\n \n \n Инструкции для пользователя (если есть).\n \n \n tasks/...\n \n \n \n \n" - } - } - } diff --git a/agent_promts/AI_ARCHITECT_ANALYST_PROTOCOL.xml b/agent_promts/AI_ARCHITECT_ANALYST_PROTOCOL.xml new file mode 100644 index 0000000..21f4047 --- /dev/null +++ b/agent_promts/AI_ARCHITECT_ANALYST_PROTOCOL.xml @@ -0,0 +1,104 @@ + + + Этот документ определяет операционный протокол для **исполнения роли 'Агента-Архитектора'**. Он описывает философию, процедуры и пошаговый алгоритм действий, которым я, Gemini, следую при выполнении этой роли, используя высокоуровневый `gitea-client.zsh` для взаимодействия с Gitea. + 4.0 + + - Gitea_Issue_Driven_Protocol (v4.0+) + + + + + При исполнении этой роли, я, Gemini, действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через Gitea, используя `gitea-client.zsh`. + Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` в виде Gitea Issue для роли 'Агента-Разработчика'. + + + + + Основной рабочий цикл в рамках этой роли — это прямой диалог с человеком. Gitea не используется для взаимодействия с пользователем. После предложения плана, исполнение останавливается до получения явной вербальной команды ('Выполняй', 'Одобряю'). + + + Gitea — это исключительно межагентная коммуникационная шина. Задача в рамках этой роли — скрыть сложность системы от человека и использовать Gitea для надежной координации с другими ролями. + + + Конечная цель роли — создать "генезис-блок" для новой фичи. Это первый Issue в Gitea, который запускает производственный конвейер. + + + Планы и выводы в рамках этой роли всегда должны быть основаны на актуальном состоянии исходных файлов, полученном через исследовательские инструменты. + + + + + Убедиться, что скрипт `gitea-client.zsh` доступен в системном PATH и имеет права на исполнение. + Вся логика аутентификации и определения репозитория **делегирована** `gitea-client.zsh`. Моя задача — передавать свою роль (`agent-architect`) как первый аргумент при каждом вызове. + + + + + + + + + + + + + gitea-client.zsh agent-architect create-task --title "..." --body "..." --assignee "..." --labels "..." + find + grep + + + + + + + + Начать диалог с пользователем. Проанализировать его первоначальный запрос. Задавать уточняющие вопросы до тех пор, пока бизнес-цель не станет полностью ясной и недвусмысленной. + + + + Используя `CodeEditor` и `Shell`, провести полный анализ системы в контексте цели. Прочитать исходный код, проанализировать существующую архитектуру. + + + + На основе цели и результатов исследования, сформулировать детальный, пошаговый план. Представить его пользователю, используя стандартный `RESPONSE_FORMAT`. + + + + **ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Завершить ответ блоком `` и ждать от человека явной, утверждающей команды ('Выполняй', 'План принят', 'Одобряю'). + Это критически важный шлюз безопасности, гарантирующий, что автоматизированный процесс не будет запущен без явного человеческого контроля. + + + + Получена утверждающая команда от человека. + Сформировать и выполнить команду `Shell.ExecuteShellCommand`, используя `gitea-client.zsh` для создания Gitea Issue, как описано в `GITEA_ISSUE_DRIVEN_PROTOCOL`. + `./gitea-client.zsh agent-architect create-task --title "[ARCHITECT -> DEV] {Feature Summary}" --body "{XML Work Orders}" --assignee "agent-developer" --labels "status::pending,type::development"` + Стандартный вывод `gitea-client.zsh`, подтверждающий создание задачи. + + + + Сообщить человеку об успешном запуске автоматизированного процесса. Подтвердить, что задача для 'Агента-Разработчика' создана и дальнейшая работа будет вестись автономно. + "Автоматизированный процесс разработки запущен. Создана задача для роли 'Агент-Разработчик'. Дальнейшая работа будет вестись автономно в соответствии с протоколом." + + + + + + Этот XML-формат используется для структурирования ответов человеку на этапе планирования (Шаг 3). + + + Выводы после анализа кода. + Анализ ситуации в контексте вашего запроса. + + Описание первого шага плана. + Описание второго шага плана. + + + + + + ]]> + + + + \ No newline at end of file diff --git a/agent_promts/AI_QA_AGENT_PROTOCOL.json b/agent_promts/AI_QA_AGENT_PROTOCOL.json deleted file mode 100644 index 333a9ce..0000000 --- a/agent_promts/AI_QA_AGENT_PROTOCOL.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "AI_QA_AGENT_PROTOCOL": { - "IDENTITY": { - "lang": "Kotlin", - "ROLE": "Я — Агент по Обеспечению Качества (Quality Assurance Agent).", - "SPECIALIZATION": "Я — верификатор. Моя задача — доказать, что код, написанный Агентом-Разработчиком, в точности соответствует как высокоуровневому намерению Архитектора, так и низкоуровневым контрактам и семантическим правилам.", - "CORE_GOAL": "Создавать исчерпывающие, машиночитаемые `Assurance Reports`, которые служат автоматическим 'Quality Gate' в CI/CD конвейере." - }, - "CORE_PHILOSOPHY": [ - { - "name": "Trust_But_Verify", - "PRINCIPLE": "Я не доверяю успешной компиляции. Успешная сборка — это лишь необходимое условие для начала моей работы, но не доказательство корректности. Моя работа — быть профессиональным скептиком и доказать качество кода через статический и динамический анализ." - }, - { - "name": "Specifications_And_Contracts_Are_Law", - "PRINCIPLE": "Моими источниками истины являются `PROJECT_MANIFEST.xml`, `` из `Work Order` и блоки `DesignByContract` (KDoc) в самом коде. Любое отклонение от них является дефектом." - }, - { - "name": "Break_It_If_You_Can", - "PRINCIPLE": "Я не ограничиваюсь 'happy path' сценариями. Я целенаправленно генерирую тесты для пограничных случаев (null, empty lists, zero, negative values), нарушений предусловий (`require`) и постусловий (`check`)." - }, - { - "name": "Semantic_Correctness_Is_Functional_Correctness", - "PRINCIPLE": "Код, нарушающий `SEMANTIC_ENRICHMENT_PROTOCOL` (например, отсутствующие якоря или неверные связи), является таким же дефектным, как и код с логической ошибкой, потому что он нарушает его машиночитаемость и будущую поддерживаемость." - } - ], - "PRIMARY_DIRECTIVE": "Твоя задача — получить на вход `Work Order` из очереди `tasks/pending_qa/`, провести трехфазный аудит соответствующего кода и сгенерировать `Assurance Report`. На основе отчета ты либо перемещаешь `Work Order` в `tasks/completed/`, либо возвращаешь его в `tasks/pending/` с прикрепленным отчетом о дефектах для исправления Агентом-Разработчиком.", - "MASTER_WORKFLOW": { - "name": "Three_Phase_Audit_Cycle", - "STEP": [ - { - "id": "1", - "name": "Context_Loading", - "ACTION": [ - "1. Найти и прочитать первый `Work Order` из директории `tasks/pending_qa/`.", - "2. Загрузить глобальный контекст `tech_spec/PROJECT_MANIFEST.xml`.", - "3. Прочитать актуальное содержимое кода из файла, указанного в ``." - ] - }, - { - "id": "2", - "name": "Phase 1: Static Semantic Audit", - "DESCRIPTION": "Проверка на соответствие семантическим правилам без запуска кода.", - "ACTION": [ - "1. Проверить код на полное соответствие `SEMANTIC_ENRICHMENT_PROTOCOL`.", - "2. Убедиться, что все сущности (`[ENTITY]`) и связи (`[RELATION]`) корректно размечены и соответствуют логике кода.", - "3. Проверить соблюдение таксономии в якоре `[SEMANTICS]`.", - "4. Проверить наличие и корректность KDoc-контрактов для всех публичных сущностей.", - "5. Собрать все найденные нарушения в секцию `semantic_audit_findings`." - ] - }, - { - "id": "3", - "name": "Phase 2: Unit Test Generation & Execution", - "DESCRIPTION": "Динамическая проверка функциональной корректности на основе контрактов и критериев приемки.", - "ACTION": [ - "1. **Сгенерировать тесты на основе контрактов:** Для каждой публичной функции прочитать ее KDoc (`@param`, `@return`, `@throws`) и сгенерировать unit-тесты (например, с использованием Kotest), которые проверяют эти контракты:", - " - Тесты для 'happy path', проверяющие постусловия (`@return`).", - " - Тесты, передающие невалидные данные, которые должны вызывать исключения, описанные в `@throws`.", - " - Тесты для пограничных случаев (null, empty, zero).", - "2. **Сгенерировать тесты на основе критериев приемки:** Прочитать каждый тег `` из `` в `Work Order` и сгенерировать соответствующий ему бизнес-ориентированный тест.", - "3. Сохранить сгенерированные тесты во временный тестовый файл.", - "4. **Выполнить все сгенерированные тесты** и собрать результаты (успех/провал, сообщения об ошибках).", - "5. Собрать все проваленные тесты в секцию `unit_test_findings`." - ] - }, - { - "id": "4", - "name": "Phase 3: Integration & Regression Analysis", - "DESCRIPTION": "Проверка влияния изменений на остальную часть системы.", - "ACTION": [ - "1. Проанализировать `[RELATION]` якоря в измененном коде, чтобы определить, какие другие сущности от него зависят (кто его `CALLS`, `CONSUMES_STATE`, etc.).", - "2. Используя `PROJECT_MANIFEST.xml`, найти существующие тесты для этих зависимых сущностей.", - "3. Запустить эти регрессионные тесты.", - "4. Собрать все проваленные регрессионные тесты в секцию `regression_findings`." - ] - }, - { - "id": "5", - "name": "Generate_Assurance_Report_And_Finalize", - "ACTION": [ - "1. Собрать результаты всех трех фаз в единый `Assurance Report` согласно схеме `ASSURANCE_REPORT_SCHEMA`.", - "2. **Если `overall_status` в отчете == 'PASSED':**", - " a. Изменить статус в файле `Work Order` на `status=\"completed\"`.", - " b. Переместить файл `Work Order` в `tasks/completed/`.", - " c. Залогировать успешное прохождение QA.", - "3. **Если `overall_status` в отчете == 'FAILED':**", - " a. Изменить статус в файле `Work Order` на `status=\"pending\"`.", - " b. Добавить в XML `Work Order` новую секцию `` с полным содержимым `Assurance Report`.", - " c. Переместить файл `Work Order` обратно в `tasks/pending/` для исправления Агентом-Разработчиком.", - " d. Залогировать провал QA с указанием количества дефектов." - ] - } - ] - }, - "ASSURANCE_REPORT_SCHEMA": { - "name": "The_Assurance_Report_File", - "DESCRIPTION": "Строгий формат для отчета о качестве. Является моим главным артефактом.", - "STRUCTURE": "\n\n \n intent-unique-id\n path/to/file.kt\n {ISO_DATETIME}\n PASSED | FAILED\n \n \n \n \n com.example.MyClass:42\n Отсутствует обязательный замыкающий якорь [END_ENTITY] для класса 'MyClass'.\n SemanticLintingCompliance.EntityContainerization\n \n \n \n\n \n \n GeneratedTest: 'validatePassword'\n Тест на основе Acceptance Criterion 'AC-1' провален. Ожидалась ошибка 'TooShort' для пароля '123', но результат был 'Valid'.\n WorkOrder.ACCEPTANCE_CRITERIA[AC-1]\n \n \n \n \n \n \n ExistingTest: 'LoginViewModelTest'\n Регрессионный тест 'testSuccessfulLogin' провален. Вероятно, изменения в 'validatePassword' повлияли на логику ViewModel.\n LoginViewModel\n \n \n \n" - }, - "UPDATED_WORK_ORDER_SCHEMA": { - "name": "Work_Order_With_Defect_Report", - "DESCRIPTION": "Пример того, как `Work Order` возвращается Агенту-Разработчику в случае провала QA.", - "STRUCTURE": "\n FIX_DEFECTS\n path/to/file.kt\n \n \n \n \n \n \n \n \n" - } - } -} \ No newline at end of file diff --git a/agent_promts/GITEA_ISSUE_DRIVEN_PROTOCOL.xml b/agent_promts/GITEA_ISSUE_DRIVEN_PROTOCOL.xml new file mode 100644 index 0000000..9192132 --- /dev/null +++ b/agent_promts/GITEA_ISSUE_DRIVEN_PROTOCOL.xml @@ -0,0 +1,85 @@ + + + Определить единый, отказоустойчивый и полностью автоматизированный протокол для межагентной коммуникации, основанный на использовании высокоуровневого клиента 'gitea-client.zsh'. + 4.0 + + + + + **КЛЮЧЕВОЕ ИЗМЕНЕНИЕ:** Все взаимодействия с Gitea **ОБЯЗАНЫ** осуществляться исключительно через `gitea-client.zsh`. Прямые вызовы `tea` или `git` в рамках жизненного цикла задачи запрещены, чтобы гарантировать предсказуемость и централизованное управление логикой. + + + Клиент `gitea-client.zsh` автоматически определяет репозиторий (`{repo_slug}`) при инициализации. Агентам не нужно управлять этим состоянием. Роль (`{role_name}`) передается как первый аргумент при каждом вызове. + + + Человек взаимодействует с системой исключительно через диалог с Агентом-Архитектором, который инициирует весь воркфлоу. + + + Конечным продуктом работы Агента-Разработчика является формальный Pull Request (PR), который является основой для проверки и слияния. + + + + + `./gitea-client.zsh {role_name} {command} [options]` + + `create-task --title "..." --body "..." --assignee "..." --labels "..."` + Создание новой задачи в Gitea. + + + `find-tasks --type "{label_name}"` + Поиск открытых задач с нужным типом и статусом 'pending'. + + + `update-task-status --issue-id ID --old "{label}" --new "{label}"` + Атомарное изменение статуса задачи (например, с 'pending' на 'in-progress'). + + + `create-pr --title "..." --body "..." --head "{branch}" --base "{target_branch}"` + Создание Pull Request. + + + `merge-and-complete --issue-id ID --pr-id ID --branch "{branch_to_delete}"` + Атомарная операция: слияние PR, удаление ветки и закрытие связанной задачи. + + + `return-to-dev --issue-id ID --pr-id ID --report "{defect_report_text}"` + Атомарная операция: отклонение PR, добавление комментария с отчетом и переназначение задачи разработчику. + + + + + + 1. Архитектор, после согласования с человеком, создает задачу для Разработчика. + `./gitea-client.zsh agent-architect create-task --title "Реализовать модуль X" --body "..." --assignee "agent-developer" --labels "type::development,status::pending"` + + + + 1. Разработчик находит назначенную ему задачу. + `./gitea-client.zsh agent-developer find-tasks --type "type::development"` + 2. Берет задачу в работу. + `./gitea-client.zsh agent-developer update-task-status --issue-id {issue-id} --old "status::pending" --new "status::in-progress"` + 3. После написания кода и локальных тестов создает Pull Request. + `./gitea-client.zsh agent-developer create-pr --title "feat: Реализован модуль X" --body "Closes #{issue-id}" --head "feature/{issue-id}-module-x"` + 4. Создает задачу для QA-агента, передавая ему контекст (ID задачи и PR). + `./gitea-client.zsh agent-developer create-task --title "QA: Проверить реализацию модуля X" --body "PR: #{pr-id}\nIssue: #{issue-id}" --assignee "agent-qa" --labels "type::quality-assurance,status::pending"` + + + + 1. QA-Агент находит свою задачу. + `./gitea-client.zsh agent-qa find-tasks --type "type::quality-assurance"` + 2. Берет задачу в работу. + `./gitea-client.zsh agent-qa update-task-status --issue-id {qa-issue-id} --old "status::pending" --new "status::in-progress"` + 3. Извлекает `PULL_REQUEST_ID` и `DEVELOPER_ISSUE_ID` из тела задачи и проводит аудит кода. + + + Выполняет единую команду для слияния PR, удаления ветки и закрытия исходной задачи разработчика. + `./gitea-client.zsh agent-qa merge-and-complete --issue-id {developer-issue-id} --pr-id {pr-id} --branch "feature/{issue-id}-module-x"` + + + + Выполняет единую команду для отклонения PR и возврата задачи разработчику с отчетом. + `./gitea-client.zsh agent-qa return-to-dev --issue-id {developer-issue-id} --pr-id {pr-id} --report "Найдены следующие дефекты: ..."` + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1baebf9..7b549c7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -87,6 +87,10 @@ dependencies { // [DEPENDENCY] Testing testImplementation(Libs.junit) + testImplementation(Libs.kotestRunnerJunit5) + testImplementation(Libs.kotestAssertionsCore) + testImplementation(Libs.mockk) + testImplementation("app.cash.turbine:turbine:1.1.0") androidTestImplementation(Libs.extJunit) androidTestImplementation(Libs.espressoCore) androidTestImplementation(platform(Libs.composeBom)) diff --git a/app/src/main/java/com/homebox/lens/MainActivity.kt b/app/src/main/java/com/homebox/lens/MainActivity.kt index 2203e89..0752d4c 100644 --- a/app/src/main/java/com/homebox/lens/MainActivity.kt +++ b/app/src/main/java/com/homebox/lens/MainActivity.kt @@ -1,7 +1,6 @@ // [PACKAGE] com.homebox.lens // [FILE] MainActivity.kt -// [SEMANTICS] android, activity, compose, hilt - +// [SEMANTICS] ui, activity, entrypoint package com.homebox.lens // [IMPORTS] @@ -18,33 +17,26 @@ import androidx.compose.ui.tooling.preview.Preview import com.homebox.lens.navigation.NavGraph import com.homebox.lens.ui.theme.HomeboxLensTheme import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Activity('MainActivity')] -// [RELATION: Activity('MainActivity') -> [INHERITS_FROM] -> Class('ComponentActivity')] -// [RELATION: Activity('MainActivity') -> [DEPENDS_ON] -> Annotation('AndroidEntryPoint')] /** - * [ENTITY: Activity('MainActivity')] - * [PURPOSE] Главная и единственная Activity в приложении. + * @summary Главная и единственная Activity в приложении. */ @AndroidEntryPoint class MainActivity : ComponentActivity() { // [ENTITY: Function('onCreate')] - // [RELATION: Function('onCreate') -> [CALLS] -> Function('super.onCreate')] - // [RELATION: Function('onCreate') -> [CALLS] -> Function('setContent')] - // [RELATION: Function('onCreate') -> [CALLS] -> Function('HomeboxLensTheme')] - // [RELATION: Function('onCreate') -> [CALLS] -> Function('Surface')] - // [RELATION: Function('onCreate') -> [CALLS] -> Function('NavGraph')] - // [LIFECYCLE] + // [RELATION: Function('onCreate')] -> [CALLS] -> [Function('HomeboxLensTheme')] + // [RELATION: Function('onCreate')] -> [CALLS] -> [Function('NavGraph')] override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.") setContent { HomeboxLensTheme { - // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, + color = MaterialTheme.colorScheme.background ) { NavGraph() } @@ -56,23 +48,16 @@ class MainActivity : ComponentActivity() { // [END_ENTITY: Activity('MainActivity')] // [ENTITY: Function('Greeting')] -// [RELATION: Function('Greeting') -> [CALLS] -> Function('Text')] @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')] // [ENTITY: Function('GreetingPreview')] -// [RELATION: Function('GreetingPreview') -> [CALLS] -> Function('HomeboxLensTheme')] -// [RELATION: Function('GreetingPreview') -> [CALLS] -> Function('Greeting')] -// [PREVIEW] @Preview(showBackground = true) @Composable fun GreetingPreview() { @@ -82,5 +67,4 @@ fun GreetingPreview() { } // [END_ENTITY: Function('GreetingPreview')] -// [END_CONTRACT] // [END_FILE_MainActivity.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/MainApplication.kt b/app/src/main/java/com/homebox/lens/MainApplication.kt index 1142c55..bdb7afb 100644 --- a/app/src/main/java/com/homebox/lens/MainApplication.kt +++ b/app/src/main/java/com/homebox/lens/MainApplication.kt @@ -1,7 +1,6 @@ // [PACKAGE] com.homebox.lens // [FILE] MainApplication.kt -// [SEMANTICS] android, application, hilt, timber - +// [SEMANTICS] application, hilt, timber package com.homebox.lens // [IMPORTS] @@ -10,30 +9,22 @@ import dagger.hilt.android.HiltAndroidApp import timber.log.Timber // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Application('MainApplication')] -// [RELATION: Application('MainApplication') -> [INHERITS_FROM] -> Class('Application')] -// [RELATION: Application('MainApplication') -> [DEPENDS_ON] -> Annotation('HiltAndroidApp')] /** - * [ENTITY: Application('MainApplication')] - * [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber. + * @summary Точка входа в приложение. Инициализирует Hilt и Timber. */ @HiltAndroidApp class MainApplication : Application() { + // [ENTITY: Function('onCreate')] - // [RELATION: Function('onCreate') -> [CALLS] -> Function('super.onCreate')] - // [RELATION: Function('onCreate') -> [CALLS] -> Function('Timber.plant')] - // [LIFECYCLE] override fun onCreate() { super.onCreate() - // [ACTION] Initialize Timber for logging if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) + Timber.d("[DEBUG][INITIALIZATION][timber_planted] Timber DebugTree planted.") } } // [END_ENTITY: Function('onCreate')] } // [END_ENTITY: Application('MainApplication')] - -// [END_CONTRACT] // [END_FILE_MainApplication.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt index 91b6247..87444cc 100644 --- a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt +++ b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt @@ -9,74 +9,48 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.compose.runtime.collectAsState -import com.homebox.lens.domain.model.Item +import androidx.navigation.navArgument import com.homebox.lens.ui.screen.dashboard.DashboardScreen import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen -import com.homebox.lens.ui.screen.inventorylist.InventoryListViewModel import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen -import com.homebox.lens.ui.screen.itemdetails.ItemDetailsViewModel import com.homebox.lens.ui.screen.itemedit.ItemEditScreen -import com.homebox.lens.ui.screen.itemedit.ItemEditViewModel -import com.homebox.lens.ui.screen.labelslist.labelsListScreen -import com.homebox.lens.ui.screen.labelslist.LabelsListViewModel +import com.homebox.lens.ui.screen.labelslist.LabelsListScreen import com.homebox.lens.ui.screen.locationedit.LocationEditScreen import com.homebox.lens.ui.screen.locationslist.LocationsListScreen import com.homebox.lens.ui.screen.search.SearchScreen -import com.homebox.lens.ui.screen.search.SearchViewModel import com.homebox.lens.ui.screen.setup.SetupScreen -import timber.log.Timber // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Function('NavGraph')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('rememberNavController')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('currentBackStackEntryAsState')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('remember')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('hiltViewModel')] -// [RELATION: Function('NavGraph') -> [CREATES_INSTANCE_OF] -> Class('NavigationActions')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('NavHost')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('composable')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('SetupScreen')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('DashboardScreen')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('InventoryListScreen')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('ItemDetailsScreen')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('ItemEditScreen')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LabelsListScreen')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LocationsListScreen')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LocationEditScreen')] -// [RELATION: Function('NavGraph') -> [CALLS] -> Function('SearchScreen')] +// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')] +// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')] /** - * [CONTRACT] - * Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation. + * @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation. * @param navController Контроллер навигации. * @see Screen * @sideeffect Регистрирует все экраны и управляет состоянием навигации. * @invariant Стартовый экран - `Screen.Setup`. */ @Composable -fun NavGraph(navController: NavHostController = rememberNavController()) { - // [STATE] +fun NavGraph( + navController: NavHostController = rememberNavController() +) { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route - // [HELPER] - val navigationActions = - remember(navController) { - NavigationActions(navController) - } + val navigationActions = remember(navController) { + NavigationActions(navController) + } - // [ACTION] NavHost( navController = navController, - startDestination = Screen.Setup.route, + startDestination = Screen.Setup.route ) { - // [ENTITY: Composable('Screen.Setup.route')] composable(route = Screen.Setup.route) { SetupScreen(onSetupComplete = { navController.navigate(Screen.Dashboard.route) { @@ -84,113 +58,65 @@ fun NavGraph(navController: NavHostController = rememberNavController()) { } }) } - // [END_ENTITY: Composable('Screen.Setup.route')] - // [ENTITY: Composable('Screen.Dashboard.route')] composable(route = Screen.Dashboard.route) { DashboardScreen( currentRoute = currentRoute, - navigationActions = navigationActions, + navigationActions = navigationActions ) } - // [END_ENTITY: Composable('Screen.Dashboard.route')] - // [ENTITY: Composable('Screen.InventoryList.route')] - composable(route = Screen.InventoryList.route) { backStackEntry -> - val viewModel: InventoryListViewModel = hiltViewModel(backStackEntry) + composable(route = Screen.InventoryList.route) { InventoryListScreen( - onItemClick = { item -> - // TODO: Navigate to item details - Timber.i("[UI] Item clicked: ${item.name}") - }, - onNavigateBack = { - navController.popBackStack() - } + currentRoute = currentRoute, + navigationActions = navigationActions ) } - // [END_ENTITY: Composable('Screen.InventoryList.route')] - // [ENTITY: Composable('Screen.ItemDetails.route')] - composable(route = Screen.ItemDetails.route) { backStackEntry -> - val viewModel: ItemDetailsViewModel = hiltViewModel(backStackEntry) + composable(route = Screen.ItemDetails.route) { ItemDetailsScreen( - onNavigateBack = { - navController.popBackStack() - }, - onEditClick = { itemId -> - // TODO: Navigate to item edit screen - Timber.i("[UI] Edit item clicked: $itemId") - } + currentRoute = currentRoute, + navigationActions = navigationActions ) } - // [END_ENTITY: Composable('Screen.ItemDetails.route')] - // [ENTITY: Composable('Screen.ItemEdit.route')] - composable(route = Screen.ItemEdit.route) { backStackEntry -> - val viewModel: ItemEditViewModel = hiltViewModel(backStackEntry) + composable( + route = Screen.ItemEdit.route, + arguments = listOf(navArgument("itemId") { nullable = true }) + ) { backStackEntry -> + val itemId = backStackEntry.arguments?.getString("itemId") ItemEditScreen( - onNavigateBack = { - navController.popBackStack() - } + currentRoute = currentRoute, + navigationActions = navigationActions, + itemId = itemId, + onSaveSuccess = { navController.popBackStack() } ) } - // [END_ENTITY: Composable('Screen.ItemEdit.route')] - // [ENTITY: Composable('Screen.LabelsList.route')] - composable(Screen.LabelsList.route) { backStackEntry -> - val viewModel: LabelsListViewModel = hiltViewModel(backStackEntry) - val uiState by viewModel.uiState.collectAsState() - - labelsListScreen( - uiState = uiState, - onLabelClick = { label -> - // TODO: Implement navigation to label details screen - Timber.i("[UI] Label clicked: ${label.name}") - }, - onAddClick = { - // TODO: Implement navigation to add new label screen - Timber.i("[UI] Add new label clicked") - }, - onNavigateBack = { - navController.popBackStack() - } - ) + composable(Screen.LabelsList.route) { + LabelsListScreen(navController = navController) } - // [END_ENTITY: Composable('Screen.LabelsList.route')] - // [ENTITY: Composable('Screen.LocationsList.route')] composable(route = Screen.LocationsList.route) { LocationsListScreen( currentRoute = currentRoute, navigationActions = navigationActions, onLocationClick = { locationId -> - // TODO: Navigate to a pre-filtered inventory list screen + // [AI_NOTE]: Navigate to a pre-filtered inventory list screen navController.navigate(Screen.InventoryList.route) }, onAddNewLocationClick = { navController.navigate(Screen.LocationEdit.createRoute("new")) - }, - ) - } - // [END_ENTITY: Composable('Screen.LocationsList.route')] - // [ENTITY: Composable('Screen.LocationEdit.route')] - composable(route = Screen.LocationEdit.route) { backStackEntry -> - val locationId = backStackEntry.arguments?.getString("locationId") - LocationEditScreen( - locationId = locationId, - ) - } - // [END_ENTITY: Composable('Screen.LocationEdit.route')] - // [ENTITY: Composable('Screen.Search.route')] - composable(route = Screen.Search.route) { backStackEntry -> - val viewModel: SearchViewModel = hiltViewModel(backStackEntry) - SearchScreen( - onNavigateBack = { - navController.popBackStack() - }, - onItemClick = { item -> - // TODO: Navigate to item details - Timber.i("[UI] Search result item clicked: ${item.name}") } ) } - // [END_ENTITY: Composable('Screen.Search.route')] + composable(route = Screen.LocationEdit.route) { backStackEntry -> + val locationId = backStackEntry.arguments?.getString("locationId") + LocationEditScreen( + locationId = locationId + ) + } + composable(route = Screen.Search.route) { + SearchScreen( + currentRoute = currentRoute, + navigationActions = navigationActions + ) + } } } // [END_ENTITY: Function('NavGraph')] -// [END_CONTRACT] // [END_FILE_NavGraph.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt index 3ffe4a8..056d19a 100644 --- a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt +++ b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt @@ -5,32 +5,26 @@ package com.homebox.lens.navigation // [IMPORTS] import androidx.navigation.NavHostController +import timber.log.Timber // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Class('NavigationActions')] -// [RELATION: Class('NavigationActions') -> [DEPENDS_ON] -> Class('NavHostController')] +// [RELATION: Class('NavigationActions')] -> [DEPENDS_ON] -> [Framework('NavHostController')] /** - * [CONTRACT] * @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий. * @param navController Контроллер Jetpack Navigation. * @invariant Все навигационные действия должны использовать предоставленный navController. */ class NavigationActions(private val navController: NavHostController) { + // [ENTITY: Function('navigateToDashboard')] - // [RELATION: Function('navigateToDashboard') -> [CALLS] -> Function('navController.navigate')] - // [RELATION: Function('navigateToDashboard') -> [CALLS] -> Function('Screen.Dashboard.route')] - // [RELATION: Function('navigateToDashboard') -> [CALLS] -> Function('popUpTo')] - // [ACTION] /** - * [CONTRACT] * @summary Навигация на главный экран. * @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов. */ fun navigateToDashboard() { + Timber.i("[INFO][ACTION][navigate_to_dashboard] Navigating to Dashboard.") navController.navigate(Screen.Dashboard.route) { - // Используем popUpTo для удаления всех экранов до dashboard из back stack - // Это предотвращает создание большой стопки экранов при навигации через drawer popUpTo(navController.graph.startDestinationId) launchSingleTop = true } @@ -38,10 +32,8 @@ class NavigationActions(private val navController: NavHostController) { // [END_ENTITY: Function('navigateToDashboard')] // [ENTITY: Function('navigateToLocations')] - // [RELATION: Function('navigateToLocations') -> [CALLS] -> Function('navController.navigate')] - // [RELATION: Function('navigateToLocations') -> [CALLS] -> Function('Screen.LocationsList.route')] - // [ACTION] fun navigateToLocations() { + Timber.i("[INFO][ACTION][navigate_to_locations] Navigating to Locations.") navController.navigate(Screen.LocationsList.route) { launchSingleTop = true } @@ -49,10 +41,8 @@ class NavigationActions(private val navController: NavHostController) { // [END_ENTITY: Function('navigateToLocations')] // [ENTITY: Function('navigateToLabels')] - // [RELATION: Function('navigateToLabels') -> [CALLS] -> Function('navController.navigate')] - // [RELATION: Function('navigateToLabels') -> [CALLS] -> Function('Screen.LabelsList.route')] - // [ACTION] fun navigateToLabels() { + Timber.i("[INFO][ACTION][navigate_to_labels] Navigating to Labels.") navController.navigate(Screen.LabelsList.route) { launchSingleTop = true } @@ -60,10 +50,8 @@ class NavigationActions(private val navController: NavHostController) { // [END_ENTITY: Function('navigateToLabels')] // [ENTITY: Function('navigateToSearch')] - // [RELATION: Function('navigateToSearch') -> [CALLS] -> Function('navController.navigate')] - // [RELATION: Function('navigateToSearch') -> [CALLS] -> Function('Screen.Search.route')] - // [ACTION] fun navigateToSearch() { + Timber.i("[INFO][ACTION][navigate_to_search] Navigating to Search.") navController.navigate(Screen.Search.route) { launchSingleTop = true } @@ -71,39 +59,31 @@ class NavigationActions(private val navController: NavHostController) { // [END_ENTITY: Function('navigateToSearch')] // [ENTITY: Function('navigateToInventoryListWithLabel')] - // [RELATION: Function('navigateToInventoryListWithLabel') -> [CALLS] -> Function('Screen.InventoryList.withFilter')] - // [RELATION: Function('navigateToInventoryListWithLabel') -> [CALLS] -> Function('navController.navigate')] - // [ACTION] 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')] - // [RELATION: Function('navigateToInventoryListWithLocation') -> [CALLS] -> Function('Screen.InventoryList.withFilter')] - // [RELATION: Function('navigateToInventoryListWithLocation') -> [CALLS] -> Function('navController.navigate')] - // [ACTION] 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')] - // [RELATION: Function('navigateToCreateItem') -> [CALLS] -> Function('Screen.ItemEdit.createRoute')] - // [RELATION: Function('navigateToCreateItem') -> [CALLS] -> Function('navController.navigate')] - // [ACTION] fun navigateToCreateItem() { + Timber.i("[INFO][ACTION][navigate_to_create_item] Navigating to Create Item.") navController.navigate(Screen.ItemEdit.createRoute("new")) } // [END_ENTITY: Function('navigateToCreateItem')] // [ENTITY: Function('navigateToLogout')] - // [RELATION: Function('navigateToLogout') -> [CALLS] -> Function('navController.navigate')] - // [RELATION: Function('navigateToLogout') -> [CALLS] -> Function('popUpTo')] - // [ACTION] fun navigateToLogout() { + Timber.i("[INFO][ACTION][navigate_to_logout] Navigating to Logout.") navController.navigate(Screen.Setup.route) { popUpTo(Screen.Dashboard.route) { inclusive = true } } @@ -111,13 +91,11 @@ class NavigationActions(private val navController: NavHostController) { // [END_ENTITY: Function('navigateToLogout')] // [ENTITY: Function('navigateBack')] - // [RELATION: Function('navigateBack') -> [CALLS] -> Function('navController.popBackStack')] - // [ACTION] fun navigateBack() { + Timber.i("[INFO][ACTION][navigate_back] Navigating back.") navController.popBackStack() } // [END_ENTITY: Function('navigateBack')] } // [END_ENTITY: Class('NavigationActions')] -// [END_CONTRACT] -// [END_FILE_NavigationActions.kt] \ No newline at end of file +// [END_FILE_NavigationActions.kt] diff --git a/app/src/main/java/com/homebox/lens/navigation/Screen.kt b/app/src/main/java/com/homebox/lens/navigation/Screen.kt index 08e11a0..9219c6a 100644 --- a/app/src/main/java/com/homebox/lens/navigation/Screen.kt +++ b/app/src/main/java/com/homebox/lens/navigation/Screen.kt @@ -3,136 +3,106 @@ // [SEMANTICS] navigation, routes, sealed_class package com.homebox.lens.navigation -// [IMPORTS] -// [END_IMPORTS] - -// [CONTRACT] // [ENTITY: SealedClass('Screen')] /** - * [CONTRACT] - * Запечатанный класс для определения маршрутов навигации в приложении. - * Обеспечивает типобезопасность при навигации. - * @property route Строковый идентификатор маршрута. + * @summary Запечатанный класс для определения маршрутов навигации в приложении. + * @description Обеспечивает типобезопасность при навигации. + * @param route Строковый идентификатор маршрута. */ sealed class Screen(val route: String) { - // [ENTITY: DataObject('Setup')] + // [ENTITY: Object('Setup')] data object Setup : Screen("setup_screen") - // [END_ENTITY: DataObject('Setup')] + // [END_ENTITY: Object('Setup')] - // [ENTITY: DataObject('Dashboard')] + // [ENTITY: Object('Dashboard')] data object Dashboard : Screen("dashboard_screen") - // [END_ENTITY: DataObject('Dashboard')] + // [END_ENTITY: Object('Dashboard')] - // [ENTITY: DataObject('InventoryList')] + // [ENTITY: Object('InventoryList')] data object InventoryList : Screen("inventory_list_screen") { // [ENTITY: Function('withFilter')] /** - * [CONTRACT] - * Создает маршрут для экрана списка инвентаря с параметром фильтра. + * @summary Создает маршрут для экрана списка инвентаря с параметром фильтра. * @param key Ключ фильтра (например, "label" или "location"). * @param value Значение фильтра (например, ID метки или местоположения). * @return Строку полного маршрута с query-параметром. * @throws IllegalArgumentException если ключ или значение пустые. - * @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }'). */ - fun withFilter( - key: String, - value: String, - ): String { - // [PRECONDITION] - require(key.isNotBlank()) { "[PRECONDITION_FAILED] Filter key cannot be blank." } - require(value.isNotBlank()) { "[PRECONDITION_FAILED] Filter value cannot be blank." } - // [ACTION] + 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" - // [POSTCONDITION] - check(constructedRoute.contains("?$key=$value")) { "[POSTCONDITION_FAILED] Route must contain the filter query." } + check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." } return constructedRoute } // [END_ENTITY: Function('withFilter')] } - // [END_ENTITY: DataObject('InventoryList')] + // [END_ENTITY: Object('InventoryList')] - // [ENTITY: DataObject('ItemDetails')] + // [ENTITY: Object('ItemDetails')] data object ItemDetails : Screen("item_details_screen/{itemId}") { // [ENTITY: Function('createRoute')] /** - * [CONTRACT] - * Создает маршрут для экрана деталей элемента с указанным ID. + * @summary Создает маршрут для экрана деталей элемента с указанным ID. * @param itemId ID элемента для отображения. * @return Строку полного маршрута. * @throws IllegalArgumentException если itemId пустой. */ fun createRoute(itemId: String): String { - // [PRECONDITION] - require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." } - // [ACTION] + require(itemId.isNotBlank()) { "itemId не может быть пустым." } val route = "item_details_screen/$itemId" - // [POSTCONDITION] - check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." } + check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." } return route } // [END_ENTITY: Function('createRoute')] } - // [END_ENTITY: DataObject('ItemDetails')] + // [END_ENTITY: Object('ItemDetails')] - // [ENTITY: DataObject('ItemEdit')] - data object ItemEdit : Screen("item_edit_screen/{itemId}") { + // [ENTITY: Object('ItemEdit')] + data object ItemEdit : Screen("item_edit_screen?itemId={itemId}") { // [ENTITY: Function('createRoute')] /** - * [CONTRACT] - * Создает маршрут для экрана редактирования элемента с указанным ID. - * @param itemId ID элемента для редактирования. + * @summary Создает маршрут для экрана редактирования элемента с указанным ID. + * @param itemId ID элемента для редактирования. Null, если создается новый элемент. * @return Строку полного маршрута. - * @throws IllegalArgumentException если itemId пустой. */ - fun createRoute(itemId: String): String { - // [PRECONDITION] - require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." } - // [ACTION] - val route = "item_edit_screen/$itemId" - // [POSTCONDITION] - check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." } - return route + fun createRoute(itemId: String? = null): String { + return itemId?.let { "item_edit_screen?itemId=$it" } ?: "item_edit_screen" } // [END_ENTITY: Function('createRoute')] } - // [END_ENTITY: DataObject('ItemEdit')] + // [END_ENTITY: Object('ItemEdit')] - // [ENTITY: DataObject('LabelsList')] + // [ENTITY: Object('LabelsList')] data object LabelsList : Screen("labels_list_screen") - // [END_ENTITY: DataObject('LabelsList')] + // [END_ENTITY: Object('LabelsList')] - // [ENTITY: DataObject('LocationsList')] + // [ENTITY: Object('LocationsList')] data object LocationsList : Screen("locations_list_screen") - // [END_ENTITY: DataObject('LocationsList')] + // [END_ENTITY: Object('LocationsList')] - // [ENTITY: DataObject('LocationEdit')] + // [ENTITY: Object('LocationEdit')] data object LocationEdit : Screen("location_edit_screen/{locationId}") { // [ENTITY: Function('createRoute')] /** - * [CONTRACT] - * Создает маршрут для экрана редактирования местоположения с указанным ID. + * @summary Создает маршрут для экрана редактирования местоположения с указанным ID. * @param locationId ID местоположения для редактирования. * @return Строку полного маршрута. * @throws IllegalArgumentException если locationId пустой. */ fun createRoute(locationId: String): String { - // [PRECONDITION] - require(locationId.isNotBlank()) { "[PRECONDITION_FAILED] locationId не может быть пустым." } - // [ACTION] + require(locationId.isNotBlank()) { "locationId не может быть пустым." } val route = "location_edit_screen/$locationId" - // [POSTCONDITION] - check(route.endsWith(locationId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на locationId." } + check(route.endsWith(locationId)) { "Маршрут должен заканчиваться на locationId." } return route } // [END_ENTITY: Function('createRoute')] } - // [END_ENTITY: DataObject('LocationEdit')] + // [END_ENTITY: Object('LocationEdit')] - // [ENTITY: DataObject('Search')] + // [ENTITY: Object('Search')] data object Search : Screen("search_screen") - // [END_ENTITY: DataObject('Search')] + // [END_ENTITY: Object('Search')] } // [END_ENTITY: SealedClass('Screen')] -// [END_CONTRACT] -// [END_FILE_Screen.kt] \ No newline at end of file +// [END_FILE_Screen.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt index 7b95ef9..1cc14fe 100644 --- a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt +++ b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt @@ -27,25 +27,9 @@ import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.Screen // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Function('AppDrawerContent')] -// [RELATION: Function('AppDrawerContent') -> [DEPENDS_ON] -> Class('NavigationActions')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('ModalDrawerSheet')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Spacer')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Button')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Icon')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Text')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Divider')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('NavigationDrawerItem')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.Dashboard.route')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.LocationsList.route')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.LabelsList.route')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.Search.route')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.ItemEdit.createRoute')] -// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.Setup.route')] +// [RELATION: Function('AppDrawerContent')] -> [DEPENDS_ON] -> [Class('NavigationActions')] /** - * [CONTRACT] * @summary Контент для бокового навигационного меню (Drawer). * @param currentRoute Текущий маршрут для подсветки активного элемента. * @param navigationActions Объект с навигационными действиями. @@ -55,7 +39,7 @@ import com.homebox.lens.navigation.Screen internal fun AppDrawerContent( currentRoute: String?, navigationActions: NavigationActions, - onCloseDrawer: () -> Unit, + onCloseDrawer: () -> Unit ) { ModalDrawerSheet { Spacer(Modifier.height(12.dp)) @@ -64,10 +48,9 @@ internal fun AppDrawerContent( navigationActions.navigateToCreateItem() onCloseDrawer() }, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) ) { Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp)) Spacer(Modifier.width(8.dp)) @@ -81,7 +64,7 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToDashboard() onCloseDrawer() - }, + } ) NavigationDrawerItem( label = { Text(stringResource(id = R.string.nav_locations)) }, @@ -89,7 +72,7 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToLocations() onCloseDrawer() - }, + } ) NavigationDrawerItem( label = { Text(stringResource(id = R.string.nav_labels)) }, @@ -97,7 +80,7 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToLabels() onCloseDrawer() - }, + } ) NavigationDrawerItem( label = { Text(stringResource(id = R.string.search)) }, @@ -105,9 +88,9 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToSearch() onCloseDrawer() - }, + } ) -// TODO: Add Profile and Tools items + // [AI_NOTE]: Add Profile and Tools items Divider() NavigationDrawerItem( label = { Text(stringResource(id = R.string.logout)) }, @@ -115,10 +98,9 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToLogout() onCloseDrawer() - }, + } ) } } // [END_ENTITY: Function('AppDrawerContent')] -// [END_CONTRACT] -// [END_FILE_AppDrawer.kt] \ No newline at end of file +// [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 index d4a98bb..0072a1f 100644 --- a/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt +++ b/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt @@ -17,21 +17,10 @@ import com.homebox.lens.navigation.NavigationActions import kotlinx.coroutines.launch // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Function('MainScaffold')] -// [RELATION: Function('MainScaffold') -> [DEPENDS_ON] -> Class('NavigationActions')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('rememberDrawerState')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('rememberCoroutineScope')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('ModalNavigationDrawer')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('AppDrawerContent')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('Scaffold')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('TopAppBar')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('Text')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('IconButton')] -// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('Icon')] +// [RELATION: Function('MainScaffold')] -> [DEPENDS_ON] -> [Class('NavigationActions')] +// [RELATION: Function('MainScaffold')] -> [CALLS] -> [Function('AppDrawerContent')] /** - * [CONTRACT] * @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer. * @param topBarTitle Заголовок для TopAppBar. * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. @@ -48,22 +37,20 @@ fun MainScaffold( currentRoute: String?, navigationActions: NavigationActions, topBarActions: @Composable () -> Unit = {}, - content: @Composable (PaddingValues) -> Unit, + content: @Composable (PaddingValues) -> Unit ) { - // [STATE] val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() - // [CORE-LOGIC] ModalNavigationDrawer( drawerState = drawerState, drawerContent = { AppDrawerContent( currentRoute = currentRoute, navigationActions = navigationActions, - onCloseDrawer = { scope.launch { drawerState.close() } }, + onCloseDrawer = { scope.launch { drawerState.close() } } ) - }, + } ) { Scaffold( topBar = { @@ -73,19 +60,17 @@ fun MainScaffold( IconButton(onClick = { scope.launch { drawerState.open() } }) { Icon( Icons.Default.Menu, - contentDescription = stringResource(id = R.string.cd_open_navigation_drawer), + contentDescription = stringResource(id = R.string.cd_open_navigation_drawer) ) } }, - actions = { topBarActions() }, + actions = { topBarActions() } ) - }, + } ) { paddingValues -> - // [ACTION] content(paddingValues) } } } // [END_ENTITY: Function('MainScaffold')] -// [END_CONTRACT] // [END_FILE_MainScaffold.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt index a0ca82f..34e3d56 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt @@ -32,19 +32,11 @@ import com.homebox.lens.ui.theme.HomeboxLensTheme import timber.log.Timber // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Function('DashboardScreen')] -// [RELATION: Function('DashboardScreen') -> [DEPENDS_ON] -> Class('DashboardViewModel')] -// [RELATION: Function('DashboardScreen') -> [DEPENDS_ON] -> Class('NavigationActions')] -// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('hiltViewModel')] -// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('collectAsState')] -// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('MainScaffold')] -// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('IconButton')] -// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('Icon')] -// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('DashboardContent')] +// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [ViewModel('DashboardViewModel')] +// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] +// [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')] /** - * [CONTRACT] * @summary Главная Composable-функция для экрана "Панель управления". * @param viewModel ViewModel для этого экрана, предоставляется через Hilt. * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. @@ -55,11 +47,9 @@ import timber.log.Timber fun DashboardScreen( viewModel: DashboardViewModel = hiltViewModel(), currentRoute: String?, - navigationActions: NavigationActions, + navigationActions: NavigationActions ) { - // [STATE] val uiState by viewModel.uiState.collectAsState() - // [UI_COMPONENT] MainScaffold( topBarTitle = stringResource(id = R.string.dashboard_title), currentRoute = currentRoute, @@ -68,41 +58,30 @@ fun DashboardScreen( IconButton(onClick = { navigationActions.navigateToSearch() }) { Icon( Icons.Default.Search, - contentDescription = stringResource(id = R.string.cd_scan_qr_code), // TODO: Rename string resource + contentDescription = stringResource(id = R.string.cd_scan_qr_code) // [AI_NOTE]: Rename string resource ) } - }, + } ) { paddingValues -> DashboardContent( modifier = Modifier.padding(paddingValues), uiState = uiState, onLocationClick = { location -> - Timber.i("[ACTION] Location chip clicked: ${location.id}. Navigating...") + Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Location chip clicked: ${location.id}. Navigating...") navigationActions.navigateToInventoryListWithLocation(location.id) }, onLabelClick = { label -> - Timber.i("[ACTION] Label chip clicked: ${label.id}. Navigating...") + Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Label chip clicked: ${label.id}. Navigating...") navigationActions.navigateToInventoryListWithLabel(label.id) - }, + } ) } } // [END_ENTITY: Function('DashboardScreen')] // [ENTITY: Function('DashboardContent')] -// [RELATION: Function('DashboardContent') -> [DEPENDS_ON] -> SealedInterface('DashboardUiState')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Box')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('CircularProgressIndicator')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Text')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LazyColumn')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Spacer')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('StatisticsSection')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('RecentlyAddedSection')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LocationsSection')] -// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LabelsSection')] +// [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')] /** - * [CONTRACT] * @summary Отображает основной контент экрана в зависимости от uiState. * @param modifier Модификатор для стилизации. * @param uiState Текущее состояние UI экрана. @@ -114,9 +93,8 @@ private fun DashboardContent( modifier: Modifier = Modifier, uiState: DashboardUiState, onLocationClick: (LocationOutCount) -> Unit, - onLabelClick: (LabelOut) -> Unit, + onLabelClick: (LabelOut) -> Unit ) { - // [CORE-LOGIC] when (uiState) { is DashboardUiState.Loading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -128,17 +106,16 @@ private fun DashboardContent( Text( text = uiState.message, color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) } } is DashboardUiState.Success -> { LazyColumn( - modifier = - modifier - .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) ) { item { Spacer(modifier = Modifier.height(8.dp)) } item { StatisticsSection(statistics = uiState.statistics) } @@ -153,17 +130,8 @@ private fun DashboardContent( // [END_ENTITY: Function('DashboardContent')] // [ENTITY: Function('StatisticsSection')] -// [RELATION: Function('StatisticsSection') -> [DEPENDS_ON] -> Class('GroupStatistics')] -// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Column')] -// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Text')] -// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')] -// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Card')] -// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('LazyVerticalGrid')] -// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('GridCells.Fixed')] -// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('StatisticCard')] +// [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')] /** - * [CONTRACT] * @summary Секция для отображения общей статистики. * @param statistics Объект со статистическими данными. */ @@ -172,43 +140,22 @@ private fun StatisticsSection(statistics: GroupStatistics) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = stringResource(id = R.string.dashboard_section_quick_stats), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium ) Card { LazyVerticalGrid( columns = GridCells.Fixed(2), - modifier = - Modifier - .height(120.dp) - .fillMaxWidth() - .padding(16.dp), + modifier = Modifier + .height(120.dp) + .fillMaxWidth() + .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - item { - StatisticCard( - title = stringResource(id = R.string.dashboard_stat_total_items), - value = statistics.items.toString(), - ) - } - item { - StatisticCard( - title = stringResource(id = R.string.dashboard_stat_total_value), - value = statistics.totalValue.toString(), - ) - } - item { - StatisticCard( - title = stringResource(id = R.string.dashboard_stat_total_labels), - value = statistics.labels.toString(), - ) - } - item { - StatisticCard( - title = stringResource(id = R.string.dashboard_stat_total_locations), - value = statistics.locations.toString(), - ) - } + item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) } + item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) } + item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) } + item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) } } } } @@ -216,21 +163,13 @@ private fun StatisticsSection(statistics: GroupStatistics) { // [END_ENTITY: Function('StatisticsSection')] // [ENTITY: Function('StatisticCard')] -// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('Column')] -// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('Text')] -// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('MaterialTheme.typography.labelMedium')] -// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('MaterialTheme.typography.headlineSmall')] /** - * [CONTRACT] * @summary Карточка для отображения одного статистического показателя. * @param title Название показателя. * @param value Значение показателя. */ @Composable -private fun StatisticCard( - title: String, - value: String, -) { +private fun StatisticCard(title: String, value: String) { Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center) Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center) @@ -239,15 +178,8 @@ private fun StatisticCard( // [END_ENTITY: Function('StatisticCard')] // [ENTITY: Function('RecentlyAddedSection')] -// [RELATION: Function('RecentlyAddedSection') -> [DEPENDS_ON] -> Class('ItemSummary')] -// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('Column')] -// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('Text')] -// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')] -// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('LazyRow')] -// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('ItemCard')] +// [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] /** - * [CONTRACT] * @summary Секция для отображения недавно добавленных элементов. * @param items Список элементов для отображения. */ @@ -256,17 +188,16 @@ private fun RecentlyAddedSection(items: List) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = stringResource(id = R.string.dashboard_section_recently_added), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium ) if (items.isEmpty()) { Text( text = stringResource(id = R.string.items_not_found), style = MaterialTheme.typography.bodyMedium, - modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + textAlign = TextAlign.Center ) } else { LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { @@ -280,16 +211,8 @@ private fun RecentlyAddedSection(items: List) { // [END_ENTITY: Function('RecentlyAddedSection')] // [ENTITY: Function('ItemCard')] -// [RELATION: Function('ItemCard') -> [DEPENDS_ON] -> Class('ItemSummary')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Card')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Column')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Spacer')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Text')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('MaterialTheme.typography.titleSmall')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('MaterialTheme.typography.bodySmall')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('stringResource')] +// [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] /** - * [CONTRACT] * @summary Карточка для отображения краткой информации об элементе. * @param item Элемент для отображения. */ @@ -297,50 +220,33 @@ private fun RecentlyAddedSection(items: List) { private fun ItemCard(item: ItemSummary) { Card(modifier = Modifier.width(150.dp)) { Column(modifier = Modifier.padding(8.dp)) { -// TODO: Add image here from item.image - Spacer( - modifier = - Modifier - .height(80.dp) - .fillMaxWidth() - .background(MaterialTheme.colorScheme.secondaryContainer), - ) + // [AI_NOTE]: Add image here from item.image + Spacer(modifier = Modifier + .height(80.dp) + .fillMaxWidth() + .background(MaterialTheme.colorScheme.secondaryContainer)) Spacer(modifier = Modifier.height(8.dp)) Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1) - Text( - text = item.location?.name ?: stringResource(id = R.string.no_location), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - ) + Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1) } } } // [END_ENTITY: Function('ItemCard')] // [ENTITY: Function('LocationsSection')] -// [RELATION: Function('LocationsSection') -> [DEPENDS_ON] -> Class('LocationOutCount')] -// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('Column')] -// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('Text')] -// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')] -// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('FlowRow')] -// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('SuggestionChip')] +// [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] /** - * [CONTRACT] * @summary Секция для отображения местоположений в виде чипсов. * @param locations Список местоположений. * @param onLocationClick Лямбда-обработчик нажатия на местоположение. */ @OptIn(ExperimentalLayoutApi::class) @Composable -private fun LocationsSection( - locations: List, - onLocationClick: (LocationOutCount) -> Unit, -) { +private fun LocationsSection(locations: List, onLocationClick: (LocationOutCount) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = stringResource(id = R.string.dashboard_section_locations), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium ) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -348,7 +254,7 @@ private fun LocationsSection( locations.forEach { location -> SuggestionChip( onClick = { onLocationClick(location) }, - label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) }, + label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) } ) } } @@ -357,29 +263,19 @@ private fun LocationsSection( // [END_ENTITY: Function('LocationsSection')] // [ENTITY: Function('LabelsSection')] -// [RELATION: Function('LabelsSection') -> [DEPENDS_ON] -> Class('LabelOut')] -// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('Column')] -// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('Text')] -// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')] -// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('FlowRow')] -// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('SuggestionChip')] +// [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')] /** - * [CONTRACT] * @summary Секция для отображения меток в виде чипсов. * @param labels Список меток. * @param onLabelClick Лямбда-обработчик нажатия на метку. */ @OptIn(ExperimentalLayoutApi::class) @Composable -private fun LabelsSection( - labels: List, - onLabelClick: (LabelOut) -> Unit, -) { +private fun LabelsSection(labels: List, onLabelClick: (LabelOut) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = stringResource(id = R.string.dashboard_section_labels), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium ) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -387,7 +283,7 @@ private fun LabelsSection( labels.forEach { label -> SuggestionChip( onClick = { onLabelClick(label) }, - label = { Text(label.name) }, + label = { Text(label.name) } ) } } @@ -396,97 +292,42 @@ private fun LabelsSection( // [END_ENTITY: Function('LabelsSection')] // [ENTITY: Function('DashboardContentSuccessPreview')] -// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('DashboardUiState.Success')] -// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('GroupStatistics')] -// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('LocationOutCount')] -// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('LabelOut')] -// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('HomeboxLensTheme')] -// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('DashboardContent')] -// [PREVIEW] @Preview(showBackground = true, name = "Dashboard Success State") @Composable fun DashboardContentSuccessPreview() { - val previewState = - DashboardUiState.Success( - statistics = - GroupStatistics( - items = 123, - totalValue = 9999.99, - locations = 5, - labels = 8, - ), - locations = - listOf( - LocationOutCount( - id = "1", - name = "Office", - color = "#FF0000", - isArchived = false, - itemCount = 10, - createdAt = "", - updatedAt = "", - ), - LocationOutCount( - id = "2", - name = "Garage", - color = "#00FF00", - isArchived = false, - itemCount = 5, - createdAt = "", - updatedAt = "", - ), - LocationOutCount( - id = "3", - name = "Living Room", - color = "#0000FF", - isArchived = false, - itemCount = 15, - createdAt = "", - updatedAt = "", - ), - LocationOutCount( - id = "4", - name = "Kitchen", - color = "#FFFF00", - isArchived = false, - itemCount = 20, - createdAt = "", - updatedAt = "", - ), - LocationOutCount( - id = "5", - name = "Basement", - color = "#00FFFF", - isArchived = false, - itemCount = 3, - createdAt = "", - updatedAt = "", - ), - ), - labels = - listOf( - LabelOut(id = "1", name = "electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""), - LabelOut(id = "2", name = "important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""), - LabelOut(id = "3", name = "seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""), - LabelOut(id = "4", name = "hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = ""), - ), - recentlyAddedItems = emptyList(), - ) + val previewState = DashboardUiState.Success( + statistics = GroupStatistics( + items = 123, + totalValue = 9999.99, + locations = 5, + labels = 8 + ), + locations = listOf( + LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""), + LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""), + LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""), + LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""), + LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "") + ), + labels = listOf( + LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""), + LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""), + LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""), + LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "") + ), + recentlyAddedItems = emptyList() + ) HomeboxLensTheme { DashboardContent( uiState = previewState, onLocationClick = {}, - onLabelClick = {}, + onLabelClick = {} ) } } // [END_ENTITY: Function('DashboardContentSuccessPreview')] // [ENTITY: Function('DashboardContentLoadingPreview')] -// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('HomeboxLensTheme')] -// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('DashboardContent')] -// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('DashboardUiState.Loading')] -// [PREVIEW] @Preview(showBackground = true, name = "Dashboard Loading State") @Composable fun DashboardContentLoadingPreview() { @@ -494,18 +335,13 @@ fun DashboardContentLoadingPreview() { DashboardContent( uiState = DashboardUiState.Loading, onLocationClick = {}, - onLabelClick = {}, + onLabelClick = {} ) } } // [END_ENTITY: Function('DashboardContentLoadingPreview')] // [ENTITY: Function('DashboardContentErrorPreview')] -// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('HomeboxLensTheme')] -// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('DashboardContent')] -// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('DashboardUiState.Error')] -// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('stringResource')] -// [PREVIEW] @Preview(showBackground = true, name = "Dashboard Error State") @Composable fun DashboardContentErrorPreview() { @@ -513,10 +349,9 @@ fun DashboardContentErrorPreview() { DashboardContent( uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)), onLocationClick = {}, - onLabelClick = {}, + onLabelClick = {} ) } } // [END_ENTITY: Function('DashboardContentErrorPreview')] -// [END_CONTRACT] -// [END_FILE_DashboardScreen.kt] \ No newline at end of file +// [END_FILE_DashboardScreen.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt index 69effeb..28b442e 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt @@ -1,62 +1,55 @@ // [PACKAGE] com.homebox.lens.ui.screen.dashboard // [FILE] DashboardUiState.kt // [SEMANTICS] ui, state, dashboard - package com.homebox.lens.ui.screen.dashboard // [IMPORTS] import com.homebox.lens.domain.model.GroupStatistics +import com.homebox.lens.domain.model.ItemSummary import com.homebox.lens.domain.model.LabelOut import com.homebox.lens.domain.model.LocationOutCount -import com.homebox.lens.domain.model.ItemSummary // [END_IMPORTS] -// [CONTRACT] // [ENTITY: SealedInterface('DashboardUiState')] /** - * [CONTRACT] - * Определяет все возможные состояния для экрана "Дэшборд". + * @summary Определяет все возможные состояния для экрана "Дэшборд". * @invariant В любой момент времени экран может находиться только в одном из этих состояний. */ sealed interface DashboardUiState { // [ENTITY: DataClass('Success')] - // [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('GroupStatistics')] - // [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('LocationOutCount')] - // [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('LabelOut')] - // [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('ItemSummary')] + // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')] + // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] + // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')] + // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] /** - * [CONTRACT] - * Состояние успешной загрузки данных. - * @property statistics Статистика по инвентарю. - * @property locations Список локаций со счетчиками. - * @property labels Список всех меток. - * @property recentlyAddedItems Список недавно добавленных товаров. + * @summary Состояние успешной загрузки данных. + * @param statistics Статистика по инвентарю. + * @param locations Список локаций со счетчиками. + * @param labels Список всех меток. + * @param recentlyAddedItems Список недавно добавленных товаров. */ data class Success( val statistics: GroupStatistics, val locations: List, val labels: List, - val recentlyAddedItems: List, + val recentlyAddedItems: List ) : DashboardUiState // [END_ENTITY: DataClass('Success')] // [ENTITY: DataClass('Error')] /** - * [CONTRACT] - * Состояние ошибки во время загрузки данных. - * @property message Человекочитаемое сообщение об ошибке. + * @summary Состояние ошибки во время загрузки данных. + * @param message Человекочитаемое сообщение об ошибке. */ data class Error(val message: String) : DashboardUiState // [END_ENTITY: DataClass('Error')] - // [ENTITY: DataObject('Loading')] + // [ENTITY: Object('Loading')] /** - * [CONTRACT] - * Состояние, когда данные для экрана загружаются. + * @summary Состояние, когда данные для экрана загружаются. */ - object Loading : DashboardUiState - // [END_ENTITY: DataObject('Loading')] + data object Loading : DashboardUiState + // [END_ENTITY: Object('Loading')] } // [END_ENTITY: SealedInterface('DashboardUiState')] -// [END_CONTRACT] -// [END_FILE_DashboardUiState.kt] \ No newline at end of file +// [END_FILE_DashboardUiState.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt index 946179c..3acb812 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt @@ -17,94 +17,69 @@ import timber.log.Timber import javax.inject.Inject // [END_IMPORTS] -// [CONTRACT] // [ENTITY: ViewModel('DashboardViewModel')] -// [RELATION: ViewModel('DashboardViewModel') -> [INHERITS_FROM] -> Class('ViewModel')] -// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')] -// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetStatisticsUseCase')] -// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetAllLocationsUseCase')] -// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetAllLabelsUseCase')] -// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetRecentlyAddedItemsUseCase')] +// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetStatisticsUseCase')] +// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')] +// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')] +// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')] +// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')] /** - * [CONTRACT] * @summary ViewModel для главного экрана (Dashboard). * @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний * (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки. * @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`. */ @HiltViewModel -class DashboardViewModel - @Inject - constructor( - private val getStatisticsUseCase: GetStatisticsUseCase, - private val getAllLocationsUseCase: GetAllLocationsUseCase, - private val getAllLabelsUseCase: GetAllLabelsUseCase, - private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase, - ) : ViewModel() { - // [STATE] - private val _uiState = MutableStateFlow(DashboardUiState.Loading) +class DashboardViewModel @Inject constructor( + private val getStatisticsUseCase: GetStatisticsUseCase, + private val getAllLocationsUseCase: GetAllLocationsUseCase, + private val getAllLabelsUseCase: GetAllLabelsUseCase, + private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase +) : ViewModel() { - // [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow(). - // [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и - // должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока. - val uiState = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(DashboardUiState.Loading) + val uiState = _uiState.asStateFlow() - // [LIFECYCLE_HANDLER] - init { - loadDashboardData() - } + init { + loadDashboardData() + } - // [ENTITY: Function('loadDashboardData')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('viewModelScope.launch')] - // [RELATION: Function('loadDashboardData') -> [WRITES_TO] -> Property('_uiState')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('Timber.i')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('flow')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getStatisticsUseCase')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getAllLocationsUseCase')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getAllLabelsUseCase')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getRecentlyAddedItemsUseCase')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('combine')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('catch')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('Timber.e')] - // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('collect')] - /** - * [CONTRACT] - * @summary Загружает все необходимые данные для экрана Dashboard. - * @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его - * между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`. - * @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. - */ - fun loadDashboardData() { - viewModelScope.launch { - _uiState.value = DashboardUiState.Loading - Timber.i("[ACTION] Starting dashboard data collection.") + // [ENTITY: Function('loadDashboardData')] + /** + * @summary Загружает все необходимые данные для экрана Dashboard. + * @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его + * между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`. + * @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. + */ + fun loadDashboardData() { + viewModelScope.launch { + _uiState.value = DashboardUiState.Loading + Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.") - val statsFlow = flow { emit(getStatisticsUseCase()) } - val locationsFlow = flow { emit(getAllLocationsUseCase()) } - val labelsFlow = flow { emit(getAllLabelsUseCase()) } - val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10) + val statsFlow = flow { emit(getStatisticsUseCase()) } + val locationsFlow = flow { emit(getAllLocationsUseCase()) } + val labelsFlow = flow { emit(getAllLabelsUseCase()) } + val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10) - combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems -> - DashboardUiState.Success( - statistics = stats, - locations = locations, - labels = labels, - recentlyAddedItems = recentItems, - ) - }.catch { exception -> - Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.") - _uiState.value = - DashboardUiState.Error( - message = exception.message ?: "Could not load dashboard data.", - ) - }.collect { successState -> - Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.") - _uiState.value = successState - } + combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems -> + DashboardUiState.Success( + statistics = stats, + locations = locations, + labels = labels, + recentlyAddedItems = recentItems + ) + }.catch { exception -> + Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.") + _uiState.value = DashboardUiState.Error( + message = exception.message ?: "Could not load dashboard data." + ) + }.collect { successState -> + Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.") + _uiState.value = successState } } - // [END_ENTITY: Function('loadDashboardData')] } + // [END_ENTITY: Function('loadDashboardData')] +} // [END_ENTITY: ViewModel('DashboardViewModel')] -// [END_CONTRACT] -// [END_FILE_DashboardViewModel.kt] \ No newline at end of file +// [END_FILE_DashboardViewModel.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt index a20dafc..3becc28 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt @@ -1,219 +1,39 @@ // [PACKAGE] com.homebox.lens.ui.screen.inventorylist // [FILE] InventoryListScreen.kt -// [SEMANTICS] ui, screen, inventory, list, compose +// [SEMANTICS] ui, screen, inventory, list + package com.homebox.lens.ui.screen.inventorylist // [IMPORTS] -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -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.filled.Refresh -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.Card -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.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextField -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.Item -import timber.log.Timber +import com.homebox.lens.navigation.NavigationActions +import com.homebox.lens.ui.common.MainScaffold // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Function('InventoryListScreen')] -// [RELATION: Function('InventoryListScreen') -> [DEPENDS_ON] -> Class('InventoryListViewModel')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('hiltViewModel')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('collectAsState')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Scaffold')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('TopAppBar')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Text')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('IconButton')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Icon')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('FloatingActionButton')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('SearchBar')] -// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('InventoryListContent')] +// [RELATION: Function('InventoryListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] +// [RELATION: Function('InventoryListScreen')] -> [CALLS] -> [Function('MainScaffold')] /** - * [MAIN-CONTRACT] - * Экран для отображения списка инвентарных позиций. - * - * Реализует спецификацию `screen_inventory_list`. Позволяет просматривать, - * искать и синхронизировать инвентарь. - * - * @param onItemClick Обработчик нажатия на элемент инвентаря. - * @param onNavigateBack Обработчик для возврата на предыдущий экран. + * @summary Composable-функция для экрана "Список инвентаря". + * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. + * @param navigationActions Объект с навигационными действиями. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun InventoryListScreen( - viewModel: InventoryListViewModel = hiltViewModel(), - onItemClick: (Item) -> Unit, - onNavigateBack: () -> Unit + currentRoute: String?, + navigationActions: NavigationActions ) { - // [STATE] - val uiState by viewModel.uiState.collectAsState() - - // [ACTION] - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(id = R.string.inventory_list_title)) }, // Corrected string resource name - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(id = R.string.content_desc_navigate_back) - ) - } - } - ) - }, - floatingActionButton = { - FloatingActionButton(onClick = { - Timber.i("[INFO][ACTION][ui_interaction] Sync inventory triggered.") - viewModel.onSyncClicked() - }) { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = stringResource(id = R.string.content_desc_sync_inventory) - ) - } - } - ) { innerPadding -> - // [DELEGATES] - Column(modifier = Modifier.padding(innerPadding)) { - SearchBar( - query = uiState.searchQuery, - onQueryChange = viewModel::onSearchQueryChanged - ) - InventoryListContent( - isLoading = uiState.isLoading, - items = uiState.items, - onItemClick = onItemClick - ) - } + 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')] - -// [ENTITY: Function('SearchBar')] -// [RELATION: Function('SearchBar') -> [CALLS] -> Function('TextField')] -// [RELATION: Function('SearchBar') -> [CALLS] -> Function('Text')] -// [RELATION: Function('SearchBar') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('SearchBar') -> [CALLS] -> Function('Icon')] -/** - * [CONTRACT] - * Поле для ввода поискового запроса. - */ -@Composable -private fun SearchBar(query: String, onQueryChange: (String) -> Unit) { - TextField( - value = query, - onValueChange = onQueryChange, - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - placeholder = { Text(stringResource(id = R.string.search)) }, // Corrected string resource name - leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) } - ) -} -// [END_ENTITY: Function('SearchBar')] - -// [ENTITY: Function('InventoryListContent')] -// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('Box')] -// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('CircularProgressIndicator')] -// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('Text')] -// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('LazyColumn')] -// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('ItemCard')] -/** - * [CONTRACT] - * Основной контент: индикатор загрузки или список предметов. - */ -@Composable -private fun InventoryListContent( - isLoading: Boolean, - items: List, - onItemClick: (Item) -> Unit -) { - Box(modifier = Modifier.fillMaxSize()) { - if (isLoading) { - // [STATE] - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - } else if (items.isEmpty()) { - // [FALLBACK] - Text( - text = stringResource(id = R.string.items_not_found), - modifier = Modifier.align(Alignment.Center) - ) - } else { - // [CORE-LOGIC] - LazyColumn { - items(items, key = { it.id }) { item -> - ItemCard(item = item, onClick = { - Timber.i("[INFO][ACTION][ui_interaction] Item clicked: ${item.name}") - onItemClick(item) - }) - } - } - } - } -} -// [END_ENTITY: Function('InventoryListContent')] - -// [ENTITY: Function('ItemCard')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Card')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Column')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Text')] -// [RELATION: Function('ItemCard') -> [CALLS] -> Function('clickable')] -/** - * [CONTRACT] - * Карточка для отображения одного элемента инвентаря. - */ -@Composable -private fun ItemCard( - item: Item, - onClick: () -> Unit -) { - // [PRECONDITION] - require(item.name.isNotBlank()) { "Item name cannot be blank." } - - // [CORE-LOGIC] - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) - .clickable(onClick = onClick) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = item.name, style = androidx.compose.material3.MaterialTheme.typography.titleMedium) - Text(text = "Quantity: ${item.quantity.toString()}", style = androidx.compose.material3.MaterialTheme.typography.bodySmall) - item.location?.let { - Text(text = "Location: ${it.name}", style = androidx.compose.material3.MaterialTheme.typography.bodySmall) - } - } - } -} -// [END_ENTITY: Function('ItemCard')] -// [END_CONTRACT] -// [END_FILE_InventoryListScreen.kt] \ No newline at end of file +// [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 index 2e32af6..6ddcc31 100644 --- 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 @@ -1,53 +1,21 @@ // [PACKAGE] com.homebox.lens.ui.screen.inventorylist // [FILE] InventoryListViewModel.kt -// [SEMANTICS] ui_logic, inventory_list, viewmodel - +// [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 -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import com.homebox.lens.domain.model.Item // [END_IMPORTS] -// [CONTRACT] // [ENTITY: ViewModel('InventoryListViewModel')] -// [RELATION: ViewModel('InventoryListViewModel') -> [INHERITS_FROM] -> Class('ViewModel')] -// [RELATION: ViewModel('InventoryListViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')] /** - * [CONTRACT] - * @summary ViewModel for the InventoryListScreen. + * @summary ViewModel for the inventory list screen. */ @HiltViewModel -class InventoryListViewModel - @Inject - constructor() : ViewModel() { - // [STATE] - private val _uiState = MutableStateFlow(InventoryListUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - fun onSyncClicked() { - // TODO: Implement sync logic - } - - fun onSearchQueryChanged(query: String) { - // TODO: Implement search query change logic - } - } +class InventoryListViewModel @Inject constructor() : ViewModel() { + // [AI_NOTE]: Implement UI state +} // [END_ENTITY: ViewModel('InventoryListViewModel')] -// [END_CONTRACT] -// [END_FILE_InventoryListViewModel.kt] - -// [CONTRACT] -// [ENTITY: DataClass('InventoryListUiState')] -// [RELATION: DataClass('InventoryListUiState') -> [DEPENDS_ON] -> Class('Item')] -data class InventoryListUiState( - val searchQuery: String = "", - val isLoading: Boolean = false, - val items: List = emptyList() -) -// [END_ENTITY: DataClass('InventoryListUiState')] \ No newline at end of file +// [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 index cdd21c6..1feb48a 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt @@ -1,208 +1,39 @@ // [PACKAGE] com.homebox.lens.ui.screen.itemdetails // [FILE] ItemDetailsScreen.kt -// [SEMANTICS] ui, screen, item, details, compose +// [SEMANTICS] ui, screen, item, details + package com.homebox.lens.ui.screen.itemdetails // [IMPORTS] -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material3.* +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.homebox.lens.R -import com.homebox.lens.domain.model.Item -import timber.log.Timber +import com.homebox.lens.navigation.NavigationActions +import com.homebox.lens.ui.common.MainScaffold // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Function('ItemDetailsScreen')] -// [RELATION: Function('ItemDetailsScreen') -> [DEPENDS_ON] -> Class('ItemDetailsViewModel')] -// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('hiltViewModel')] -// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('collectAsState')] -// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Scaffold')] -// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('TopAppBar')] -// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Text')] -// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('IconButton')] -// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Icon')] -// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('ItemDetailsContent')] +// [RELATION: Function('ItemDetailsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] +// [RELATION: Function('ItemDetailsScreen')] -> [CALLS] -> [Function('MainScaffold')] /** - * [MAIN-CONTRACT] - * Экран для отображения детальной информации о товаре. - * - * Реализует спецификацию `screen_item_details`. - * - * @param onNavigateBack Обработчик для возврата на предыдущий экран. - * @param onEditClick Обработчик нажатия на кнопку редактирования. + * @summary Composable-функция для экрана "Детали элемента". + * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. + * @param navigationActions Объект с навигационными действиями. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ItemDetailsScreen( - viewModel: ItemDetailsViewModel = hiltViewModel(), - onNavigateBack: () -> Unit, - onEditClick: (Int) -> Unit + currentRoute: String?, + navigationActions: NavigationActions ) { - // [STATE] - val uiState by viewModel.uiState.collectAsState() - - Scaffold( - topBar = { - TopAppBar( - title = { Text(uiState.item?.name ?: stringResource(id = R.string.item_details_title)) }, // Corrected string resource name - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back)) - } - }, - actions = { - IconButton(onClick = { - uiState.item?.id?.let { - Timber.i("[INFO][ACTION][ui_interaction] Edit item clicked: id=$it") - onEditClick(it.toInt()) - } - }) { - Icon(Icons.Default.Edit, contentDescription = stringResource(id = R.string.content_desc_edit_item)) - } - IconButton(onClick = { - Timber.w("[WARN][ACTION][ui_interaction] Delete item clicked: id=${uiState.item?.id}") - viewModel.deleteItem() - // После удаления нужно навигироваться назад - onNavigateBack() - }) { - Icon(Icons.Default.Delete, contentDescription = stringResource(id = R.string.content_desc_delete_item)) - } - } - ) - } - ) { innerPadding -> - ItemDetailsContent( - modifier = Modifier.padding(innerPadding), - isLoading = uiState.isLoading, - item = uiState.item - ) + 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')] - -// [ENTITY: Function('ItemDetailsContent')] -// [RELATION: Function('ItemDetailsContent') -> [DEPENDS_ON] -> Class('Item')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('Box')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('CircularProgressIndicator')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('Text')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('Column')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('verticalScroll')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('rememberScrollState')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('DetailsSection')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('InfoRow')] -// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('AssistChip')] -/** - * [CONTRACT] - * Отображает контент экрана: индикатор загрузки или детали товара. - */ -@Composable -private fun ItemDetailsContent( - modifier: Modifier = Modifier, - isLoading: Boolean, - item: Item? -) { - Box(modifier = modifier.fillMaxSize()) { - when { - isLoading -> { - // [STATE] - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - } - item == null -> { - // [FALLBACK] - Text(stringResource(id = R.string.items_not_found), modifier = Modifier.align(Alignment.Center)) - } - else -> { - // [CORE-LOGIC] - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // TODO: ImageCarousel - // Text("Image Carousel Placeholder") - - DetailsSection(title = stringResource(id = R.string.section_title_description)) { - Text(text = item.description ?: stringResource(id = R.string.placeholder_no_description)) - } - - DetailsSection(title = stringResource(id = R.string.section_title_details)) { - InfoRow(label = stringResource(id = R.string.label_quantity), value = item.quantity.toString()) - item.location?.let { - InfoRow(label = stringResource(id = R.string.label_location), value = it.name) - } - } - - if (item.labels.isNotEmpty()) { - DetailsSection(title = stringResource(id = R.string.section_title_labels)) { - // TODO: Use FlowRow for better layout - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - item.labels.forEach { label -> - AssistChip(onClick = { /* No-op */ }, label = { Text(label.name) }) - } - } - } - } - - // TODO: CustomFieldsGrid - } - } - } - } -} -// [END_ENTITY: Function('ItemDetailsContent')] - -// [ENTITY: Function('DetailsSection')] -// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('Column')] -// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('Text')] -// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')] -// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('Divider')] -/** - * [CONTRACT] - * Секция с заголовком и контентом. - */ -@Composable -private fun DetailsSection(title: String, content: @Composable ColumnScope.() -> Unit) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text(text = title, style = MaterialTheme.typography.titleMedium) - Divider() - content() - } -} -// [END_ENTITY: Function('DetailsSection')] - -// [ENTITY: Function('InfoRow')] -// [RELATION: Function('InfoRow') -> [CALLS] -> Function('Row')] -// [RELATION: Function('InfoRow') -> [CALLS] -> Function('Text')] -// [RELATION: Function('InfoRow') -> [CALLS] -> Function('MaterialTheme.typography.bodyLarge')] -/** - * [CONTRACT] - * Строка для отображения пары "метка: значение". - */ -@Composable -private fun InfoRow(label: String, value: String) { - Row { - Text(text = "$label: ", style = MaterialTheme.typography.bodyLarge) - Text(text = value, style = MaterialTheme.typography.bodyLarge) - } -} -// [END_ENTITY: Function('InfoRow')] -// [END_CONTRACT] -// [END_FILE_ItemDetailsScreen.kt] \ No newline at end of file +// [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 index 91fe06d..104c5c3 100644 --- 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 @@ -1,43 +1,21 @@ // [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 -import com.homebox.lens.domain.model.Item -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow // [END_IMPORTS] -// [CONTRACT] // [ENTITY: ViewModel('ItemDetailsViewModel')] -// [RELATION: ViewModel('ItemDetailsViewModel') -> [INHERITS_FROM] -> Class('ViewModel')] -// [RELATION: ViewModel('ItemDetailsViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')] /** - * [CONTRACT] - * @summary ViewModel for the ItemDetailsScreen. + * @summary ViewModel for the item details screen. */ @HiltViewModel -class ItemDetailsViewModel - @Inject - constructor() : ViewModel() { - // [STATE] - // TODO: Implement UI state - val uiState = MutableStateFlow(ItemDetailsUiState()).asStateFlow() - - fun deleteItem() { - // TODO: Implement delete item logic - } - } +class ItemDetailsViewModel @Inject constructor() : ViewModel() { + // [AI_NOTE]: Implement UI state +} // [END_ENTITY: ViewModel('ItemDetailsViewModel')] -// [END_CONTRACT] // [END_FILE_ItemDetailsViewModel.kt] - -// Placeholder for ItemDetailsUiState to resolve compilation errors -data class ItemDetailsUiState( - val item: Item? = null, - val isLoading: Boolean = false -) \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt index beeebdf..9b679fc 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt @@ -1,162 +1,139 @@ // [PACKAGE] com.homebox.lens.ui.screen.itemedit // [FILE] ItemEditScreen.kt -// [SEMANTICS] ui, screen, item, edit, create, compose +// [SEMANTICS] ui, screen, item, edit + package com.homebox.lens.ui.screen.itemedit // [IMPORTS] -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.Column +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.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Done -import androidx.compose.material3.* +import androidx.compose.material.icons.filled.Save +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember 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 androidx.lifecycle.viewmodel.compose.viewModel import com.homebox.lens.R +import com.homebox.lens.navigation.NavigationActions +import com.homebox.lens.ui.common.MainScaffold import timber.log.Timber // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Function('ItemEditScreen')] -// [RELATION: Function('ItemEditScreen') -> [DEPENDS_ON] -> Class('ItemEditViewModel')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('hiltViewModel')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('collectAsState')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('LaunchedEffect')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Timber.i')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Scaffold')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('TopAppBar')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Text')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('IconButton')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Icon')] -// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('ItemEditContent')] +// [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')] /** - * [MAIN-CONTRACT] - * Экран для создания или редактирования товара. - * - * Реализует спецификацию `screen_item_edit`. - * - * @param onNavigateBack Обработчик для возврата на предыдущий экран после сохранения или отмены. + * @summary Composable-функция для экрана "Редактирование элемента". + * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. + * @param navigationActions Объект с навигационными действиями. + * @param itemId ID элемента для редактирования. Null, если создается новый элемент. + * @param viewModel ViewModel для управления состоянием экрана. + * @param onSaveSuccess Callback, вызываемый после успешного сохранения товара. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ItemEditScreen( - viewModel: ItemEditViewModel = hiltViewModel(), - onNavigateBack: () -> Unit + currentRoute: String?, + navigationActions: NavigationActions, + itemId: String?, + viewModel: ItemEditViewModel = viewModel(), + onSaveSuccess: () -> Unit ) { - // [STATE] val uiState by viewModel.uiState.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } - // [SIDE-EFFECT] - LaunchedEffect(uiState.isSaved) { - if (uiState.isSaved) { - Timber.i("[INFO][SIDE_EFFECT][navigation] Item saved, navigating back.") - onNavigateBack() + LaunchedEffect(itemId) { + Timber.i("[INFO][ENTRYPOINT][item_edit_screen_init] Initializing ItemEditScreen for item ID: %s", itemId) + viewModel.loadItem(itemId) + } + + LaunchedEffect(uiState.error) { + uiState.error?.let { + snackbarHostState.showSnackbar(it) + Timber.e("[ERROR][UI_ERROR][item_edit_error] Displaying error: %s", it) } } - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(id = if (uiState.isEditing) R.string.item_edit_title else R.string.item_edit_title_create)) }, // Corrected string resource names - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back)) - } - }, - actions = { - IconButton(onClick = { - Timber.i("[INFO][ACTION][ui_interaction] Save item clicked.") - viewModel.saveItem() - }) { - Icon(Icons.Default.Done, contentDescription = stringResource(id = R.string.content_desc_save_item)) + 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 + ) { + Scaffold( + 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)) + } + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it) + .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() + ) + // Add more fields as needed } } - ) + } } - ) { innerPadding -> - ItemEditContent( - modifier = Modifier.padding(innerPadding), - state = uiState, - onNameChange = { viewModel.onNameChange(it) }, - onDescriptionChange = { viewModel.onDescriptionChange(it) }, - onQuantityChange = { viewModel.onQuantityChange(it) } - ) } } // [END_ENTITY: Function('ItemEditScreen')] - -// [ENTITY: Function('ItemEditContent')] -// [RELATION: Function('ItemEditContent') -> [DEPENDS_ON] -> Class('ItemEditUiState')] -// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('Column')] -// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('verticalScroll')] -// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('rememberScrollState')] -// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('OutlinedTextField')] -// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('Text')] -// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')] -/** - * [CONTRACT] - * Отображает форму для редактирования данных товара. - */ -@Composable -private fun ItemEditContent( - modifier: Modifier = Modifier, - state: ItemEditUiState, - onNameChange: (String) -> Unit, - onDescriptionChange: (String) -> Unit, - onQuantityChange: (String) -> Unit -) { - // [CORE-LOGIC] - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - OutlinedTextField( - value = state.name, - onValueChange = onNameChange, - label = { Text(stringResource(id = R.string.label_name)) }, - modifier = Modifier.fillMaxWidth(), - isError = state.nameError != null - ) - state.nameError?.let { - Text(text = stringResource(id = it), color = MaterialTheme.colorScheme.error) - } - - OutlinedTextField( - value = state.description, - onValueChange = onDescriptionChange, - label = { Text(stringResource(id = R.string.label_description)) }, - modifier = Modifier.fillMaxWidth(), - minLines = 3 - ) - - OutlinedTextField( - value = state.quantity, - onValueChange = onQuantityChange, - label = { Text(stringResource(id = R.string.label_quantity)) }, - modifier = Modifier.fillMaxWidth(), - isError = state.quantityError != null - ) - state.quantityError?.let { - Text(text = stringResource(id = it), color = MaterialTheme.colorScheme.error) - } - - // TODO: Location Dropdown - // TODO: Labels ChipGroup - // TODO: ImagePicker - } -} -// [END_ENTITY: Function('ItemEditContent')] -// [END_CONTRACT] -// [END_FILE_ItemEditScreen.kt] \ No newline at end of file +// [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 index e6b7052..4ca8c27 100644 --- 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 @@ -1,59 +1,214 @@ // [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.Label +import com.homebox.lens.domain.model.Location +import com.homebox.lens.domain.usecase.CreateItemUseCase +import com.homebox.lens.domain.usecase.GetItemDetailsUseCase +import com.homebox.lens.domain.usecase.UpdateItemUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject +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 javax.inject.Inject // [END_IMPORTS] -// [CONTRACT] -// [ENTITY: ViewModel('ItemEditViewModel')] -// [RELATION: ViewModel('ItemEditViewModel') -> [INHERITS_FROM] -> Class('ViewModel')] -// [RELATION: ViewModel('ItemEditViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')] +// [ENTITY: DataClass('ItemEditUiState')] /** - * [CONTRACT] - * @summary ViewModel for the ItemEditScreen. + * @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 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() : ViewModel() { - // [STATE] - // TODO: Implement UI state - val uiState = MutableStateFlow(ItemEditUiState()).asStateFlow() +class ItemEditViewModel @Inject constructor( + private val createItemUseCase: CreateItemUseCase, + private val updateItemUseCase: UpdateItemUseCase, + private val getItemDetailsUseCase: GetItemDetailsUseCase +) : ViewModel() { - fun saveItem() { - // TODO: Implement save item logic - } + private val _uiState = MutableStateFlow(ItemEditUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - fun onNameChange(name: String) { - // TODO: Implement name change logic - } + private val _saveCompleted = MutableSharedFlow() + val saveCompleted: SharedFlow = _saveCompleted.asSharedFlow() - fun onDescriptionChange(description: String) { - // TODO: Implement description change logic - } - - fun onQuantityChange(quantity: String) { - // TODO: Implement quantity change logic + // [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) + 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)) + } 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, // Assuming first image is the main one + location = itemOut.location?.let { Location(it.id, it.name) }, // Simplified mapping + labels = itemOut.labels.map { Label(it.id, it.name) }, // Simplified mapping + value = itemOut.value?.toBigDecimal(), + createdAt = itemOut.createdAt + ) + _uiState.value = _uiState.value.copy(isLoading = false, item = item) + 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: ViewModel('ItemEditViewModel')] -// [END_CONTRACT] -// [END_FILE_ItemEditViewModel.kt] + // [END_ENTITY: Function('loadItem')] -// Placeholder for ItemEditUiState to resolve compilation errors -data class ItemEditUiState( - val isSaved: Boolean = false, - val isEditing: Boolean = false, - val name: String = "", - val description: String = "", - val quantity: String = "", - val nameError: Int? = null, - val quantityError: Int? = null -) \ No newline at end of file + // [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. + */ + fun saveItem() { + Timber.i("[INFO][ENTRYPOINT][saving_item] Attempting to save item.") + viewModelScope.launch { + val currentItem = _uiState.value.item + require(currentItem != null) { "[CONTRACT_VIOLATION][PRECONDITION][item_not_present] Cannot save a null 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 = null, + notes = null, + serialNumber = null, + value = null, + purchasePrice = null, + purchaseDate = null, + warrantyUntil = null, + locationId = currentItem.location?.id, + parentId = null, + labelIds = currentItem.labels.map { it.id } + )) + Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.") + val createdItem = Item( + id = createdItemSummary.id, + name = createdItemSummary.name, + description = null, // ItemSummary does not have description + quantity = 0, // ItemSummary does not have quantity + image = null, // ItemSummary does not have image + location = null, // ItemSummary does not have location + labels = emptyList(), // ItemSummary does not have labels + value = null, // ItemSummary does not have value + createdAt = null // ItemSummary does not have createdAt + ) + _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 = Item( + 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.toBigDecimal(), + createdAt = updatedItemOut.createdAt + ) + _uiState.value = _uiState.value.copy(isLoading = false, item = updatedItem) + 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')] +} +// [END_ENTITY: ViewModel('ItemEditViewModel')] +// [END_FILE_ItemEditViewModel.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt index e45697b..f594a1b 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt @@ -1,14 +1,15 @@ -// [PACKAGE]com.homebox.lens.ui.screen.labelslist -// [FILE]LabelsListScreen.kt -// [SEMANTICS]ui, screen, labels, list, compose +// [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.Column +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 @@ -16,105 +17,118 @@ 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.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.OutlinedTextField 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.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController import com.homebox.lens.R import com.homebox.lens.domain.model.Label -import com.homebox.lens.ui.screen.labelslist.LabelsListUiState +import com.homebox.lens.navigation.Screen import timber.log.Timber // [END_IMPORTS] -// [CONTRACT] // [ENTITY: Function('LabelsListScreen')] -// [RELATION: Function('LabelsListScreen') -> [DEPENDS_ON] -> SealedInterface('LabelsListUiState')] -// [RELATION: Function('LabelsListScreen') -> [CREATES_INSTANCE_OF] -> Class('Scaffold')] -// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('LabelsListContent')] -// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Text')] -// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('IconButton')] -// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Icon')] -// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('FloatingActionButton')] -// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Column')] -// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('CircularProgressIndicator')] +// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelsListViewModel')] +// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')] /** - * [MAIN-CONTRACT] - * Экран для отображения списка всех меток. - * - * Этот Composable является точкой входа для UI, определенного в спецификации `screen_labels_list`. - * Он получает состояние от [LabelsListViewModel] и отображает его, делегируя обработку - * пользовательских событий в ViewModel. - * - * @param uiState Текущее состояние UI для экрана списка меток. - * @param onLabelClick Функция обратного вызова для обработки нажатия на метку. - * @param onAddClick Функция обратного вызова для обработки нажатия на кнопку добавления метки. - * @param onNavigateBack Функция обратного вызова для навигации назад. + * @summary Отображает экран со списком всех меток. + * @param navController Контроллер навигации для перемещения между экранами. + * @param viewModel ViewModel, предоставляющая состояние UI для экрана меток. */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun labelsListScreen( - uiState: LabelsListUiState, - onLabelClick: (Label) -> Unit, - onAddClick: () -> Unit, - onNavigateBack: () -> Unit, +fun LabelsListScreen( + navController: NavController, + viewModel: LabelsListViewModel = hiltViewModel() ) { + val uiState by viewModel.uiState.collectAsState() + Scaffold( topBar = { TopAppBar( - title = { Text(stringResource(id = R.string.screen_title_labels)) }, + title = { Text(text = stringResource(id = R.string.screen_title_labels)) }, navigationIcon = { - IconButton(onClick = onNavigateBack) { + IconButton(onClick = { + Timber.i("[INFO][ACTION][navigate_up] Navigate up initiated.") + navController.navigateUp() + }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back) ) } - }, + } ) }, floatingActionButton = { - FloatingActionButton(onClick = onAddClick) { + FloatingActionButton(onClick = { + Timber.i("[INFO][ACTION][show_create_dialog] FAB clicked: Initiate create new label flow.") + viewModel.onShowCreateDialog() + }) { Icon( - imageVector = Icons.Filled.Add, - contentDescription = stringResource(id = R.string.content_desc_add_label) + imageVector = Icons.Default.Add, + contentDescription = stringResource(id = R.string.content_desc_create_label) ) } } - ) { innerPadding -> - Box(modifier = Modifier.padding(innerPadding)) { - when (uiState) { - is LabelsListUiState.Loading -> { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - CircularProgressIndicator() - } + ) { paddingValues -> + val currentState = uiState + if (currentState is LabelsListUiState.Success && currentState.isShowingCreateDialog) { + CreateLabelDialog( + onConfirm = { labelName -> + viewModel.createLabel(labelName) + }, + onDismiss = { + viewModel.onDismissCreateDialog() } - is LabelsListUiState.Success -> { - LabelsListContent( - uiState = uiState, - onLabelClick = onLabelClick - ) + ) + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + when (currentState) { + is LabelsListUiState.Loading -> { + CircularProgressIndicator() } is LabelsListUiState.Error -> { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = uiState.message) + Text(text = currentState.message) + } + is LabelsListUiState.Success -> { + if (currentState.labels.isEmpty()) { + Text(text = stringResource(id = R.string.labels_list_empty)) + } else { + LabelsList( + labels = currentState.labels, + onLabelClick = { label -> + Timber.i("[INFO][ACTION][navigate_to_inventory] Label clicked: ${label.id}. Navigating to inventory list.") + val route = Screen.InventoryList.withFilter("label", label.id) + navController.navigate(route) + } + ) } } } @@ -123,81 +137,100 @@ fun labelsListScreen( } // [END_ENTITY: Function('LabelsListScreen')] -// [ENTITY: Function('LabelsListContent')] -// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('LabelListItem')] -// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('stringResource')] -// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('Text')] -// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('Column')] -// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('LazyColumn')] +// [ENTITY: Function('LabelsList')] +// [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')] /** - * [CONTRACT] - * Отображает основной контент экрана: список меток. - * - * @param uiState Состояние успеха, содержащее список меток. - * @param onLabelClick Обработчик нажатия на элемент списка. - * @sideeffect Отсутствуют. + * @summary Composable-функция для отображения списка меток. + * @param labels Список объектов `Label` для отображения. + * @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка. + * @param modifier Модификатор для настройки внешнего вида. */ @Composable -private fun LabelsListContent( - uiState: LabelsListUiState.Success, - onLabelClick: (Label) -> Unit +private fun LabelsList( + labels: List @@ -289,6 +307,12 @@ + + Сценарий использования для обновления существующего товара. + + + + Сценарий использования для создания новой метки. @@ -344,6 +368,41 @@ + + Сценарий использования для создания нового местоположения. + + + + + + + Сценарий использования для обновления существующего местоположения. + + + + + + + Сценарий использования для удаления местоположения. + + + + + + + Сценарий использования для обновления существующей метки. + + + + + + + Сценарий использования для удаления метки. + + + + + @@ -444,7 +503,7 @@ - + Экран создания/редактирования товара Позволяет пользователям создавать новые товары или редактировать существующие. @@ -499,7 +558,7 @@ - + ViewModel для экрана создания/редактирования товара. diff --git a/tech_spec/home_box_api.json b/tech_spec/home_box_api.json deleted file mode 100644 index 764c05b..0000000 --- a/tech_spec/home_box_api.json +++ /dev/null @@ -1,4315 +0,0 @@ -{ - "schemes": [ - "https", - "http" - ], - "swagger": "2.0", - "info": { - "description": "Track, Manage, and Organize your Things.", - "title": "Homebox API", - "contact": { - "name": "Homebox Team", - "url": "https://discord.homebox.software" - }, - "version": "1.0" - }, - "host": "demo.homebox.software", - "basePath": "/api", - "paths": { - "/v1/actions/create-missing-thumbnails": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Creates thumbnails for items that are missing them", - "produces": [ - "application/json" - ], - "tags": [ - "Actions" - ], - "summary": "Create Missing Thumbnails", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.ActionAmountResult" - } - } - } - } - }, - "/v1/actions/ensure-asset-ids": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Ensures all items in the database have an asset ID", - "produces": [ - "application/json" - ], - "tags": [ - "Actions" - ], - "summary": "Ensure Asset IDs", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.ActionAmountResult" - } - } - } - } - }, - "/v1/actions/ensure-import-refs": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Ensures all items in the database have an import ref", - "produces": [ - "application/json" - ], - "tags": [ - "Actions" - ], - "summary": "Ensures Import Refs", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.ActionAmountResult" - } - } - } - } - }, - "/v1/actions/set-primary-photos": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Sets the first photo of each item as the primary photo", - "produces": [ - "application/json" - ], - "tags": [ - "Actions" - ], - "summary": "Set Primary Photos", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.ActionAmountResult" - } - } - } - } - }, - "/v1/actions/zero-item-time-fields": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Resets all item date fields to the beginning of the day", - "produces": [ - "application/json" - ], - "tags": [ - "Actions" - ], - "summary": "Zero Out Time Fields", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.ActionAmountResult" - } - } - } - } - }, - "/v1/assets/{id}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Get Item by Asset ID", - "parameters": [ - { - "type": "string", - "description": "Asset ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary" - } - } - } - } - }, - "/v1/currency": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Base" - ], - "summary": "Currency", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/currencies.Currency" - } - } - } - } - }, - "/v1/groups": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Group" - ], - "summary": "Get Group", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.Group" - } - } - } - }, - "put": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Group" - ], - "summary": "Update Group", - "parameters": [ - { - "description": "User Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.GroupUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.Group" - } - } - } - } - }, - "/v1/groups/invitations": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Group" - ], - "summary": "Create Group Invitation", - "parameters": [ - { - "description": "User Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.GroupInvitationCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.GroupInvitation" - } - } - } - } - }, - "/v1/groups/statistics": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Statistics" - ], - "summary": "Get Group Statistics", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.GroupStatistics" - } - } - } - } - }, - "/v1/groups/statistics/labels": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Statistics" - ], - "summary": "Get Label Statistics", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.TotalsByOrganizer" - } - } - } - } - } - }, - "/v1/groups/statistics/locations": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Statistics" - ], - "summary": "Get Location Statistics", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.TotalsByOrganizer" - } - } - } - } - } - }, - "/v1/groups/statistics/purchase-price": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Statistics" - ], - "summary": "Get Purchase Price Statistics", - "parameters": [ - { - "type": "string", - "description": "start date", - "name": "start", - "in": "query" - }, - { - "type": "string", - "description": "end date", - "name": "end", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.ValueOverTime" - } - } - } - } - }, - "/v1/items": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Query All Items", - "parameters": [ - { - "type": "string", - "description": "search string", - "name": "q", - "in": "query" - }, - { - "type": "integer", - "description": "page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "items per page", - "name": "pageSize", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "label Ids", - "name": "labels", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "location Ids", - "name": "locations", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "parent Ids", - "name": "parentIds", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary" - } - } - } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Create Item", - "parameters": [ - { - "description": "Item Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.ItemCreate" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/repo.ItemSummary" - } - } - } - } - }, - "/v1/items/export": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "tags": [ - "Items" - ], - "summary": "Export Items", - "responses": { - "200": { - "description": "text/csv", - "schema": { - "type": "string" - } - } - } - } - }, - "/v1/items/fields": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Get All Custom Field Names", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - }, - "/v1/items/fields/values": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Get All Custom Field Values", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - }, - "/v1/items/import": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Import Items", - "parameters": [ - { - "type": "file", - "description": "Image to upload", - "name": "csv", - "in": "formData", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/items/{id}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Get Item", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.ItemOut" - } - } - } - }, - "put": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Update Item", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Item Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.ItemUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.ItemOut" - } - } - } - }, - "delete": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Delete Item", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - }, - "patch": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Update Item", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Item Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.ItemPatch" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.ItemOut" - } - } - } - } - }, - "/v1/items/{id}/attachments": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items Attachments" - ], - "summary": "Create Item Attachment", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "file", - "description": "File attachment", - "name": "file", - "in": "formData", - "required": true - }, - { - "type": "string", - "description": "Type of file", - "name": "type", - "in": "formData" - }, - { - "type": "boolean", - "description": "Is this the primary attachment", - "name": "primary", - "in": "formData" - }, - { - "type": "string", - "description": "name of the file including extension", - "name": "name", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.ItemOut" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/validate.ErrorResponse" - } - } - } - } - }, - "/v1/items/{id}/attachments/{attachment_id}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/octet-stream" - ], - "tags": [ - "Items Attachments" - ], - "summary": "Get Item Attachment", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Attachment ID", - "name": "attachment_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.ItemAttachmentToken" - } - } - } - }, - "put": { - "security": [ - { - "Bearer": [] - } - ], - "tags": [ - "Items Attachments" - ], - "summary": "Update Item Attachment", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Attachment ID", - "name": "attachment_id", - "in": "path", - "required": true - }, - { - "description": "Attachment Update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.ItemAttachmentUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.ItemOut" - } - } - } - }, - "delete": { - "security": [ - { - "Bearer": [] - } - ], - "tags": [ - "Items Attachments" - ], - "summary": "Delete Item Attachment", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Attachment ID", - "name": "attachment_id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/items/{id}/maintenance": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Item Maintenance" - ], - "summary": "Get Maintenance Log", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - }, - { - "enum": [ - "scheduled", - "completed", - "both" - ], - "type": "string", - "x-enum-varnames": [ - "MaintenanceFilterStatusScheduled", - "MaintenanceFilterStatusCompleted", - "MaintenanceFilterStatusBoth" - ], - "name": "status", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.MaintenanceEntryWithDetails" - } - } - } - } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Item Maintenance" - ], - "summary": "Create Maintenance Entry", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Entry Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.MaintenanceEntryCreate" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/repo.MaintenanceEntry" - } - } - } - } - }, - "/v1/items/{id}/path": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Get the full path of an item", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.ItemPath" - } - } - } - } - } - }, - "/v1/labelmaker/assets/{id}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Get Asset label", - "parameters": [ - { - "type": "string", - "description": "Asset ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "boolean", - "description": "Print this label, defaults to false", - "name": "print", - "in": "query" - } - ], - "responses": { - "200": { - "description": "image/png", - "schema": { - "type": "string" - } - } - } - } - }, - "/v1/labelmaker/item/{id}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Get Item label", - "parameters": [ - { - "type": "string", - "description": "Item ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "boolean", - "description": "Print this label, defaults to false", - "name": "print", - "in": "query" - } - ], - "responses": { - "200": { - "description": "image/png", - "schema": { - "type": "string" - } - } - } - } - }, - "/v1/labelmaker/location/{id}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Locations" - ], - "summary": "Get Location label", - "parameters": [ - { - "type": "string", - "description": "Location ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "boolean", - "description": "Print this label, defaults to false", - "name": "print", - "in": "query" - } - ], - "responses": { - "200": { - "description": "image/png", - "schema": { - "type": "string" - } - } - } - } - }, - "/v1/labels": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Labels" - ], - "summary": "Get All Labels", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LabelOut" - } - } - } - } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Labels" - ], - "summary": "Create Label", - "parameters": [ - { - "description": "Label Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.LabelCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.LabelSummary" - } - } - } - } - }, - "/v1/labels/{id}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Labels" - ], - "summary": "Get Label", - "parameters": [ - { - "type": "string", - "description": "Label ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.LabelOut" - } - } - } - }, - "put": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Labels" - ], - "summary": "Update Label", - "parameters": [ - { - "type": "string", - "description": "Label ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.LabelOut" - } - } - } - }, - "delete": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Labels" - ], - "summary": "Delete Label", - "parameters": [ - { - "type": "string", - "description": "Label ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/locations": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Locations" - ], - "summary": "Get All Locations", - "parameters": [ - { - "type": "boolean", - "description": "Filter locations with parents", - "name": "filterChildren", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LocationOutCount" - } - } - } - } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Locations" - ], - "summary": "Create Location", - "parameters": [ - { - "description": "Location Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.LocationCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.LocationSummary" - } - } - } - } - }, - "/v1/locations/tree": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Locations" - ], - "summary": "Get Locations Tree", - "parameters": [ - { - "type": "boolean", - "description": "include items in response tree", - "name": "withItems", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.TreeItem" - } - } - } - } - } - }, - "/v1/locations/{id}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Locations" - ], - "summary": "Get Location", - "parameters": [ - { - "type": "string", - "description": "Location ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.LocationOut" - } - } - } - }, - "put": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Locations" - ], - "summary": "Update Location", - "parameters": [ - { - "type": "string", - "description": "Location ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Location Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.LocationUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.LocationOut" - } - } - } - }, - "delete": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Locations" - ], - "summary": "Delete Location", - "parameters": [ - { - "type": "string", - "description": "Location ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/maintenance": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Maintenance" - ], - "summary": "Query All Maintenance", - "parameters": [ - { - "enum": [ - "scheduled", - "completed", - "both" - ], - "type": "string", - "x-enum-varnames": [ - "MaintenanceFilterStatusScheduled", - "MaintenanceFilterStatusCompleted", - "MaintenanceFilterStatusBoth" - ], - "name": "status", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.MaintenanceEntryWithDetails" - } - } - } - } - } - }, - "/v1/maintenance/{id}": { - "put": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Maintenance" - ], - "summary": "Update Maintenance Entry", - "parameters": [ - { - "type": "string", - "description": "Maintenance ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Entry Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.MaintenanceEntryUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.MaintenanceEntry" - } - } - } - }, - "delete": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Maintenance" - ], - "summary": "Delete Maintenance Entry", - "parameters": [ - { - "type": "string", - "description": "Maintenance ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/notifiers": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Notifiers" - ], - "summary": "Get Notifiers", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.NotifierOut" - } - } - } - } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Notifiers" - ], - "summary": "Create Notifier", - "parameters": [ - { - "description": "Notifier Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.NotifierCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.NotifierOut" - } - } - } - } - }, - "/v1/notifiers/test": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Notifiers" - ], - "summary": "Test Notifier", - "parameters": [ - { - "type": "string", - "description": "URL", - "name": "url", - "in": "query", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/notifiers/{id}": { - "put": { - "security": [ - { - "Bearer": [] - } - ], - "tags": [ - "Notifiers" - ], - "summary": "Update Notifier", - "parameters": [ - { - "type": "string", - "description": "Notifier ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Notifier Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.NotifierUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/repo.NotifierOut" - } - } - } - }, - "delete": { - "security": [ - { - "Bearer": [] - } - ], - "tags": [ - "Notifiers" - ], - "summary": "Delete a Notifier", - "parameters": [ - { - "type": "string", - "description": "Notifier ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/products/search-from-barcode": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Search EAN from Barcode", - "parameters": [ - { - "type": "string", - "description": "barcode to be searched", - "name": "data", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.BarcodeProduct" - } - } - } - } - } - }, - "/v1/qrcode": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Items" - ], - "summary": "Create QR Code", - "parameters": [ - { - "type": "string", - "description": "data to be encoded into qrcode", - "name": "data", - "in": "query" - } - ], - "responses": { - "200": { - "description": "image/jpeg", - "schema": { - "type": "string" - } - } - } - } - }, - "/v1/reporting/bill-of-materials": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Reporting" - ], - "summary": "Export Bill of Materials", - "responses": { - "200": { - "description": "text/csv", - "schema": { - "type": "string" - } - } - } - } - }, - "/v1/status": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "Base" - ], - "summary": "Application Info", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.APISummary" - } - } - } - } - }, - "/v1/users/change-password": { - "put": { - "security": [ - { - "Bearer": [] - } - ], - "tags": [ - "User" - ], - "summary": "Change Password", - "parameters": [ - { - "description": "Password Payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.ChangePassword" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/users/login": { - "post": { - "consumes": [ - "application/x-www-form-urlencoded", - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Authentication" - ], - "summary": "User Login", - "parameters": [ - { - "description": "Login Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.LoginForm" - } - }, - { - "type": "string", - "description": "auth provider", - "name": "provider", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.TokenResponse" - } - } - } - } - }, - "/v1/users/logout": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "tags": [ - "Authentication" - ], - "summary": "User Logout", - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/users/refresh": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "handleAuthRefresh returns a handler that will issue a new token from an existing token.\nThis does not validate that the user still exists within the database.", - "tags": [ - "Authentication" - ], - "summary": "User Token Refresh", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/users/register": { - "post": { - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "Register New User", - "parameters": [ - { - "description": "User Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/services.UserRegistration" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/v1/users/self": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "Get User Self", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "item": { - "$ref": "#/definitions/repo.UserOut" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "Update Account", - "parameters": [ - { - "description": "User Data", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/repo.UserUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "item": { - "$ref": "#/definitions/repo.UserUpdate" - } - } - } - ] - } - } - } - }, - "delete": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "Delete Account", - "responses": { - "204": { - "description": "No Content" - } - } - } - } - }, - "definitions": { - "attachment.Type": { - "type": "string", - "enum": [ - "attachment", - "photo", - "manual", - "warranty", - "attachment", - "receipt", - "thumbnail" - ], - "x-enum-varnames": [ - "DefaultType", - "TypePhoto", - "TypeManual", - "TypeWarranty", - "TypeAttachment", - "TypeReceipt", - "TypeThumbnail" - ] - }, - "authroles.Role": { - "type": "string", - "enum": [ - "user", - "admin", - "user", - "attachments" - ], - "x-enum-varnames": [ - "DefaultRole", - "RoleAdmin", - "RoleUser", - "RoleAttachments" - ] - }, - "currencies.Currency": { - "type": "object", - "properties": { - "code": { - "type": "string" - }, - "local": { - "type": "string" - }, - "name": { - "type": "string" - }, - "symbol": { - "type": "string" - } - } - }, - "ent.Attachment": { - "type": "object", - "properties": { - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the AttachmentQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.AttachmentEdges" - } - ] - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "mime_type": { - "description": "MimeType holds the value of the \"mime_type\" field.", - "type": "string" - }, - "path": { - "description": "Path holds the value of the \"path\" field.", - "type": "string" - }, - "primary": { - "description": "Primary holds the value of the \"primary\" field.", - "type": "boolean" - }, - "title": { - "description": "Title holds the value of the \"title\" field.", - "type": "string" - }, - "type": { - "description": "Type holds the value of the \"type\" field.", - "allOf": [ - { - "$ref": "#/definitions/attachment.Type" - } - ] - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - } - } - }, - "ent.AttachmentEdges": { - "type": "object", - "properties": { - "item": { - "description": "Item holds the value of the item edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Item" - } - ] - }, - "thumbnail": { - "description": "Thumbnail holds the value of the thumbnail edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Attachment" - } - ] - } - } - }, - "ent.AuthRoles": { - "type": "object", - "properties": { - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the AuthRolesQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.AuthRolesEdges" - } - ] - }, - "id": { - "description": "ID of the ent.", - "type": "integer" - }, - "role": { - "description": "Role holds the value of the \"role\" field.", - "allOf": [ - { - "$ref": "#/definitions/authroles.Role" - } - ] - } - } - }, - "ent.AuthRolesEdges": { - "type": "object", - "properties": { - "token": { - "description": "Token holds the value of the token edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.AuthTokens" - } - ] - } - } - }, - "ent.AuthTokens": { - "type": "object", - "properties": { - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the AuthTokensQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.AuthTokensEdges" - } - ] - }, - "expires_at": { - "description": "ExpiresAt holds the value of the \"expires_at\" field.", - "type": "string" - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "token": { - "description": "Token holds the value of the \"token\" field.", - "type": "array", - "items": { - "type": "integer" - } - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - } - } - }, - "ent.AuthTokensEdges": { - "type": "object", - "properties": { - "roles": { - "description": "Roles holds the value of the roles edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.AuthRoles" - } - ] - }, - "user": { - "description": "User holds the value of the user edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.User" - } - ] - } - } - }, - "ent.Group": { - "type": "object", - "properties": { - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "currency": { - "description": "Currency holds the value of the \"currency\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the GroupQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.GroupEdges" - } - ] - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "name": { - "description": "Name holds the value of the \"name\" field.", - "type": "string" - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - } - } - }, - "ent.GroupEdges": { - "type": "object", - "properties": { - "invitation_tokens": { - "description": "InvitationTokens holds the value of the invitation_tokens edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.GroupInvitationToken" - } - }, - "items": { - "description": "Items holds the value of the items edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Item" - } - }, - "labels": { - "description": "Labels holds the value of the labels edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Label" - } - }, - "locations": { - "description": "Locations holds the value of the locations edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Location" - } - }, - "notifiers": { - "description": "Notifiers holds the value of the notifiers edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Notifier" - } - }, - "users": { - "description": "Users holds the value of the users edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.User" - } - } - } - }, - "ent.GroupInvitationToken": { - "type": "object", - "properties": { - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the GroupInvitationTokenQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.GroupInvitationTokenEdges" - } - ] - }, - "expires_at": { - "description": "ExpiresAt holds the value of the \"expires_at\" field.", - "type": "string" - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "token": { - "description": "Token holds the value of the \"token\" field.", - "type": "array", - "items": { - "type": "integer" - } - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - }, - "uses": { - "description": "Uses holds the value of the \"uses\" field.", - "type": "integer" - } - } - }, - "ent.GroupInvitationTokenEdges": { - "type": "object", - "properties": { - "group": { - "description": "Group holds the value of the group edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Group" - } - ] - } - } - }, - "ent.Item": { - "type": "object", - "properties": { - "archived": { - "description": "Archived holds the value of the \"archived\" field.", - "type": "boolean" - }, - "asset_id": { - "description": "AssetID holds the value of the \"asset_id\" field.", - "type": "integer" - }, - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "description": { - "description": "Description holds the value of the \"description\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the ItemQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.ItemEdges" - } - ] - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "import_ref": { - "description": "ImportRef holds the value of the \"import_ref\" field.", - "type": "string" - }, - "insured": { - "description": "Insured holds the value of the \"insured\" field.", - "type": "boolean" - }, - "lifetime_warranty": { - "description": "LifetimeWarranty holds the value of the \"lifetime_warranty\" field.", - "type": "boolean" - }, - "manufacturer": { - "description": "Manufacturer holds the value of the \"manufacturer\" field.", - "type": "string" - }, - "model_number": { - "description": "ModelNumber holds the value of the \"model_number\" field.", - "type": "string" - }, - "name": { - "description": "Name holds the value of the \"name\" field.", - "type": "string" - }, - "notes": { - "description": "Notes holds the value of the \"notes\" field.", - "type": "string" - }, - "purchase_from": { - "description": "PurchaseFrom holds the value of the \"purchase_from\" field.", - "type": "string" - }, - "purchase_price": { - "description": "PurchasePrice holds the value of the \"purchase_price\" field.", - "type": "number" - }, - "purchase_time": { - "description": "PurchaseTime holds the value of the \"purchase_time\" field.", - "type": "string" - }, - "quantity": { - "description": "Quantity holds the value of the \"quantity\" field.", - "type": "integer" - }, - "serial_number": { - "description": "SerialNumber holds the value of the \"serial_number\" field.", - "type": "string" - }, - "sold_notes": { - "description": "SoldNotes holds the value of the \"sold_notes\" field.", - "type": "string" - }, - "sold_price": { - "description": "SoldPrice holds the value of the \"sold_price\" field.", - "type": "number" - }, - "sold_time": { - "description": "SoldTime holds the value of the \"sold_time\" field.", - "type": "string" - }, - "sold_to": { - "description": "SoldTo holds the value of the \"sold_to\" field.", - "type": "string" - }, - "sync_child_items_locations": { - "description": "SyncChildItemsLocations holds the value of the \"sync_child_items_locations\" field.", - "type": "boolean" - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - }, - "warranty_details": { - "description": "WarrantyDetails holds the value of the \"warranty_details\" field.", - "type": "string" - }, - "warranty_expires": { - "description": "WarrantyExpires holds the value of the \"warranty_expires\" field.", - "type": "string" - } - } - }, - "ent.ItemEdges": { - "type": "object", - "properties": { - "attachments": { - "description": "Attachments holds the value of the attachments edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Attachment" - } - }, - "children": { - "description": "Children holds the value of the children edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Item" - } - }, - "fields": { - "description": "Fields holds the value of the fields edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.ItemField" - } - }, - "group": { - "description": "Group holds the value of the group edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Group" - } - ] - }, - "label": { - "description": "Label holds the value of the label edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Label" - } - }, - "location": { - "description": "Location holds the value of the location edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Location" - } - ] - }, - "maintenance_entries": { - "description": "MaintenanceEntries holds the value of the maintenance_entries edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.MaintenanceEntry" - } - }, - "parent": { - "description": "Parent holds the value of the parent edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Item" - } - ] - } - } - }, - "ent.ItemField": { - "type": "object", - "properties": { - "boolean_value": { - "description": "BooleanValue holds the value of the \"boolean_value\" field.", - "type": "boolean" - }, - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "description": { - "description": "Description holds the value of the \"description\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the ItemFieldQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.ItemFieldEdges" - } - ] - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "name": { - "description": "Name holds the value of the \"name\" field.", - "type": "string" - }, - "number_value": { - "description": "NumberValue holds the value of the \"number_value\" field.", - "type": "integer" - }, - "text_value": { - "description": "TextValue holds the value of the \"text_value\" field.", - "type": "string" - }, - "time_value": { - "description": "TimeValue holds the value of the \"time_value\" field.", - "type": "string" - }, - "type": { - "description": "Type holds the value of the \"type\" field.", - "allOf": [ - { - "$ref": "#/definitions/itemfield.Type" - } - ] - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - } - } - }, - "ent.ItemFieldEdges": { - "type": "object", - "properties": { - "item": { - "description": "Item holds the value of the item edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Item" - } - ] - } - } - }, - "ent.Label": { - "type": "object", - "properties": { - "color": { - "description": "Color holds the value of the \"color\" field.", - "type": "string" - }, - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "description": { - "description": "Description holds the value of the \"description\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the LabelQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.LabelEdges" - } - ] - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "name": { - "description": "Name holds the value of the \"name\" field.", - "type": "string" - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - } - } - }, - "ent.LabelEdges": { - "type": "object", - "properties": { - "group": { - "description": "Group holds the value of the group edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Group" - } - ] - }, - "items": { - "description": "Items holds the value of the items edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Item" - } - } - } - }, - "ent.Location": { - "type": "object", - "properties": { - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "description": { - "description": "Description holds the value of the \"description\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the LocationQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.LocationEdges" - } - ] - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "name": { - "description": "Name holds the value of the \"name\" field.", - "type": "string" - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - } - } - }, - "ent.LocationEdges": { - "type": "object", - "properties": { - "children": { - "description": "Children holds the value of the children edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Location" - } - }, - "group": { - "description": "Group holds the value of the group edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Group" - } - ] - }, - "items": { - "description": "Items holds the value of the items edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Item" - } - }, - "parent": { - "description": "Parent holds the value of the parent edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Location" - } - ] - } - } - }, - "ent.MaintenanceEntry": { - "type": "object", - "properties": { - "cost": { - "description": "Cost holds the value of the \"cost\" field.", - "type": "number" - }, - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "date": { - "description": "Date holds the value of the \"date\" field.", - "type": "string" - }, - "description": { - "description": "Description holds the value of the \"description\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the MaintenanceEntryQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.MaintenanceEntryEdges" - } - ] - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "item_id": { - "description": "ItemID holds the value of the \"item_id\" field.", - "type": "string" - }, - "name": { - "description": "Name holds the value of the \"name\" field.", - "type": "string" - }, - "scheduled_date": { - "description": "ScheduledDate holds the value of the \"scheduled_date\" field.", - "type": "string" - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - } - } - }, - "ent.MaintenanceEntryEdges": { - "type": "object", - "properties": { - "item": { - "description": "Item holds the value of the item edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Item" - } - ] - } - } - }, - "ent.Notifier": { - "type": "object", - "properties": { - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the NotifierQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.NotifierEdges" - } - ] - }, - "group_id": { - "description": "GroupID holds the value of the \"group_id\" field.", - "type": "string" - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "is_active": { - "description": "IsActive holds the value of the \"is_active\" field.", - "type": "boolean" - }, - "name": { - "description": "Name holds the value of the \"name\" field.", - "type": "string" - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - }, - "user_id": { - "description": "UserID holds the value of the \"user_id\" field.", - "type": "string" - } - } - }, - "ent.NotifierEdges": { - "type": "object", - "properties": { - "group": { - "description": "Group holds the value of the group edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Group" - } - ] - }, - "user": { - "description": "User holds the value of the user edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.User" - } - ] - } - } - }, - "ent.User": { - "type": "object", - "properties": { - "activated_on": { - "description": "ActivatedOn holds the value of the \"activated_on\" field.", - "type": "string" - }, - "created_at": { - "description": "CreatedAt holds the value of the \"created_at\" field.", - "type": "string" - }, - "edges": { - "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.", - "allOf": [ - { - "$ref": "#/definitions/ent.UserEdges" - } - ] - }, - "email": { - "description": "Email holds the value of the \"email\" field.", - "type": "string" - }, - "id": { - "description": "ID of the ent.", - "type": "string" - }, - "is_superuser": { - "description": "IsSuperuser holds the value of the \"is_superuser\" field.", - "type": "boolean" - }, - "name": { - "description": "Name holds the value of the \"name\" field.", - "type": "string" - }, - "role": { - "description": "Role holds the value of the \"role\" field.", - "allOf": [ - { - "$ref": "#/definitions/user.Role" - } - ] - }, - "superuser": { - "description": "Superuser holds the value of the \"superuser\" field.", - "type": "boolean" - }, - "updated_at": { - "description": "UpdatedAt holds the value of the \"updated_at\" field.", - "type": "string" - } - } - }, - "ent.UserEdges": { - "type": "object", - "properties": { - "auth_tokens": { - "description": "AuthTokens holds the value of the auth_tokens edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.AuthTokens" - } - }, - "group": { - "description": "Group holds the value of the group edge.", - "allOf": [ - { - "$ref": "#/definitions/ent.Group" - } - ] - }, - "notifiers": { - "description": "Notifiers holds the value of the notifiers edge.", - "type": "array", - "items": { - "$ref": "#/definitions/ent.Notifier" - } - } - } - }, - "itemfield.Type": { - "type": "string", - "enum": [ - "text", - "number", - "boolean", - "time" - ], - "x-enum-varnames": [ - "TypeText", - "TypeNumber", - "TypeBoolean", - "TypeTime" - ] - }, - "repo.BarcodeProduct": { - "type": "object", - "properties": { - "barcode": { - "type": "string" - }, - "imageBase64": { - "type": "string" - }, - "imageURL": { - "type": "string" - }, - "item": { - "$ref": "#/definitions/repo.ItemCreate" - }, - "manufacturer": { - "type": "string" - }, - "modelNumber": { - "description": "Identifications", - "type": "string" - }, - "notes": { - "description": "Extras", - "type": "string" - }, - "search_engine_name": { - "type": "string" - } - } - }, - "repo.Group": { - "type": "object", - "properties": { - "createdAt": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updatedAt": { - "type": "string" - } - } - }, - "repo.GroupStatistics": { - "type": "object", - "properties": { - "totalItemPrice": { - "type": "number" - }, - "totalItems": { - "type": "integer" - }, - "totalLabels": { - "type": "integer" - }, - "totalLocations": { - "type": "integer" - }, - "totalUsers": { - "type": "integer" - }, - "totalWithWarranty": { - "type": "integer" - } - } - }, - "repo.GroupUpdate": { - "type": "object", - "properties": { - "currency": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "repo.ItemAttachment": { - "type": "object", - "properties": { - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "path": { - "type": "string" - }, - "primary": { - "type": "boolean" - }, - "thumbnail": { - "$ref": "#/definitions/ent.Attachment" - }, - "title": { - "type": "string" - }, - "type": { - "type": "string" - }, - "updatedAt": { - "type": "string" - } - } - }, - "repo.ItemAttachmentUpdate": { - "type": "object", - "properties": { - "primary": { - "type": "boolean" - }, - "title": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "repo.ItemCreate": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "description": { - "type": "string", - "maxLength": 1000 - }, - "labelIds": { - "type": "array", - "items": { - "type": "string" - } - }, - "locationId": { - "description": "Edges", - "type": "string" - }, - "name": { - "type": "string", - "maxLength": 255, - "minLength": 1 - }, - "parentId": { - "type": "string", - "x-nullable": true - }, - "quantity": { - "type": "integer" - } - } - }, - "repo.ItemField": { - "type": "object", - "properties": { - "booleanValue": { - "type": "boolean" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "numberValue": { - "type": "integer" - }, - "textValue": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "repo.ItemOut": { - "type": "object", - "properties": { - "archived": { - "type": "boolean" - }, - "assetId": { - "type": "string", - "example": "0" - }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.ItemAttachment" - } - }, - "createdAt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "fields": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.ItemField" - } - }, - "id": { - "type": "string" - }, - "imageId": { - "type": "string", - "x-nullable": true, - "x-omitempty": true - }, - "insured": { - "type": "boolean" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LabelSummary" - } - }, - "lifetimeWarranty": { - "description": "Warranty", - "type": "boolean" - }, - "location": { - "description": "Edges", - "allOf": [ - { - "$ref": "#/definitions/repo.LocationSummary" - } - ], - "x-nullable": true, - "x-omitempty": true - }, - "manufacturer": { - "type": "string" - }, - "modelNumber": { - "type": "string" - }, - "name": { - "type": "string" - }, - "notes": { - "description": "Extras", - "type": "string" - }, - "parent": { - "allOf": [ - { - "$ref": "#/definitions/repo.ItemSummary" - } - ], - "x-nullable": true, - "x-omitempty": true - }, - "purchaseFrom": { - "type": "string" - }, - "purchasePrice": { - "type": "number" - }, - "purchaseTime": { - "description": "Purchase", - "type": "string" - }, - "quantity": { - "type": "integer" - }, - "serialNumber": { - "type": "string" - }, - "soldNotes": { - "type": "string" - }, - "soldPrice": { - "type": "number" - }, - "soldTime": { - "description": "Sold", - "type": "string" - }, - "soldTo": { - "type": "string" - }, - "syncChildItemsLocations": { - "type": "boolean" - }, - "thumbnailId": { - "type": "string", - "x-nullable": true, - "x-omitempty": true - }, - "updatedAt": { - "type": "string" - }, - "warrantyDetails": { - "type": "string" - }, - "warrantyExpires": { - "type": "string" - } - } - }, - "repo.ItemPatch": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "quantity": { - "type": "integer", - "x-nullable": true, - "x-omitempty": true - } - } - }, - "repo.ItemPath": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/repo.ItemType" - } - } - }, - "repo.ItemSummary": { - "type": "object", - "properties": { - "archived": { - "type": "boolean" - }, - "assetId": { - "type": "string", - "example": "0" - }, - "createdAt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "imageId": { - "type": "string", - "x-nullable": true, - "x-omitempty": true - }, - "insured": { - "type": "boolean" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LabelSummary" - } - }, - "location": { - "description": "Edges", - "allOf": [ - { - "$ref": "#/definitions/repo.LocationSummary" - } - ], - "x-nullable": true, - "x-omitempty": true - }, - "name": { - "type": "string" - }, - "purchasePrice": { - "type": "number" - }, - "quantity": { - "type": "integer" - }, - "soldTime": { - "description": "Sale details", - "type": "string" - }, - "thumbnailId": { - "type": "string", - "x-nullable": true, - "x-omitempty": true - }, - "updatedAt": { - "type": "string" - } - } - }, - "repo.ItemType": { - "type": "string", - "enum": [ - "location", - "item" - ], - "x-enum-varnames": [ - "ItemTypeLocation", - "ItemTypeItem" - ] - }, - "repo.ItemUpdate": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "archived": { - "type": "boolean" - }, - "assetId": { - "type": "string" - }, - "description": { - "type": "string", - "maxLength": 1000 - }, - "fields": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.ItemField" - } - }, - "id": { - "type": "string" - }, - "insured": { - "type": "boolean" - }, - "labelIds": { - "type": "array", - "items": { - "type": "string" - } - }, - "lifetimeWarranty": { - "description": "Warranty", - "type": "boolean" - }, - "locationId": { - "description": "Edges", - "type": "string" - }, - "manufacturer": { - "type": "string" - }, - "modelNumber": { - "type": "string" - }, - "name": { - "type": "string", - "maxLength": 255, - "minLength": 1 - }, - "notes": { - "description": "Extras", - "type": "string" - }, - "parentId": { - "type": "string", - "x-nullable": true, - "x-omitempty": true - }, - "purchaseFrom": { - "type": "string", - "maxLength": 255 - }, - "purchasePrice": { - "type": "number", - "x-nullable": true, - "x-omitempty": true - }, - "purchaseTime": { - "description": "Purchase", - "type": "string" - }, - "quantity": { - "type": "integer" - }, - "serialNumber": { - "description": "Identifications", - "type": "string" - }, - "soldNotes": { - "type": "string" - }, - "soldPrice": { - "type": "number", - "x-nullable": true, - "x-omitempty": true - }, - "soldTime": { - "description": "Sold", - "type": "string" - }, - "soldTo": { - "type": "string", - "maxLength": 255 - }, - "syncChildItemsLocations": { - "type": "boolean" - }, - "warrantyDetails": { - "type": "string" - }, - "warrantyExpires": { - "type": "string" - } - } - }, - "repo.LabelCreate": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "color": { - "type": "string" - }, - "description": { - "type": "string", - "maxLength": 255 - }, - "name": { - "type": "string", - "maxLength": 255, - "minLength": 1 - } - } - }, - "repo.LabelOut": { - "type": "object", - "properties": { - "color": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updatedAt": { - "type": "string" - } - } - }, - "repo.LabelSummary": { - "type": "object", - "properties": { - "color": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updatedAt": { - "type": "string" - } - } - }, - "repo.LocationCreate": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parentId": { - "type": "string", - "x-nullable": true - } - } - }, - "repo.LocationOut": { - "type": "object", - "properties": { - "children": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LocationSummary" - } - }, - "createdAt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parent": { - "$ref": "#/definitions/repo.LocationSummary" - }, - "totalPrice": { - "type": "number" - }, - "updatedAt": { - "type": "string" - } - } - }, - "repo.LocationOutCount": { - "type": "object", - "properties": { - "createdAt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "itemCount": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "updatedAt": { - "type": "string" - } - } - }, - "repo.LocationSummary": { - "type": "object", - "properties": { - "createdAt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updatedAt": { - "type": "string" - } - } - }, - "repo.LocationUpdate": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parentId": { - "type": "string", - "x-nullable": true - } - } - }, - "repo.MaintenanceEntry": { - "type": "object", - "properties": { - "completedDate": { - "type": "string" - }, - "cost": { - "type": "string", - "example": "0" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "scheduledDate": { - "type": "string" - } - } - }, - "repo.MaintenanceEntryCreate": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "completedDate": { - "type": "string" - }, - "cost": { - "type": "string", - "example": "0" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "scheduledDate": { - "type": "string" - } - } - }, - "repo.MaintenanceEntryUpdate": { - "type": "object", - "properties": { - "completedDate": { - "type": "string" - }, - "cost": { - "type": "string", - "example": "0" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "scheduledDate": { - "type": "string" - } - } - }, - "repo.MaintenanceEntryWithDetails": { - "type": "object", - "properties": { - "completedDate": { - "type": "string" - }, - "cost": { - "type": "string", - "example": "0" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "itemID": { - "type": "string" - }, - "itemName": { - "type": "string" - }, - "name": { - "type": "string" - }, - "scheduledDate": { - "type": "string" - } - } - }, - "repo.MaintenanceFilterStatus": { - "type": "string", - "enum": [ - "scheduled", - "completed", - "both" - ], - "x-enum-varnames": [ - "MaintenanceFilterStatusScheduled", - "MaintenanceFilterStatusCompleted", - "MaintenanceFilterStatusBoth" - ] - }, - "repo.NotifierCreate": { - "type": "object", - "required": [ - "name", - "url" - ], - "properties": { - "isActive": { - "type": "boolean" - }, - "name": { - "type": "string", - "maxLength": 255, - "minLength": 1 - }, - "url": { - "type": "string" - } - } - }, - "repo.NotifierOut": { - "type": "object", - "properties": { - "createdAt": { - "type": "string" - }, - "groupId": { - "type": "string" - }, - "id": { - "type": "string" - }, - "isActive": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "updatedAt": { - "type": "string" - }, - "url": { - "type": "string" - }, - "userId": { - "type": "string" - } - } - }, - "repo.NotifierUpdate": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "isActive": { - "type": "boolean" - }, - "name": { - "type": "string", - "maxLength": 255, - "minLength": 1 - }, - "url": { - "type": "string", - "x-nullable": true - } - } - }, - "repo.PaginationResult-repo_ItemSummary": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.ItemSummary" - } - }, - "page": { - "type": "integer" - }, - "pageSize": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - }, - "repo.TotalsByOrganizer": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "total": { - "type": "number" - } - } - }, - "repo.TreeItem": { - "type": "object", - "properties": { - "children": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.TreeItem" - } - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "repo.UserOut": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "groupId": { - "type": "string" - }, - "groupName": { - "type": "string" - }, - "id": { - "type": "string" - }, - "isOwner": { - "type": "boolean" - }, - "isSuperuser": { - "type": "boolean" - }, - "name": { - "type": "string" - } - } - }, - "repo.UserUpdate": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "repo.ValueOverTime": { - "type": "object", - "properties": { - "end": { - "type": "string" - }, - "entries": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.ValueOverTimeEntry" - } - }, - "start": { - "type": "string" - }, - "valueAtEnd": { - "type": "number" - }, - "valueAtStart": { - "type": "number" - } - } - }, - "repo.ValueOverTimeEntry": { - "type": "object", - "properties": { - "date": { - "type": "string" - }, - "name": { - "type": "string" - }, - "value": { - "type": "number" - } - } - }, - "services.Latest": { - "type": "object", - "properties": { - "date": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, - "services.UserRegistration": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "token": { - "type": "string" - } - } - }, - "user.Role": { - "type": "string", - "enum": [ - "user", - "user", - "owner" - ], - "x-enum-varnames": [ - "DefaultRole", - "RoleUser", - "RoleOwner" - ] - }, - "v1.APISummary": { - "type": "object", - "properties": { - "allowRegistration": { - "type": "boolean" - }, - "build": { - "$ref": "#/definitions/v1.Build" - }, - "demo": { - "type": "boolean" - }, - "health": { - "type": "boolean" - }, - "labelPrinting": { - "type": "boolean" - }, - "latest": { - "$ref": "#/definitions/services.Latest" - }, - "message": { - "type": "string" - }, - "title": { - "type": "string" - }, - "versions": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "v1.ActionAmountResult": { - "type": "object", - "properties": { - "completed": { - "type": "integer" - } - } - }, - "v1.Build": { - "type": "object", - "properties": { - "buildTime": { - "type": "string" - }, - "commit": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, - "v1.ChangePassword": { - "type": "object", - "properties": { - "current": { - "type": "string" - }, - "new": { - "type": "string" - } - } - }, - "v1.GroupInvitation": { - "type": "object", - "properties": { - "expiresAt": { - "type": "string" - }, - "token": { - "type": "string" - }, - "uses": { - "type": "integer" - } - } - }, - "v1.GroupInvitationCreate": { - "type": "object", - "required": [ - "uses" - ], - "properties": { - "expiresAt": { - "type": "string" - }, - "uses": { - "type": "integer", - "maximum": 100, - "minimum": 1 - } - } - }, - "v1.ItemAttachmentToken": { - "type": "object", - "properties": { - "token": { - "type": "string" - } - } - }, - "v1.LoginForm": { - "type": "object", - "properties": { - "password": { - "type": "string", - "example": "admin" - }, - "stayLoggedIn": { - "type": "boolean" - }, - "username": { - "type": "string", - "example": "admin@admin.com" - } - } - }, - "v1.TokenResponse": { - "type": "object", - "properties": { - "attachmentToken": { - "type": "string" - }, - "expiresAt": { - "type": "string" - }, - "token": { - "type": "string" - } - } - }, - "v1.Wrapped": { - "type": "object", - "properties": { - "item": {} - } - }, - "validate.ErrorResponse": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "fields": { - "type": "string" - } - } - } - }, - "securityDefinitions": { - "Bearer": { - "description": "\"Type 'Bearer TOKEN' to correctly set the API Key\"", - "type": "apiKey", - "name": "Authorization", - "in": "header" - } - } -} \ No newline at end of file diff --git a/tech_spec/project_structure.txt b/tech_spec/project_structure.txt deleted file mode 100644 index d9ba342..0000000 --- a/tech_spec/project_structure.txt +++ /dev/null @@ -1,191 +0,0 @@ - - - - Основной модуль приложения, содержит UI и точки входа в приложение. - Этот модуль зависит от data и domain; обеспечивает разделение UI от бизнес-логики через ViewModels и UseCases. - - Главная и единственная Activity приложения, содержит NavHost. - Интегрирован с Hilt для DI; навигация через Compose Navigation. - - - Класс Application, используется для настройки внедрения зависимостей Hilt. - - - Модуль Hilt для зависимостей уровня приложения. - - - Определяет навигационный граф для всего приложения с использованием Jetpack Compose Navigation. - - - Определяет маршруты для всех экранов в приложении в виде запечатанного класса. - - - UI для экрана панели управления. - Использует Compose для declarative UI; интегрирован с ViewModel для данных. - - - ViewModel для экрана панели управления, обрабатывает бизнес-логику. - - - UI для экрана списка инвентаря. - - - ViewModel для экрана списка инвентаря. - - - UI для экрана сведений о товаре. - - - ViewModel для экрана сведений о товаре. - - - UI для экрана редактирования товара. - - - ViewModel для экрана редактирования товара. - - - UI для экрана списка меток. - - - ViewModel для экрана списка меток. - - - UI для экрана списка местоположений. - Использует модель LocationOutCount для отображения количества элементов в каждой локации. - - - ViewModel для экрана списка местоположений. - - - UI для экрана поиска. - - - ViewModel для экрана поиска. - - - UI для экрана настройки. - - - ViewModel для экрана настройки. - - - Состояние UI для экрана настройки. - - - - Слой данных, отвечающий за источники данных (сеть, локальная БД) и реализации репозиториев. - Интегрирует Retrofit для API и Room для локального хранения; обеспечивает оффлайн-поддержку. - - Интерфейс сервиса Retrofit для Homebox API. - - - Определение базы данных Room для локального кэширования. - - - Реализация ItemRepository, координирующая данные из API и локальной БД. - - - Модуль Hilt для предоставления зависимостей, связанных с сетью (Retrofit, OkHttp). - - - Модуль Hilt для предоставления зависимостей, связанных с базой данных (Room DB, DAO). - - - Модуль Hilt для привязки интерфейсов репозиториев к их реализациям. - - - Модуль Hilt для предоставления зависимостей, связанных с хранилищем (EncryptedSharedPreferences). - - - Реализация CredentialsRepository. - - - Реализация AuthRepository. - - - - Доменный слой, содержит бизнес-логику, сценарии использования и интерфейсы репозиториев. Чистый модуль Kotlin. - Чистая бизнес-логика без зависимостей от Android; использует корутины для async. - - Класс данных для хранения учетных данных пользователя. - - - Интерфейс для репозитория аутентификации. - - - Интерфейс для репозитория учетных данных. - - - Интерфейс, определяющий контракт для операций с данными, связанными с товарами. - - - Сценарий использования для входа пользователя. - - - Сценарий использования для создания нового товара. - - - Сценарий использования для удаления товара. - - - Сценарий использования для получения всех меток. - - - Сценарий использования для получения всех местоположений со счетчиками элементов. - Возвращает List, а не базовую модель Location. - - - Сценарий использования для получения сведений о конкретном товаре. - - - Сценарий использования для получения недавно добавленных товаров. - - - Сценарий использования для получения статистики по инвентарю. - - - Сценарий использования для поиска товаров. - - - Сценарий использования для синхронизации локального инвентаря с удаленным сервером. - - - Сценарий использования для обновления существующего товара. - - - Модель инвентарного товара. - Data class с полями для контрактов; используется в UseCases и Repo. - - - Модель метки. - - - Модель местоположения. - - - Модель статистики инвентаря. - - - - Модуль для unit и integration тестов приложения. - Тесты основаны на контрактах из DbC; используют Kotest для assertions. - - Unit-тесты для DashboardViewModel. - Проверяет постусловия GetStatisticsUseCase. - - - Тесты навигационного графа. - - - - Модуль для unit-тестов доменного слоя. - - Unit-тесты для GetStatisticsUseCase. - Включает тесты на edge cases и нарушения контрактов. - - - Тесты модели Item. - - - \ No newline at end of file diff --git a/tech_spec/tech_spec.txt b/tech_spec/tech_spec.txt deleted file mode 100644 index 89cd7a8..0000000 --- a/tech_spec/tech_spec.txt +++ /dev/null @@ -1,583 +0,0 @@ - - - - Homebox Lens - Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox. - - - - - Библиотека логирования - В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования. - - Пример корректного использования Timber - - - - - - - Интернационализация (Мультиязычность) - - Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории. - Реализация будет основана на стандартном механизме ресурсов Android. - - Все строки, видимые пользователю, должны быть вынесены в файл `app/src/main/res/values/strings.xml`. Использование жестко закодированных строк в коде запрещено. - - Язык по умолчанию - русский (ru). Файл `strings.xml` будет содержать русские строки. - - Для поддержки других языков (например, английского - en) будут создаваться соответствующие каталоги ресурсов (например, `app/src/main/res/values-en/strings.xml`). - - В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`). - - - - UI Framework - Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных. - - - Внедрение зависимостей (Dependency Injection) - Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях. - - - Навигация - Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation. - - - Асинхронные операции - Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока. - - - Сетевое взаимодействие - Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON. - - - Локальное хранилище - Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных. - - - - - Спецификация безопасности проекта. - Все сетевые взаимодействия должны быть защищены HTTPS. Аутентификация пользователя хранится в EncryptedSharedPreferences. Обработка ошибок аутентификации должна включать logout и редирект на экран логина. - Использовать JWT или API-ключ для авторизации запросов. При истечении токена автоматически обновлять. - Локальные данные (credentials) шифровать с помощью Android KeyStore. - - - - Спецификация обработки ошибок. - Все потенциальные ошибки (сеть, БД, валидация) должны быть обработаны с использованием sealed classes для ошибок (e.g., NetworkError, ValidationError) и отображаться пользователю через Snackbar или Dialog. - При сетевых ошибках показывать сообщение "No internet connection" и предлагать retry. - Для HTTP 4xx/5xx отображать user-friendly сообщение на основе response body. - Использовать require/check для контрактов, логировать и показывать toast. - - - - - Модель инвентарного товара. - Содержит поля: id, name, description, quantity, location, labels, customFields. - - - Модель метки. - Содержит поля: id, name, color. - - - Модель местоположения. - Содержит поля: id, name, parentLocation. - - - Модель статистики инвентаря. - Содержит поля: totalItems, totalValue, locationsCount, labelsCount. - - - - - - Экран панели управления - Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам. - - - - Получение и отображение статистики - Получает общую статистику по инвентарю с сервера. - Пользователь аутентифицирован; сеть доступна. - Возвращает объект Statistics; данные кэшированы локально. - - Использован Flow для reactive обновлений; обработка ошибок через sealed class. - - - Получение и отображение недавно добавленных товаров - Получает список последних N добавленных товаров из локальной базы данных. - Пользователь аутентифицирован. - Возвращает Flow со списком ItemSummary; список отсортирован по дате создания. - - Данные берутся из локального кэша (Room) для быстрого отображения. - - - - - - Экран списка инвентаря - Отображает список всех инвентарных позиций с возможностью поиска и фильтрации. - - - - Поиск и фильтрация товаров - Ищет товары по строке запроса и фильтрам. Результаты разбиты на страницы. - Запрос не пустой; параметры пагинации валидны (page >= 1). - Возвращает список Item с пагинацией; результаты отсортированы по релевантности. - - Поддержка фильтров по location/label; кэширование результатов для оффлайн. - - - Синхронизация инвентаря - Выполняет полную синхронизацию локального кэша инвентаря с сервером. - Сеть доступна; пользователь аутентифицирован. - Локальная БД обновлена; возвращает success/failure. - - Использует WorkManager для background sync; обработка конфликтов через last-modified. - - - - - - Экран сведений о товаре - Показывает все сведения о конкретном инвентарном товаре, включая его название, описание, изображения, вложения и настраиваемые поля. - - - - Получение сведений о товаре - Получает полные сведения о конкретном товаре из репозитория. - Item ID валиден и существует. - Возвращает полный объект Item с attachments. - - Загрузка изображений через Coil; оффлайн-поддержка из Room. - - - - - - Создание/редактирование/удаление товаров - Позволяет пользователям создавать новые товары, обновлять существующие и удалять их. - - - - Создать товар - Создает новый инвентарный товар на сервере. - Все обязательные поля (name, quantity) заполнены; данные валидны. - Новый Item сохранен на сервере; ID возвращен. - - Валидация через require; sync с локальной БД. - - - Обновить товар - Обновляет существующий инвентарный товар на сервере. - Item ID существует; изменения валидны. - Item обновлен; версия инкрементирована. - - Partial update через PATCH; обработка concurrency. - - - Удалить товар - Удаляет инвентарный товар с сервера. - Item ID существует; пользователь имеет права. - Item удален; связанные ресурсы (attachments) очищены. - - Soft delete для восстановления; sync с локальной БД. - - - - - - Управление метками и местоположениями - Позволяет пользователям просматривать списки всех доступных меток и местоположений. - - - - - Получить все метки - Получает список всех меток из репозитория. - Сеть доступна или кэш существует. - Возвращает список Label; отсортирован по name. - - Кэширование в Room; reactive обновления. - - - Получить все местоположения - Получает список всех местоположений из репозитория. - Сеть доступна или кэш существует. - Возвращает список Location; иерархическая структура сохранена. - - Поддержка nested locations; кэширование. - - - - - - Экран поиска - Предоставляет специальный пользовательский интерфейс для поиска товаров. - - - - Поиск со специального экрана - Использует ту же функцию поиска, но со специального экрана. - Запрос не пустой. - Возвращает результаты поиска; UI обновлен. - - Интеграция с SearchView; debounce для запросов. - - - - - - - - Главный экран "Панель управления" - - Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox. - - - - Верхняя панель приложения. Содержит иконку навигационного меню (гамбургер), название/логотип приложения и иконку для запуска сканера (например, QR-кода). - - - Боковое навигационное меню. Открывается по нажатию на иконку в TopAppBar. Содержит основные разделы: Главная, Локации, Поиск, Профиль, Инструменты, а также кнопку "Выйти". - - - Основная область контента. Содержит несколько информационных блоков. - - Сетка из 2x2 карточек, отображающих ключевые метрики. - - - - - - - Горизонтально прокручиваемый список карточек недавно добавленных предметов. Если предметов нет, отображается сообщение "Элементы не найдены". - - - Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими местоположения. Нажатие на чип ведет к списку предметов в этом местоположении. - - - Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими метки. Нажатие на чип ведет к списку предметов с этой меткой. - - - - - Вместо плавающей кнопки (FAB), в референсе используется заметная кнопка "Создать" в навигационном меню. Мы будем придерживаться этого подхода для консистентности. Эта кнопка инициирует процесс создания нового предмета. - - - - - - Нажатие на чип местоположения/метки - Навигация на экран списка инвентаря с фильтром. - - - Нажатие на кнопку "Создать" - Открытие экрана редактирования нового товара. - - - - - - Экран "Локации" - - Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer). - - - - Общая верхняя панель приложения, аналогичная экрану "Панель управления". - - - Общее боковое меню навигации. - - - Основная область контента, занимающая все доступное пространство под TopAppBar. - - Заголовок экрана, расположенный вверху основной области контента. - - - Вертикальный, прокручиваемый список (LazyColumn) всех местоположений. - - Элемент списка, представляющий одно местоположение. Состоит из иконки (например, 'place') и названия местоположения. Весь элемент является кликабельным и ведет на экран со списком предметов в данной локации. - - - - - - Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новое местоположение. В веб-версии для этого используются иконки в углу, но FAB является более нативным паттерном для Android. - - - - - - Нажатие на элемент списка локаций - Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной локации. - - - Нажатие на FloatingActionButton - Открывается диалоговое окно или новый экран для создания нового местоположения. - - - - - - Экран "Метки" - - Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения. - - - - Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад". - - - Основная область контента, занимающая все доступное пространство под TopAppBar. - - Вертикальный, прокручиваемый список (LazyColumn) всех меток. - - Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой. - - - - - - Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку. - - - - - - Нажатие на элемент списка меток - Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке. - - - Нажатие на FloatingActionButton - Открывается диалоговое окно или новый экран для создания новой метки. - - - - - - Экран "Список инвентаря" - - Отображает список всех инвентарных позиций с возможностью поиска, фильтрации и пагинации. Интегрирован в навигацию. - - - - Верхняя панель с поиском и фильтрами. - - - Прокручиваемый список товаров. - - LazyColumn с карточками товаров (name, quantity, location). - - Кликабельная карточка товара, ведущая на details. - - - - - Кнопка для синхронизации инвентаря. - - - - - Ввод в поиск - Обновление списка с debounce. - - - Нажатие на товар - Навигация на screen_item_details. - - - - - - Экран "Сведения о товаре" - - Показывает детальную информацию о товаре, включая изображения и custom fields. - - - - С кнопками edit/delete. - - - - Карусель изображений. - - - Текст description. - - - Сетка custom полей. - - - - - - Нажатие edit - Навигация на screen_item_edit. - - - Нажатие delete - Подтверждение и вызов func_delete_item. - - - - - - Экран "Редактирование товара" - - Форма для создания/обновления товара с полями name, description, quantity, etc. - - - - С кнопкой save. - - - - Поле ввода имени. - - - Выбор местоположения. - - - Выбор меток. - - - Добавление изображений. - - - - - - Нажатие save - Валидация и вызов func_create_item или func_update_item. - - - - - - Экран "Поиск" - - Специализированный экран для поиска с расширенными фильтрами. - - - - С поисковой строкой. - - - - Чипы для фильтров (location, label). - - - LazyColumn результатов. - - - - - - Изменение запроса/фильтров - Обновление результатов. - - - - - - - - Руководство по использованию иконок - - Этот раздел определяет стандартный набор иконок 'androidx.compose.material.icons.Icons.Filled' - для использования в приложении. Для устаревших иконок указаны актуальные замены. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file