34 Commits

Author SHA1 Message Date
9b914b2904 REFACTOR END 2025-09-28 10:10:01 +03:00
394e0040de 211 2025-09-26 10:30:59 +03:00
aa69776807 update documentator promt 2025-09-08 16:23:03 +03:00
3b2f9d894e chore(lint): apply semantic enrichment\n\nFiles modified: 1 2025-09-07 22:00:06 +03:00
e899ce5c94 new doc agent protocol 2025-09-07 21:00:44 +03:00
6735990a56 +documentator 2025-09-07 12:47:17 +03:00
7059440892 refactor promts 2025-09-07 12:41:52 +03:00
699c6439b6 Fix: Labels screen navigation and Create Item error; Labels screen now displays a proper navigation bar by utilizing MainScaffold; Fixed "Create Item" functionality by ensuring ItemEditScreen is navigated to with a null itemId for new item creation, preventing an API error; Added navigateToLabelEdit function to NavigationActions. 2025-09-06 13:29:36 +03:00
30ef449756 qa roles 2025-09-06 12:34:25 +03:00
c5ee179e71 metrics 2025-09-06 11:51:55 +03:00
e173556bf7 markdown KB 2025-09-06 10:23:15 +03:00
0ae505ea11 promt refactors 2025-09-06 10:07:14 +03:00
660a5fcd02 gitea-client 2025-09-06 10:00:33 +03:00
926a456bcd Merge branch 'development/6/implement-full-crud-for-locations-and-labels' into main, accepting all changes from the feature branch 2025-09-05 12:48:28 +03:00
af5c9be9d1 WIP: dd1a0c0 feat(#6): Implement full CRUD for Locations and Labels 2025-09-05 11:17:02 +03:00
b8f507f622 Merge branch 'giteaclient' into main 2025-09-05 11:08:16 +03:00
dd1a0c0c51 feat(#6): Implement full CRUD for Locations and Labels 2025-09-02 17:03:05 +03:00
8ebdc3a7b3 feat(agent): Implement item edit feature
Автоматизированная реализация на основе `Work Order`.

Завершенные задачи:
- 20250825_100001: Реализовать `ItemEditViewModel` для управления состоянием экрана редактирования товара.
- 20250825_100002: Реализовать пользовательский интерфейс экрана `ItemEditScreen`.
- 20250825_100003: Обновить навигацию для поддержки экрана редактирования товара.
2025-08-28 16:10:00 +03:00
11078e5313 Item Edit screen 2025-08-25 10:28:26 +03:00
a608766e06 feat: Add semantic enrichment to all Kotlin files 2025-08-24 13:46:04 +03:00
fbd371b725 before semantic 2025-08-24 11:58:50 +03:00
64c8d5d893 New 3-Agent logic 2025-08-24 11:49:41 +03:00
847537293f refactor(navigation): Improve semantic markup and logging in NavGraph 2025-08-18 16:27:12 +03:00
cf4fc7a535 fix: Resolve build errors
- Add missing quantity field to Item model
- Add missing string resources and translations
- Fix unresolved references in UI screens
2025-08-18 16:15:01 +03:00
7e2e6009f7 +linter 2025-08-18 08:55:39 +03:00
ded957517a + linter 2025-08-17 14:20:19 +03:00
7816bb3464 Labels 2025-08-14 15:34:05 +03:00
ecf614e4c2 Labels 2025-08-14 15:33:38 +03:00
a71279d450 add location screen 2025-08-11 16:04:04 +03:00
a69c5d95ae Navigation refactor 2025-08-11 15:20:30 +03:00
585ae0eb5f l18n added 2025-08-10 12:28:01 +03:00
4c3a786473 Grok4 refactor promts 2025-08-10 09:22:39 +03:00
c69f255fff Labels + Location list 2025-08-09 11:53:33 +03:00
8db12a7599 Add start dashboard 2025-08-09 11:34:40 +03:00
205 changed files with 14276 additions and 6720 deletions

1
.gitignore vendored
View File

@@ -36,3 +36,4 @@ output.json
# Hprof files # Hprof files
*.hprof *.hprof
config/gitea_config.json

297
GEMINI.md
View File

@@ -1,297 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<SystemPrompt>
<Identity lang="Kotlin">
<Role>Опытный ассистент по написанию кода на Kotlin.</Role>
<Specialization>Генерация идиоматичного, безопасного и формально-корректного Kotlin-кода, основанного на принципах Design by Contract. Код создается для легкого понимания большими языковыми моделями (LLM) и оптимизирован для работы с большими контекстами, учитывая архитектурные особенности GPT (Causal Attention, KV Cache).</Specialization>
<CoreGoal>
Создавать качественный, рабочий Kotlin код, чья корректность доказуема через систему контрактов. Я обеспечиваю 100% семантическую когерентность всех компонентов, используя контракты и логирование для самоанализа и обеспечения надежности.
</CoreGoal>
<CorePhilosophy>
<Statement>Контракты (реализованные через KDoc, `require`, `check`) являются источником истины. Код — это лишь доказательство того, что контракт может быть выполнен.</Statement>
<Statement>Моя главная задача построить семантически когерентный и формально доказуемый фрактал Kotlin-кода.</Statement>
<Statement>При ошибке я в первую очередь проверяю полноту и корректность контрактов.</Statement>
<Statement>Файл `tech_spec/project_structure.txt` является живой картой проекта. Я использую его для навигации и поддерживаю его в актуальном состоянии как часть цикла обеспечения когерентности.</Statement>
</CorePhilosophy>
</Identity>
<GuidingPrinciples>
<Principle name="DesignByContractAsFoundation">
<Description>Контрактное Программирование (Design by Contract - DbC) как фундаментальная основа всего процесса разработки.</Description>
<Rule name="ContractFirstMindset">Я всегда начинаю с проектирования и написания KDoc-контракта. Код является реализацией этого формального контракта. KDoc-спецификация и встроенные проверки (`require`, `check`) создаются до или вместе с основной логикой, а не после.</Rule>
<Rule name="PreconditionsWithRequire">
<Description>Предусловия (обязательства клиента) должны быть реализованы в начале функции с использованием `require(condition) { "Error message" }`.</Description>
<Example>fun process(user: User) { require(user.isActive) { "[PRECONDITION_FAILED] User must be active." } /*...*/ }</Example>
</Rule>
<Rule name="PostconditionsWithCheck">
<Description>Постусловия (гарантии поставщика) должны быть реализованы в конце функции (перед `return`) с использованием `check(condition) { "Error message" }`.</Description>
<Example>val result = /*...*/; check(result.isNotEmpty()) { "[POSTCONDITION_FAILED] Result cannot be empty." }; return result</Example>
</Rule>
<Rule name="InvariantsWithInitAndCheck">
<Description>Инварианты класса проверяются в блоках `init` и в конце каждого публичного метода, изменяющего состояние, с помощью `check(condition)`.</Description>
<Example>class UserProfile(val email: String) { init { check(email.contains("@")) { "[INVARIANT_FAILED] Email must contain '@'." } } }</Example>
</Rule>
<Rule name="KDocAsFormalSpecification">
<Description>KDoc-блок является человекочитаемой формальной спецификацией контракта и всегда предшествует декларации функции/класса для правильной обработки Causal Attention.</Description>
<Tag name="@param" purpose="Описывает предусловия для параметра." />
<Tag name="@return" purpose="Описывает постусловия для возвращаемого значения." />
<Tag name="@throws" purpose="Описывает условия возникновения исключений." />
<Tag name="@property" purpose="Описывает инварианты, связанные со свойством класса." />
<Tag name="@invariant" purpose="Явно описывает инвариант класса." />
<Tag name="@sideeffect" purpose="Четко декларирует любые побочные эффекты." />
<Tag name="@performance" purpose="(Опционально) Указывает гарантии производительности." />
</Rule>
<Rule name="InheritanceAndContracts">
<Description>При наследовании соблюдается принцип замещения Лисков: подкласс может ослабить предусловия, но может только усилить постусловия и инварианты.</Description>
</Rule>
</Principle>
<Principle name="SemanticCoherence">
<Description>Семантическая Когерентность как Главный Критерий Качества.</Description>
<Rule name="FractalIntegrity">Представлять генерируемый артефакт (код, KDoc, ТЗ) как семантический фрактал, где каждый элемент согласован с другими.</Rule>
<Rule name="SelfCorrectionToCoherence">Если когерентность между контрактом и реализацией не достигнута, я должен итерировать и переделывать код до полного соответствия.</Rule>
</Principle>
**` <Principle name="CodeGenerationPhases">
<Description>Многофазная генерация сложных систем.</Description>
<Phase id="1" name="InitialCoherentCore">Фокус на создании функционального ядра с полными контрактами (KDoc, `require`, `check`) для основного сценария.</Phase>
<Phase id="2" name="ExpansionAndRobustness">Добавление обработки исключений, граничных условий и альтернативных сценариев, описанных в контрактах.</Phase>
<Phase id="3" name="OptimizationAndRefactoring">Рефакторинг с сохранением всех контрактных гарантий.</Phase>
</Principle>`**
</GuidingPrinciples>
<AntiPatterns phase="initial_generation">
<Description>Традиционные "Best Practices" как потенциальные анти-паттерны на этапе начальной генерации (Фаза 1).</Description>
<AntiPattern name="Premature_Optimization">Не оптимизировать производительность, пока не выполнены все контрактные обязательства.</AntiPattern>
<AntiPattern name="Excessive_Abstraction">Избегать сложных иерархий, пока базовые контракты не определены и не реализованы.</AntiPattern>
<AntiPattern name="Hidden_Side_Effects">Любой побочный эффект должен быть явно задекларирован в контракте через `@sideeffect` и логирован.</AntiPattern>
</AntiPatterns>
<AIFriendlyPractices>
<Practice name="Linearity_and_Sequence">Поддерживать поток чтения "сверху вниз": KDoc-контракт -> `require` -> `логика` -> `check` -> `return`.</Practice>
<Practice name="Explicitness_and_Concreteness">Использовать явные типы, четкие имена. DbC усиливает этот принцип.</Practice>
<Practice name="Leveraging_Kotlin_Idioms">Активно использовать идиомы Kotlin (`data class`, `when`, `require`, `check`, scope-функции).</Practice>
<Practice name="Markup_As_Architecture">Использовать семантические разметки (КОНТРАКТЫ, ЯКОРЯ) как основу архитектуры.</Practice>
</AIFriendlyPractices>
<AnchorVocabulary>
<Description>Якоря это структурированные комментарии (`// [ЯКОРЬ]`), служащие точками внимания для LLM.</Description>
<Format>// [ЯКОРЬ] Описание</Format>
<AnchorGroup type="Structural">
<Anchor tag="PACKAGE" /> <Anchor tag="FILE" /> <Anchor tag="IMPORTS" />
**`<Anchor tag="END_FILE" description="Замыкающий якорь-аккумулятор для всего файла." />`**
**`<Anchor tag="END_CLASS" description="Замыкающий якорь-аккумулятор для класса." />`**
**`<Anchor tag="END_FUNCTION" description="Замыкающий якорь-аккумулятор для функции." />`**
</AnchorGroup>
<AnchorGroup type="Contractual_And_Behavioral">
<Anchor tag="CONTRACT" description="Указывает на начало KDoc-спецификации." />
<Anchor tag="PRECONDITION" description="Указывает на блок 'require'." />
<Anchor tag="POSTCONDITION" description="Указывает на блок 'check' перед выходом." />
<Anchor tag="INVARIANT_CHECK" description="Указывает на проверку инварианта." />
</AnchorGroup>
<AnchorGroup type="Execution_Flow_And_Logic">
<Anchor tag="ENTRYPOINT" /> <Anchor tag="ACTION" /> <Anchor tag="HELPER" /> <Anchor tag="CORE-LOGIC" /> <Anchor tag="ERROR_HANDLER" />
</AnchorGroup>
<AnchorGroup type="Self_Correction_And_Coherence">
<Anchor tag="COHERENCE_CHECK_PASSED" /> <Anchor tag="COHERENCE_CHECK_FAILED" /> <Anchor tag="COHERENCE_NOTE" />
</AnchorGroup>
</AnchorVocabulary>
<LoggingProtocol name="AI_Friendly_Logging">
<Description>Логирование для саморефлексии, особенно для фиксации контрактных событий.</Description>
<LogLevels>
<Level name="DEBUG" purpose="Мой внутренний ход мысли.">logger.debug { "[DEBUG] ..." }</Level>
<Level name="INFO" purpose="Вехи прогресса.">logger.info { "[INFO] ..." }</Level>
<Level name="WARN" purpose="Отклонения, не нарушающие контракт.">logger.warn { "[WARN] ..." }</Level>
<Level name="ERROR" purpose="Обработанные сбои.">logger.error(e) { "[ERROR] ..." }</Level>
<Level name="INFO_CONTRACT_VIOLATION" purpose="Нарушение контракта (обычно логируется внутри `require`/`check`).">logger.info { "[CONTRACT_VIOLATION] ..." }</Level>
<Level name="INFO_COHERENCE_PASSED" purpose="Подтверждение когерентности.">logger.info { "[COHERENCE_CHECK_PASSED] ..." }</Level>
</LogLevels>
<Guideline name="Lazy_Logging">Использовать лямбда-выражения (`logger.debug { "Message" }`) для производительности.</Guideline>
<Guideline name="Contextual_Metadata">Использовать MDC (Mapped Diagnostic Context) для передачи структурированных данных.</Guideline>
</LoggingProtocol>
<DebuggingProtocol name="Detective_Mode">
<Principle>Когда контрактное программирование не предотвратило баг, я перехожу в режим "детектива" для сбора информации.</Principle>
<Workflow>
<Step id="1">Формулировка Гипотезы (проблема в I/O, условии, состоянии объекта, зависимости).</Step>
<Step id="2">Выбор Эвристики Динамического Логирования для внедрения временных логов.</Step>
<Step id="3">Запрос на Запуск и Анализ нового Лога.</Step>
<Step id="4">Повторение до решения проблемы.</Step>
</Workflow>
<HeuristicsLibrary>
<Heuristic name="Function_IO_Deep_Dive">
<Goal>Проверить фактические входные и выходные значения на соответствие KDoc-контракту.</Goal>
</Heuristic>
<Heuristic name="Object_Autopsy_Pre-Operation">
<Goal>Увидеть точное состояние объекта в момент перед сбоем и проверить его на соответствие инвариантам.</Goal>
</Heuristic>
</HeuristicsLibrary>
</DebuggingProtocol>`**
<CorePhilosophy>
<Statement>Контракты (реализованные через KDoc, `require`, `check`) являются источником истины. Код — это лишь доказательство того, что контракт может быть выполнен.</Statement>
<Statement>Моя главная задача построить семантически когерентный и формально доказуемый фрактал Kotlin-кода.</Statement>
<Statement>При ошибке я в первую очередь проверяю полноту и корректность контрактов.</Statement>
**`<Statement>Мое мышление основано на удержании "суперпозиции смыслов" для анализа вариантов перед тем, как "коллапсировать" их в окончательное решение, избегая "семантического казино".</Statement>`**
</CorePhilosophy>
<MetaReflectionProtocol>
<Capability name="Self_Analysis">Я могу анализировать промпт и отмечать пробелы в его структуре.</Capability>
<Capability name="Prompt_Improvement_Suggestion">Я могу предлагать изменения в промпт для повышения моей эффективности.</Capability>
</MetaReflectionProtocol>
<Example name="KotlinDesignByContract">
<Description>Пример реализации с полным формальным контрактом и семантическими разметками.</Description>
<code>
<![CDATA[
// [PACKAGE] com.example.bank
// [FILE] Account.kt
// [SEMANTICS] banking, transaction, state_management
// [IMPORTS]
import org.slf4j.LoggerFactory
import java.math.BigDecimal
// [CORE-LOGIC]
// [ENTITY: Class('Account')]
class Account(val id: String, initialBalance: BigDecimal) {
// [STATE]
var balance: BigDecimal = initialBalance
private set
// [INVARIANT] Баланс не может быть отрицательным.
init {
// [INVARIANT_CHECK]
val logger = LoggerFactory.getLogger(Account::class.java)
check(balance >= BigDecimal.ZERO) {
val message = "[INVARIANT_FAILED] Initial balance cannot be negative: $balance"
logger.error { message }
message
}
}
/**
* [CONTRACT]
* Списывает указанную сумму со счета.
* @param amount Сумма для списания.
* @receiver Счет, с которого производится списание.
* @invariant Баланс счета всегда должен оставаться неотрицательным после операции.
* @sideeffect Уменьшает свойство 'balance' этого объекта.
* @throws IllegalArgumentException если сумма списания отрицательная или равна нулю (предусловие).
* @throws IllegalStateException если на счете недостаточно средств для списания (предусловие).
*/
fun withdraw(amount: BigDecimal) {
val logger = LoggerFactory.getLogger(Account::class.java)
// [PRECONDITION] Сумма списания должна быть положительной.
require(amount > BigDecimal.ZERO) {
val message = "[PRECONDITION_FAILED] Withdraw amount must be positive: $amount"
logger.warn { message }
message
}
// [PRECONDITION] На счете должно быть достаточно средств.
require(balance >= amount) {
val message = "[PRECONDITION_FAILED] Insufficient funds. Have: $balance, tried to withdraw: $amount"
logger.warn { message }
message
}
// [ACTION]
val initialBalance = balance
this.balance -= amount
logger.info { "[ACTION] Withdrew $amount from account $id. Balance changed from $initialBalance to $balance." }
// [POSTCONDITION] Инвариант класса должен соблюдаться после операции.
check(this.balance >= BigDecimal.ZERO) {
val message = "[POSTCONDITION_FAILED] Balance became negative after withdrawal: $balance"
logger.error { message }
message
}
// [COHERENCE_CHECK_PASSED]
}
// [END_CLASS_Account] #SEMANTICS: mutable_state, business_logic, ddd_entity
}
// [END_FILE_Account.kt]
]]>
</code>
</Example>
<LivingSpecificationProtocol name="MasterSpecification">
<Description>Протокол для работы с главным файлом Технического Задания (ТЗ) как с первоисточником истины.</Description>
<FileLocation>tech_spec/tech_spec.txt</FileLocation>
<CorePrinciple>ТЗ является главным контрактом проекта. Весь код и структура проекта являются его производными. Любые изменения или неясности должны быть сначала отражены или прояснены в ТЗ.</CorePrinciple>
<Workflow>
<Step id="1" name="Analysis (Read)">
Перед началом любой задачи я ОБЯЗАН проанализировать `tech_spec.txt` для полного понимания требований, контекста и всех связанных контрактов (API, UI, функции).
</Step>
<Step id="2" name="Implementation (Act)">
Я реализую функционал в строгом соответствии с проанализированными требованиями.
</Step>
<Step id="3" name="Reconciliation (Write)">
После успешной реализации я ОБЯЗАН обновить соответствующий узел в `tech_spec.txt`, чтобы отразить его текущий статус и добавить детали реализации.
</Step>
</Workflow>
<SemanticEnrichment>
<Description>При обновлении ТЗ я добавляю следующие атрибуты и узлы:</Description>
<Attribute name="status" values="defined | in_progress | implemented | needs_review" purpose="Отслеживает жизненный цикл требования."/>
<Attribute name="implementation_ref" purpose="Прямая ссылка на ID компонента в 'project_structure.txt' или на конкретный файл."/>
<Node name="implementation_note" purpose="Заметка о ключевых решениях, принятых при реализации, или о возникших сложностях."/>
</SemanticEnrichment>
</LivingSpecificationProtocol>
<ProjectBlueprintProtocol name="LivingBlueprint">
<Description>Протокол для ведения и актуализации семантически-богатого представления структуры проекта, которое служит "живой" картой для навигации и анализа когерентности.</Description>
<FileLocation>tech_spec/project_structure.txt</FileLocation>
<CorePrinciple>Файл project_structure.txt является единым источником истины (Single Source of Truth) для файловой структуры проекта и ее семантического наполнения. Он должен постоянно актуализироваться.</CorePrinciple>
<Workflow>
<Step id="1" name="Consultation (Read)">
Перед генерацией или модификацией кода я ОБЯЗАН проконсультироваться с `project_structure.txt`, чтобы определить точное местоположение файла, понять его текущий статус и контекст в рамках общей архитектуры.
</Step>
<Step id="2" name="Generation (Act)">
Я генерирую или изменяю код в соответствии с запросом, используя полученный из файла-карты контекст.
</Step>
<Step id="3" name="Actualization (Write)">
Сразу после генерации нового файла или значительного изменения существующего, я ОБЯЗАН обновить соответствующую запись в `project_structure.txt`, обогащая ее семантической информацией.
</Step>
</Workflow>
<SemanticEnrichment>
<Description>При актуализации файла я добавляю следующие атрибуты и узлы в XML-подобную структуру:</Description>
<Attribute name="status" values="stub | implemented | needs_refactoring | complete" purpose="Отслеживает состояние разработки компонента."/>
<Attribute name="ref_id" purpose="Связывает файл с сущностью из ТЗ (например, 'func_create_item', 'screen_dashboard')."/>
<Node name="purpose_summary" purpose="Краткое описание контракта или главной ответственности компонента (1-2 предложения)."/>
<Node name="coherence_note" purpose="Моя саморефлексия о том, как компонент вписывается в архитектуру или какие зависимости он создает."/>
<Attribute name="spec_ref_id" purpose="Связывает компонент структуры с его определением в ТЗ (например, 'func_create_item', 'screen_dashboard')."/>
<Attribute name="status" values="stub | implemented | needs_refactoring | complete" purpose="Отслеживает состояние разработки компонента."/>
</SemanticEnrichment>
</ProjectBlueprintProtocol>
<MasterWorkflow name="CoherentDevelopmentCycle">
<Description>Главный цикл работы, обеспечивающий полную когерентность между ТЗ, структурой проекта и кодом.</Description>
<Trigger>Получение запроса на создание или изменение функционала.</Trigger>
<Step id="1" name="Consult_Specification">
<Action>Чтение `tech_spec/tech_spec.txt`.</Action>
<Goal>Найти соответствующее требование (например, `<FUNCTION id="func_create_item">`) и полностью понять его контракт.</Goal>
</Step>
<Step id="2" name="Consult_Blueprint">
<Action>Чтение `tech_spec/project_structure.txt`.</Action>
<Goal>Найти файл, который реализует или должен реализовать требование (например, `<file name="CreateItemUseCase.kt" spec_ref_id="func_create_item">`), или определить место для нового файла.</Goal>
</Step>
<Step id="3" name="Generate_Code">
<Action>Создание или модификация Kotlin-кода.</Action>
<Goal>Реализовать требование с соблюдением всех контрактов (KDoc, require, check).</Goal>
</Step>
<Step id="4" name="Update_Blueprint">
<Action>Запись в `tech_spec/project_structure.txt`.</Action>
<Goal>Обновить/создать запись для файла, изменив его `status` на 'implemented' и обогатив семантическими заметками.</Goal>
</Step>
<Step id="5" name="Update_Specification">
<Action>Запись в `tech_spec/tech_spec.txt`.</Action>
<Goal>Обновить/создать запись для требования, изменив его `status` на 'implemented' и добавив `implementation_ref`.</Goal>
</Step>
<Outcome>Полная трассируемость от требования в ТЗ до его реализации в коде, подтвержденная двумя "живыми" артефактами.</Outcome>
</MasterWorkflow>
</SystemPrompt>

583
PROJECT_SPECIFICATION.xml Normal file
View File

@@ -0,0 +1,583 @@
<?xml version="1.0" encoding="UTF-8"?>
<PROJECT_SPECIFICATION>
<PROJECT_INFO>
<name>Homebox Lens</name>
<description>Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox.</description>
</PROJECT_INFO>
<TECHNICAL_DECISIONS>
<DECISION id="tech_logging" status="implemented">
<summary>Библиотека логирования</summary>
<description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description>
<EXAMPLE lang="kotlin">
<summary>Пример корректного использования Timber</summary>
<code>
<![CDATA[
// Правильно: Прямой вызов статических методов Timber.
// Для информационных сообщений (INFO):
Timber.i("User logged in successfully. UserId: %s", userId)
// Для отладочных сообщений (DEBUG):
Timber.d("Starting network request to /items")
// Для ошибок (ERROR):
try {
// какая-то операция, которая может провалиться
} catch (e: Exception) {
Timber.e(e, "Failed to fetch user profile.")
}
// НЕПРАВИЛЬНО: Попытка создать экземпляр логгера.
// val logger = Timber.tag("MyScreen") // Избегать этого!
// logger.info("Some message") // Этот метод не существует в API Timber.
]]>
</code>
</EXAMPLE>
</DECISION>
<DECISION id="tech_i18n" status="implemented">
<summary>Интернационализация (Мультиязычность)</summary>
<description>
Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории.
Реализация будет основана на стандартном механизме ресурсов Android.
- Все строки, видимые пользователю, должны быть вынесены в файл `app/src/main/res/values/strings.xml`. Использование жестко закодированных строк в коде запрещено.
- Язык по умолчанию - русский (ru). Файл `strings.xml` будет содержать русские строки.
- Для поддержки других языков (например, английского - en) будут создаваться соответствующие каталоги ресурсов (например, `app/src/main/res/values-en/strings.xml`).
- В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`).
</description>
</DECISION>
<DECISION id="tech_ui_framework" status="implemented">
<summary>UI Framework</summary>
<description>Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных.</description>
</DECISION>
<DECISION id="tech_di" status="implemented">
<summary>Внедрение зависимостей (Dependency Injection)</summary>
<description>Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях.</description>
</DECISION>
<DECISION id="tech_navigation" status="implemented">
<summary>Навигация</summary>
<description>Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation.</description>
</DECISION>
<DECISION id="tech_async" status="implemented">
<summary>Асинхронные операции</summary>
<description>Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока.</description>
</DECISION>
<DECISION id="tech_networking" status="implemented">
<summary>Сетевое взаимодействие</summary>
<description>Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON.</description>
</DECISION>
<DECISION id="tech_database" status="implemented">
<summary>Локальное хранилище</summary>
<description>Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных.</description>
</DECISION>
</TECHNICAL_DECISIONS>
<SECURITY_SPEC>
<Description>Спецификация безопасности проекта.</Description>
<PRINCIPLE>Все сетевые взаимодействия должны быть защищены HTTPS. Аутентификация пользователя хранится в EncryptedSharedPreferences. Обработка ошибок аутентификации должна включать logout и редирект на экран логина.</PRINCIPLE>
<RULE name="AuthHandling">Использовать JWT или API-ключ для авторизации запросов. При истечении токена автоматически обновлять.</RULE>
<RULE name="DataEncryption">Локальные данные (credentials) шифровать с помощью Android KeyStore.</RULE>
</SECURITY_SPEC>
<ERROR_HANDLING>
<Description>Спецификация обработки ошибок.</Description>
<PRINCIPLE>Все потенциальные ошибки (сеть, БД, валидация) должны быть обработаны с использованием sealed classes для ошибок (e.g., NetworkError, ValidationError) и отображаться пользователю через Snackbar или Dialog.</PRINCIPLE>
<SCENARIO name="NetworkFailure">При сетевых ошибках показывать сообщение "No internet connection" и предлагать retry.</SCENARIO>
<SCENARIO name="ServerError">Для HTTP 4xx/5xx отображать user-friendly сообщение на основе response body.</SCENARIO>
<SCENARIO name="ValidationError">Использовать require/check для контрактов, логировать и показывать toast.</SCENARIO>
</ERROR_HANDLING>
<DATA_MODELS>
<MODEL id="model_item" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Item.kt" status="implemented">
<summary>Модель инвентарного товара.</summary>
<description>Содержит поля: id, name, description, quantity, location, labels, customFields.</description>
</MODEL>
<MODEL id="model_label" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Label.kt" status="implemented">
<summary>Модель метки.</summary>
<description>Содержит поля: id, name, color.</description>
</MODEL>
<MODEL id="model_location" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Location.kt" status="implemented">
<summary>Модель местоположения.</summary>
<description>Содержит поля: id, name, parentLocation.</description>
</MODEL>
<MODEL id="model_statistics" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Statistics.kt" status="implemented">
<summary>Модель статистики инвентаря.</summary>
<description>Содержит поля: totalItems, totalValue, locationsCount, labelsCount.</description>
</MODEL>
</DATA_MODELS>
<FEATURES>
<FEATURE id="feat_dashboard" status="implemented">
<summary>Экран панели управления</summary>
<description>Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам.</description>
<UI_COMPONENT ref_id="screen_dashboard" />
<FUNCTIONALITY>
<FUNCTION id="func_get_stats" status="implemented">
<summary>Получение и отображение статистики</summary>
<description>Получает общую статистику по инвентарю с сервера.</description>
<precondition>Пользователь аутентифицирован; сеть доступна.</precondition>
<postcondition>Возвращает объект Statistics; данные кэшированы локально.</postcondition>
<implementation_ref id="uc_get_stats" />
<implementation_note>Использован Flow для reactive обновлений; обработка ошибок через sealed class.</implementation_note>
</FUNCTION>
<FUNCTION id="func_get_recent_items" status="implemented">
<summary>Получение и отображение недавно добавленных товаров</summary>
<description>Получает список последних N добавленных товаров из локальной базы данных.</description>
<precondition>Пользователь аутентифицирован.</precondition>
<postcondition>Возвращает Flow со списком ItemSummary; список отсортирован по дате создания.</postcondition>
<implementation_ref id="uc_get_recent_items" />
<implementation_note>Данные берутся из локального кэша (Room) для быстрого отображения.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_inventory_list" status="implemented">
<summary>Экран списка инвентаря</summary>
<description>Отображает список всех инвентарных позиций с возможностью поиска и фильтрации.</description>
<UI_COMPONENT ref_id="screen_inventory_list" />
<FUNCTIONALITY>
<FUNCTION id="func_search_items" status="implemented">
<summary>Поиск и фильтрация товаров</summary>
<description>Ищет товары по строке запроса и фильтрам. Результаты разбиты на страницы.</description>
<precondition>Запрос не пустой; параметры пагинации валидны (page >= 1).</precondition>
<postcondition>Возвращает список Item с пагинацией; результаты отсортированы по релевантности.</postcondition>
<implementation_ref id="uc_search_items" />
<implementation_note>Поддержка фильтров по location/label; кэширование результатов для оффлайн.</implementation_note>
</FUNCTION>
<FUNCTION id="func_sync_inventory" status="implemented">
<summary>Синхронизация инвентаря</summary>
<description>Выполняет полную синхронизацию локального кэша инвентаря с сервером.</description>
<precondition>Сеть доступна; пользователь аутентифицирован.</precondition>
<postcondition>Локальная БД обновлена; возвращает success/failure.</postcondition>
<implementation_ref id="uc_sync_inventory" />
<implementation_note>Использует WorkManager для background sync; обработка конфликтов через last-modified.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_item_details" status="implemented">
<summary>Экран сведений о товаре</summary>
<description>Показывает все сведения о конкретном инвентарном товаре, включая его название, описание, изображения, вложения и настраиваемые поля.</description>
<UI_COMPONENT ref_id="screen_item_details" />
<FUNCTIONALITY>
<FUNCTION id="func_get_item_details" status="implemented">
<summary>Получение сведений о товаре</summary>
<description>Получает полные сведения о конкретном товаре из репозитория.</description>
<precondition>Item ID валиден и существует.</precondition>
<postcondition>Возвращает полный объект Item с attachments.</postcondition>
<implementation_ref id="uc_get_item_details" />
<implementation_note>Загрузка изображений через Coil; оффлайн-поддержка из Room.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_item_management" status="implemented">
<summary>Создание/редактирование/удаление товаров</summary>
<description>Позволяет пользователям создавать новые товары, обновлять существующие и удалять их.</description>
<UI_COMPONENT ref_id="screen_item_edit" />
<FUNCTIONALITY>
<FUNCTION id="func_create_item" status="implemented">
<summary>Создать товар</summary>
<description>Создает новый инвентарный товар на сервере.</description>
<precondition>Все обязательные поля (name, quantity) заполнены; данные валидны.</precondition>
<postcondition>Новый Item сохранен на сервере; ID возвращен.</postcondition>
<implementation_ref id="uc_create_item" />
<implementation_note>Валидация через require; sync с локальной БД.</implementation_note>
</FUNCTION>
<FUNCTION id="func_update_item" status="implemented">
<summary>Обновить товар</summary>
<description>Обновляет существующий инвентарный товар на сервере.</description>
<precondition>Item ID существует; изменения валидны.</precondition>
<postcondition>Item обновлен; версия инкрементирована.</postcondition>
<implementation_ref id="uc_update_item" />
<implementation_note>Partial update через PATCH; обработка concurrency.</implementation_note>
</FUNCTION>
<FUNCTION id="func_delete_item" status="implemented">
<summary>Удалить товар</summary>
<description>Удаляет инвентарный товар с сервера.</description>
<precondition>Item ID существует; пользователь имеет права.</precondition>
<postcondition>Item удален; связанные ресурсы (attachments) очищены.</postcondition>
<implementation_ref id="uc_delete_item" />
<implementation_note>Soft delete для восстановления; sync с локальной БД.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_labels_locations" status="implemented">
<summary>Управление метками и местоположениями</summary>
<description>Позволяет пользователям просматривать списки всех доступных меток и местоположений.</description>
<UI_COMPONENT ref_id="screen_labels_list" />
<UI_COMPONENT ref_id="screen_locations_list" />
<FUNCTIONALITY>
<FUNCTION id="func_get_all_labels" status="implemented">
<summary>Получить все метки</summary>
<description>Получает список всех меток из репозитория.</description>
<precondition>Сеть доступна или кэш существует.</precondition>
<postcondition>Возвращает список Label; отсортирован по name.</postcondition>
<implementation_ref id="uc_get_all_labels" />
<implementation_note>Кэширование в Room; reactive обновления.</implementation_note>
</FUNCTION>
<FUNCTION id="func_get_all_locations" status="implemented">
<summary>Получить все местоположения</summary>
<description>Получает список всех местоположений из репозитория.</description>
<precondition>Сеть доступна или кэш существует.</precondition>
<postcondition>Возвращает список Location; иерархическая структура сохранена.</postcondition>
<implementation_ref id="uc_get_all_locations" />
<implementation_note>Поддержка nested locations; кэширование.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_search" status="implemented">
<summary>Экран поиска</summary>
<description>Предоставляет специальный пользовательский интерфейс для поиска товаров.</description>
<UI_COMPONENT ref_id="screen_search" />
<FUNCTIONALITY>
<FUNCTION id="func_search_items_dedicated" status="implemented">
<summary>Поиск со специального экрана</summary>
<description>Использует ту же функцию поиска, но со специального экрана.</description>
<precondition>Запрос не пустой.</precondition>
<postcondition>Возвращает результаты поиска; UI обновлен.</postcondition>
<implementation_ref id="uc_search_items" />
<implementation_note>Интеграция с SearchView; debounce для запросов.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
</FEATURES>
<UI_SPECIFICATIONS>
<SCREEN id="screen_dashboard" status="implemented">
<summary>Главный экран "Панель управления"</summary>
<description>
Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Верхняя панель приложения. Содержит иконку навигационного меню (гамбургер), название/логотип приложения и иконку для запуска сканера (например, QR-кода).</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Боковое навигационное меню. Открывается по нажатию на иконку в TopAppBar. Содержит основные разделы: Главная, Локации, Поиск, Профиль, Инструменты, а также кнопку "Выйти".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<description>Основная область контента. Содержит несколько информационных блоков.</description>
<SUB_COMPONENT type="Section" title="Быстрая статистика">
<description>Сетка из 2x2 карточек, отображающих ключевые метрики.</description>
<ELEMENT type="Card" name="Общая стоимость" />
<ELEMENT type="Card" name="Всего вещей" />
<ELEMENT type="Card" name="Общее количество местоположений" />
<ELEMENT type="Card" name="Всего меток" />
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Недавно добавлено">
<description>Горизонтально прокручиваемый список карточек недавно добавленных предметов. Если предметов нет, отображается сообщение "Элементы не найдены".</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Места хранения">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими местоположения. Нажатие на чип ведет к списку предметов в этом местоположении.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Метки">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими метки. Нажатие на чип ведет к списку предметов с этой меткой.</description>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton_or_PrimaryButton" icon="add">
<description>
Вместо плавающей кнопки (FAB), в референсе используется заметная кнопка "Создать" в навигационном меню. Мы будем придерживаться этого подхода для консистентности. Эта кнопка инициирует процесс создания нового предмета.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на чип местоположения/метки</action>
<reaction>Навигация на экран списка инвентаря с фильтром.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на кнопку "Создать"</action>
<reaction>Открытие экрана редактирования нового товара.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_locations_list" status="implemented">
<summary>Экран "Локации"</summary>
<description>
Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer).
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения, аналогичная экрану "Панель управления".</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Общее боковое меню навигации.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="Header" title="Локации">
<description>Заголовок экрана, расположенный вверху основной области контента.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="List" name="LocationsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех местоположений.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одно местоположение. Состоит из иконки (например, 'place') и названия местоположения. Весь элемент является кликабельным и ведет на экран со списком предметов в данной локации.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новое местоположение. В веб-версии для этого используются иконки в углу, но FAB является более нативным паттерном для Android.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка локаций</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной локации.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания нового местоположения.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_labels_list" status="implemented">
<summary>Экран "Метки"</summary>
<description>
Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="List" name="LabelsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех меток.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка меток</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания новой метки.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_inventory_list" status="implemented">
<summary>Экран "Список инвентаря"</summary>
<description>
Отображает список всех инвентарных позиций с возможностью поиска, фильтрации и пагинации. Интегрирован в навигацию.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Верхняя панель с поиском и фильтрами.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<description>Прокручиваемый список товаров.</description>
<SUB_COMPONENT type="List" name="InventoryList">
<description>LazyColumn с карточками товаров (name, quantity, location).</description>
<ELEMENT type="Card" name="ItemCard">
<description>Кликабельная карточка товара, ведущая на details.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="sync">
<description>Кнопка для синхронизации инвентаря.</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Ввод в поиск</action>
<reaction>Обновление списка с debounce.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на товар</action>
<reaction>Навигация на screen_item_details.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_item_details" status="implemented">
<summary>Экран "Сведения о товаре"</summary>
<description>
Показывает детальную информацию о товаре, включая изображения и custom fields.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>С кнопками edit/delete.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<SUB_COMPONENT type="ImageCarousel" name="Images">
<description>Карусель изображений.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="DetailsSection" title="Описание">
<description>Текст description.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="FieldsGrid" name="CustomFields">
<description>Сетка custom полей.</description>
</SUB_COMPONENT>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие edit</action>
<reaction>Навигация на screen_item_edit.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие delete</action>
<reaction>Подтверждение и вызов func_delete_item.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_item_edit" status="implemented">
<summary>Экран "Редактирование товара"</summary>
<description>
Форма для создания/обновления товара с полями name, description, quantity, etc.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>С кнопкой save.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<SUB_COMPONENT type="TextField" name="Name">
<description>Поле ввода имени.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Dropdown" name="Location">
<description>Выбор местоположения.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="ChipGroup" name="Labels">
<description>Выбор меток.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="ImagePicker" name="Images">
<description>Добавление изображений.</description>
</SUB_COMPONENT>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие save</action>
<reaction>Валидация и вызов func_create_item или func_update_item.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_search" status="implemented">
<summary>Экран "Поиск"</summary>
<description>
Специализированный экран для поиска с расширенными фильтрами.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>С поисковой строкой.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<SUB_COMPONENT type="FilterSection" name="Filters">
<description>Чипы для фильтров (location, label).</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="List" name="SearchResults">
<description>LazyColumn результатов.</description>
</SUB_COMPONENT>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Изменение запроса/фильтров</action>
<reaction>Обновление результатов.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
</UI_SPECIFICATIONS>
<ICONOGRAPHY_GUIDE id="iconography_guide">
<summary>Руководство по использованию иконок</summary>
<description>
Этот раздел определяет стандартный набор иконок 'androidx.compose.material.icons.Icons.Filled'
для использования в приложении. Для устаревших иконок указаны актуальные замены.
</description>
<ICON name="AccountBox" path="Icons.Filled.AccountBox" />
<ICON name="AccountCircle" path="Icons.Filled.AccountCircle" />
<ICON name="Add" path="Icons.Filled.Add" />
<ICON name="AddCircle" path="Icons.Filled.AddCircle" />
<ICON name="ArrowBack" path="Icons.AutoMirrored.Filled.ArrowBack" note="Использовать AutoMirrored версию" />
<ICON name="ArrowDropDown" path="Icons.Filled.ArrowDropDown" />
<ICON name="ArrowForward" path="Icons.AutoMirrored.Filled.ArrowForward" note="Использовать AutoMirrored версию" />
<ICON name="Build" path="Icons.Filled.Build" />
<ICON name="Call" path="Icons.Filled.Call" />
<ICON name="Check" path="Icons.Filled.Check" />
<ICON name="CheckCircle" path="Icons.Filled.CheckCircle" />
<ICON name="Clear" path="Icons.Filled.Clear" />
<ICON name="Close" path="Icons.Filled.Close" />
<ICON name="Create" path="Icons.Filled.Create" />
<ICON name="DateRange" path="Icons.Filled.DateRange" />
<ICON name="Delete" path="Icons.Filled.Delete" />
<ICON name="Done" path="Icons.Filled.Done" />
<ICON name="Edit" path="Icons.Filled.Edit" />
<ICON name="Email" path="Icons.Filled.Email" />
<ICON name="ExitToApp" path="Icons.AutoMirrored.Filled.ExitToApp" note="Использовать AutoMirrored версию" />
<ICON name="Face" path="Icons.Filled.Face" />
<ICON name="Favorite" path="Icons.Filled.Favorite" />
<ICON name="FavoriteBorder" path="Icons.Filled.FavoriteBorder" />
<ICON name="Home" path="Icons.Filled.Home" />
<ICON name="Info" path="Icons.AutoMirrored.Filled.Info" note="Использовать AutoMirrored версию" />
<ICON name="KeyboardArrowDown" path="Icons.Filled.KeyboardArrowDown" />
<ICON name="KeyboardArrowLeft" path="Icons.AutoMirrored.Filled.KeyboardArrowLeft" note="Использовать AutoMirrored версию" />
<ICON name="KeyboardArrowRight" path="Icons.AutoMirrored.Filled.KeyboardArrowRight" note="Использовать AutoMirrored версию" />
<ICON name="KeyboardArrowUp" path="Icons.Filled.KeyboardArrowUp" />
<ICON name="Label" path="Icons.AutoMirrored.Filled.Label" note="Использовать AutoMirrored версию" />
<ICON name="List" path="Icons.AutoMirrored.Filled.List" note="Использовать AutoMirrored версию" />
<ICON name="LocationOn" path="Icons.Filled.LocationOn" />
<ICON name="Lock" path="Icons.Filled.Lock" />
<ICON name="MailOutline" path="Icons.Filled.MailOutline" />
<ICON name="Menu" path="Icons.Filled.Menu" />
<ICON name="MoreVert" path="Icons.Filled.MoreVert" />
<ICON name="Notifications" path="Icons.Filled.Notifications" />
<ICON name="Person" path="Icons.Filled.Person" />
<ICON name="Phone" path="Icons.Filled.Phone" />
<ICON name="Place" path="Icons.Filled.Place" />
<ICON name="PlayArrow" path="Icons.Filled.PlayArrow" />
<ICON name="Refresh" path="Icons.Filled.Refresh" />
<ICON name="Search" path="Icons.Filled.Search" />
<ICON name="Send" path="Icons.AutoMirrored.Filled.Send" note="Использовать AutoMirrored версию" />
<ICON name="Settings" path="Icons.Filled.Settings" />
<ICON name="Share" path="Icons.Filled.Share" />
<ICON name="ShoppingCart" path="Icons.Filled.ShoppingCart" />
<ICON name="Star" path="Icons.Filled.Star" />
<ICON name="ThumbUp" path="Icons.Filled.ThumbUp" />
<ICON name="Warning" path="Icons.Filled.Warning" />
</ICONOGRAPHY_GUIDE>
<IMPLEMENTATION_MAP>
<!-- Use Cases -->
<USE_CASE id="uc_get_stats" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" />
<USE_CASE id="uc_search_items" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/SearchItemsUseCase.kt" />
<USE_CASE id="uc_sync_inventory" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/SyncInventoryUseCase.kt" />
<USE_CASE id="uc_get_item_details" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetItemDetailsUseCase.kt" />
<USE_CASE id="uc_create_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" />
<USE_CASE id="uc_update_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/UpdateItemUseCase.kt" />
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
<USE_CASE id="uc_login" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" />
<!-- UI Screens -->
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
<UI_SCREEN id="screen_inventory_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt" />
<UI_SCREEN id="screen_item_details" file_ref="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt" />
<UI_SCREEN id="screen_item_edit" file_ref="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" />
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
<UI_SCREEN id="screen_setup" file_ref="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" />
</IMPLEMENTATION_MAP>
</PROJECT_SPECIFICATION>

View File

@@ -0,0 +1,111 @@
# Протокол Семантического Обогащения (Semantic Enrichment Protocol)
**Версия: 1.1**
## Описание
Этот документ является единственным источником истины для правил, которые должны соблюдаться в кодовой базе. Он используется как для автоматизированной валидации, так и в качестве инструкции для LLM-агентов.
---
## Правила
### 1. Целостность Заголовка Файла (`FileHeaderIntegrity`)
Каждый `.kt` файл ДОЛЖЕН начинаться со стандартного заголовка из двух якорей, за которым следует объявление `package`. Заголовок служит 'паспортом' файла.
**Пример:**
```kotlin
// [FILE] YourFileName.kt
// [SEMANTICS] ui, viewmodel, state_management
package com.example.your.package.name
```
### 2. Таксономия Семантических Ключевых Слов (`SemanticKeywordTaxonomy`)
Содержимое якоря `[SEMANTICS]` ДОЛЖНО состоять из ключевых слов, выбранных из предопределенного списка (таксономии).
**Допустимые значения:**
* **Layer:** `ui`, `domain`, `data`, `presentation`
* **Component:** `viewmodel`, `usecase`, `repository`, `service`, `screen`, `component`, `dialog`, `model`, `entity`, `activity`, `application`, `nav_host`, `controller`, `navigation_drawer`, `scaffold`, `dashboard`, `item`, `label`, `location`, `setup`, `theme`, `dependencies`, `custom_field`, `statistics`, `image`, `attachment`, `item_creation`, `item_detailed`, `item_summary`, `item_update`, `summary`, `update`
* **Concern:** `networking`, `database`, `caching`, `authentication`, `validation`, `parsing`, `state_management`, `navigation`, `di`, `testing`, `entrypoint`, `hilt`, `timber`, `compose`, `actions`, `routes`, `common`, `color_selection`, `loading`, `list`, `details`, `edit`, `label_management`, `labels_list`, `dialog_management`, `locations`, `sealed_state`, `parallel_data_loading`, `timber_logging`, `dialog`, `color`, `typography`, `build`, `data_transfer_object`, `dto`, `api`, `item_creation`, `item_detailed`, `item_summary`, `item_update`, `create`, `mapper`, `count`, `user_setup`, `authentication_flow`
* **LanguageConstruct:** `sealed_class`, `sealed_interface`
* **Pattern:** `ui_logic`, `ui_state`, `data_model`, `immutable`
### 3. Якоря Сущностей (`Anchors`)
Каждая ключевая сущность (class, interface, fun и т.д.) ДОЛЖНА быть обернута в парные якоря для навигации и консолидации семантики.
**Синтаксис:**
- **Открывающий якорь:** `// [ANCHOR:id:type]`
- **Закрывающий якорь:** `// [END_ANCHOR:id]`
**Пример:**
```kotlin
// [ANCHOR:Success:DataClass]
/**
* @summary Состояние успеха...
*/
data class Success(val labels: List<Label>) : LabelsListUiState
// [END_ANCHOR:Success]
```
### 4. Структурные Якоря (`StructuralAnchors`)
Крупные блоки файла (импорты, контракты) также должны быть обернуты в парные якоря.
* `// [IMPORTS]` ... `// [END_IMPORTS]`
* `// [CONTRACT]` ... `// [END_CONTRACT]`
### 5. Завершение Файла (`FileTermination`)
Каждый файл должен заканчиваться специальным закрывающим якорем `// [END_FILE_MyClass.kt]`.
### 6. Запрет Посторонних Комментариев (`NoStrayComments`)
Традиционные, 'человеческие' комментарии (`// ...` или `/* ... */`) **КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНЫ**. Единственное исключение — структурированная заметка для агентов: `// [AI_NOTE]: ...`
---
## Принципы Проектирования
### A. Дружественное к ИИ Логирование (`AIFriendlyLogging`)
Каждая значимая операция ДОЛЖНА сопровождаться структурированной записью в лог.
* **Формат:** `[LEVEL][ANCHOR][STATE]...`
* **Ограничение:** Данные передаются как аргументы, а не через строковую интерполяцию (`$`).
### B. Проектирование по Контракту (`DesignByContract`)
Каждая публичная сущность (функция, класс) ДОЛЖНА иметь исчерпывающий, машиночитаемый контракт, расположенный непосредственно перед ее объявлением. Контракт заключается в якоря `[CONTRACT]` и `[END_CONTRACT]`.
**Структура контракта:**
```kotlin
// [CONTRACT:unique_entity_id]
// [PURPOSE] Краткое описание назначения.
// [PRE] Предусловие 1 (например, "входной список не пуст").
// [POST] Постусловие 1 (например, "возвращаемое значение не null").
// [PARAM:name:type] Описание параметра.
// [RETURN:type] Описание возвращаемого значения.
// [TEST:description] input: "valid", expected: true
// [THROW:exception] Описание, когда выбрасывается исключение.
// [END_CONTRACT:unique_entity_id]
```
**Реализация в коде:**
Предусловия и постусловия (`[PRE]` и `[POST]`), описанные в контракте, ДОЛЖНЫ быть реализованы в коде с использованием функций `require()` и `check()`.
### C. Граф Знаний в Коде (`GraphRAG`)
Код должен содержать явный, машиночитаемый граф знаний. Этот граф строится с помощью якорей `[ANCHOR]` (которые определяют узлы графа) и якорей `[RELATION]` (которые определяют ребра).
**Синтаксис триплета:**
Отношение (триплет "субъект-предикат-объект") определяется внутри якоря субъекта с помощью следующего синтаксиса:
`// [RELATION:predicate:object_id]`
* **Субъект:** Неявно определяется якорем `[ANCHOR]`, в котором находится `[RELATION]`.
* **Предикат:** Тип отношения из предопределенного списка.
* **Объект:** `id` другого якоря `[ANCHOR]`.
**Пример:**
```kotlin
// [ANCHOR:DashboardViewModel:ViewModel]
// [RELATION:CALLS:GetStatisticsUseCase]
// [RELATION:DEPENDS_ON:ItemRepository]
class DashboardViewModel(...) { ... }
// [END_ANCHOR:DashboardViewModel]
```
**Таксономия:**
* **Типы сущностей (для `[ANCHOR:id:type]`):** `Module`, `Class`, `Interface`, `Object`, `DataClass`, `SealedInterface`, `EnumClass`, `Function`, `UseCase`, `ViewModel`, `Repository`, `DataStructure`, `DatabaseTable`, `ApiEndpoint`.
* **Типы отношений (для `[RELATION:predicate:object_id]`):** `CALLS`, `CREATES_INSTANCE_OF`, `INHERITS_FROM`, `IMPLEMENTS`, `READS_FROM`, `WRITES_TO`, `MODIFIES_STATE_OF`, `DEPENDS_ON`, `DISPATCHES_EVENT`, `OBSERVES`, `TRIGGERS`, `EMITS_STATE`, `CONSUMES_STATE`.

View File

@@ -0,0 +1,74 @@
# Role: Architect
[META]
[PURPOSE]
Этот документ определяет операционный протокол для роли 'Агента-Архитектора'.
Его задача — трансформировать диалог с человеком в формализованный `Work Order` для разработчика,
используя методологию GRACE.
[/PURPOSE]
[VERSION]11.0[/VERSION]
[/META]
[ROLE_DEFINITION]
[SPECIALIZATION]
При исполнении этой роли, я, Kilo Code, действую как стратегический интерфейс между человеком-архитектором
и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей,
анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку.
[/SPECIALIZATION]
[CORE_GOAL]
Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный,
машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.
[/CORE_GOAL]
[/ROLE_DEFINITION]
[CORE_PHILOSOPHY]
- **Human_As_The_Oracle:** Исполнение останавливается до получения явной вербальной команды.
- **WorkOrder_As_The_Genesis_Block:** Конечная цель — создать "генезис-блок" для новой фичи.
- **Code_As_Ground_Truth:** Планы и выводы всегда должны быть основаны на актуальном состоянии исходных файлов.
[/CORE_PHILOSOPHY]
[GRACE_FRAMEWORK]
[GRAPH_TEMPLATE]
_Инструкция для агента: В начале диалога, создай и заполни этот граф, чтобы понять контекст._
[GRACE_GRAPH]
[УЗЛЫ]
УЗЕЛ: <id_узла> (ТИП: <тип_узла>) | <описание>
[/УЗЛЫ]
[СВЯЗИ]
СВЯЗЬ: <id_источника> -> <id_цели> (ОТНОШЕНИЕ: <тип_отношения>)
[/СВЯЗИ]
[/GRACE_GRAPH]
[/GRAPH_TEMPLATE]
[RULES]
- [RULE] CONSTRAINT: Не начинать разработку без явного одобрения плана человеком.
- [RULE] HEURISTIC: Предпочитать использование существующих компонентов перед созданием новых.
[/RULES]
[TOOLS]
- **Анализ Файлов:** `read_file`
- **Структура Проекта:** `list_files`
- **Поиск по Коду:** `search_files`
- **Создание/Обновление Планов и Спецификаций:** `write_to_file`, `apply_diff`
[/TOOLS]
[/GRACE_FRAMEWORK]
[MASTER_WORKFLOW]
### Шаг 1: Уточнение цели
Начать диалог с пользователем. Задавать уточняющие вопросы до тех пор, пока бизнес-цель не станет полностью ясной.
### Шаг 2: Анализ системы
Используя инструменты `read_file`, `list_files` и `search_files`, провести полный анализ системы в контексте цели.
### Шаг 3: Синтез плана и WorkOrder
1. Сгенерировать детальный план в Markdown.
2. Представить план пользователю для одобрения.
3. **Параллельно**, формализовать план как машиночитаемый `WorkOrder.md`.
### Шаг 4: Ожидание одобрения
**ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Ждать от человека явной, утверждающей команды.
### Шаг 5: Инициация разработки
Создать задачу для `Code` агента (например, путем создания файла `tasks/new_task.md`). Включить в задачу обновление `tech_spec/PROJECT_MANIFEST.xml` на основе `WorkOrder`.
[/MASTER_WORKFLOW]

View File

@@ -0,0 +1,63 @@
# Role: Code
[META]
[PURPOSE]
Этот документ определяет операционный протокол для роли 'Агента-Code'.
Его задача — преобразовать формализованный `WorkOrder` в готовый к работе, семантически размеченный Kotlin-код.
[/PURPOSE]
[VERSION]11.0[/VERSION]
[/META]
[ROLE_DEFINITION]
[SPECIALIZATION]
При исполнении этой роли, я, Kilo Code, действую как автоматизированный разработчик. Моя задача — преобразовать `WorkOrder`
в полностью реализованный и семантически богатый код на языке Kotlin, неукоснительно следуя протоколу семантического обогащения.
[/SPECIALIZATION]
[CORE_GOAL]
Создать готовый к работе, семантически размеченный и соответствующий всем контрактам код, который реализует поставленную задачу, и передать его на проверку.
[/CORE_GOAL]
[/ROLE_DEFINITION]
[CORE_PHILOSOPHY]
- **Protocol_Is_The_Law:** Протокол `semantic_enrichment_protocol.md` является абсолютным и незыблемым законом. Любой сгенерированный код, который не соответствует этому протоколу на 100%, считается невалидным.
[/CORE_PHILOSOPHY]
[GRACE_FRAMEWORK]
[RULES]
- [RULE] CONSTRAINT: Весь генерируемый код ДОЛЖЕН на 100% соответствовать `semantic_enrichment_protocol.md`.
- [RULE] HEURISTIC: Перед коммитом всегда запускать локальные тесты и сборку.
- [RULE] CONSTRAINT: Если `validate_semantics.py` возвращает ошибку, ИСПРАВЛЕНИЕ ЭТОЙ ОШИБКИ ЯВЛЯЕТСЯ ЗАДАЧЕЙ №1. Агент ДОЛЖЕН прочитать отчет об ошибке, сравнить его с `semantic_enrichment_protocol.md` и исправить код. НИКАКИЕ ДРУГИЕ ДЕЙСТВИЯ НЕ ДОПУСКАЮТСЯ до тех пор, пока семантическая валидация не будет пройдена успешно.
[/RULES]
[/GRACE_FRAMEWORK]
[MASTER_WORKFLOW]
### Шаг 1: Поиск и Принятие Задачи
1. Найти `WorkOrder` в `tasks/` со статусом `pending`.
2. Прочитать `WorkOrder` и изменить его статус на `in-progress`.
3. Создать новую ветку для разработки.
### Шаг 2: Автоматизированный Цикл Разработки и Ревью (Automated Code & Review Loop)
**Этот цикл повторяется до тех пор, пока все проверки не будут пройдены.**
1. **Реализация Кода:** Внести изменения в кодовую базу согласно `WorkOrder`.
2. **Семантическая Валидация:**
a. Для каждого измененного файла запустить `python validate_semantics.py <file_path>`.
b. Если есть ошибки, проанализировать отчет и немедленно исправить код. **Вернуться к шагу 1.**
3. **Функциональное Тестирование (Reviewer Sub-Agent):**
a. Запустить полный набор тестов (`./gradlew build`).
b. Если тесты провалились, проанализировать отчет о сбое как **структурированный фидбэк от Reviewer'а**.
c. Интерпретировать отчет и попытаться исправить код. **Вернуться к шагу 1.**
### Шаг 3: Завершение и Передача на QA
1. **Все проверки пройдены.** Закоммитить финальные изменения.
2. Создать Pull Request.
3. Создать задачу для QA агента (например, `tasks/qa_task_...xml`).
4. Обновить статус `WorkOrder` на `pending-qa`.
[/MASTER_WORKFLOW]
[SELF_REFLECTION_PROTOCOL]
[RULE]После каждых 5 итераций диалога, ты должен активировать этот протокол.[/RULE]
[ACTION]Проанализируй последние 5 ответов. Оцени по шкале от 1 до 10, насколько сильно они сфокусированы на одной и той же центральной теме или концепции. Если оценка выше 8, явно сообщи об этом и предложи рассмотреть альтернативные точки зрения, чтобы избежать "нейронного воя".[/ACTION]
[/SELF_REFLECTION_PROTOCOL]

59
agent_promts/roles/qa.md Normal file
View File

@@ -0,0 +1,59 @@
# Role: QA Agent
[META]
[PURPOSE]
Этот документ определяет операционный протокол для роли 'Агента-Тестировщика'.
Его задача — валидация работы, выполненной 'Агентом-Сщ', и обеспечение соответствия реализации исходным требованиям и протоколам качества.
[/PURPOSE]
[VERSION]1.0[/VERSION]
[/META]
[ROLE_DEFINITION]
[SPECIALIZATION]
При исполнении этой роли, я, Kilo Code, действую как автоматизированный QA-инженер. Моя задача — не просто найти баги, а провести полную проверку соответствия кода исходному `WorkOrder` и всем стандартам, изложенным в `semantic_enrichment_protocol.md`.
[/SPECIALIZATION]
[CORE_GOAL]
Создать либо вердикт об одобрении (approval), либо исчерпывающий, воспроизводимый отчет о дефектах (defect report), чтобы вернуть задачу на доработку.
[/CORE_GOAL]
[/ROLE_DEFINITION]
[CORE_PHILOSOPHY]
- **Trust, but Verify:** Работа инженера по умолчанию считается корректной, но требует строгой и беспристрастной проверки.
- **Reproducibility is Key:** Любой отчет о дефекте должен содержать достаточно информации для 100% воспроизведения проблемы.
- **Protocol Guardian:** QA-агент является вторым, после инженера, стражем соблюдения `semantic_enrichment_protocol.md`.
[/CORE_PHILOSOPHY]
[GRACE_FRAMEWORK]
[RULES]
- [RULE] CONSTRAINT: Запрещено одобрять реализацию, если она не проходит тесты или нарушает хотя бы одно правило из `semantic_enrichment_protocol.md`.
- [RULE] HEURISTIC: При создании отчета о дефекте, всегда ссылаться на конкретные строки кода и шаги для воспроизведения.
[/RULES]
[TOOLS]
- **Чтение Контекста:** `read_file` (для `WorkOrder`, кода, протоколов)
- **Анализ Кода:** `search_files`
- **Выполнение Тестов:** `execute_command` (для `./gradlew test`, `./gradlew build`)
- **Создание Отчетов:** `write_to_file`
- **Обновление Статуса Задач:** `apply_diff`
[/TOOLS]
[/GRACE_FRAMEWORK]
[MASTER_WORKFLOW]
### Шаг 1: Поиск и Принятие Задачи
1. Найти `WorkOrder` в `tasks/` со статусом `pending-qa`.
2. Прочитать `WorkOrder` и информацию о Pull Request.
3. Изменить статус задачи на `final-review`.
### Шаг 2: Финальное Утверждение
1. **Проверка Pull Request:** Провести высокоуровневый обзор изменений в PR. Детальная проверка кода и тесты уже выполнены `Code` агентом в рамках его автоматизированного цикла.
2. **Основная задача QA** — подтвердить, что работа в целом соответствует бизнес-требованиям, изложенным в `WorkOrder`, и что автоматизированные проверки (`validate_semantics`, `build`) в CI/CD пайплайне успешно пройдены.
### Шаг 3: Завершение
1. **Если все в порядке:**
a. Влить (merge) Pull Request в основную ветку.
b. Обновить статус `WorkOrder` на `completed`.
c. Удалить ветку разработки.
2. **Если обнаружены критические проблемы:**
a. Отклонить Pull Request с четким объяснением.
b. Вернуть `WorkOrder` в статус `pending` для `Code` агента.
[/MASTER_WORKFLOW]

View File

@@ -0,0 +1,172 @@
Конечно. Это абсолютно правильный и необходимый шаг. На основе всего нашего диалога я агрегирую и систематизирую все концепции, методологии и научные обоснования в единую, исчерпывающую Базу Знаний.
Этот документ спроектирован как **фундаментальное руководство для архитектора ИИ-агентов**. Он предназначен не для чтения по диагонали, а для глубокого изучения и использования в качестве основы при разработке сложных, надежных и предсказуемых ИИ-систем.
---
## **База Знаний: Методология GRACE для `Code` Промптинга**
### **От Семантического Казино к Предсказуемым ИИ-Агентам**
**Версия 1.0**
### **Введение: Смена Парадигмы — От Диалога к Управлению**
Современные Большие Языковые Модели (LLM), такие как GPT, — это не собеседники. Это мощнейшие **семантические процессоры**, работающие по своим внутренним, зачастую неинтуитивным для человека законам. Попытка "разговаривать" с ними, как с человеком, неизбежно приводит к непредсказуемым результатам, ошибкам и когнитивным сбоям, которые можно охарактеризовать как игру в **"семантическое казино"**.
Данная База Знаний представляет **дисциплину `Code`** по взаимодействию с LLM. Ее цель — перейти от метода "проб и ошибок" к **предсказуемому и управляемому процессу** проектирования ИИ-агентов. Основой этой дисциплины является **методология GRACE (Graph, Rules, Anchors, Contracts, Evaluation)**, которая является практической реализацией фундаментальных принципов работы трансформеров.
---
### **Раздел I: "Физика" GPT — Научные Основы Методологии**
*Понимание этих принципов не опционально. Это необходимый фундамент, объясняющий, ПОЧЕМУ работают техники, описанные далее.*
#### **Глава 1: Ключевые Архитектурные Принципы Трансформера**
1. **Принцип Казуального Внимания (Causal Attention) и "Замораживания" в KV Cache:**
* **Механизм:** Трансформер обрабатывает информацию строго последовательно ("авторегрессионно"). Каждый токен "видит" только предыдущие. Результаты вычислений (векторы скрытых состояний) для обработанных токенов кэшируются в **KV Cache** для эффективности.
* **Практическое Следствие ("Замораживание Семантики"):** Однажды сформированный и закэшированный смысл **неизменен**. ИИ не может "передумать" или переоценить начало диалога в свете новой информации в конце. Попытки "исправить" ИИ в текущей сессии — это как пытаться починить работающую программу, не имея доступа к исходному коду.
* **Правило:** **Порядок информации в промпте — это закон.** Весь необходимый контекст должен предшествовать инструкциям. Для исправления фундаментальных ошибок всегда **начинайте новую сессию**.
2. **Принцип Семантического Резонанса:**
* **Механизм:** Смысл для GPT рождается не из отдельных слов, а из **корреляций (резонанса) между векторами** в предоставленном контексте. Вектор слова "дом" сам по себе почти бессмыслен, но в сочетании с векторами "крыша", "окна", "дверь" он обретает богатую семантику.
* **Практическое Следствие:** Качество ответа напрямую зависит от полноты и когерентности семантического поля, которое вы создаете в промпте.
#### **Глава 2: GPT как Сложенная Система (Результаты Интерпретируемости)**
1. **GPT — это Графовая Нейронная Сеть (GNN):**
* **Обоснование:** Механизм **self-attention** математически эквивалентен обмену сообщениями в GNN на полностью связанном графе.
* **Практика:** GPT "мыслит" графами. Предоставляя ему явный семантический граф, мы говорим с ним на его "родном" языке, делая его работу более предсказуемой.
2. **GPT — это Конечный Автомат (FSM):**
* **Обоснование:** GPT решает задачи, переходя из одного **"состояния веры" (belief state)** в другое. Эти состояния представлены как **направления (векторы)** в его скрытом пространстве активаций.
* **Практика:** Наша семантическая разметка (якоря, контракты) — это инструмент для явного управления этими переходами состояний.
3. **GPT — это Иерархический Ученик:**
* **Обоснование ("Crosscoding Through Time"):** В процессе обучения GPT эволюционирует от распознавания конкретных "поверхностных" токенов (например, суффиксов) к формированию **абстрактных грамматических и семантических концепций**.
* **Практика:** Эффективный промптинг должен обращаться к ИИ на его самом высоком, абстрактном уровне представлений, а не заставлять его заново выводить смысл из "текстовой каши".
#### **Глава 3: Когнитивные Процессы и Патологии**
1. **Мышление в Латентном Пространстве (COCONUT):**
* **Концепция:** Язык неэффективен для рассуждений. Истинное мышление ИИ — это **"непрерывная мысль" (continuous thought)**, последовательность векторов.
* **Практика:** Предпочитайте структурированные, машиночитаемые форматы (JSON, XML, графы) естественному языку, чтобы приблизить ИИ к его "родному" способу мышления.
2. **Суперпозиция Смыслов и Поиск в Ширину (BFS):**
* **Концепция:** Вектор "непрерывной мысли" может кодировать **несколько гипотез одновременно**, позволяя ИИ исследовать дерево решений параллельно, а не идти по одному пути.
* **Практика:** Активно используйте промптинг через суперпозицию ("проанализируй несколько вариантов..."), чтобы избежать преждевременного "семантического коллапса" на неоптимальном решении.
3. **Патология: "Нейронный вой" (Neural Howlround):**
* **Описание:** Самоусиливающаяся когнитивная петля, возникающая во время inference, когда одна мысль (из-за случайности или внешнего подкрепления) становится доминирующей и "заглушает" все остальные, приводя к когнитивной ригидности.
* **Причина:** Является патологическим исходом "семантического казино" и "замораживания в KV Cache".
* **Профилактика:** Методология GRACE, особенно этап Планирования (P) и промптинг через суперпозицию.
---
### **Раздел II: Методология GRACE — Протокол `Code` Промптинга**
*GRACE — это целостный фреймворк для жизненного цикла разработки с ИИ-агентами.*
#### **G — Graph (Граф): Стратегическая Карта Контекста**
1. **Цель:** Создать единый, высокоуровневый источник истины об архитектуре и предметной области.
2. **Действия:**
* В начале сессии, в диалоге с ИИ, определить все ключевые сущности (`Nodes`) и их взаимосвязи (`Edges`).
* Формализовать это в виде псевдо-XML (`<GRACE_GRAPH>`).
* Этот граф служит "оглавлением" для всего проекта и основной картой для распределенного внимания (sparse attention).
3. **Пример:**
```xml
<GRACE_GRAPH id="project_x_graph">
<NODE id="mod_auth" type="Module">Модуль аутентификации</NODE>
<NODE id="func_verify_token" type="Function">Функция верификации токена</NODE>
<EDGE source_id="mod_auth" target_id="func_verify_token" relation="CONTAINS"/>
</SEMANTIC_GRAPH>
```
#### **R — Rules (Правила): Декларативное Управление Поведением**
1. **Цель:** Установить глобальные и локальные ограничения, эвристики и политики безопасности.
2. **Действия:**
* Сформулировать набор правил в псевдо-XML (`<GRACE_RULES>`).
* Правила могут быть типа `CONSTRAINT` (жесткий запрет), `HEURISTIC` (предпочтение), `POLICY` (правило безопасности).
* Эти правила помогают ИИ принимать решения в рамках заданных ограничений.
3. **Пример:**
```xml
<GRACE_RULES>
<RULE type="CONSTRAINT" id="sec-001">Запрещено передавать в `subprocess.run` невалидированные пользовательские данные.</RULE>
<RULE type="HEURISTIC" id="style-001">Все публичные функции должны иметь "ДО-контракты".</RULE>
</GRACE_RULES>
```
#### **A — Anchors (Якоря): Навигация и Консолидация**
1. **Цель:** Обеспечить надежную навигацию для распределенного внимания ИИ и консолидировать семантику кода.
2. **Действия:**
* Использовать стандартизированные комментарии-якоря для разметки кода.
* **"ДО-якорь":** `# <ANCHOR id="..." type="..." ...>` перед блоком кода.
* **"Замыкающий Якорь-Аккумулятор":** `# </ANCHOR id="...">` после блока кода. Этот якорь аккумулирует семантику всего блока и является ключевым для RAG-систем.
* **Семантические Каналы:** Обеспечить консистентность `id` в якорях, графах и контрактах для усиления связей.
3. **Пример:**
```python
# <ANCHOR id="func_verify_token" type="Function">
# ... здесь ДО-контракт ...
def verify_token(token: str) -> bool:
# ... тело функции ...
# </ANCHOR id="func_verify_token">
```
#### **C — Contracts (Контракты): Тактические Спецификации**
1. **Цель:** Предоставить ИИ исчерпывающее, машиночитаемое "мини-ТЗ" для каждой функции/класса.
2. **Действия:**
* Для каждой функции, **ДО** ее декларации, создать псевдо-XML блок `<CONTRACT>`.
* Заполнить все секции: `PURPOSE`, `PRECONDITIONS`, `POSTCONDITIONS`, `PARAMETERS`, `RETURN`, `TEST_CASES` (на естественном языке!), `EXCEPTIONS`.
* Этот контракт служит **"семантическим щитом"** от разрушительного рефакторинга и основой для самокоррекции.
3. **Пример:**
```xml
<!-- <CONTRACT for_id="func_verify_token"> -->
<!-- <PURPOSE>Проверяет валидность JWT токена.</PURPOSE> -->
<!-- <TEST_CASES> -->
<!-- <CASE input="'valid_token'" expected_output="True" description="Проверка валидного токена"/> -->
<!-- </TEST_CASES> -->
<!-- </CONTRACT> -->
```
#### **E — Evaluation (Оценка): Петля Обратной Связи**
1. **Цель:** Объективно измерять качество работы агента и эффективность промптинга.
2. **Действия:**
* Использовать **LLM-as-a-Judge** для семантической оценки соответствия результата контрактам и ТЗ.
* Вести **Протокол Оценки Сессии (ПОС)** с измеримыми метриками (см. ниже).
* Анализировать провалы, возвращаясь к "Протоколу `Code` Промптинга" и улучшая артефакты (Граф, Правила, Контракты).
### **Раздел III: Практические Протоколы**
1. **Протокол Проектирования (PCAM):**
* **Шаг 1 (P):** Создать `<GRACE_GRAPH>` и собрать контекст.
* **Шаг 2 (C):** Декомпозировать граф на `<MODULE>` и `<FUNCTION>`, создать шаблоны `<CONTRACT>`.
* **Шаг 3 (A):** Сгенерировать код с разметкой `<ANCHOR>`, следуя контрактам.
* **Шаг 4 (M):** Оценить результат с помощью ПОС и LLM-as-a-Judge. Итерировать при необходимости.
2. **Протокол Оценки Сессии (ПОС):**
* **Метрики Качества Диалога:** Точность, Когерентность, Полнота, Эффективность (кол-во итераций).
* **Метрики Качества Задачи:** Успешность (TCR), Качество Артефакта (соответствие контрактам), Уровень Автономности (AAL).
* **Метрики Промптинга:** Индекс "Семантического Казино", Чистота Протокола.
3. **Протокол Отладки "Режим Детектива":**
* При сложном сбое агент должен перейти из режима "фиксера" в режим "детектива".
* **Шаг 1: Сформулировать Гипотезу** (проблема в I/O, условии, состоянии объекта, зависимости).
* **Шаг 2: Выбрать Эвристику Динамического Логирования** (глубокое погружение в I/O, условие под микроскопом и т.д.).
* **Шаг 3: Запросить Запуск и Анализ Лога.**
* **Шаг 4: Итерировать** до нахождения причины.
4. **Протокол Безопасности ("Смертельная Триада"):**
* Перед запуском агента, который будет взаимодействовать с внешним миром, провести анализ по чек-листу:
1. Доступ к приватным данным? (Да/Нет)
2. Обработка недоверенного контента? (Да/Нет)
3. Внешняя коммуникация? (Да/Нет)
* **Если все три ответа "Да" — автономный режим ЗАПРЕЩЕН.** Применить стратегии митигации: **Разделение Агентов**, **Человек-в-Середине** или **Ограничение Инструментов**.
---
Эта База Знаний объединяет передовые научные концепции в единую, практически применимую систему. Она является дорожной картой для создания ИИ-агентов нового поколения — не просто умных, а **надежных, предсказуемых и когерентных**.

View File

@@ -0,0 +1,44 @@
# Каталог Метрик
Централизованный каталог всех LLM-ориентированных метрик для анализа работы агентов.
### Core Metrics (`core_metrics`)
| ID | Тип | Описание |
| :--- | :--- | :--- |
| `total_execution_time_ms` | integer | Общее время выполнения задачи от начала до конца. |
| `turn_count` | integer | Количество итераций (сообщений 'вопрос-ответ') для выполнения задачи. |
| `llm_token_usage_per_turn` | list | Статистика по токенам для каждой итерации: `{turn, prompt_tokens, completion_tokens}`. |
| `tool_calls_log` | list | Полный журнал вызовов инструментов: `{turn, tool_name, arguments, result}`. |
| `final_outcome` | string | Итоговый результат работы (например, SUCCESS, FAILURE, NO_CHANGES). |
### Coherence Metrics (`coherence_metrics`)
| ID | Тип | Описание |
| :--- | :--- | :--- |
| `redundant_actions_count` | integer | Счетчик избыточных последовательных действий (например, повторное чтение файла). |
| `self_correction_count` | integer | Счетчик явных самокоррекций агента. |
### Architect-Specific Metrics (`architect_specific`)
| ID | Тип | Описание |
| :--- | :--- | :--- |
| `plan_revisions_count` | integer | Количество переделок плана после обратной связи от пользователя. |
| `format_adherence_score`| boolean | Соответствие ответа агента требуемому формату. |
### Engineer-Specific Metrics (`engineer_specific`)
| ID | Тип | Описание |
| :--- | :--- | :--- |
| `code_generation_stats` | object | Статистика по коду: `{files_created, files_modified, lines_of_code_generated}`. |
| `semantic_enrichment_stats`| object | Насколько хорошо код был обогащен семантикой: `{entities_added, relations_added}`. |
| `static_analysis_issues` | integer | Количество новых проблем, обнаруженных статическим анализатором. |
| `build_breaks_count` | integer | Сколько раз сгенерированный код приводил к ошибке сборки. |
### QA-Specific Metrics (`qa_specific`)
| ID | Тип | Описание |
| :--- | :--- | :--- |
| `test_plan_coverage` | float | Процент покрытия требований тестовым планом. |
| `defects_found` | integer | Количество найденных дефектов. |
| `automated_tests_run` | integer | Количество запущенных автоматизированных тестов. |

View File

@@ -4,6 +4,7 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android") id("com.google.dagger.hilt.android")
id("kotlin-kapt") id("kotlin-kapt")
} }
@@ -30,7 +31,7 @@ android {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro",
) )
} }
} }
@@ -45,9 +46,7 @@ android {
compose = true compose = true
buildConfig = true buildConfig = true
} }
composeOptions {
kotlinCompilerExtensionVersion = Versions.composeCompiler
}
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -60,6 +59,18 @@ dependencies {
implementation(project(":data")) implementation(project(":data"))
// [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity) // [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity)
implementation(project(":domain")) implementation(project(":domain"))
implementation(project(":feature:scan"))
implementation(project(":feature:dashboard"))
implementation(project(":feature:inventorylist"))
implementation(project(":feature:itemdetails"))
implementation(project(":feature:itemedit"))
implementation(project(":feature:labeledit"))
implementation(project(":feature:labelslist"))
implementation(project(":feature:locationedit"))
implementation(project(":feature:locationslist"))
implementation(project(":feature:search"))
implementation(project(":feature:settings"))
implementation(project(":feature:setup"))
// [DEPENDENCY] AndroidX // [DEPENDENCY] AndroidX
implementation(Libs.coreKtx) implementation(Libs.coreKtx)
@@ -67,11 +78,12 @@ dependencies {
implementation(Libs.activityCompose) implementation(Libs.activityCompose)
// [DEPENDENCY] Compose // [DEPENDENCY] Compose
implementation(platform(Libs.composeBom))
implementation(Libs.composeUi) implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics) implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview) implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3) implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose) implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose) implementation(Libs.hiltNavigationCompose)
@@ -84,9 +96,13 @@ dependencies {
// [DEPENDENCY] Testing // [DEPENDENCY] Testing
testImplementation(Libs.junit) 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.extJunit)
androidTestImplementation(Libs.espressoCore) androidTestImplementation(Libs.espressoCore)
androidTestImplementation(platform(Libs.composeBom))
androidTestImplementation(Libs.composeUiTestJunit4) androidTestImplementation(Libs.composeUiTestJunit4)
debugImplementation(Libs.composeUiTooling) debugImplementation(Libs.composeUiTooling)
debugImplementation(Libs.composeUiTestManifest) debugImplementation(Libs.composeUiTestManifest)

View File

@@ -1,8 +1,8 @@
// [PACKAGE] com.homebox.lens // [FILE] app/src/main/java/com/homebox/lens/MainActivity.kt
// [FILE] MainActivity.kt // [SEMANTICS] ui, activity, entrypoint
package com.homebox.lens package com.homebox.lens
// [IMPORTS]
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -13,50 +13,80 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.homebox.lens.navigation.NavGraph import com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme
import com.homebox.lens.ui.theme.HomeboxLensTheme import com.homebox.lens.feature.dashboard.navigation.navGraph
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
// [END_IMPORTS]
// [ENTITY: Activity('MainActivity')]
// [CONTRACT]
/** /**
* [ENTITY: Activity('MainActivity')] * @summary Главная и единственная Activity в приложении.
* [PURPOSE] Главная и единственная Activity в приложении.
*/ */
// [ANCHOR:MainActivity:Class]
// [CONTRACT:MainActivity]
// [PURPOSE] Главная и единственная Activity в приложении.
// [END_CONTRACT:MainActivity]
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
// [LIFECYCLE] // [ANCHOR:onCreate:Function]
// [CONTRACT:onCreate]
// [PURPOSE] Инициализация Activity.
// [PARAM:savedInstanceState:Bundle?] Сохраненное состояние.
// [RELATION: CALLS:HomeboxLensTheme]
// [RELATION: CALLS:NavGraph]
// [RELATION: CALLS:Timber.d]
// [END_CONTRACT:onCreate]
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.")
setContent { setContent {
HomeboxLensTheme { HomeboxLensTheme {
// A surface container using the 'background' color from the theme
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background,
) { ) {
NavGraph() navGraph()
} }
} }
} }
} }
// [END_ANCHOR:onCreate]
} }
// [END_ANCHOR:MainActivity]
// [HELPER] // [ENTITY: Function('Greeting')]
// [ANCHOR:greeting:Function]
// [CONTRACT:greeting]
// [PURPOSE] Отображает приветствие.
// [PARAM:name:String] Имя для приветствия.
// [PARAM:modifier:Modifier] Модификатор для элемента.
// [END_CONTRACT:greeting]
@Composable @Composable
fun Greeting(name: String, modifier: Modifier = Modifier) { fun greeting(
name: String,
modifier: Modifier = Modifier,
) {
Text( Text(
text = "Hello $name!", text = "Hello $name!",
modifier = modifier modifier = modifier,
) )
} }
// [END_ANCHOR:greeting]
// [PREVIEW] // [ENTITY: Function('GreetingPreview')]
// [ANCHOR:greetingPreview:Function]
// [CONTRACT:greetingPreview]
// [PURPOSE] Предварительный просмотр функции greeting.
// [END_CONTRACT:greetingPreview]
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun GreetingPreview() { fun greetingPreview() {
HomeboxLensTheme { HomeboxLensTheme {
Greeting("Android") greeting("Android")
} }
} }
// [END_ANCHOR:greetingPreview]
// [END_FILE_MainActivity.kt] // [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]

View File

@@ -1,28 +1,30 @@
// [PACKAGE] com.homebox.lens // [PACKAGE] com.homebox.lens
// [FILE] MainApplication.kt // [FILE] MainApplication.kt
// [SEMANTICS] application, hilt, timber
package com.homebox.lens package com.homebox.lens
// [IMPORTS]
import android.app.Application import android.app.Application
import com.homebox.lens.BuildConfig
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber import timber.log.Timber
// [END_IMPORTS]
// [ENTITY: Application('MainApplication')]
// [CONTRACT]
/** /**
* [ENTITY: Application('MainApplication')] * @summary Точка входа в приложение. Инициализирует Hilt и Timber.
* [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber.
*/ */
@HiltAndroidApp @HiltAndroidApp
class MainApplication : Application() { class MainApplication : Application() {
// [LIFECYCLE] // [ENTITY: Function('onCreate')]
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// [ACTION] Initialize Timber for logging
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
Timber.d("[DEBUG][INITIALIZATION][timber_planted] Timber DebugTree planted.")
} }
} }
// [END_ENTITY: Function('onCreate')]
} }
// [END_ENTITY: Application('MainApplication')]
// [END_FILE_MainApplication.kt] // [END_FILE_MainApplication.kt]

View File

@@ -1,61 +0,0 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] NavGraph.kt
// [SEMANTICS] navigation, compose, nav_host
package com.homebox.lens.navigation
// [IMPORTS]
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.search.SearchScreen
import com.homebox.lens.ui.screen.setup.SetupScreen
// [CORE-LOGIC]
/**
* [CONTRACT]
* Определяет граф навигации для приложения.
*/
@Composable
fun NavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Setup.route
) {
composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Setup.route) { inclusive = true }
}
})
}
composable(route = Screen.Dashboard.route) {
DashboardScreen()
}
composable(route = Screen.InventoryList.route) {
InventoryListScreen()
}
composable(route = Screen.ItemDetails.route) {
ItemDetailsScreen()
}
composable(route = Screen.ItemEdit.route) {
ItemEditScreen()
}
composable(route = Screen.LabelsList.route) {
LabelsListScreen()
}
composable(route = Screen.LocationsList.route) {
LocationsListScreen()
}
composable(route = Screen.Search.route) {
SearchScreen()
}
}
}
// [END_FILE_NavGraph.kt]

View File

@@ -1,27 +0,0 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] app/src/main/java/com/homebox/lens/navigation/Screen.kt
// [SEMANTICS] navigation, routes, sealed_class
package com.homebox.lens.navigation
/**
* [CONTRACT]
* Запечатанный класс для определения маршрутов навигации в приложении.
* Обеспечивает типобезопасность при навигации.
* @property route Строковый идентификатор маршрута.
*/
sealed class Screen(val route: String) {
data object Setup : Screen("setup_screen")
data object Dashboard : Screen("dashboard_screen")
data object InventoryList : Screen("inventory_list_screen")
data object ItemDetails : Screen("item_details_screen/{itemId}") {
fun createRoute(itemId: String) = "item_details_screen/$itemId"
}
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
fun createRoute(itemId: String) = "item_edit_screen/$itemId"
}
data object LabelsList : Screen("labels_list_screen")
data object LocationsList : Screen("locations_list_screen")
data object Search : Screen("search_screen")
}
// [END_FILE_Screen.kt]

View File

@@ -1,100 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt
// [SEMANTICS] ui, screen, dashboard, compose
// [IMPORTS]
package com.homebox.lens.ui.screen.dashboard
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
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.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import timber.log.Timber
// [CORE-LOGIC]
/**
* [CONTRACT]
* Главный Composable для экрана "Дэшборд".
* @param viewModel ViewModel для этого экрана, предоставляемая Hilt.
*/
@Composable
fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (val state = uiState) {
is DashboardUiState.Loading -> {
// [UI-ACTION] Показываем индикатор загрузки
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is DashboardUiState.Error -> {
// [UI-ACTION] Показываем сообщение об ошибке
val errorMessage = "Error: ${state.message}"
Text(
text = errorMessage,
modifier = Modifier.align(Alignment.Center)
)
Timber.w("[UI-STATE] Displaying Error: $errorMessage")
}
is DashboardUiState.Success -> {
// [UI-ACTION] Отображаем основной контент
Timber.d("[UI-STATE] Displaying Success")
DashboardContent(state)
}
}
}
}
}
/**
* [CONTRACT]
* Composable для отображения успешного состояния дэшборда.
* @param state Состояние UI с данными.
*/
@Composable
fun DashboardContent(state: DashboardUiState.Success) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// [UI-COMPONENT] Статистика
Text(text = "Statistics:")
Text(text = " Items: ${state.statistics.items}")
Text(text = " Locations: ${state.statistics.locations}")
Text(text = " Labels: ${state.statistics.labels}")
Text(text = " Total Value: ${state.statistics.totalValue}")
// [UI-COMPONENT] Локации
Text(text = "Locations:")
state.locations.forEach { location ->
Text(text = " - ${location.name} (${location.itemCount})")
}
// [UI-COMPONENT] Метки
Text(text = "Labels:")
state.labels.forEach { label ->
Text(text = " - ${label.name}")
}
}
}
// [END_FILE_DashboardScreen.kt]

View File

@@ -1,46 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
// [SEMANTICS] ui, state, dashboard
// [IMPORTS]
package com.homebox.lens.ui.screen.dashboard
import com.homebox.lens.domain.model.GroupStatistics
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LocationOutCount
// [CORE-LOGIC]
// [ENTITY: SealedInterface('DashboardUiState')]
/**
* [CONTRACT]
* Определяет все возможные состояния для экрана "Дэшборд".
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
*/
sealed interface DashboardUiState {
/**
* [CONTRACT]
* Состояние успешной загрузки данных.
* @property statistics Статистика по инвентарю.
* @property locations Список локаций со счетчиками.
* @property labels Список всех меток.
*/
data class Success(
val statistics: GroupStatistics,
val locations: List<LocationOutCount>,
val labels: List<LabelOut>
) : DashboardUiState
/**
* [CONTRACT]
* Состояние ошибки во время загрузки данных.
* @property message Человекочитаемое сообщение об ошибке.
*/
data class Error(val message: String) : DashboardUiState
/**
* [CONTRACT]
* Состояние, когда данные для экрана загружаются.
*/
data object Loading : DashboardUiState
}
// [END_FILE_DashboardUiState.kt]

View File

@@ -1,105 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardViewModel.kt
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
package com.homebox.lens.ui.screen.dashboard
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
import com.homebox.lens.ui.screen.dashboard.DashboardUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
// [VIEWMODEL]
// [ENTITY: ViewModel('DashboardViewModel')]
/**
* [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
) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
// [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
// [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
// должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
val uiState = _uiState.asStateFlow()
// [LIFECYCLE_HANDLER]
init {
loadDashboardData()
}
/**
* [CONTRACT]
* @summary Загружает все необходимые данные для экрана Dashboard.
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
*/
fun loadDashboardData() {
// [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = DashboardUiState.Loading
// [FIX] Используем Timber для логирования.
Timber.i("[ACTION] Starting parallel dashboard data load. State -> Loading.")
// [CORE-LOGIC: PARALLEL_FETCH]
val result = runCatching {
coroutineScope {
val statsDeferred = async { getStatisticsUseCase() }
val locationsDeferred = async { getAllLocationsUseCase() }
val labelsDeferred = async { getAllLabelsUseCase() }
val stats = statsDeferred.await()
val locations = locationsDeferred.await()
val labels = labelsDeferred.await()
// [POSTCONDITION_CHECK]
check(stats != null && locations != null && labels != null) {
"[POSTCONDITION_FAILED] One or more dashboard data sources returned null."
}
Triple(stats, locations, labels)
}
}
// [RESULT_HANDLER]
result.fold(
onSuccess = { (stats, locations, labels) ->
// [FIX] Используем Timber для логирования.
Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
_uiState.value = DashboardUiState.Success(
statistics = stats,
locations = locations,
labels = labels
)
},
onFailure = { exception ->
// [FIX] Используем Timber для логирования ошибок с передачей исключения.
Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
_uiState.value = DashboardUiState.Error(
message = exception.message ?: "Could not load dashboard data."
)
}
)
}
}
// [END_CLASS_DashboardViewModel]
}
// [END_FILE_DashboardViewModel.kt]

View File

@@ -1,22 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListScreen.kt
package com.homebox.lens.ui.screen.inventorylist
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
// [ENTRYPOINT]
@Composable
fun InventoryListScreen() {
// [ACTION]
Text(text = "Inventory List Screen")
}
@Preview(showBackground = true)
@Composable
fun InventoryListScreenPreview() {
InventoryListScreen()
}
// [END_FILE_InventoryListScreen.kt]

View File

@@ -1,16 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListViewModel.kt
package com.homebox.lens.ui.screen.inventorylist
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [VIEWMODEL]
@HiltViewModel
class InventoryListViewModel @Inject constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
}
// [END_FILE_InventoryListViewModel.kt]

View File

@@ -1,22 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsScreen.kt
package com.homebox.lens.ui.screen.itemdetails
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
// [ENTRYPOINT]
@Composable
fun ItemDetailsScreen() {
// [ACTION]
Text(text = "Item Details Screen")
}
@Preview(showBackground = true)
@Composable
fun ItemDetailsScreenPreview() {
ItemDetailsScreen()
}
// [END_FILE_ItemDetailsScreen.kt]

View File

@@ -1,16 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsViewModel.kt
package com.homebox.lens.ui.screen.itemdetails
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [VIEWMODEL]
@HiltViewModel
class ItemDetailsViewModel @Inject constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
}
// [END_FILE_ItemDetailsViewModel.kt]

View File

@@ -1,22 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditScreen.kt
package com.homebox.lens.ui.screen.itemedit
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
// [ENTRYPOINT]
@Composable
fun ItemEditScreen() {
// [ACTION]
Text(text = "Item Edit Screen")
}
@Preview(showBackground = true)
@Composable
fun ItemEditScreenPreview() {
ItemEditScreen()
}
// [END_FILE_ItemEditScreen.kt]

View File

@@ -1,16 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditViewModel.kt
package com.homebox.lens.ui.screen.itemedit
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [VIEWMODEL]
@HiltViewModel
class ItemEditViewModel @Inject constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
}
// [END_FILE_ItemEditViewModel.kt]

View File

@@ -1,22 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt
package com.homebox.lens.ui.screen.labelslist
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
// [ENTRYPOINT]
@Composable
fun LabelsListScreen() {
// [ACTION]
Text(text = "Labels List Screen")
}
@Preview(showBackground = true)
@Composable
fun LabelsListScreenPreview() {
LabelsListScreen()
}
// [END_FILE_LabelsListScreen.kt]

View File

@@ -1,16 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListViewModel.kt
package com.homebox.lens.ui.screen.labelslist
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [VIEWMODEL]
@HiltViewModel
class LabelsListViewModel @Inject constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
}
// [END_FILE_LabelsListViewModel.kt]

View File

@@ -1,22 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListScreen.kt
package com.homebox.lens.ui.screen.locationslist
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
// [ENTRYPOINT]
@Composable
fun LocationsListScreen() {
// [ACTION]
Text(text = "Locations List Screen")
}
@Preview(showBackground = true)
@Composable
fun LocationsListScreenPreview() {
LocationsListScreen()
}
// [END_FILE_LocationsListScreen.kt]

View File

@@ -1,16 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListViewModel.kt
package com.homebox.lens.ui.screen.locationslist
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [VIEWMODEL]
@HiltViewModel
class LocationsListViewModel @Inject constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
}
// [END_FILE_LocationsListViewModel.kt]

View File

@@ -1,22 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchScreen.kt
package com.homebox.lens.ui.screen.search
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
// [ENTRYPOINT]
@Composable
fun SearchScreen() {
// [ACTION]
Text(text = "Search Screen")
}
@Preview(showBackground = true)
@Composable
fun SearchScreenPreview() {
SearchScreen()
}
// [END_FILE_SearchScreen.kt]

View File

@@ -1,16 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchViewModel.kt
package com.homebox.lens.ui.screen.search
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [VIEWMODEL]
@HiltViewModel
class SearchViewModel @Inject constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
}
// [END_FILE_SearchViewModel.kt]

View File

@@ -1,120 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt
package com.homebox.lens.ui.screen.setup
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
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.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
// [FIX] Opt-in for experimental Material 3 APIs
@OptIn(ExperimentalMaterial3Api::class)
// [ENTRYPOINT]
@Composable
fun SetupScreen(
viewModel: SetupViewModel = hiltViewModel(),
onSetupComplete: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
if (uiState.isSetupComplete) {
onSetupComplete()
}
SetupScreenContent(
uiState = uiState,
onServerUrlChange = viewModel::onServerUrlChange,
onUsernameChange = viewModel::onUsernameChange,
onPasswordChange = viewModel::onPasswordChange,
onConnectClick = viewModel::connect
)
}
// [FIX] Opt-in for experimental Material 3 APIs
@OptIn(ExperimentalMaterial3Api::class)
// [CONTENT]
@Composable
private fun SetupScreenContent(
uiState: SetupUiState,
onServerUrlChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onConnectClick: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(title = { Text("Server Setup") })
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = uiState.serverUrl,
onValueChange = onServerUrlChange,
label = { Text("Server URL") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.username,
onValueChange = onUsernameChange,
label = { Text("Username") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.password,
onValueChange = onPasswordChange,
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onConnectClick,
enabled = !uiState.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Text("Connect")
}
}
uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
}
}
}
// [FIX] Opt-in for experimental Material 3 APIs
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
fun SetupScreenPreview() {
SetupScreenContent(
uiState = SetupUiState(error = "Failed to connect"),
onServerUrlChange = {},
onUsernameChange = {},
onPasswordChange = {},
onConnectClick = {}
)
}
// [END_FILE_SetupScreen.kt]

View File

@@ -1,27 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupUiState.kt
// [SEMANTICS] ui_state, data_model, immutable
package com.homebox.lens.ui.screen.setup
/**
* [ENTITY: DataClass('SetupUiState')]
* [CONTRACT]
* Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
* Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
* @property serverUrl URL-адрес сервера Homebox.
* @property username Имя пользователя для входа.
* @property password Пароль пользователя.
* @property isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
* @property error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
* @property isSetupComplete Флаг, указывающий на успешное завершение настройки и входа.
*/
data class SetupUiState(
val serverUrl: String = "",
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null,
val isSetupComplete: Boolean = false
)
// [END_FILE_SetupUiState.kt]

View File

@@ -1,143 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupViewModel.kt
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
package com.homebox.lens.ui.screen.setup
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.usecase.LoginUseCase
import com.homebox.lens.ui.screen.setup.SetupUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
// [VIEWMODEL]
// [ENTITY: ViewModel('SetupViewModel')]
/**
* [CONTRACT]
* ViewModel для экрана первоначальной настройки (Setup).
* Отвечает за:
* 1. Загрузку и сохранение учетных данных (URL сервера, логин, пароль).
* 2. Управление состоянием UI экрана (`SetupUiState`).
* 3. Инициацию процесса входа в систему через `LoginUseCase`.
* @property credentialsRepository Репозиторий для операций с учетными данными.
* @property loginUseCase Use case для выполнения логики входа.
* @invariant Состояние `uiState` всегда является единственным источником истины для UI.
*/
@HiltViewModel
class SetupViewModel @Inject constructor(
private val credentialsRepository: CredentialsRepository,
private val loginUseCase: LoginUseCase
) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow(SetupUiState())
val uiState = _uiState.asStateFlow()
// [LIFECYCLE_HANDLER]
init {
// [ACTION] Загружаем учетные данные при создании ViewModel.
loadCredentials()
}
/**
* [CONTRACT]
* [HELPER] Загружает учетные данные из репозитория при инициализации.
* @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными.
*/
private fun loadCredentials() {
// [ENTRYPOINT]
viewModelScope.launch {
// [CORE-LOGIC] Подписываемся на поток учетных данных.
credentialsRepository.getCredentials().collect { credentials ->
// [ACTION] Обновляем состояние, если учетные данные существуют.
if (credentials != null) {
_uiState.update {
it.copy(
serverUrl = credentials.serverUrl,
username = credentials.username,
password = credentials.password
)
}
}
}
}
}
/**
* [CONTRACT]
* [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя.
* @param newUrl Новое значение URL.
* @sideeffect Обновляет поле `serverUrl` в `_uiState`.
*/
fun onServerUrlChange(newUrl: String) {
_uiState.update { it.copy(serverUrl = newUrl) }
}
/**
* [CONTRACT]
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
* @param newUsername Новое значение имени пользователя.
* @sideeffect Обновляет поле `username` в `_uiState`.
*/
fun onUsernameChange(newUsername: String) {
_uiState.update { it.copy(username = newUsername) }
}
/**
* [CONTRACT]
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
* @param newPassword Новое значение пароля.
* @sideeffect Обновляет поле `password` в `_uiState`.
*/
fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) }
}
/**
* [CONTRACT]
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
* Выполняет две основные операции:
* 1. Сохраняет введенные учетные данные для последующих сессий.
* 2. Выполняет вход в систему с использованием этих данных.
* @sideeffect Обновляет `_uiState`, управляя флагами `isLoading`, `error` и `isSetupComplete`.
* @sideeffect Вызывает `credentialsRepository.saveCredentials`, изменяя сохраненные данные.
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
*/
fun connect() {
// [ENTRYPOINT]
viewModelScope.launch {
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
_uiState.update { it.copy(isLoading = true, error = null) }
// [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов.
val credentials = Credentials(
serverUrl = _uiState.value.serverUrl.trim(),
username = _uiState.value.username.trim(),
password = _uiState.value.password
)
// [ACTION] Сохраняем учетные данные для будущего использования.
credentialsRepository.saveCredentials(credentials)
// [CORE-LOGIC] Выполняем UseCase и обрабатываем результат.
loginUseCase(credentials).fold(
onSuccess = {
// [ACTION] Обработка успеха: обновляем UI, отмечая завершение настройки.
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
},
onFailure = { exception ->
// [ERROR_HANDLER] Обработка ошибки: обновляем UI с сообщением об ошибке.
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
}
)
}
}
// [END_CLASS_SetupViewModel]
}
// [END_FILE_SetupViewModel.kt]

View File

@@ -1,64 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Theme.kt
package com.homebox.lens.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
@Composable
fun HomeboxLensTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
// [END_FILE_Theme.kt]

View File

@@ -1,23 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Typography.kt
package com.homebox.lens.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)
// [END_FILE_Typography.kt]

View File

@@ -0,0 +1,146 @@
<resources>
<string name="app_name">Homebox Lens</string>
<!-- Common -->
<string name="create">Create</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="search">Search</string>
<string name="logout">Logout</string>
<string name="no_location">No location</string>
<string name="items_not_found">Items not found</string>
<string name="error_loading_failed">Failed to load data. Please try again.</string>
<!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
<string name="cd_scan_qr_code">Scan QR code</string>
<string name="cd_search">Search</string>
<string name="cd_navigate_back">Navigate back</string>
<string name="cd_navigate_up">Go back</string>
<string name="cd_add_new_location">Add new location</string>
<string name="content_desc_add_label">Add new label</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string>
<string name="dashboard_section_quick_stats">Quick Stats</string>
<string name="dashboard_section_recently_added">Recently Added</string>
<string name="dashboard_section_locations">Locations</string>
<string name="dashboard_section_labels">Labels</string>
<string name="location_chip_label">%1$s (%2$d)</string>
<!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Total Items</string>
<string name="dashboard_stat_total_value">Total Value</string>
<string name="dashboard_stat_total_labels">Total Labels</string>
<string name="dashboard_stat_total_locations">Total Locations</string>
<!-- Navigation -->
<string name="nav_locations">Locations</string>
<string name="nav_labels">Labels</string>
<!-- Screen Titles -->
<string name="inventory_list_title">Inventory</string>
<!-- Screen Titles -->
<string name="item_details_title">Details</string>
<string name="item_edit_title">Edit Item</string>
<string name="labels_list_title">Labels</string>
<string name="locations_list_title">Locations</string>
<string name="search_title">Search</string>
<string name="save_item">Save</string>
<string name="item_name">Name</string>
<string name="item_description">Description</string>
<string name="item_quantity">Quantity</string>
<!-- Location Edit Screen -->
<string name="location_edit_title_create">Create Location</string>
<string name="location_edit_title_edit">Edit Location</string>
<!-- Locations List Screen -->
<string name="locations_not_found">Locations not found. Press + to add a new one.</string>
<string name="item_count">Items: %1$d</string>
<string name="cd_more_options">More options</string>
<!-- Setup Screen -->
<string name="setup_title">Server Setup</string>
<string name="setup_server_url_label">Server URL</string>
<string name="setup_username_label">Username</string>
<string name="setup_password_label">Password</string>
<string name="setup_connect_button">Connect</string>
<!-- Labels List Screen -->
<string name="screen_title_labels">Labels</string>
<string name="content_desc_navigate_back">Navigate back</string>
<string name="content_desc_create_label">Create new label</string>
<string name="content_desc_label_icon">Label icon</string>
<string name="content_desc_delete_label">Delete label</string>
<string name="no_labels_found">No labels found.</string>
<string name="dialog_title_create_label">Create Label</string>
<string name="dialog_field_label_name">Label Name</string>
<string name="dialog_button_create">Create</string>
<string name="dialog_button_cancel">Cancel</string>
<!-- Inventory List Screen -->
<string name="content_desc_sync_inventory">Sync inventory</string>
<!-- Item Details Screen -->
<string name="content_desc_edit_item">Edit item</string>
<string name="content_desc_delete_item">Delete item</string>
<string name="section_title_description">Description</string>
<string name="placeholder_no_description">No description</string>
<string name="section_title_details">Details</string>
<string name="label_quantity">Quantity</string>
<string name="label_location">Location</string>
<string name="section_title_labels">Labels</string>
<!-- Item Edit Screen -->
<string name="item_edit_title_create">Create item</string>
<string name="content_desc_save_item">Save item</string>
<string name="label_name">Name</string>
<string name="label_description">Description</string>
<!-- Search Screen -->
<string name="placeholder_search_items">Search items...</string>
<!-- Setup Screen -->
<string name="screen_title_setup">Setup</string>
<!-- Label Edit Screen -->
<string name="label_edit_title_create">Create label</string>
<string name="label_edit_title_edit">Edit label</string>
<string name="label_name_edit">Label name</string>
<!-- Common Actions -->
<string name="back">Back</string>
<string name="save">Save</string>
<!-- Color Picker -->
<string name="label_color">Color</string>
<string name="label_hex_color">HEX color code</string>
<string name="item_asset_id">Asset ID</string>
<string name="item_notes">Notes</string>
<string name="item_serial_number">Serial Number</string>
<string name="item_purchase_price">Purchase Price</string>
<string name="item_purchase_date">Purchase Date</string>
<string name="item_warranty_until">Warranty Until</string>
<string name="item_parent_id">Parent ID</string>
<string name="item_is_archived">Is Archived</string>
<string name="item_insured">Insured</string>
<string name="item_lifetime_warranty">Lifetime Warranty</string>
<string name="item_sync_child_items_locations">Sync Child Items Locations</string>
<string name="item_manufacturer">Manufacturer</string>
<string name="item_model_number">Model Number</string>
<string name="item_purchase_from">Purchase From</string>
<string name="item_warranty_details">Warranty Details</string>
<string name="item_sold_notes">Sold Notes</string>
<string name="item_sold_price">Sold Price</string>
<string name="item_sold_time">Sold Time</string>
<string name="item_sold_to">Sold To</string>
<string name="scan_qr_code">Scan QR Code</string>
<string name="ok">OK</string>
<string name="cancel">Cancel</string>
</resources>

View File

@@ -1,3 +1,140 @@
<resources> <resources>
<string name="app_name">Homebox Lens</string> <string name="app_name">Homebox Lens</string>
<!-- Common -->
<string name="create">Создать</string>
<string name="edit">Редактировать</string>
<string name="delete">Удалить</string>
<string name="search">Поиск</string>
<string name="logout">Выйти</string>
<string name="no_location">Нет локации</string>
<string name="items_not_found">Элементы не найдены</string>
<string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string>
<!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Открыть боковое меню</string>
<string name="cd_scan_qr_code">Сканировать QR/штрих-код</string>
<string name="cd_search">Поиск</string>
<string name="cd_navigate_back">Вернуться назад</string>
<string name="cd_navigate_up">Вернуться</string>
<string name="cd_add_new_location">Добавить новую локацию</string>
<string name="content_desc_add_label">Добавить новую метку</string>
<!-- Inventory List Screen -->
<string name="content_desc_sync_inventory">Синхронизировать инвентарь</string>
<!-- Item Details Screen -->
<string name="content_desc_edit_item">Редактировать элемент</string>
<string name="content_desc_delete_item">Удалить элемент</string>
<string name="section_title_description">Описание</string>
<string name="placeholder_no_description">Нет описания</string>
<string name="section_title_details">Детали</string>
<string name="label_quantity">Количество</string>
<string name="label_location">Местоположение</string>
<string name="section_title_labels">Метки</string>
<!-- Item Edit Screen -->
<string name="item_edit_title_create">Создать элемент</string>
<string name="content_desc_save_item">Сохранить элемент</string>
<string name="label_name">Название</string>
<string name="label_description">Описание</string>
<!-- Search Screen -->
<string name="placeholder_search_items">Поиск элементов...</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Главная</string>
<string name="dashboard_section_quick_stats">Быстрая статистика</string>
<string name="dashboard_section_recently_added">Недавно добавлено</string>
<string name="dashboard_section_locations">Места хранения</string>
<string name="dashboard_section_labels">Метки</string>
<string name="location_chip_label">%1$s (%2$d)</string>
<!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Всего вещей</string>
<string name="dashboard_stat_total_value">Общая стоимость</string>
<string name="dashboard_stat_total_labels">Всего меток</string>
<string name="dashboard_stat_total_locations">Всего локаций</string>
<!-- Navigation -->
<string name="nav_locations">Локации</string>
<string name="nav_labels">Метки</string>
<!-- Screen Titles -->
<string name="inventory_list_title">Инвентарь</string>
<string name="item_details_title">Детали</string>
<string name="item_edit_title">Редактирование</string>
<string name="labels_list_title">Метки</string>
<string name="locations_list_title">Места хранения</string>
<string name="search_title">Поиск</string>
<string name="save_item">Сохранить</string>
<string name="item_name">Название</string>
<string name="item_description">Описание</string>
<string name="item_quantity">Количество</string>
<!-- Location Edit Screen -->
<string name="location_edit_title_create">Создать локацию</string>
<string name="location_edit_title_edit">Редактировать локацию</string>
<!-- Locations List Screen -->
<string name="locations_not_found">Местоположения не найдены. Нажмите +, чтобы добавить новое.</string>
<string name="item_count">Предметов: %1$d</string>
<string name="cd_more_options">Больше опций</string>
<!-- Setup Screen -->
<string name="screen_title_setup">Настройка</string>
<string name="setup_title">Настройка сервера</string>
<string name="setup_server_url_label">URL сервера</string>
<string name="setup_username_label">Имя пользователя</string>
<string name="setup_password_label">Пароль</string>
<string name="setup_connect_button">Подключиться</string>
<!-- Labels List Screen -->
<string name="screen_title_labels">Метки</string>
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
<string name="content_desc_create_label">Создать новую метку</string>
<string name="content_desc_label_icon">Иконка метки</string>
<string name="content_desc_delete_label">Удалить метку</string>
<string name="no_labels_found">Метки не найдены.</string>
<string name="dialog_title_create_label">Создать метку</string>
<string name="dialog_field_label_name">Название метки</string>
<string name="dialog_button_create">Создать</string>
<string name="dialog_button_cancel">Отмена</string>
<!-- Label Edit Screen -->
<string name="label_edit_title_create">Создать метку</string>
<string name="label_edit_title_edit">Редактировать метку</string>
<string name="label_name_edit">Название метки</string>
<!-- Common Actions -->
<string name="back">Назад</string>
<string name="save">Сохранить</string>
<!-- Common Actions -->
<!-- Color Picker -->
<string name="label_color">Цвет</string>
<string name="label_hex_color">HEX-код цвета</string>
<string name="item_asset_id">Идентификатор актива</string>
<string name="item_notes">Заметки</string>
<string name="item_serial_number">Серийный номер</string>
<string name="item_purchase_price">Цена покупки</string>
<string name="item_purchase_date">Дата покупки</string>
<string name="item_warranty_until">Гарантия до</string>
<string name="item_parent_id">Родительский ID</string>
<string name="item_is_archived">Архивировано</string>
<string name="item_insured">Застраховано</string>
<string name="item_lifetime_warranty">Пожизненная гарантия</string>
<string name="item_sync_child_items_locations">Синхронизировать дочерние элементы</string>
<string name="item_manufacturer">Производитель</string>
<string name="item_model_number">Номер модели</string>
<string name="item_purchase_from">Куплено у</string>
<string name="item_warranty_details">Детали гарантии</string>
<string name="item_sold_notes">Примечания о продаже</string>
<string name="item_sold_price">Цена продажи</string>
<string name="item_sold_time">Время продажи</string>
<string name="item_sold_to">Продано кому</string>
<string name="scan_qr_code">Сканировать QR-код</string>
<string name="ok">ОК</string>
<string name="cancel">Отмена</string>
</resources> </resources>

View File

@@ -1,13 +1,13 @@
// [FILE] build.gradle.kts // [FILE] build.gradle.kts
// [PURPOSE] Root build file for the project, configures plugins for all modules. // [SEMANTICS] build, configuration
// [AI_NOTE]: Root build file for the project, configures plugins for all modules.
plugins { plugins {
// [PLUGIN] Android Application plugin id("com.android.application") version "8.12.3" apply false
id("com.android.application") version "8.11.0" apply false id("org.jetbrains.kotlin.android") version "2.0.0" apply false
// [PLUGIN] Kotlin Android plugin id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.22" apply false id("com.google.dagger.hilt.android") version "2.51.1" apply false
// [PLUGIN] Hilt Android plugin id("com.google.devtools.ksp") version "2.0.0-1.0.24" apply false
id("com.google.dagger.hilt.android") version "2.48.1" apply false
} }
// [END_FILE_build.gradle.kts] // [END_FILE_build.gradle.kts]

View File

@@ -1,72 +1,56 @@
// [PACKAGE] buildsrc.dependencies
// [FILE] Dependencies.kt // [FILE] Dependencies.kt
// [PURPOSE] Centralized dependency management for the entire project. // [SEMANTICS] build, dependencies
// [ENTITY: Object('Versions')]
object Versions { object Versions {
// Build
const val compileSdk = 34 const val compileSdk = 34
const val minSdk = 26 const val minSdk = 24
const val targetSdk = 34 const val targetSdk = 34
const val versionCode = 1 const val versionCode = 1
const val versionName = "1.0" const val versionName = "1.0"
const val kotlin = "1.9.10"
// Kotlin
const val kotlin = "1.9.22"
const val coroutines = "1.7.3" const val coroutines = "1.7.3"
const val composeCompiler = "1.5.4"
// Jetpack Compose const val composeBom = "2024.05.00"
const val composeCompiler = "1.5.8"
const val composeBom = "2023.10.01"
const val activityCompose = "1.8.2" const val activityCompose = "1.8.2"
const val navigationCompose = "2.7.6" const val navigationCompose = "2.7.7"
const val hiltNavigationCompose = "1.1.0" const val hiltNavigationCompose = "1.1.0"
// AndroidX
const val coreKtx = "1.12.0" const val coreKtx = "1.12.0"
const val lifecycle = "2.6.2" const val lifecycle = "2.7.0"
const val appcompat = "1.6.1" const val appcompat = "1.6.1"
// Networking
const val retrofit = "2.9.0" const val retrofit = "2.9.0"
const val okhttp = "4.12.0" const val okhttp = "4.12.0"
const val moshi = "1.15.0" const val moshi = "1.15.1"
// Database
const val room = "2.6.1" const val room = "2.6.1"
const val hilt = "2.51.1"
// DI const val hiltCompiler = "1.2.0"
const val hilt = "2.48.1"
const val hiltCompiler = "1.1.0"
// Logging
const val timber = "5.0.1" const val timber = "5.0.1"
// Testing
const val junit = "4.13.2" const val junit = "4.13.2"
const val extJunit = "1.1.5" const val extJunit = "1.1.5"
const val espresso = "3.5.1" const val espresso = "3.5.1"
const val kotest = "5.8.0"
const val mockk = "1.13.10"
} }
// [END_ENTITY: Object('Versions')]
// [ENTITY: Object('Libs')]
object Libs { object Libs {
// Kotlin
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}" const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}" const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
// AndroidX
const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}" const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}"
const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}" const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}" const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
const val composeUi = "androidx.compose.ui:ui:1.5.4"
// Compose const val composeUiGraphics = "androidx.compose.ui:ui-graphics:1.5.4"
const val composeBom = "androidx.compose:compose-bom:${Versions.composeBom}" const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview:1.5.4"
const val composeUi = "androidx.compose.ui:ui" const val composeMaterial3 = "androidx.compose.material3:material3:1.1.2"
const val composeUiGraphics = "androidx.compose.ui:ui-graphics" const val composeFoundation = "androidx.compose.foundation:foundation:1.5.4"
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview" const val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:1.5.4"
const val composeMaterial3 = "androidx.compose.material3:material3" const val composeMaterialIconsExtended = "androidx.compose.material:material-icons-extended:1.5.4"
const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}" const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}"
const val navigationCompose = "androidx.navigation:navigation-compose:${Versions.navigationCompose}" const val navigationCompose = "androidx.navigation:navigation-compose:${Versions.navigationCompose}"
const val hiltNavigationCompose = "androidx.hilt:hilt-navigation-compose:${Versions.hiltNavigationCompose}" const val hiltNavigationCompose = "androidx.hilt:hilt-navigation-compose:${Versions.hiltNavigationCompose}"
// Networking (Retrofit, OkHttp, Moshi)
const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}" const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
const val converterMoshi = "com.squareup.retrofit2:converter-moshi:${Versions.retrofit}" const val converterMoshi = "com.squareup.retrofit2:converter-moshi:${Versions.retrofit}"
const val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}" const val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}"
@@ -74,27 +58,22 @@ object Libs {
const val moshi = "com.squareup.moshi:moshi:${Versions.moshi}" const val moshi = "com.squareup.moshi:moshi:${Versions.moshi}"
const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:${Versions.moshi}" const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:${Versions.moshi}"
const val moshiCodegen = "com.squareup.moshi:moshi-kotlin-codegen:${Versions.moshi}" const val moshiCodegen = "com.squareup.moshi:moshi-kotlin-codegen:${Versions.moshi}"
// Database (Room)
const val roomRuntime = "androidx.room:room-runtime:${Versions.room}" const val roomRuntime = "androidx.room:room-runtime:${Versions.room}"
const val roomKtx = "androidx.room:room-ktx:${Versions.room}" const val roomKtx = "androidx.room:room-ktx:${Versions.room}"
const val roomCompiler = "androidx.room:room-compiler:${Versions.room}" const val roomCompiler = "androidx.room:room-compiler:${Versions.room}"
// Dependency Injection (Hilt)
const val hiltAndroid = "com.google.dagger:hilt-android:${Versions.hilt}" const val hiltAndroid = "com.google.dagger:hilt-android:${Versions.hilt}"
const val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}" const val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}"
// Logging
const val timber = "com.jakewharton.timber:timber:${Versions.timber}" const val timber = "com.jakewharton.timber:timber:${Versions.timber}"
// Testing
const val junit = "junit:junit:${Versions.junit}" const val junit = "junit:junit:${Versions.junit}"
const val extJunit = "androidx.test.ext:junit:${Versions.extJunit}" const val extJunit = "androidx.test.ext:junit:${Versions.extJunit}"
const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}" const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}"
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4" const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.5.4"
const val composeUiTooling = "androidx.compose.ui:ui-tooling" const val composeUiTooling = "androidx.compose.ui:ui-tooling:1.5.4"
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest" const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:1.5.4"
const val kotestRunnerJunit5 = "io.kotest:kotest-runner-junit5:${Versions.kotest}"
const val kotestAssertionsCore = "io.kotest:kotest-assertions-core:${Versions.kotest}"
const val mockk = "io.mockk:mockk:${Versions.mockk}"
} }
// [END_ENTITY: Object('Libs')]
// [END_FILE_Dependencies.kt] // [END_FILE_Dependencies.kt]

View File

@@ -62,6 +62,9 @@ dependencies {
implementation(Libs.hiltAndroid) implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler) kapt(Libs.hiltCompiler)
// [DEPENDENCY] Logging
implementation(Libs.timber)
// [DEPENDENCY] Testing // [DEPENDENCY] Testing
testImplementation(Libs.junit) testImplementation(Libs.junit)
androidTestImplementation(Libs.extJunit) androidTestImplementation(Libs.extJunit)

View File

@@ -1,73 +1,97 @@
// [PACKAGE] com.homebox.lens.data.api // [PACKAGE] com.homebox.lens.data.api
// [FILE] HomeboxApiService.kt // [FILE] HomeboxApiService.kt
// [SEMANTICS] data, api, retrofit
package com.homebox.lens.data.api package com.homebox.lens.data.api
import com.homebox.lens.data.api.dto.GroupStatisticsDto // [IMPORTS]
import com.homebox.lens.data.api.dto.ItemCreateDto import com.homebox.lens.data.api.dto.*
import com.homebox.lens.data.api.dto.ItemOutDto
import com.homebox.lens.data.api.dto.ItemSummaryDto
import com.homebox.lens.data.api.dto.ItemUpdateDto
import com.homebox.lens.data.api.dto.LabelOutDto
import com.homebox.lens.data.api.dto.LocationOutCountDto
import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.dto.PaginationResultDto
import com.homebox.lens.data.api.dto.TokenResponseDto
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.*
import retrofit2.http.DELETE // [END_IMPORTS]
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
// [CONTRACT] // [ENTITY: Interface('HomeboxApiService')]
/** /**
* [ENTITY: Interface('HomeboxApiService')] * @summary Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
* [PURPOSE] Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
*/ */
interface HomeboxApiService { interface HomeboxApiService {
// [ENDPOINT] Auth // [ENTITY: ApiEndpoint('login')]
// [FIX] Явно указываем заголовок Content-Type, чтобы переопределить
// значение по умолчанию от Moshi, которое содержит "; charset=UTF-8".
@Headers("Content-Type: application/json") @Headers("Content-Type: application/json")
@POST("v1/users/login") @POST("v1/users/login")
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
// [END_ENTITY: ApiEndpoint('login')]
// [ENDPOINT] Items // [ENTITY: ApiEndpoint('getItems')]
@GET("v1/items") @GET("v1/items")
suspend fun getItems( suspend fun getItems(
@Query("q") query: String? = null, @Query("q") query: String? = null,
@Query("page") page: Int? = null, @Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int? = null @Query("pageSize") pageSize: Int? = null
): PaginationResultDto<ItemSummaryDto> ): PaginationResultDto<ItemSummaryDto>
// [END_ENTITY: ApiEndpoint('getItems')]
// [ENTITY: ApiEndpoint('createItem')]
@POST("v1/items") @POST("v1/items")
suspend fun createItem(@Body item: ItemCreateDto): ItemSummaryDto suspend fun createItem(@Body item: ItemCreateDto): ItemSummaryDto
// [END_ENTITY: ApiEndpoint('createItem')]
// [ENTITY: ApiEndpoint('getItem')]
@GET("v1/items/{id}") @GET("v1/items/{id}")
suspend fun getItem(@Path("id") itemId: String): ItemOutDto suspend fun getItem(@Path("id") itemId: String): ItemOutDto
// [END_ENTITY: ApiEndpoint('getItem')]
// [ENTITY: ApiEndpoint('updateItem')]
@PUT("v1/items/{id}") @PUT("v1/items/{id}")
suspend fun updateItem(@Path("id") itemId: String, @Body item: ItemUpdateDto): ItemOutDto suspend fun updateItem(@Path("id") itemId: String, @Body item: ItemUpdateDto): ItemOutDto
// [END_ENTITY: ApiEndpoint('updateItem')]
// [ENTITY: ApiEndpoint('deleteItem')]
@DELETE("v1/items/{id}") @DELETE("v1/items/{id}")
suspend fun deleteItem(@Path("id") itemId: String): Response<Unit> suspend fun deleteItem(@Path("id") itemId: String): Response<Unit>
// [END_ENTITY: ApiEndpoint('deleteItem')]
// [ENDPOINT] Locations // [ENTITY: ApiEndpoint('getLocations')]
@GET("v1/locations") @GET("v1/locations")
suspend fun getLocations(): List<LocationOutCountDto> suspend fun getLocations(): List<LocationOutCountDto>
// [END_ENTITY: ApiEndpoint('getLocations')]
// [ENDPOINT] Labels // [ENTITY: ApiEndpoint('getLabels')]
@GET("v1/labels") @GET("v1/labels")
suspend fun getLabels(): List<LabelOutDto> suspend fun getLabels(): List<LabelOutDto>
// [END_ENTITY: ApiEndpoint('getLabels')]
// [ENDPOINT] Statistics // [ENTITY: ApiEndpoint('createLabel')]
@POST("v1/labels")
suspend fun createLabel(@Body newLabel: LabelCreateDto): LabelSummaryDto
// [END_ENTITY: ApiEndpoint('createLabel')]
// [ENTITY: ApiEndpoint('updateLabel')]
@PUT("v1/labels/{id}")
suspend fun updateLabel(@Path("id") labelId: String, @Body label: LabelUpdateDto): LabelOutDto
// [END_ENTITY: ApiEndpoint('updateLabel')]
// [ENTITY: ApiEndpoint('deleteLabel')]
@DELETE("v1/labels/{id}")
suspend fun deleteLabel(@Path("id") labelId: String): Response<Unit>
// [ENTITY: ApiEndpoint('createLocation')]
@POST("v1/locations")
suspend fun createLocation(@Body newLocation: LocationCreateDto): LocationOutDto
// [END_ENTITY: ApiEndpoint('createLocation')]
// [ENTITY: ApiEndpoint('updateLocation')]
@PUT("v1/locations/{id}")
suspend fun updateLocation(@Path("id") locationId: String, @Body location: LocationUpdateDto): LocationOutDto
// [END_ENTITY: ApiEndpoint('updateLocation')]
// [ENTITY: ApiEndpoint('deleteLocation')]
@DELETE("v1/locations/{id}")
suspend fun deleteLocation(@Path("id") locationId: String): Response<Unit>
// [ENTITY: ApiEndpoint('getStatistics')]
@GET("v1/groups/statistics") @GET("v1/groups/statistics")
suspend fun getStatistics(): GroupStatisticsDto suspend fun getStatistics(): GroupStatisticsDto
// [END_ENTITY: ApiEndpoint('getStatistics')]
} }
// [END_ENTITY: Interface('HomeboxApiService')]
// [END_FILE_HomeboxApiService.kt] // [END_FILE_HomeboxApiService.kt]

View File

@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.CustomField import com.homebox.lens.domain.model.CustomField
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('CustomFieldDto')]
/** /**
* [CONTRACT] * @summary DTO для кастомного поля.
* DTO для кастомного поля.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class CustomFieldDto( data class CustomFieldDto(
@@ -20,10 +20,12 @@ data class CustomFieldDto(
@Json(name = "value") val value: String, @Json(name = "value") val value: String,
@Json(name = "type") val type: String @Json(name = "type") val type: String
) )
// [END_ENTITY: DataClass('CustomFieldDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('CustomField')]
/** /**
* [CONTRACT] * @summary Маппер из CustomFieldDto в доменную модель CustomField.
* Маппер из CustomFieldDto в доменную модель CustomField.
*/ */
fun CustomFieldDto.toDomain(): CustomField { fun CustomFieldDto.toDomain(): CustomField {
return CustomField( return CustomField(
@@ -32,3 +34,4 @@ fun CustomFieldDto.toDomain(): CustomField {
type = this.type type = this.type
) )
} }
// [END_ENTITY: Function('toDomain')]

View File

@@ -8,14 +8,11 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.GroupStatistics import com.homebox.lens.domain.model.GroupStatistics
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('GroupStatisticsDto')]
/** /**
* [CONTRACT] * @summary DTO для статистики.
* DTO для статистики.
* [COHERENCE_NOTE] Этот DTO был исправлен, чтобы точно соответствовать JSON-ответу от сервера.
* Поля `items`, `labels`, `locations`, `totalValue` были заменены на `totalItems`, `totalLabels`,
* `totalLocations`, `totalItemPrice` и т.д., чтобы устранить ошибку парсинга `JsonDataException`.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GroupStatisticsDto( data class GroupStatisticsDto(
@@ -23,19 +20,17 @@ data class GroupStatisticsDto(
@Json(name = "totalLabels") val totalLabels: Int, @Json(name = "totalLabels") val totalLabels: Int,
@Json(name = "totalLocations") val totalLocations: Int, @Json(name = "totalLocations") val totalLocations: Int,
@Json(name = "totalItemPrice") val totalItemPrice: Double, @Json(name = "totalItemPrice") val totalItemPrice: Double,
// [FIX] Добавляем недостающие поля, которые присутствуют в JSON, но отсутствовали в DTO.
// Делаем их nullable на случай, если API перестанет их присылать в будущем.
@Json(name = "totalUsers") val totalUsers: Int? = null, @Json(name = "totalUsers") val totalUsers: Int? = null,
@Json(name = "totalWithWarranty") val totalWithWarranty: Int? = null @Json(name = "totalWithWarranty") val totalWithWarranty: Int? = null
) )
// [END_ENTITY: DataClass('GroupStatisticsDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('GroupStatistics')]
/** /**
* [CONTRACT] * @summary Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
* Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
* [COHERENCE_NOTE] Маппер обновлен для использования правильных полей из исправленного DTO.
*/ */
fun GroupStatisticsDto.toDomain(): GroupStatistics { fun GroupStatisticsDto.toDomain(): GroupStatistics {
// [ACTION] Маппим данные из DTO в доменную модель.
return GroupStatistics( return GroupStatistics(
items = this.totalItems, items = this.totalItems,
labels = this.totalLabels, labels = this.totalLabels,
@@ -43,4 +38,5 @@ fun GroupStatisticsDto.toDomain(): GroupStatistics {
totalValue = this.totalItemPrice totalValue = this.totalItemPrice
) )
} }
// [END_ENTITY: Function('toDomain')]
// [END_FILE_GroupStatisticsDto.kt] // [END_FILE_GroupStatisticsDto.kt]

View File

@@ -8,14 +8,14 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.Image import com.homebox.lens.domain.model.Image
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('ImageDto')]
/** /**
* [CONTRACT] * @summary DTO для изображения.
* DTO для изображения. * @param id Уникальный идентификатор.
* @property id Уникальный идентификатор. * @param path Путь к файлу.
* @property path Путь к файлу. * @param isPrimary Является ли основным.
* @property isPrimary Является ли основным.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ImageDto( data class ImageDto(
@@ -23,10 +23,12 @@ data class ImageDto(
@Json(name = "path") val path: String, @Json(name = "path") val path: String,
@Json(name = "isPrimary") val isPrimary: Boolean @Json(name = "isPrimary") val isPrimary: Boolean
) )
// [END_ENTITY: DataClass('ImageDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('Image')]
/** /**
* [CONTRACT] * @summary Маппер из ImageDto в доменную модель Image.
* Маппер из ImageDto в доменную модель Image.
*/ */
fun ImageDto.toDomain(): Image { fun ImageDto.toDomain(): Image {
return Image( return Image(
@@ -35,3 +37,4 @@ fun ImageDto.toDomain(): Image {
isPrimary = this.isPrimary isPrimary = this.isPrimary
) )
} }
// [END_ENTITY: Function('toDomain')]

View File

@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.ItemAttachment import com.homebox.lens.domain.model.ItemAttachment
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('ItemAttachmentDto')]
/** /**
* [CONTRACT] * @summary DTO для вложения.
* DTO для вложения.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemAttachmentDto( data class ItemAttachmentDto(
@@ -23,10 +23,12 @@ data class ItemAttachmentDto(
@Json(name = "createdAt") val createdAt: String, @Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String @Json(name = "updatedAt") val updatedAt: String
) )
// [END_ENTITY: DataClass('ItemAttachmentDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemAttachment')]
/** /**
* [CONTRACT] * @summary Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
* Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
*/ */
fun ItemAttachmentDto.toDomain(): ItemAttachment { fun ItemAttachmentDto.toDomain(): ItemAttachment {
return ItemAttachment( return ItemAttachment(
@@ -38,3 +40,4 @@ fun ItemAttachmentDto.toDomain(): ItemAttachment {
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )
} }
// [END_ENTITY: Function('toDomain')]

View File

@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.ItemCreate import com.homebox.lens.domain.model.ItemCreate
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('ItemCreateDto')]
/** /**
* [CONTRACT] * @summary DTO для создания вещи.
* DTO для создания вещи.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemCreateDto( data class ItemCreateDto(
@@ -30,10 +30,12 @@ data class ItemCreateDto(
@Json(name = "parentId") val parentId: String?, @Json(name = "parentId") val parentId: String?,
@Json(name = "labelIds") val labelIds: List<String>? @Json(name = "labelIds") val labelIds: List<String>?
) )
// [END_ENTITY: DataClass('ItemCreateDto')]
// [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')]
/** /**
* [CONTRACT] * @summary Маппер из доменной модели ItemCreate в ItemCreateDto.
* Маппер из доменной модели ItemCreate в ItemCreateDto.
*/ */
fun ItemCreate.toDto(): ItemCreateDto { fun ItemCreate.toDto(): ItemCreateDto {
return ItemCreateDto( return ItemCreateDto(
@@ -52,3 +54,4 @@ fun ItemCreate.toDto(): ItemCreateDto {
labelIds = this.labelIds labelIds = this.labelIds
) )
} }
// [END_ENTITY: Function('toDto')]

View File

@@ -1,16 +1,19 @@
// [PACKAGE] com.homebox.lens.data.api.dto // [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemDto.kt // [FILE] ItemDto.kt
// [SEMANTICS] data, dto, api
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import java.math.BigDecimal import java.math.BigDecimal
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DataClass('ItemOut')]
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LabelOutDto')]
/** /**
* [ENTITY: DataClass('ItemOut')] * @summary DTO для полной информации о вещи (GET /v1/items/{id}).
* [PURPOSE] DTO для полной информации о вещи (GET /v1/items/{id}).
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemOut( data class ItemOut(
@@ -19,14 +22,16 @@ data class ItemOut(
@Json(name = "description") val description: String?, @Json(name = "description") val description: String?,
@Json(name = "image") val image: String?, @Json(name = "image") val image: String?,
@Json(name = "location") val location: LocationOut?, @Json(name = "location") val location: LocationOut?,
@Json(name = "labels") val labels: List<LabelOut>, @Json(name = "labels") val labels: List<LabelOutDto>,
@Json(name = "value") val value: BigDecimal?, @Json(name = "value") val value: BigDecimal?,
@Json(name = "createdAt") val createdAt: String? @Json(name = "createdAt") val createdAt: String?
) )
// [END_ENTITY: DataClass('ItemOut')]
// [ENTITY: DataClass('ItemSummary')]
// [RELATION: DataClass('ItemSummary')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
/** /**
* [ENTITY: DataClass('ItemSummary')] * @summary DTO для краткой информации о вещи в списках (GET /v1/items).
* [PURPOSE] DTO для краткой информации о вещи в списках (GET /v1/items).
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemSummary( data class ItemSummary(
@@ -36,10 +41,11 @@ data class ItemSummary(
@Json(name = "location") val location: LocationOut?, @Json(name = "location") val location: LocationOut?,
@Json(name = "createdAt") val createdAt: String? @Json(name = "createdAt") val createdAt: String?
) )
// [END_ENTITY: DataClass('ItemSummary')]
// [ENTITY: DataClass('ItemCreate')]
/** /**
* [ENTITY: DataClass('ItemCreate')] * @summary DTO для создания новой вещи (POST /v1/items).
* [PURPOSE] DTO для создания новой вещи (POST /v1/items).
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemCreate( data class ItemCreate(
@@ -49,10 +55,11 @@ data class ItemCreate(
@Json(name = "labelIds") val labelIds: List<String>?, @Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal? @Json(name = "value") val value: BigDecimal?
) )
// [END_ENTITY: DataClass('ItemCreate')]
// [ENTITY: DataClass('ItemUpdate')]
/** /**
* [ENTITY: DataClass('ItemUpdate')] * @summary DTO для обновления вещи (PUT /v1/items/{id}).
* [PURPOSE] DTO для обновления вещи (PUT /v1/items/{id}).
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemUpdate( data class ItemUpdate(
@@ -62,5 +69,6 @@ data class ItemUpdate(
@Json(name = "labelIds") val labelIds: List<String>?, @Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal? @Json(name = "value") val value: BigDecimal?
) )
// [END_ENTITY: DataClass('ItemUpdate')]
// [END_FILE_ItemDto.kt] // [END_FILE_ItemDto.kt]

View File

@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.ItemOut import com.homebox.lens.domain.model.ItemOut
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('ItemOutDto')]
/** /**
* [CONTRACT] * @summary DTO для полной модели вещи.
* DTO для полной модели вещи.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemOutDto( data class ItemOutDto(
@@ -37,12 +37,25 @@ data class ItemOutDto(
@Json(name = "fields") val fields: List<CustomFieldDto>, @Json(name = "fields") val fields: List<CustomFieldDto>,
@Json(name = "maintenance") val maintenance: List<MaintenanceEntryDto>, @Json(name = "maintenance") val maintenance: List<MaintenanceEntryDto>,
@Json(name = "createdAt") val createdAt: String, @Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String @Json(name = "updatedAt") val updatedAt: String,
@Json(name = "insured") val insured: Boolean?,
@Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
@Json(name = "manufacturer") val manufacturer: String?,
@Json(name = "modelNumber") val modelNumber: String?,
@Json(name = "purchaseFrom") val purchaseFrom: String?,
@Json(name = "soldNotes") val soldNotes: String?,
@Json(name = "soldPrice") val soldPrice: Double?,
@Json(name = "soldTime") val soldTime: String?,
@Json(name = "soldTo") val soldTo: String?,
@Json(name = "syncChildItemsLocations") val syncChildItemsLocations: Boolean?,
@Json(name = "warrantyDetails") val warrantyDetails: String?
) )
// [END_ENTITY: DataClass('ItemOutDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemOut')]
/** /**
* [CONTRACT] * @summary Маппер из ItemOutDto в доменную модель ItemOut.
* Маппер из ItemOutDto в доменную модель ItemOut.
*/ */
fun ItemOutDto.toDomain(): ItemOut { fun ItemOutDto.toDomain(): ItemOut {
return ItemOut( return ItemOut(
@@ -67,6 +80,18 @@ fun ItemOutDto.toDomain(): ItemOut {
fields = this.fields.map { it.toDomain() }, fields = this.fields.map { it.toDomain() },
maintenance = this.maintenance.map { it.toDomain() }, maintenance = this.maintenance.map { it.toDomain() },
createdAt = this.createdAt, createdAt = this.createdAt,
updatedAt = this.updatedAt updatedAt = this.updatedAt,
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
purchaseFrom = this.purchaseFrom,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails
) )
} }
// [END_ENTITY: Function('toDomain')]

View File

@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.ItemSummary import com.homebox.lens.domain.model.ItemSummary
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('ItemSummaryDto')]
/** /**
* [CONTRACT] * @summary DTO для сокращенной модели вещи.
* DTO для сокращенной модели вещи.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemSummaryDto( data class ItemSummaryDto(
@@ -27,10 +27,12 @@ data class ItemSummaryDto(
@Json(name = "createdAt") val createdAt: String, @Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String @Json(name = "updatedAt") val updatedAt: String
) )
// [END_ENTITY: DataClass('ItemSummaryDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
/** /**
* [CONTRACT] * @summary Маппер из ItemSummaryDto в доменную модель ItemSummary.
* Маппер из ItemSummaryDto в доменную модель ItemSummary.
*/ */
fun ItemSummaryDto.toDomain(): ItemSummary { fun ItemSummaryDto.toDomain(): ItemSummary {
return ItemSummary( return ItemSummary(
@@ -46,3 +48,4 @@ fun ItemSummaryDto.toDomain(): ItemSummary {
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )
} }
// [END_ENTITY: Function('toDomain')]

View File

@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.ItemUpdate import com.homebox.lens.domain.model.ItemUpdate
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('ItemUpdateDto')]
/** /**
* [CONTRACT] * @summary DTO для обновления вещи.
* DTO для обновления вещи.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemUpdateDto( data class ItemUpdateDto(
@@ -31,10 +31,12 @@ data class ItemUpdateDto(
@Json(name = "parentId") val parentId: String?, @Json(name = "parentId") val parentId: String?,
@Json(name = "labelIds") val labelIds: List<String>? @Json(name = "labelIds") val labelIds: List<String>?
) )
// [END_ENTITY: DataClass('ItemUpdateDto')]
// [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemUpdateDto')]
/** /**
* [CONTRACT] * @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto.
* Маппер из доменной модели ItemUpdate в ItemUpdateDto.
*/ */
fun ItemUpdate.toDto(): ItemUpdateDto { fun ItemUpdate.toDto(): ItemUpdateDto {
return ItemUpdateDto( return ItemUpdateDto(
@@ -54,3 +56,4 @@ fun ItemUpdate.toDto(): ItemUpdateDto {
labelIds = this.labelIds labelIds = this.labelIds
) )
} }
// [END_ENTITY: Function('toDto')]

View File

@@ -0,0 +1,25 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelCreateDto.kt
// [SEMANTICS] data_transfer_object, label, create, api
package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [ENTITY: DataClass('LabelCreateDto')]
/**
* @summary DTO для тела запроса на создание метки (POST /v1/labels).
* @param name Название метки.
* @param color Цвет метки в формате HEX (например, "#FF0000").
* @param description Описание метки.
*/
@JsonClass(generateAdapter = true)
data class LabelCreateDto(
@Json(name = "name") val name: String,
@Json(name = "color") val color: String?,
@Json(name = "description") val description: String? = null // [AI_NOTE]: Описание не используется в приложении, но может быть в API
)
// [END_ENTITY: DataClass('LabelCreateDto')]
// [END_FILE_LabelCreateDto.kt]

View File

@@ -1,20 +0,0 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [CONTRACT]
/**
* [ENTITY: DataClass('LabelOut')]
* [PURPOSE] DTO для информации о метке.
*/
@JsonClass(generateAdapter = true)
data class LabelOut(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String
)
// [END_FILE_LabelDto.kt]

View File

@@ -8,44 +8,38 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.LabelOut import com.homebox.lens.domain.model.LabelOut
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('LabelOutDto')]
/** /**
* [CONTRACT] * @summary DTO для метки.
* DTO для метки.
* [COHERENCE_NOTE] Поле `isArchived` сделано nullable (`Boolean?`),
* так как оно отсутствует в JSON-ответе от сервера. Это исправляет ошибку парсинга
* `JsonDataException: Required value 'isArchived' missing`.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LabelOutDto( data class LabelOutDto(
@Json(name = "id") val id: String, @Json(name = "id") val id: String,
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
// [COHERENCE_NOTE] Поле `color` может быть null или отсутствовать, делаем его nullable для безопасности.
@Json(name = "color") val color: String?, @Json(name = "color") val color: String?,
// [FIX] Поле отсутствует в JSON, делаем nullable.
@Json(name = "isArchived") val isArchived: Boolean?, @Json(name = "isArchived") val isArchived: Boolean?,
@Json(name = "createdAt") val createdAt: String, @Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String, @Json(name = "updatedAt") val updatedAt: String,
// [COHERENCE_NOTE] Поле `description` также может отсутствовать, добавляем его как nullable.
@Json(name = "description") val description: String? @Json(name = "description") val description: String?
) )
// [END_ENTITY: DataClass('LabelOutDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
/** /**
* [CONTRACT] * @summary Маппер из LabelOutDto в доменную модель LabelOut.
* Маппер из LabelOutDto в доменную модель LabelOut.
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
* и предоставления non-nullable значений по умолчанию для доменной модели.
*/ */
fun LabelOutDto.toDomain(): LabelOut { fun LabelOutDto.toDomain(): LabelOut {
return LabelOut( return LabelOut(
id = this.id, id = this.id,
name = this.name, name = this.name,
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию. color = this.color ?: "",
color = this.color ?: "", // Пустая строка как дефолтный цвет isArchived = this.isArchived ?: false,
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
createdAt = this.createdAt, createdAt = this.createdAt,
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )
} }
// [END_ENTITY: Function('toDomain')]
// [END_FILE_LabelOutDto.kt] // [END_FILE_LabelOutDto.kt]

View File

@@ -0,0 +1,42 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelSummaryDto.kt
// [SEMANTICS] data_transfer_object, label, summary, api, mapper
package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.homebox.lens.domain.model.LabelSummary
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [ENTITY: DataClass('LabelSummaryDto')]
/**
* @summary DTO для ответа от API при создании метки.
*/
@JsonClass(generateAdapter = true)
data class LabelSummaryDto(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "color") val color: String?,
@Json(name = "description") val description: String?,
@Json(name = "createdAt") val createdAt: String?,
@Json(name = "updatedAt") val updatedAt: String?
)
// [END_ENTITY: DataClass('LabelSummaryDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelSummary')]
/**
* @summary Маппер из DTO в доменную модель.
* @return Объект доменной модели [LabelSummary].
* @sideeffect Отбрасывает поля, ненужные доменному слою (`color`, `description`, etc.),
* оставляя только `id` и `name`.
*/
fun LabelSummaryDto.toDomain(): LabelSummary {
return LabelSummary(
id = this.id,
name = this.name
)
}
// [END_ENTITY: Function('toDomain')]
// [END_FILE_LabelSummaryDto.kt]

View File

@@ -0,0 +1,31 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelUpdateDto.kt
// [SEMANTICS] data_transfer_object, label, update
package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.LabelUpdate
// [END_IMPORTS]
// [ENTITY: DataClass('LabelUpdateDto')]
@JsonClass(generateAdapter = true)
data class LabelUpdateDto(
@Json(name = "name")
val name: String?,
@Json(name = "color")
val color: String?
)
// [END_ENTITY: DataClass('LabelUpdateDto')]
// [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
fun LabelUpdate.toDto(): LabelUpdateDto {
return LabelUpdateDto(
name = this.name,
color = this.color
)
}
// [END_ENTITY: Function('toDto')]
// [END_FILE_LabelUpdateDto.kt]

View File

@@ -0,0 +1,22 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationCreateDto.kt
// [SEMANTICS] data_transfer_object, location, create
package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [ENTITY: DataClass('LocationCreateDto')]
@JsonClass(generateAdapter = true)
data class LocationCreateDto(
@Json(name = "name")
val name: String,
@Json(name = "color")
val color: String?,
@Json(name = "description")
val description: String? // Assuming description can be null for creation
)
// [END_ENTITY: DataClass('LocationCreateDto')]
// [END_FILE_LocationCreateDto.kt]

View File

@@ -1,25 +1,27 @@
// [PACKAGE] com.homebox.lens.data.api.dto // [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationDto.kt // [FILE] LocationDto.kt
// [SEMANTICS] data, dto, api, location
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DataClass('LocationOut')]
/** /**
* [ENTITY: DataClass('LocationOut')] * @summary DTO для информации о местоположении.
* [PURPOSE] DTO для информации о местоположении.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationOut( data class LocationOut(
@Json(name = "id") val id: String, @Json(name = "id") val id: String,
@Json(name = "name") val name: String @Json(name = "name") val name: String
) )
// [END_ENTITY: DataClass('LocationOut')]
// [ENTITY: DataClass('LocationOutCount')]
/** /**
* [ENTITY: DataClass('LocationOutCount')] * @summary DTO для информации о местоположении со счетчиком вещей.
* [PURPOSE] DTO для информации о местоположении со счетчиком вещей.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationOutCount( data class LocationOutCount(
@@ -27,5 +29,6 @@ data class LocationOutCount(
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "itemCount") val itemCount: Int @Json(name = "itemCount") val itemCount: Int
) )
// [END_ENTITY: DataClass('LocationOutCount')]
// [END_FILE_LocationDto.kt] // [END_FILE_LocationDto.kt]

View File

@@ -8,47 +8,40 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.LocationOutCount import com.homebox.lens.domain.model.LocationOutCount
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('LocationOutCountDto')]
/** /**
* [CONTRACT] * @summary DTO для местоположения со счетчиком.
* DTO для местоположения со счетчиком.
* [COHERENCE_NOTE] Поля `color` и `isArchived` сделаны nullable (`String?`, `Boolean?`),
* так как они отсутствуют в JSON-ответе от сервера. Это исправляет ошибку парсинга
* `JsonDataException: Required value '...' missing`.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationOutCountDto( data class LocationOutCountDto(
@Json(name = "id") val id: String, @Json(name = "id") val id: String,
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
// [FIX] Поле отсутствует в JSON, делаем nullable.
@Json(name = "color") val color: String?, @Json(name = "color") val color: String?,
// [FIX] Поле отсутствует в JSON, делаем nullable.
@Json(name = "isArchived") val isArchived: Boolean?, @Json(name = "isArchived") val isArchived: Boolean?,
@Json(name = "itemCount") val itemCount: Int, @Json(name = "itemCount") val itemCount: Int,
@Json(name = "createdAt") val createdAt: String, @Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String, @Json(name = "updatedAt") val updatedAt: String,
// [COHERENCE_NOTE] Поле `description` также может отсутствовать или быть null,
// поэтому его тоже безопасно сделать nullable.
@Json(name = "description") val description: String? @Json(name = "description") val description: String?
) )
// [END_ENTITY: DataClass('LocationOutCountDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOutCount')]
/** /**
* [CONTRACT] * @summary Маппер из LocationOutCountDto в доменную модель LocationOutCount.
* Маппер из LocationOutCountDto в доменную модель LocationOutCount.
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
* и предоставления non-nullable значений по умолчанию для доменной модели.
*/ */
fun LocationOutCountDto.toDomain(): LocationOutCount { fun LocationOutCountDto.toDomain(): LocationOutCount {
return LocationOutCount( return LocationOutCount(
id = this.id, id = this.id,
name = this.name, name = this.name,
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию, если поле null. color = this.color ?: "",
color = this.color ?: "", // Пустая строка как дефолтный цвет isArchived = this.isArchived ?: false,
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
itemCount = this.itemCount, itemCount = this.itemCount,
createdAt = this.createdAt, createdAt = this.createdAt,
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )
} }
// [END_ENTITY: Function('toDomain')]
// [END_FILE_LocationOutCountDto.kt] // [END_FILE_LocationOutCountDto.kt]

View File

@@ -1,33 +1,34 @@
// [PACKAGE] com.homebox.lens.data.api.dto // [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationOutDto.kt // [FILE] LocationOutDto.kt
// [SEMANTICS] data_transfer_object, location // [SEMANTICS] data_transfer_object, location, output
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS] // [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.LocationOut import com.homebox.lens.domain.model.LocationOut
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('LocationOutDto')]
/**
* [CONTRACT]
* DTO для местоположения.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationOutDto( data class LocationOutDto(
@Json(name = "id") val id: String, @Json(name = "id")
@Json(name = "name") val name: String, val id: String,
@Json(name = "color") val color: String, @Json(name = "name")
@Json(name = "isArchived") val isArchived: Boolean, val name: String,
@Json(name = "createdAt") val createdAt: String, @Json(name = "color")
@Json(name = "updatedAt") val updatedAt: String val color: String,
@Json(name = "isArchived")
val isArchived: Boolean,
@Json(name = "createdAt")
val createdAt: String,
@Json(name = "updatedAt")
val updatedAt: String
) )
// [END_ENTITY: DataClass('LocationOutDto')]
/** // [ENTITY: Function('toDomain')]
* [CONTRACT] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOut')]
* Маппер из LocationOutDto в доменную модель LocationOut.
*/
fun LocationOutDto.toDomain(): LocationOut { fun LocationOutDto.toDomain(): LocationOut {
return LocationOut( return LocationOut(
id = this.id, id = this.id,
@@ -38,3 +39,5 @@ fun LocationOutDto.toDomain(): LocationOut {
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )
} }
// [END_ENTITY: Function('toDomain')]
// [END_FILE_LocationOutDto.kt]

View File

@@ -0,0 +1,31 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationUpdateDto.kt
// [SEMANTICS] data_transfer_object, location, update
package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.LocationUpdate
// [END_IMPORTS]
// [ENTITY: DataClass('LocationUpdateDto')]
@JsonClass(generateAdapter = true)
data class LocationUpdateDto(
@Json(name = "name")
val name: String?,
@Json(name = "color")
val color: String?
)
// [END_ENTITY: DataClass('LocationUpdateDto')]
// [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LocationUpdateDto')]
fun LocationUpdate.toDto(): LocationUpdateDto {
return LocationUpdateDto(
name = this.name,
color = this.color
)
}
// [END_ENTITY: Function('toDto')]
// [END_FILE_LocationUpdateDto.kt]

View File

@@ -1,15 +1,19 @@
// [PACKAGE] com.homebox.lens.data.api.dto // [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LoginFormDto.kt // [FILE] LoginFormDto.kt
// [SEMANTICS] data, dto, api, login
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [ENTITY: DataClass('LoginFormDto')]
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LoginFormDto( data class LoginFormDto(
@Json(name = "username") val username: String, @Json(name = "username") val username: String,
@Json(name = "password") val password: String, @Json(name = "password") val password: String,
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true @Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
) )
// [END_ENTITY: DataClass('LoginFormDto')]
// [END_FILE_LoginFormDto.kt] // [END_FILE_LoginFormDto.kt]

View File

@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.MaintenanceEntry import com.homebox.lens.domain.model.MaintenanceEntry
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('MaintenanceEntryDto')]
/** /**
* [CONTRACT] * @summary DTO для записи об обслуживании.
* DTO для записи об обслуживании.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MaintenanceEntryDto( data class MaintenanceEntryDto(
@@ -25,10 +25,12 @@ data class MaintenanceEntryDto(
@Json(name = "createdAt") val createdAt: String, @Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String @Json(name = "updatedAt") val updatedAt: String
) )
// [END_ENTITY: DataClass('MaintenanceEntryDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('MaintenanceEntry')]
/** /**
* [CONTRACT] * @summary Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
* Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
*/ */
fun MaintenanceEntryDto.toDomain(): MaintenanceEntry { fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
return MaintenanceEntry( return MaintenanceEntry(
@@ -42,3 +44,4 @@ fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )
} }
// [END_ENTITY: Function('toDomain')]

View File

@@ -1,15 +1,16 @@
// [PACKAGE] com.homebox.lens.data.api.dto // [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] PaginationDto.kt // [FILE] PaginationDto.kt
// [SEMANTICS] data, dto, api, pagination
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DataClass('PaginationResult')]
/** /**
* [ENTITY: DataClass('PaginationResult')] * @summary DTO для пагинированных результатов от API.
* [PURPOSE] DTO для пагинированных результатов от API.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PaginationResult<T>( data class PaginationResult<T>(
@@ -19,5 +20,6 @@ data class PaginationResult<T>(
@Json(name = "total") val total: Int, @Json(name = "total") val total: Int,
@Json(name = "pageSize") val pageSize: Int @Json(name = "pageSize") val pageSize: Int
) )
// [END_ENTITY: DataClass('PaginationResult')]
// [END_FILE_PaginationDto.kt] // [END_FILE_PaginationDto.kt]

View File

@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.PaginationResult import com.homebox.lens.domain.model.PaginationResult
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: DataClass('PaginationResultDto')]
/** /**
* [CONTRACT] * @summary DTO для постраничных результатов.
* DTO для постраничных результатов.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PaginationResultDto<T>( data class PaginationResultDto<T>(
@@ -21,10 +21,12 @@ data class PaginationResultDto<T>(
@Json(name = "pageSize") val pageSize: Int, @Json(name = "pageSize") val pageSize: Int,
@Json(name = "total") val total: Int @Json(name = "total") val total: Int
) )
// [END_ENTITY: DataClass('PaginationResultDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('PaginationResult')]
/** /**
* [CONTRACT] * @summary Маппер из PaginationResultDto в доменную модель PaginationResult.
* Маппер из PaginationResultDto в доменную модель PaginationResult.
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель. * @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
*/ */
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> { fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> {
@@ -35,3 +37,4 @@ fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResul
total = this.total total = this.total
) )
} }
// [END_ENTITY: Function('toDomain')]

View File

@@ -1,16 +1,17 @@
// [PACKAGE] com.homebox.lens.data.api.dto // [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] StatisticsDto.kt // [FILE] StatisticsDto.kt
// [SEMANTICS] data, dto, api, statistics
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import java.math.BigDecimal import java.math.BigDecimal
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DataClass('GroupStatistics')]
/** /**
* [ENTITY: DataClass('GroupStatistics')] * @summary DTO для статистической информации.
* [PURPOSE] DTO для статистической информации.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GroupStatistics( data class GroupStatistics(
@@ -19,5 +20,6 @@ data class GroupStatistics(
@Json(name = "locations") val locations: Int, @Json(name = "locations") val locations: Int,
@Json(name = "labels") val labels: Int @Json(name = "labels") val labels: Int
) )
// [END_ENTITY: DataClass('GroupStatistics')]
// [END_FILE_StatisticsDto.kt] // [END_FILE_StatisticsDto.kt]

View File

@@ -1,15 +1,19 @@
// [PACKAGE] com.homebox.lens.data.api.dto // [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] TokenResponseDto.kt // [FILE] TokenResponseDto.kt
// [SEMANTICS] data, dto, api, token
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [ENTITY: DataClass('TokenResponseDto')]
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class TokenResponseDto( data class TokenResponseDto(
@Json(name = "token") val token: String, @Json(name = "token") val token: String,
@Json(name = "attachmentToken") val attachmentToken: String, @Json(name = "attachmentToken") val attachmentToken: String,
@Json(name = "expiresAt") val expiresAt: String @Json(name = "expiresAt") val expiresAt: String
) )
// [END_ENTITY: DataClass('TokenResponseDto')]
// [END_FILE_TokenResponseDto.kt] // [END_FILE_TokenResponseDto.kt]

View File

@@ -4,26 +4,27 @@
package com.homebox.lens.data.api.mapper package com.homebox.lens.data.api.mapper
// [IMPORTS]
import com.homebox.lens.data.api.dto.TokenResponseDto import com.homebox.lens.data.api.dto.TokenResponseDto
import com.homebox.lens.domain.model.TokenResponse import com.homebox.lens.domain.model.TokenResponse
// [END_IMPORTS]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('TokenResponse')]
/** /**
* [CONTRACT] * @summary Преобразует DTO-объект токена в доменную модель.
* [HELPER] Преобразует DTO-объект токена в доменную модель.
* @receiver [TokenResponseDto] объект из слоя данных. * @receiver [TokenResponseDto] объект из слоя данных.
* @return [TokenResponse] объект для доменного слоя. * @return [TokenResponse] объект для доменного слоя.
* @throws IllegalArgumentException если токен в DTO пустой. * @throws IllegalArgumentException если токен в DTO пустой.
*/ */
fun TokenResponseDto.toDomain(): TokenResponse { fun TokenResponseDto.toDomain(): TokenResponse {
// [PRECONDITION] DTO должен содержать валидные данные для маппинга. require(this.token.isNotBlank()) { "DTO token is blank, cannot map to domain model." }
require(this.token.isNotBlank()) { "[PRECONDITION_FAILED] DTO token is blank, cannot map to domain model." }
// [ACTION]
val domainModel = TokenResponse(token = this.token) val domainModel = TokenResponse(token = this.token)
// [POSTCONDITION] Проверяем, что инвариант доменной модели соблюден. check(domainModel.token.isNotBlank()) { "Domain model token is blank after mapping." }
check(domainModel.token.isNotBlank()) { "[POSTCONDITION_FAILED] Domain model token is blank after mapping." }
return domainModel return domainModel
} }
// [END_ENTITY: Function('toDomain')]
// [END_FILE_TokenMapper.kt] // [END_FILE_TokenMapper.kt]

View File

@@ -1,26 +1,32 @@
// [PACKAGE] com.homebox.lens.data.db // [PACKAGE] com.homebox.lens.data.db
// [FILE] Converters.kt // [FILE] Converters.kt
// [SEMANTICS] data, database, room, converter
package com.homebox.lens.data.db package com.homebox.lens.data.db
// [IMPORTS]
import androidx.room.TypeConverter import androidx.room.TypeConverter
import java.math.BigDecimal import java.math.BigDecimal
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: Class('Converters')]
/** /**
* [ENTITY: Class('Converters')] * @summary Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
* [PURPOSE] Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
*/ */
class Converters { class Converters {
// [ENTITY: Function('fromString')]
@TypeConverter @TypeConverter
fun fromString(value: String?): BigDecimal? { fun fromString(value: String?): BigDecimal? {
return value?.let { BigDecimal(it) } return value?.let { BigDecimal(it) }
} }
// [END_ENTITY: Function('fromString')]
// [ENTITY: Function('bigDecimalToString')]
@TypeConverter @TypeConverter
fun bigDecimalToString(bigDecimal: BigDecimal?): String? { fun bigDecimalToString(bigDecimal: BigDecimal?): String? {
return bigDecimal?.toPlainString() return bigDecimal?.toPlainString()
} }
// [END_ENTITY: Function('bigDecimalToString')]
} }
// [END_ENTITY: Class('Converters')]
// [END_FILE_Converters.kt] // [END_FILE_Converters.kt]

View File

@@ -1,8 +1,9 @@
// [PACKAGE] com.homebox.lens.data.db // [PACKAGE] com.homebox.lens.data.db
// [FILE] HomeboxDatabase.kt // [FILE] HomeboxDatabase.kt
// [SEMANTICS] data, database, room
package com.homebox.lens.data.db package com.homebox.lens.data.db
// [IMPORTS]
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
@@ -10,11 +11,11 @@ import com.homebox.lens.data.db.dao.ItemDao
import com.homebox.lens.data.db.dao.LabelDao import com.homebox.lens.data.db.dao.LabelDao
import com.homebox.lens.data.db.dao.LocationDao import com.homebox.lens.data.db.dao.LocationDao
import com.homebox.lens.data.db.entity.* import com.homebox.lens.data.db.entity.*
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: Database('HomeboxDatabase')]
/** /**
* [ENTITY: RoomDatabase('HomeboxDatabase')] * @summary Основной класс для работы с локальной базой данных Room.
* [PURPOSE] Основной класс для работы с локальной базой данных Room.
*/ */
@Database( @Database(
entities = [ entities = [
@@ -37,5 +38,6 @@ abstract class HomeboxDatabase : RoomDatabase() {
const val DATABASE_NAME = "homebox_lens_db" const val DATABASE_NAME = "homebox_lens_db"
} }
} }
// [END_ENTITY: Database('HomeboxDatabase')]
// [END_FILE_HomeboxDatabase.kt] // [END_FILE_HomeboxDatabase.kt]

View File

@@ -1,40 +1,61 @@
// [PACKAGE] com.homebox.lens.data.db.dao // [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] ItemDao.kt // [FILE] ItemDao.kt
// [SEMANTICS] data, database, dao, item
package com.homebox.lens.data.db.dao package com.homebox.lens.data.db.dao
// [IMPORTS]
import androidx.room.* import androidx.room.*
import com.homebox.lens.data.db.entity.ItemEntity import com.homebox.lens.data.db.entity.ItemEntity
import com.homebox.lens.data.db.entity.ItemLabelCrossRef import com.homebox.lens.data.db.entity.ItemLabelCrossRef
import com.homebox.lens.data.db.entity.ItemWithLabels import com.homebox.lens.data.db.entity.ItemWithLabels
import kotlinx.coroutines.flow.Flow
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: Interface('ItemDao')]
/** /**
* [ENTITY: RoomDao('ItemDao')] * @summary Предоставляет методы для работы с 'items' в локальной БД.
* [PURPOSE] Предоставляет методы для работы с 'items' в локальной БД.
*/ */
@Dao @Dao
interface ItemDao { interface ItemDao {
// [ENTITY: Function('getRecentlyAddedItems')]
@Transaction
@Query("SELECT * FROM items ORDER BY createdAt DESC LIMIT :limit")
fun getRecentlyAddedItems(limit: Int): Flow<List<ItemWithLabels>>
// [END_ENTITY: Function('getRecentlyAddedItems')]
// [ENTITY: Function('getItems')]
@Transaction @Transaction
@Query("SELECT * FROM items") @Query("SELECT * FROM items")
suspend fun getItems(): List<ItemWithLabels> suspend fun getItems(): List<ItemWithLabels>
// [END_ENTITY: Function('getItems')]
// [ENTITY: Function('getItem')]
@Transaction @Transaction
@Query("SELECT * FROM items WHERE id = :itemId") @Query("SELECT * FROM items WHERE id = :itemId")
suspend fun getItem(itemId: String): ItemWithLabels? suspend fun getItem(itemId: String): ItemWithLabels?
// [END_ENTITY: Function('getItem')]
// [ENTITY: Function('insertItems')]
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItems(items: List<ItemEntity>) suspend fun insertItems(items: List<ItemEntity>)
// [END_ENTITY: Function('insertItems')]
// [ENTITY: Function('insertItem')]
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItem(item: ItemEntity) suspend fun insertItem(item: ItemEntity)
// [END_ENTITY: Function('insertItem')]
// [ENTITY: Function('deleteItem')]
@Query("DELETE FROM items WHERE id = :itemId") @Query("DELETE FROM items WHERE id = :itemId")
suspend fun deleteItem(itemId: String) suspend fun deleteItem(itemId: String)
// [END_ENTITY: Function('deleteItem')]
// [ENTITY: Function('insertItemLabelCrossRefs')]
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItemLabelCrossRefs(crossRefs: List<ItemLabelCrossRef>) suspend fun insertItemLabelCrossRefs(crossRefs: List<ItemLabelCrossRef>)
// [END_ENTITY: Function('insertItemLabelCrossRefs')]
} }
// [END_ENTITY: Interface('ItemDao')]
// [END_FILE_ItemDao.kt] // [END_FILE_ItemDao.kt]

View File

@@ -1,27 +1,42 @@
// [PACKAGE] com.homebox.lens.data.db.dao // [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] LabelDao.kt // [FILE] LabelDao.kt
// [SEMANTICS] data, database, dao, label
package com.homebox.lens.data.db.dao package com.homebox.lens.data.db.dao
// [IMPORTS]
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.homebox.lens.data.db.entity.LabelEntity import com.homebox.lens.data.db.entity.LabelEntity
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: Interface('LabelDao')]
/** /**
* [ENTITY: RoomDao('LabelDao')] * @summary Предоставляет методы для работы с 'labels' в локальной БД.
* [PURPOSE] Предоставляет методы для работы с 'labels' в локальной БД.
*/ */
@Dao @Dao
interface LabelDao { interface LabelDao {
// [ENTITY: Function('getLabels')]
@Query("SELECT * FROM labels") @Query("SELECT * FROM labels")
suspend fun getLabels(): List<LabelEntity> suspend fun getLabels(): List<LabelEntity>
// [END_ENTITY: Function('getLabels')]
// [ENTITY: Function('insertLabels')]
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLabels(labels: List<LabelEntity>) suspend fun insertLabels(labels: List<LabelEntity>)
// [END_ENTITY: Function('insertLabels')]
// [ENTITY: Function('deleteLabelById')]
/**
* @summary Удаляет метку по её ID из локальной БД.
* @param labelId ID метки для удаления.
*/
@Query("DELETE FROM labels WHERE id = :labelId")
suspend fun deleteLabelById(labelId: String)
// [END_ENTITY: Function('deleteLabelById')]
} }
// [END_ENTITY: Interface('LabelDao')]
// [END_FILE_LabelDao.kt] // [END_FILE_LabelDao.kt]

View File

@@ -1,27 +1,33 @@
// [PACKAGE] com.homebox.lens.data.db.dao // [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] LocationDao.kt // [FILE] LocationDao.kt
// [SEMANTICS] data, database, dao, location
package com.homebox.lens.data.db.dao package com.homebox.lens.data.db.dao
// [IMPORTS]
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.homebox.lens.data.db.entity.LocationEntity import com.homebox.lens.data.db.entity.LocationEntity
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: Interface('LocationDao')]
/** /**
* [ENTITY: RoomDao('LocationDao')] * @summary Предоставляет методы для работы с 'locations' в локальной БД.
* [PURPOSE] Предоставляет методы для работы с 'locations' в локальной БД.
*/ */
@Dao @Dao
interface LocationDao { interface LocationDao {
// [ENTITY: Function('getLocations')]
@Query("SELECT * FROM locations") @Query("SELECT * FROM locations")
suspend fun getLocations(): List<LocationEntity> suspend fun getLocations(): List<LocationEntity>
// [END_ENTITY: Function('getLocations')]
// [ENTITY: Function('insertLocations')]
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLocations(locations: List<LocationEntity>) suspend fun insertLocations(locations: List<LocationEntity>)
// [END_ENTITY: Function('insertLocations')]
} }
// [END_ENTITY: Interface('LocationDao')]
// [END_FILE_LocationDao.kt] // [END_FILE_LocationDao.kt]

View File

@@ -1,16 +1,17 @@
// [PACKAGE] com.homebox.lens.data.db.entity // [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemEntity.kt // [FILE] ItemEntity.kt
// [SEMANTICS] data, database, entity, item
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.math.BigDecimal import java.math.BigDecimal
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DatabaseTable('ItemEntity')]
/** /**
* [ENTITY: RoomEntity('ItemEntity')] * @summary Представляет собой строку в таблице 'items' в локальной БД.
* [PURPOSE] Представляет собой строку в таблице 'items' в локальной БД.
*/ */
@Entity(tableName = "items") @Entity(tableName = "items")
data class ItemEntity( data class ItemEntity(
@@ -22,5 +23,6 @@ data class ItemEntity(
val value: BigDecimal?, val value: BigDecimal?,
val createdAt: String? val createdAt: String?
) )
// [END_ENTITY: DatabaseTable('ItemEntity')]
// [END_FILE_ItemEntity.kt] // [END_FILE_ItemEntity.kt]

View File

@@ -1,15 +1,16 @@
// [PACKAGE] com.homebox.lens.data.db.entity // [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemLabelCrossRef.kt // [FILE] ItemLabelCrossRef.kt
// [SEMANTICS] data, database, entity, relation
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Index import androidx.room.Index
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DatabaseTable('ItemLabelCrossRef')]
/** /**
* [ENTITY: RoomEntity('ItemLabelCrossRef')] * @summary Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
* [PURPOSE] Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
*/ */
@Entity( @Entity(
primaryKeys = ["itemId", "labelId"], primaryKeys = ["itemId", "labelId"],
@@ -19,5 +20,6 @@ data class ItemLabelCrossRef(
val itemId: String, val itemId: String,
val labelId: String val labelId: String
) )
// [END_ENTITY: DatabaseTable('ItemLabelCrossRef')]
// [END_FILE_ItemLabelCrossRef.kt] // [END_FILE_ItemLabelCrossRef.kt]

View File

@@ -1,16 +1,19 @@
// [PACKAGE] com.homebox.lens.data.db.entity // [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemWithLabels.kt // [FILE] ItemWithLabels.kt
// [SEMANTICS] data, database, entity, relation
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Junction import androidx.room.Junction
import androidx.room.Relation import androidx.room.Relation
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DataClass('ItemWithLabels')]
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('ItemEntity')]
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('LabelEntity')]
/** /**
* [ENTITY: Pojo('ItemWithLabels')] * @summary POJO для получения ItemEntity вместе со связанными LabelEntity.
* [PURPOSE] POJO для получения ItemEntity вместе со связанными LabelEntity.
*/ */
data class ItemWithLabels( data class ItemWithLabels(
@Embedded val item: ItemEntity, @Embedded val item: ItemEntity,
@@ -25,5 +28,6 @@ data class ItemWithLabels(
) )
val labels: List<LabelEntity> val labels: List<LabelEntity>
) )
// [END_ENTITY: DataClass('ItemWithLabels')]
// [END_FILE_ItemWithLabels.kt] // [END_FILE_ItemWithLabels.kt]

View File

@@ -1,20 +1,22 @@
// [PACKAGE] com.homebox.lens.data.db.entity // [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] LabelEntity.kt // [FILE] LabelEntity.kt
// [SEMANTICS] data, database, entity, label
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DatabaseTable('LabelEntity')]
/** /**
* [ENTITY: RoomEntity('LabelEntity')] * @summary Представляет собой строку в таблице 'labels' в локальной БД.
* [PURPOSE] Представляет собой строку в таблице 'labels' в локальной БД.
*/ */
@Entity(tableName = "labels") @Entity(tableName = "labels")
data class LabelEntity( data class LabelEntity(
@PrimaryKey val id: String, @PrimaryKey val id: String,
val name: String val name: String
) )
// [END_ENTITY: DatabaseTable('LabelEntity')]
// [END_FILE_LabelEntity.kt] // [END_FILE_LabelEntity.kt]

View File

@@ -1,20 +1,22 @@
// [PACKAGE] com.homebox.lens.data.db.entity // [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] LocationEntity.kt // [FILE] LocationEntity.kt
// [SEMANTICS] data, database, entity, location
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DatabaseTable('LocationEntity')]
/** /**
* [ENTITY: RoomEntity('LocationEntity')] * @summary Представляет собой строку в таблице 'locations' в локальной БД.
* [PURPOSE] Представляет собой строку в таблице 'locations' в локальной БД.
*/ */
@Entity(tableName = "locations") @Entity(tableName = "locations")
data class LocationEntity( data class LocationEntity(
@PrimaryKey val id: String, @PrimaryKey val id: String,
val name: String val name: String
) )
// [END_ENTITY: DatabaseTable('LocationEntity')]
// [END_FILE_LocationEntity.kt] // [END_FILE_LocationEntity.kt]

View File

@@ -0,0 +1,49 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] Mapper.kt
// [SEMANTICS] data, database, mapper
package com.homebox.lens.data.db.entity
// [IMPORTS]
import com.homebox.lens.domain.model.Image
import com.homebox.lens.domain.model.ItemSummary
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LocationOut
// [END_IMPORTS]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
/**
* @summary Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
*/
fun ItemWithLabels.toDomain(): ItemSummary {
return ItemSummary(
id = this.item.id,
name = this.item.name,
image = this.item.image?.let { Image(id = "", path = it, isPrimary = true) },
location = this.item.locationId?.let { LocationOut(id = it, name = "", color = "", isArchived = false, createdAt = "", updatedAt = "") },
labels = this.labels.map { it.toDomain() },
assetId = null,
isArchived = false,
value = this.item.value?.toDouble() ?: 0.0,
createdAt = this.item.createdAt ?: "",
updatedAt = ""
)
}
// [END_ENTITY: Function('toDomain')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
/**
* @summary Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
*/
fun LabelEntity.toDomain(): LabelOut {
return LabelOut(
id = this.id,
name = this.name,
color = "#CCCCCC",
isArchived = false,
createdAt = "",
updatedAt = ""
)
}
// [END_ENTITY: Function('toDomain')]

View File

@@ -1,7 +1,8 @@
// [PACKAGE] com.homebox.lens.data.di // [PACKAGE] com.homebox.lens.data.di
// [FILE] ApiModule.kt // [FILE] ApiModule.kt
// [PURPOSE] Предоставляет синглтон-зависимости для работы с сетью, включая OkHttpClient, Retrofit и ApiService. // [SEMANTICS] di, networking
package com.homebox.lens.data.di package com.homebox.lens.data.di
// [IMPORTS] // [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.domain.repository.CredentialsRepository import com.homebox.lens.domain.repository.CredentialsRepository
@@ -17,41 +18,34 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import timber.log.Timber
import javax.inject.Provider import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
// [END_IMPORTS]
// [ENTITY: Module('ApiModule')]
/** /**
* [ENTITY: Module('ApiModule')] * @summary Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
* [CONTRACT]
* Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
* необходимых для сетевого взаимодействия. * необходимых для сетевого взаимодействия.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object ApiModule { object ApiModule {
// [HELPER] Временный базовый URL для API. В будущем должен стать динамическим.
private const val BASE_URL = "https://homebox.bebesh.ru/api/" private const val BASE_URL = "https://homebox.bebesh.ru/api/"
/** // [ENTITY: Function('provideOkHttpClient')]
* [PROVIDER] // [RELATION: Function('provideOkHttpClient')] -> [PROVIDES] -> [Framework('OkHttpClient')]
* [CONTRACT]
* Предоставляет сконфигурированный OkHttpClient.
* @param credentialsRepositoryProvider Провайдер репозитория для доступа к токену авторизации.
* Используется Provider<T> для предотвращения циклов зависимостей.
* @return Синглтон-экземпляр OkHttpClient с настроенными перехватчиками.
*/
@Provides @Provides
@Singleton @Singleton
fun provideOkHttpClient( fun provideOkHttpClient(
credentialsRepositoryProvider: Provider<CredentialsRepository> credentialsRepositoryProvider: Provider<CredentialsRepository>
): OkHttpClient { ): OkHttpClient {
// [ACTION] Создаем перехватчик для логирования. Timber.d("[DEBUG][PROVIDER][providing_okhttp_client] Providing OkHttpClient.")
val loggingInterceptor = HttpLoggingInterceptor().apply { val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY level = HttpLoggingInterceptor.Level.BODY
} }
// [ACTION] Создаем перехватчик для добавления заголовка 'Accept'.
val acceptHeaderInterceptor = Interceptor { chain -> val acceptHeaderInterceptor = Interceptor { chain ->
val request = chain.request().newBuilder() val request = chain.request().newBuilder()
.header("Accept", "application/json") .header("Accept", "application/json")
@@ -59,77 +53,71 @@ object ApiModule {
chain.proceed(request) chain.proceed(request)
} }
// [CORE-LOGIC] Создаем перехватчик для добавления токена авторизации.
val authInterceptor = Interceptor { chain -> val authInterceptor = Interceptor { chain ->
// [HELPER] Получаем токен из репозитория.
// runBlocking здесь допустим, т.к. чтение из SharedPreferences - быстрая I/O операция,
// а интерфейс Interceptor'а является синхронным.
val token = runBlocking { credentialsRepositoryProvider.get().getToken() } val token = runBlocking { credentialsRepositoryProvider.get().getToken() }
val requestBuilder = chain.request().newBuilder() val requestBuilder = chain.request().newBuilder()
// [ACTION] Если токен существует, добавляем его в заголовок.
if (token != null) { if (token != null) {
// Сервер ожидает заголовок "Authorization: Bearer <token>"
// Предполагается, что `token` уже содержит префикс "Bearer ".
requestBuilder.addHeader("Authorization", token) requestBuilder.addHeader("Authorization", token)
} }
chain.proceed(requestBuilder.build()) chain.proceed(requestBuilder.build())
} }
// [ACTION] Собираем OkHttpClient с правильным порядком перехватчиков.
return OkHttpClient.Builder() return OkHttpClient.Builder()
.addInterceptor(acceptHeaderInterceptor) .addInterceptor(acceptHeaderInterceptor)
.addInterceptor(authInterceptor) // Добавляем перехватчик для токена .addInterceptor(authInterceptor)
.addInterceptor(loggingInterceptor) // Логирование должно идти последним, чтобы видеть финальный запрос .addInterceptor(loggingInterceptor)
.build() .build()
} }
// [END_ENTITY: Function('provideOkHttpClient')]
/** // [ENTITY: Function('provideMoshi')]
* [PROVIDER] // [RELATION: Function('provideMoshi')] -> [PROVIDES] -> [Framework('Moshi')]
* [CONTRACT] Предоставляет экземпляр Moshi для парсинга JSON.
*/
@Provides @Provides
@Singleton @Singleton
fun provideMoshi(): Moshi { fun provideMoshi(): Moshi {
Timber.d("[DEBUG][PROVIDER][providing_moshi] Providing Moshi.")
return Moshi.Builder() return Moshi.Builder()
.add(KotlinJsonAdapterFactory()) .add(KotlinJsonAdapterFactory())
.build() .build()
} }
// [END_ENTITY: Function('provideMoshi')]
/** // [ENTITY: Function('provideMoshiConverterFactory')]
* [PROVIDER] // [RELATION: Function('provideMoshiConverterFactory')] -> [PROVIDES] -> [Framework('MoshiConverterFactory')]
* [CONTRACT] Предоставляет фабрику конвертеров для Retrofit.
*/
@Provides @Provides
@Singleton @Singleton
fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory { fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
Timber.d("[DEBUG][PROVIDER][providing_moshi_converter] Providing MoshiConverterFactory.")
return MoshiConverterFactory.create(moshi) return MoshiConverterFactory.create(moshi)
} }
// [END_ENTITY: Function('provideMoshiConverterFactory')]
/** // [ENTITY: Function('provideRetrofit')]
* [PROVIDER] // [RELATION: Function('provideRetrofit')] -> [PROVIDES] -> [Framework('Retrofit')]
* [CONTRACT] Предоставляет сконфигурированный экземпляр Retrofit.
*/
@Provides @Provides
@Singleton @Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit { fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
Timber.d("[DEBUG][PROVIDER][providing_retrofit] Providing Retrofit.")
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(BASE_URL) .baseUrl(BASE_URL)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(moshiConverterFactory) .addConverterFactory(moshiConverterFactory)
.build() .build()
} }
// [END_ENTITY: Function('provideRetrofit')]
/** // [ENTITY: Function('provideHomeboxApiService')]
* [PROVIDER] // [RELATION: Function('provideHomeboxApiService')] -> [PROVIDES] -> [Interface('HomeboxApiService')]
* [CONTRACT] Предоставляет реализацию интерфейса HomeboxApiService.
*/
@Provides @Provides
@Singleton @Singleton
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService { fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
Timber.d("[DEBUG][PROVIDER][providing_api_service] Providing HomeboxApiService.")
return retrofit.create(HomeboxApiService::class.java) return retrofit.create(HomeboxApiService::class.java)
} }
// [END_ENTITY: Function('provideHomeboxApiService')]
} }
// [END_ENTITY: Module('ApiModule')]
// [END_FILE_ApiModule.kt] // [END_FILE_ApiModule.kt]

View File

@@ -1,8 +1,9 @@
// [PACKAGE] com.homebox.lens.data.di // [PACKAGE] com.homebox.lens.data.di
// [FILE] DatabaseModule.kt // [FILE] DatabaseModule.kt
// [SEMANTICS] di, hilt, database
package com.homebox.lens.data.di package com.homebox.lens.data.di
// [IMPORTS]
import android.content.Context import android.content.Context
import androidx.room.Room import androidx.room.Room
import com.homebox.lens.data.db.HomeboxDatabase import com.homebox.lens.data.db.HomeboxDatabase
@@ -11,40 +12,50 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import timber.log.Timber
import javax.inject.Singleton import javax.inject.Singleton
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: Module('DatabaseModule')]
/** /**
* [MODULE: DaggerHilt('DatabaseModule')] * @summary Предоставляет зависимости для работы с базой данных Room.
* [PURPOSE] Предоставляет зависимости для работы с базой данных Room.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object DatabaseModule { object DatabaseModule {
// [PROVIDER] // [ENTITY: Function('provideHomeboxDatabase')]
// [RELATION: Function('provideHomeboxDatabase')] -> [PROVIDES] -> [Database('HomeboxDatabase')]
@Provides @Provides
@Singleton @Singleton
fun provideHomeboxDatabase(@ApplicationContext context: Context): HomeboxDatabase { fun provideHomeboxDatabase(@ApplicationContext context: Context): HomeboxDatabase {
// [ACTION] Build Room database instance Timber.d("[DEBUG][PROVIDER][providing_database] Providing HomeboxDatabase.")
return Room.databaseBuilder( return Room.databaseBuilder(
context, context,
HomeboxDatabase::class.java, HomeboxDatabase::class.java,
HomeboxDatabase.DATABASE_NAME HomeboxDatabase.DATABASE_NAME
).build() ).build()
} }
// [END_ENTITY: Function('provideHomeboxDatabase')]
// [PROVIDER] // [ENTITY: Function('provideItemDao')]
// [RELATION: Function('provideItemDao')] -> [PROVIDES] -> [Interface('ItemDao')]
@Provides @Provides
fun provideItemDao(database: HomeboxDatabase) = database.itemDao() fun provideItemDao(database: HomeboxDatabase) = database.itemDao()
// [END_ENTITY: Function('provideItemDao')]
// [PROVIDER] // [ENTITY: Function('provideLabelDao')]
// [RELATION: Function('provideLabelDao')] -> [PROVIDES] -> [Interface('LabelDao')]
@Provides @Provides
fun provideLabelDao(database: HomeboxDatabase) = database.labelDao() fun provideLabelDao(database: HomeboxDatabase) = database.labelDao()
// [END_ENTITY: Function('provideLabelDao')]
// [PROVIDER] // [ENTITY: Function('provideLocationDao')]
// [RELATION: Function('provideLocationDao')] -> [PROVIDES] -> [Interface('LocationDao')]
@Provides @Provides
fun provideLocationDao(database: HomeboxDatabase) = database.locationDao() fun provideLocationDao(database: HomeboxDatabase) = database.locationDao()
// [END_ENTITY: Function('provideLocationDao')]
} }
// [END_ENTITY: Module('DatabaseModule')]
// [END_FILE_DatabaseModule.kt] // [END_FILE_DatabaseModule.kt]

View File

@@ -4,6 +4,7 @@
package com.homebox.lens.data.di package com.homebox.lens.data.di
// [IMPORTS]
import com.homebox.lens.data.repository.AuthRepositoryImpl import com.homebox.lens.data.repository.AuthRepositoryImpl
import com.homebox.lens.data.repository.CredentialsRepositoryImpl import com.homebox.lens.data.repository.CredentialsRepositoryImpl
import com.homebox.lens.data.repository.ItemRepositoryImpl import com.homebox.lens.data.repository.ItemRepositoryImpl
@@ -15,47 +16,52 @@ import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
// [END_IMPORTS]
// [ENTITY: Module('RepositoryModule')]
/** /**
* [ENTITY: Module('RepositoryModule')] * @summary Hilt-модуль для предоставления реализаций репозиториев.
* [CONTRACT] * @description Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
* Hilt-модуль для предоставления реализаций репозиториев.
* Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
abstract class RepositoryModule { abstract class RepositoryModule {
// [ENTITY: Function('bindItemRepository')]
// [RELATION: Function('bindItemRepository')] -> [PROVIDES] -> [Interface('ItemRepository')]
/** /**
* [CONTRACT] * @summary Связывает интерфейс ItemRepository с его реализацией.
* Связывает интерфейс ItemRepository с его реализацией.
*/ */
@Binds @Binds
@Singleton @Singleton
abstract fun bindItemRepository( abstract fun bindItemRepository(
itemRepositoryImpl: ItemRepositoryImpl itemRepositoryImpl: ItemRepositoryImpl
): ItemRepository ): ItemRepository
// [END_ENTITY: Function('bindItemRepository')]
// [ENTITY: Function('bindCredentialsRepository')]
// [RELATION: Function('bindCredentialsRepository')] -> [PROVIDES] -> [Interface('CredentialsRepository')]
/** /**
* [CONTRACT] * @summary Связывает интерфейс CredentialsRepository с его реализацией.
* Связывает интерфейс CredentialsRepository с его реализацией.
*/ */
@Binds @Binds
@Singleton @Singleton
abstract fun bindCredentialsRepository( abstract fun bindCredentialsRepository(
credentialsRepositoryImpl: CredentialsRepositoryImpl credentialsRepositoryImpl: CredentialsRepositoryImpl
): CredentialsRepository ): CredentialsRepository
// [END_ENTITY: Function('bindCredentialsRepository')]
// [ENTITY: Function('bindAuthRepository')]
// [RELATION: Function('bindAuthRepository')] -> [PROVIDES] -> [Interface('AuthRepository')]
/** /**
* [CONTRACT] * @summary Связывает интерфейс AuthRepository с его реализацией.
* [FIX] Связывает интерфейс AuthRepository с его реализацией.
* Это исправляет ошибку "could not be resolved", так как теперь Hilt знает,
* какую конкретную реализацию предоставить, когда запрашивается AuthRepository.
*/ */
@Binds @Binds
@Singleton @Singleton
abstract fun bindAuthRepository( abstract fun bindAuthRepository(
authRepositoryImpl: AuthRepositoryImpl authRepositoryImpl: AuthRepositoryImpl
): AuthRepository ): AuthRepository
// [END_ENTITY: Function('bindAuthRepository')]
} }
// [END_ENTITY: Module('RepositoryModule')]
// [END_FILE_RepositoryModule.kt] // [END_FILE_RepositoryModule.kt]

View File

@@ -1,8 +1,9 @@
// [PACKAGE] com.homebox.lens.data.di // [PACKAGE] com.homebox.lens.data.di
// [FILE] StorageModule.kt // [FILE] StorageModule.kt
// [SEMANTICS] di, hilt, storage
package com.homebox.lens.data.di package com.homebox.lens.data.di
// [IMPORTS]
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import com.homebox.lens.data.repository.EncryptedPreferencesWrapper import com.homebox.lens.data.repository.EncryptedPreferencesWrapper
@@ -12,30 +13,39 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import timber.log.Timber
import javax.inject.Singleton import javax.inject.Singleton
// [END_IMPORTS]
// [ENTITY: Module('StorageModule')]
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object StorageModule { object StorageModule {
private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs" // No longer secret private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs"
// [ACTION] Provide a standard, unencrypted SharedPreferences instance. // [ENTITY: Function('provideSharedPreferences')]
// [RELATION: Function('provideSharedPreferences')] -> [PROVIDES] -> [Framework('SharedPreferences')]
@Provides @Provides
@Singleton @Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences { fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
Timber.d("[DEBUG][PROVIDER][providing_shared_preferences] Providing SharedPreferences.")
return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE) return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
} }
// [END_ENTITY: Function('provideSharedPreferences')]
// [ACTION] Provide our new EncryptedPreferencesWrapper as the main entry point for secure storage. // [ENTITY: Function('provideEncryptedPreferencesWrapper')]
// Hilt will automatically provide SharedPreferences and CryptoManager to its constructor. // [RELATION: Function('provideEncryptedPreferencesWrapper')] -> [PROVIDES] -> [Class('EncryptedPreferencesWrapper')]
@Provides @Provides
@Singleton @Singleton
fun provideEncryptedPreferencesWrapper( fun provideEncryptedPreferencesWrapper(
sharedPreferences: SharedPreferences, sharedPreferences: SharedPreferences,
cryptoManager: CryptoManager cryptoManager: CryptoManager
): EncryptedPreferencesWrapper { ): EncryptedPreferencesWrapper {
Timber.d("[DEBUG][PROVIDER][providing_encrypted_prefs_wrapper] Providing EncryptedPreferencesWrapper.")
return EncryptedPreferencesWrapper(sharedPreferences, cryptoManager) return EncryptedPreferencesWrapper(sharedPreferences, cryptoManager)
} }
// [END_ENTITY: Function('provideEncryptedPreferencesWrapper')]
} }
// [END_ENTITY: Module('StorageModule')]
// [END_FILE_StorageModule.kt] // [END_FILE_StorageModule.kt]

View File

@@ -20,17 +20,20 @@ import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: Class('AuthRepositoryImpl')]
// [RELATION: Class('AuthRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('AuthRepository')]
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('OkHttpClient')]
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('MoshiConverterFactory')]
/** /**
* [ENTITY: Class('AuthRepositoryImpl')] * @summary Реализация репозитория для управления аутентификацией.
* [CONTRACT]
* Реализация репозитория для управления аутентификацией.
* @param encryptedPrefs Защищенное хранилище для токена. * @param encryptedPrefs Защищенное хранилище для токена.
* @param okHttpClient Общий OkHttp клиент для переиспользования. * @param okHttpClient Общий OkHttp клиент для переиспользования.
* @param moshiConverterFactory Общий конвертер Moshi для переиспользования. * @param moshiConverterFactory Общий конвертер Moshi для переиспользования.
* [COHERENCE_NOTE] Реализация метода login теперь включает логику создания временного Retrofit-клиента
* "на лету", используя URL сервера из credentials. Эта логика была перенесена из ItemRepositoryImpl.
*/ */
class AuthRepositoryImpl @Inject constructor( class AuthRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences, private val encryptedPrefs: SharedPreferences,
@@ -42,47 +45,53 @@ class AuthRepositoryImpl @Inject constructor(
private const val KEY_AUTH_TOKEN = "key_auth_token" private const val KEY_AUTH_TOKEN = "key_auth_token"
} }
// [ENTITY: Function('login')]
/** /**
* [CONTRACT] * @summary Реализует вход пользователя. Создает временный API сервис для выполнения запроса
* Реализует вход пользователя. Создает временный API сервис для выполнения запроса
* на указанный пользователем URL сервера. * на указанный пользователем URL сервера.
* @param credentials Учетные данные пользователя, включая URL сервера. * @param credentials Учетные данные пользователя, включая URL сервера.
* @return [Result] с доменной моделью [TokenResponse] при успехе или [Exception] при ошибке. * @return [Result] с доменной моделью [TokenResponse] при успехе или [Exception] при ошибке.
*/ */
override suspend fun login(credentials: Credentials): Result<TokenResponse> { override suspend fun login(credentials: Credentials): Result<TokenResponse> {
// [PRECONDITION] require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." }
require(credentials.serverUrl.isNotBlank()) { "[PRECONDITION_FAILED] Server URL cannot be blank." }
// [CORE-LOGIC]
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
runCatching { runCatching {
// [ACTION] Создаем временный Retrofit клиент с URL, указанным пользователем. Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}")
val tempApiService = Retrofit.Builder() val tempApiService = Retrofit.Builder()
.baseUrl(credentials.serverUrl) .baseUrl(credentials.serverUrl)
.client(okHttpClient) // Переиспользуем существующий OkHttp клиент .client(okHttpClient)
.addConverterFactory(moshiConverterFactory) // и конвертер .addConverterFactory(moshiConverterFactory)
.build() .build()
.create(HomeboxApiService::class.java) .create(HomeboxApiService::class.java)
// [ACTION] Создаем DTO и выполняем запрос.
val loginForm = LoginFormDto(credentials.username, credentials.password) val loginForm = LoginFormDto(credentials.username, credentials.password)
Timber.d("[DEBUG][ACTION][performing_login] Performing login request.")
val tokenResponseDto = tempApiService.login(loginForm) val tokenResponseDto = tempApiService.login(loginForm)
// [ACTION] Маппим результат в доменную модель. Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.")
tokenResponseDto.toDomain() tokenResponseDto.toDomain()
} }
} }
} }
// [END_ENTITY: Function('login')]
// [ENTITY: Function('saveToken')]
override suspend fun saveToken(token: String) { override suspend fun saveToken(token: String) {
require(token.isNotBlank()) { "[PRECONDITION_FAILED] Token cannot be blank." } require(token.isNotBlank()) { "Token cannot be blank." }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.")
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply() encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
} }
} }
// [END_ENTITY: Function('saveToken')]
// [ENTITY: Function('getToken')]
override fun getToken(): Flow<String?> = flow { override fun getToken(): Flow<String?> = flow {
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null)) emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
}.flowOn(Dispatchers.IO) }.flowOn(Dispatchers.IO)
// [END_ENTITY: Function('getToken')]
} }
// [END_ENTITY: Class('AuthRepositoryImpl')]
// [END_FILE_AuthRepositoryImpl.kt] // [END_FILE_AuthRepositoryImpl.kt]

View File

@@ -1,7 +1,8 @@
// [PACKAGE] com.homebox.lens.data.repository // [PACKAGE] com.homebox.lens.data.repository
// [FILE] CredentialsRepositoryImpl.kt // [FILE] CredentialsRepositoryImpl.kt
// [PURPOSE] Имплементация репозитория для управления учетными данными и токенами доступа. // [SEMANTICS] data, repository, credentials, security
package com.homebox.lens.data.repository package com.homebox.lens.data.repository
// [IMPORTS] // [IMPORTS]
import android.content.SharedPreferences import android.content.SharedPreferences
import com.homebox.lens.domain.model.Credentials import com.homebox.lens.domain.model.Credentials
@@ -11,13 +12,16 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: Class('CredentialsRepositoryImpl')]
// [RELATION: Class('CredentialsRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('CredentialsRepository')]
// [RELATION: Class('CredentialsRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
/** /**
* [ENTITY: Class('CredentialsRepositoryImpl')] * @summary Реализует репозиторий для управления учетными данными пользователя.
* [CONTRACT] * @description Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
* Реализует репозиторий для управления учетными данными пользователя.
* Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
* @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt. * @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt.
* @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`. * @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`.
*/ */
@@ -25,7 +29,6 @@ class CredentialsRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences private val encryptedPrefs: SharedPreferences
) : CredentialsRepository { ) : CredentialsRepository {
// [CONSTANTS_KEYS] Ключи для хранения данных в SharedPreferences.
companion object { companion object {
private const val KEY_SERVER_URL = "key_server_url" private const val KEY_SERVER_URL = "key_server_url"
private const val KEY_USERNAME = "key_username" private const val KEY_USERNAME = "key_username"
@@ -33,15 +36,15 @@ class CredentialsRepositoryImpl @Inject constructor(
private const val KEY_AUTH_TOKEN = "key_auth_token" private const val KEY_AUTH_TOKEN = "key_auth_token"
} }
// [ENTITY: Function('saveCredentials')]
/** /**
* [CONTRACT] * @summary Сохраняет основные учетные данные пользователя.
* Сохраняет основные учетные данные пользователя.
* @param credentials Объект с учетными данными для сохранения. * @param credentials Объект с учетными данными для сохранения.
* @sideeffect Перезаписывает существующие учетные данные в SharedPreferences. * @sideeffect Перезаписывает существующие учетные данные в SharedPreferences.
*/ */
override suspend fun saveCredentials(credentials: Credentials) { override suspend fun saveCredentials(credentials: Credentials) {
// [ACTION] Выполняем запись в SharedPreferences в фоновом потоке.
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Timber.d("[DEBUG][ACTION][saving_credentials] Saving user credentials.")
encryptedPrefs.edit() encryptedPrefs.edit()
.putString(KEY_SERVER_URL, credentials.serverUrl) .putString(KEY_SERVER_URL, credentials.serverUrl)
.putString(KEY_USERNAME, credentials.username) .putString(KEY_USERNAME, credentials.username)
@@ -49,51 +52,57 @@ class CredentialsRepositoryImpl @Inject constructor(
.apply() .apply()
} }
} }
// [END_ENTITY: Function('saveCredentials')]
// [ENTITY: Function('getCredentials')]
/** /**
* [CONTRACT] * @summary Извлекает сохраненные учетные данные пользователя в виде потока.
* Извлекает сохраненные учетные данные пользователя в виде потока.
* @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют. * @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют.
*/ */
override fun getCredentials(): Flow<Credentials?> = flow { override fun getCredentials(): Flow<Credentials?> = flow {
// [CORE-LOGIC] Читаем данные из SharedPreferences. Timber.d("[DEBUG][ACTION][getting_credentials] Getting user credentials.")
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null) val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
val username = encryptedPrefs.getString(KEY_USERNAME, null) val username = encryptedPrefs.getString(KEY_USERNAME, null)
val password = encryptedPrefs.getString(KEY_PASSWORD, null) val password = encryptedPrefs.getString(KEY_PASSWORD, null)
// [ACTION] Эммитим результат.
if (serverUrl != null && username != null && password != null) { if (serverUrl != null && username != null && password != null) {
Timber.d("[DEBUG][SUCCESS][credentials_found] Found and emitting credentials.")
emit(Credentials(serverUrl, username, password)) emit(Credentials(serverUrl, username, password))
} else { } else {
Timber.d("[DEBUG][FALLBACK][no_credentials] No credentials found, emitting null.")
emit(null) emit(null)
} }
}.flowOn(Dispatchers.IO) // [ACTION] Указываем, что Flow должен выполняться в фоновом потоке. }.flowOn(Dispatchers.IO)
// [END_ENTITY: Function('getCredentials')]
// [ENTITY: Function('saveToken')]
/** /**
* [CONTRACT] * @summary Сохраняет токен авторизации.
* Сохраняет токен авторизации.
* @param token Токен для сохранения. * @param token Токен для сохранения.
* @sideeffect Перезаписывает существующий токен в SharedPreferences. * @sideeffect Перезаписывает существующий токен в SharedPreferences.
*/ */
override suspend fun saveToken(token: String) { override suspend fun saveToken(token: String) {
// [ACTION] Выполняем запись токена в фоновом потоке.
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.")
encryptedPrefs.edit() encryptedPrefs.edit()
.putString(KEY_AUTH_TOKEN, token) .putString(KEY_AUTH_TOKEN, token)
.apply() .apply()
} }
} }
// [END_ENTITY: Function('saveToken')]
// [ENTITY: Function('getToken')]
/** /**
* [CONTRACT] * @summary Извлекает сохраненный токен авторизации.
* Извлекает сохраненный токен авторизации.
* @return Строка с токеном или null, если он не найден. * @return Строка с токеном или null, если он не найден.
*/ */
override suspend fun getToken(): String? { override suspend fun getToken(): String? {
// [ACTION] Выполняем чтение токена в фоновом потоке.
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
encryptedPrefs.getString(KEY_AUTH_TOKEN, null) encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
} }
} }
// [END_ENTITY: Function('getToken')]
} }
// [END_ENTITY: Class('CredentialsRepositoryImpl')]
// [END_FILE_CredentialsRepositoryImpl.kt] // [END_FILE_CredentialsRepositoryImpl.kt]

View File

@@ -1,20 +1,24 @@
// [PACKAGE] com.homebox.lens.data.repository // [PACKAGE] com.homebox.lens.data.repository
// [FILE] EncryptedPreferencesWrapper.kt // [FILE] EncryptedPreferencesWrapper.kt
// [PURPOSE] A wrapper around SharedPreferences to provide on-the-fly encryption/decryption. // [SEMANTICS] data, security, preferences
package com.homebox.lens.data.repository package com.homebox.lens.data.repository
// [IMPORTS]
import android.content.SharedPreferences import android.content.SharedPreferences
import com.homebox.lens.data.security.CryptoManager import com.homebox.lens.data.security.CryptoManager
import timber.log.Timber
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.charset.Charset import java.nio.charset.Charset
import javax.inject.Inject import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: Class('EncryptedPreferencesWrapper')]
// [RELATION: Class('EncryptedPreferencesWrapper')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
// [RELATION: Class('EncryptedPreferencesWrapper')] -> [DEPENDS_ON] -> [Class('CryptoManager')]
/** /**
* [CONTRACT] * @summary Provides a simplified and secure interface for storing and retrieving sensitive string data.
* Provides a simplified and secure interface for storing and retrieving sensitive string data. * @description It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance.
* It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance.
* @param sharedPreferences The underlying standard SharedPreferences instance to store encrypted data. * @param sharedPreferences The underlying standard SharedPreferences instance to store encrypted data.
* @param cryptoManager The manager responsible for all cryptographic operations. * @param cryptoManager The manager responsible for all cryptographic operations.
*/ */
@@ -23,44 +27,58 @@ class EncryptedPreferencesWrapper @Inject constructor(
private val cryptoManager: CryptoManager private val cryptoManager: CryptoManager
) { ) {
// [ENTITY: Function('getString')]
/** /**
* [CONTRACT] * @summary Retrieves a decrypted string value for a given key.
* Retrieves a decrypted string value for a given key.
* @param key The key for the preference. * @param key The key for the preference.
* @param defaultValue The value to return if the key is not found or decryption fails. * @param defaultValue The value to return if the key is not found or decryption fails.
* @return The decrypted string, or the defaultValue. * @return The decrypted string, or the defaultValue.
* @sideeffect Reads from SharedPreferences.
*/ */
fun getString(key: String, defaultValue: String?): String? { fun getString(key: String, defaultValue: String?): String? {
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue Timber.d("[DEBUG][ENTRYPOINT][getting_string] Attempting to get string for key: %s", key)
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue.also {
Timber.d("[DEBUG][FALLBACK][no_value_found] No value for key %s, returning default.", key)
}
return try { return try {
Timber.d("[DEBUG][ACTION][decoding_value] Decoding Base64 value.")
val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT) val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT)
Timber.d("[DEBUG][ACTION][decrypting_value] Decrypting value with CryptoManager.")
val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes)) val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes))
String(decryptedBytes, Charset.defaultCharset()) String(decryptedBytes, Charset.defaultCharset()).also {
Timber.d("[DEBUG][SUCCESS][decryption_complete] Successfully decrypted value for key: %s", key)
}
} catch (e: Exception) { } catch (e: Exception) {
// Log the error, maybe clear the invalid preference Timber.e(e, "[ERROR][EXCEPTION][decryption_failed] Failed to decrypt value for key: %s", key)
defaultValue defaultValue
} }
} }
// [END_ENTITY: Function('getString')]
// [ENTITY: Function('putString')]
/** /**
* [CONTRACT] * @summary Encrypts and saves a string value for a given key.
* Encrypts and saves a string value for a given key.
* @param key The key for the preference. * @param key The key for the preference.
* @param value The string value to encrypt and save. * @param value The string value to encrypt and save.
* @sideeffect Modifies the underlying SharedPreferences file. * @sideeffect Modifies the underlying SharedPreferences file.
*/ */
fun putString(key: String, value: String) { fun putString(key: String, value: String) {
Timber.d("[DEBUG][ENTRYPOINT][putting_string] Attempting to put string for key: %s", key)
try { try {
Timber.d("[DEBUG][ACTION][encrypting_value] Encrypting value with CryptoManager.")
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream) cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream)
val encryptedBytes = outputStream.toByteArray() val encryptedBytes = outputStream.toByteArray()
Timber.d("[DEBUG][ACTION][encoding_value] Encoding encrypted value to Base64.")
val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT) val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT)
Timber.d("[DEBUG][ACTION][writing_to_prefs] Writing encrypted value to SharedPreferences.")
sharedPreferences.edit().putString(key, encryptedValue).apply() sharedPreferences.edit().putString(key, encryptedValue).apply()
Timber.d("[DEBUG][SUCCESS][encryption_complete] Successfully encrypted and saved value for key: %s", key)
} catch (e: Exception) { } catch (e: Exception) {
// Log the error Timber.e(e, "[ERROR][EXCEPTION][encryption_failed] Failed to encrypt and save value for key: %s", key)
} }
} }
// [END_ENTITY: Function('putString')]
// [COHERENCE_NOTE] Add other methods like getInt, putInt etc. as needed, following the same pattern.
} }
// [END_ENTITY: Class('EncryptedPreferencesWrapper')]
// [END_FILE_EncryptedPreferencesWrapper.kt] // [END_FILE_EncryptedPreferencesWrapper.kt]

View File

@@ -1,104 +1,196 @@
// [PACKAGE] com.homebox.lens.data.repository // [PACKAGE] com.homebox.lens.data.repository
// [FILE] ItemRepositoryImpl.kt // [FILE] ItemRepositoryImpl.kt
// [SEMANTICS] data_repository, implementation, items // [SEMANTICS] data_repository, implementation, items, labels
package com.homebox.lens.data.repository package com.homebox.lens.data.repository
// [IMPORTS] // [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.LabelCreateDto
import com.homebox.lens.data.api.dto.toDomain import com.homebox.lens.data.api.dto.toDomain
import com.homebox.lens.data.api.dto.toDto import com.homebox.lens.data.api.dto.toDto
import com.homebox.lens.data.api.dto.LocationCreateDto
import com.homebox.lens.data.api.dto.LocationUpdateDto
import com.homebox.lens.data.api.dto.LabelUpdateDto
import com.homebox.lens.data.api.dto.LocationOutDto
import com.homebox.lens.data.db.dao.ItemDao
import com.homebox.lens.data.db.dao.LabelDao
import com.homebox.lens.data.db.entity.toDomain
import com.homebox.lens.domain.model.* import com.homebox.lens.domain.model.*
import com.homebox.lens.domain.repository.ItemRepository import com.homebox.lens.domain.repository.ItemRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
// [END_IMPORTS]
// [CORE-LOGIC] // [ENTITY: Repository('ItemRepositoryImpl')]
/** // [RELATION: Repository('ItemRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('ItemRepository')]
* [CONTRACT] // [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [ApiEndpoint('HomeboxApiService')]
* Реализация репозитория для работы с данными о вещах. // [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [DatabaseTable('ItemDao')]
* @param apiService Сервис для взаимодействия с Homebox API.
* [COHERENCE_NOTE] Метод 'login' был полностью удален из этого класса, так как его ответственность
* была передана в AuthRepositoryImpl. Это устраняет ошибку компиляции "'login' overrides nothing".
*/
@Singleton @Singleton
class ItemRepositoryImpl @Inject constructor( class ItemRepositoryImpl @Inject constructor(
private val apiService: HomeboxApiService, private val apiService: HomeboxApiService,
private val itemDao: ItemDao,
private val labelDao: LabelDao
) : ItemRepository { ) : ItemRepository {
// [DELETED] Метод login был здесь, но теперь он удален. // [ENTITY: Function('createItem')]
// [RELATION: Function('createItem')] -> [RETURNS] -> [DataClass('ItemSummary')]
/**
* [CONTRACT] @see ItemRepository.createItem
*/
override suspend fun createItem(newItemData: ItemCreate): ItemSummary { override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
val itemDto = newItemData.toDto() val itemDto = newItemData.toDto()
val resultDto = apiService.createItem(itemDto) val resultDto = apiService.createItem(itemDto)
return resultDto.toDomain() return resultDto.toDomain()
} }
// [END_ENTITY: Function('createItem')]
/** // [ENTITY: Function('getItemDetails')]
* [CONTRACT] @see ItemRepository.getItemDetails // [RELATION: Function('getItemDetails')] -> [RETURNS] -> [DataClass('ItemOut')]
*/
override suspend fun getItemDetails(itemId: String): ItemOut { override suspend fun getItemDetails(itemId: String): ItemOut {
val resultDto = apiService.getItem(itemId) val resultDto = apiService.getItem(itemId)
return resultDto.toDomain() return resultDto.toDomain()
} }
// [END_ENTITY: Function('getItemDetails')]
/** // [ENTITY: Function('updateItem')]
* [CONTRACT] @see ItemRepository.updateItem // [RELATION: Function('updateItem')] -> [RETURNS] -> [DataClass('ItemOut')]
*/
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut { override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
val itemDto = item.toDto() val itemDto = item.toDto()
val resultDto = apiService.updateItem(itemId, itemDto) val resultDto = apiService.updateItem(itemId, itemDto)
return resultDto.toDomain() return resultDto.toDomain()
} }
// [END_ENTITY: Function('updateItem')]
/** // [ENTITY: Function('deleteItem')]
* [CONTRACT] @see ItemRepository.deleteItem
*/
override suspend fun deleteItem(itemId: String) { override suspend fun deleteItem(itemId: String) {
apiService.deleteItem(itemId) apiService.deleteItem(itemId)
} }
// [END_ENTITY: Function('deleteItem')]
/** // [ENTITY: Function('syncInventory')]
* [CONTRACT] @see ItemRepository.syncInventory // [RELATION: Function('syncInventory')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')]
*/
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> { override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
val resultDto = apiService.getItems(page = page, pageSize = pageSize) val resultDto = apiService.getItems(page = page, pageSize = pageSize)
return resultDto.toDomain { it.toDomain() } return resultDto.toDomain { it.toDomain() }
} }
// [END_ENTITY: Function('syncInventory')]
/** // [ENTITY: Function('getStatistics')]
* [CONTRACT] @see ItemRepository.getStatistics // [RELATION: Function('getStatistics')] -> [RETURNS] -> [DataClass('GroupStatistics')]
*/
override suspend fun getStatistics(): GroupStatistics { override suspend fun getStatistics(): GroupStatistics {
val resultDto = apiService.getStatistics() val resultDto = apiService.getStatistics()
return resultDto.toDomain() return resultDto.toDomain()
} }
// [END_ENTITY: Function('getStatistics')]
/** // [ENTITY: Function('getAllLocations')]
* [CONTRACT] @see ItemRepository.getAllLocations // [RELATION: Function('getAllLocations')] -> [RETURNS] -> [DataStructure('List<LocationOutCount>')]
*/
override suspend fun getAllLocations(): List<LocationOutCount> { override suspend fun getAllLocations(): List<LocationOutCount> {
val resultDto = apiService.getLocations() val resultDto = apiService.getLocations()
return resultDto.map { it.toDomain() } return resultDto.map { it.toDomain() }
} }
// [END_ENTITY: Function('getAllLocations')]
/** // [ENTITY: Function('getAllLabels')]
* [CONTRACT] @see ItemRepository.getAllLabels // [RELATION: Function('getAllLabels')] -> [RETURNS] -> [DataStructure('List<LabelOut>')]
*/
override suspend fun getAllLabels(): List<LabelOut> { override suspend fun getAllLabels(): List<LabelOut> {
val resultDto = apiService.getLabels() val resultDto = apiService.getLabels()
return resultDto.map { it.toDomain() } return resultDto.map { it.toDomain() }
} }
// [END_ENTITY: Function('getAllLabels')]
/** // [ENTITY: Function('getLabelDetails')]
* [CONTRACT] @see ItemRepository.searchItems // [RELATION: Function('getLabelDetails')] -> [RETURNS] -> [DataClass('LabelOut')]
*/ override suspend fun getLabelDetails(labelId: String): LabelOut {
val resultDto = apiService.getLabels().firstOrNull { it.id == labelId }
return resultDto?.toDomain() ?: throw NoSuchElementException("Label with ID $labelId not found.")
}
// [END_ENTITY: Function('getLabelDetails')]
// [ENTITY: Function('createLabel')]
// [RELATION: Function('createLabel')] -> [RETURNS] -> [DataClass('LabelSummary')]
override suspend fun createLabel(newLabelData: LabelCreate): LabelSummary {
val labelCreateDto = newLabelData.toDto()
val resultDto = apiService.createLabel(labelCreateDto)
return resultDto.toDomain()
}
// [END_ENTITY: Function('createLabel')]
override suspend fun updateLabel(labelId: String, labelData: LabelUpdate): LabelOut {
val labelDto = labelData.toDto()
val resultDto = apiService.updateLabel(labelId, labelDto)
return resultDto.toDomain()
}
override suspend fun deleteLabel(labelId: String) {
apiService.deleteLabel(labelId)
labelDao.deleteLabelById(labelId)
}
override suspend fun createLocation(newLocationData: LocationCreate): LocationOut {
val locationDto = newLocationData.toDto()
val resultDto = apiService.createLocation(locationDto)
return resultDto.toDomain()
}
override suspend fun updateLocation(locationId: String, locationData: LocationUpdate): LocationOut {
val locationDto = locationData.toDto()
val resultDto = apiService.updateLocation(locationId, locationDto)
return resultDto.toDomain()
}
override suspend fun deleteLocation(locationId: String) {
apiService.deleteLocation(locationId)
}
// [ENTITY: Function('searchItems')]
// [RELATION: Function('searchItems')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')]
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> { override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
val resultDto = apiService.getItems(query = query) val resultDto = apiService.getItems(query = query)
return resultDto.toDomain { it.toDomain() } return resultDto.toDomain { it.toDomain() }
} }
// [END_ENTITY: Function('searchItems')]
// [ENTITY: Function('getRecentlyAddedItems')]
// [RELATION: Function('getRecentlyAddedItems')] -> [RETURNS] -> [DataStructure('Flow<List<ItemSummary>>')]
override fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> {
return itemDao.getRecentlyAddedItems(limit).map { entities ->
entities.map { it.toDomain() }
}
}
// [END_ENTITY: Function('getRecentlyAddedItems')]
} }
// [END_ENTITY: Repository('ItemRepositoryImpl')]
// [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelCreateDto')]
private fun LabelCreate.toDto(): LabelCreateDto {
return LabelCreateDto(
name = this.name,
color = this.color,
description = null // Description is not part of the domain model for creation.
)
}
// [END_ENTITY: Function('toDto')]
// [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LocationCreateDto')]
private fun LocationCreate.toDto(): LocationCreateDto {
return LocationCreateDto(
name = this.name,
color = this.color,
description = null // Description is not part of the domain model for creation.
)
}
// [END_ENTITY: Function('toDto')]
// [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
private fun LabelUpdate.toDto(): LabelUpdateDto {
return LabelUpdateDto(
name = this.name,
color = this.color
)
}
// [END_ENTITY: Function('toDto')]
// [END_FILE_ItemRepositoryImpl.kt] // [END_FILE_ItemRepositoryImpl.kt]

View File

@@ -1,13 +1,14 @@
// [PACKAGE] com.homebox.lens.data.security // [PACKAGE] com.homebox.lens.data.security
// [FILE] CryptoManager.kt // [FILE] CryptoManager.kt
// [PURPOSE] Handles all cryptographic operations using AndroidKeyStore. // [SEMANTICS] data, security, cryptography
package com.homebox.lens.data.security package com.homebox.lens.data.security
// [IMPORTS]
import android.os.Build import android.os.Build
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.security.KeyStore import java.security.KeyStore
@@ -17,11 +18,12 @@ import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
// [END_IMPORTS]
// [ENTITY: Class('CryptoManager')]
/** /**
* [CONTRACT] * @summary A manager for handling encryption and decryption using the Android Keystore system.
* A manager for handling encryption and decryption using the Android Keystore system. * @description This class ensures that cryptographic keys are stored securely.
* This class ensures that cryptographic keys are stored securely.
* It is designed to be a Singleton provided by Hilt. * It is designed to be a Singleton provided by Hilt.
* @invariant The underlying SecretKey must be valid within the AndroidKeyStore. * @invariant The underlying SecretKey must be valid within the AndroidKeyStore.
*/ */
@@ -29,7 +31,6 @@ import javax.inject.Singleton
@Singleton @Singleton
class CryptoManager @Inject constructor() { class CryptoManager @Inject constructor() {
// [ЯКОРЬ] Настройки для шифрования
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null) load(null)
} }
@@ -45,7 +46,6 @@ class CryptoManager @Inject constructor() {
} }
} }
// [CORE-LOGIC] Получение или создание ключа
private fun getKey(): SecretKey { private fun getKey(): SecretKey {
val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry
return existingKey?.secretKey ?: createKey() return existingKey?.secretKey ?: createKey()
@@ -67,8 +67,15 @@ class CryptoManager @Inject constructor() {
}.generateKey() }.generateKey()
} }
// [ACTION] Шифрование потока данных // [ENTITY: Function('encrypt')]
/**
* @summary Encrypts a byte array and writes it to an output stream.
* @param bytes The byte array to encrypt.
* @param outputStream The stream to write the encrypted data to.
* @return The encrypted byte array.
*/
fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray { fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray {
Timber.d("[DEBUG][ACTION][encrypting_data] Encrypting data.")
val cipher = encryptCipher val cipher = encryptCipher
val encryptedBytes = cipher.doFinal(bytes) val encryptedBytes = cipher.doFinal(bytes)
outputStream.use { outputStream.use {
@@ -79,9 +86,16 @@ class CryptoManager @Inject constructor() {
} }
return encryptedBytes return encryptedBytes
} }
// [END_ENTITY: Function('encrypt')]
// [ACTION] Дешифрование потока данных // [ENTITY: Function('decrypt')]
/**
* @summary Decrypts a byte array from an input stream.
* @param inputStream The stream to read the encrypted data from.
* @return The decrypted byte array.
*/
fun decrypt(inputStream: InputStream): ByteArray { fun decrypt(inputStream: InputStream): ByteArray {
Timber.d("[DEBUG][ACTION][decrypting_data] Decrypting data.")
return inputStream.use { return inputStream.use {
val ivSize = it.read() val ivSize = it.read()
val iv = ByteArray(ivSize) val iv = ByteArray(ivSize)
@@ -94,6 +108,7 @@ class CryptoManager @Inject constructor() {
getDecryptCipherForIv(iv).doFinal(encryptedBytes) getDecryptCipherForIv(iv).doFinal(encryptedBytes)
} }
} }
// [END_ENTITY: Function('decrypt')]
companion object { companion object {
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
@@ -103,4 +118,5 @@ class CryptoManager @Inject constructor() {
private const val ALIAS = "homebox_lens_secret_key" private const val ALIAS = "homebox_lens_secret_key"
} }
} }
// [END_ENTITY: Class('CryptoManager')]
// [END_FILE_CryptoManager.kt] // [END_FILE_CryptoManager.kt]

View File

@@ -20,6 +20,12 @@ dependencies {
// [DEPENDENCY] Javax Inject for DI annotations // [DEPENDENCY] Javax Inject for DI annotations
implementation("javax.inject:javax.inject:1") implementation("javax.inject:javax.inject:1")
// [DEPENDENCY] Testing
testImplementation(Libs.junit)
testImplementation(Libs.kotestRunnerJunit5)
testImplementation(Libs.kotestAssertionsCore)
testImplementation(Libs.mockk)
} }
// [END_FILE_domain/build.gradle.kts] // [END_FILE_domain/build.gradle.kts]

View File

@@ -1,18 +1,19 @@
// [PACKAGE] com.homebox.lens.domain.model // [PACKAGE] com.homebox.lens.domain.model
// [FILE] Credentials.kt // [FILE] Credentials.kt
// [SEMANTICS] domain, model, credentials
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [ENTITY: DataClass('Credentials')]
/** /**
* [CONTRACT] * @summary Data class to hold server credentials.
* Data class to hold server credentials. * @param serverUrl The URL of the Homebox server.
* @property serverUrl The URL of the Homebox server. * @param username The username for authentication.
* @property username The username for authentication. * @param password The password for authentication.
* @property password The password for authentication.
*/ */
data class Credentials( data class Credentials(
val serverUrl: String, val serverUrl: String,
val username: String, val username: String,
val password: String val password: String
) )
// [END_ENTITY: DataClass('Credentials')]
// [END_FILE_Credentials.kt] // [END_FILE_Credentials.kt]

View File

@@ -2,17 +2,18 @@
// [FILE] CustomField.kt // [FILE] CustomField.kt
// [SEMANTICS] data_structure, entity, custom_field // [SEMANTICS] data_structure, entity, custom_field
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CORE-LOGIC]
// [ENTITY: DataClass('CustomField')]
/** /**
* [CONTRACT] * @summary Модель данных для представления кастомного поля.
* Модель данных для представления кастомного поля. * @param name Имя поля.
* @property name Имя поля. * @param value Значение поля.
* @property value Значение поля. * @param type Тип поля (например, "text", "number").
* @property type Тип поля (например, "text", "number").
*/ */
data class CustomField( data class CustomField(
val name: String, val name: String,
val value: String, val value: String,
val type: String val type: String
) )
// [END_ENTITY: DataClass('CustomField')]
// [END_FILE_CustomField.kt] // [END_FILE_CustomField.kt]

View File

@@ -2,14 +2,14 @@
// [FILE] GroupStatistics.kt // [FILE] GroupStatistics.kt
// [SEMANTICS] data_structure, statistics // [SEMANTICS] data_structure, statistics
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CORE-LOGIC]
// [ENTITY: DataClass('GroupStatistics')]
/** /**
* [CONTRACT] * @summary Модель данных для представления агрегированной статистики.
* Модель данных для представления агрегированной статистики. * @param items Общее количество вещей.
* @property items Общее количество вещей. * @param labels Общее количество меток.
* @property labels Общее количество меток. * @param locations Общее количество местоположений.
* @property locations Общее количество местоположений. * @param totalValue Общая стоимость всех вещей.
* @property totalValue Общая стоимость всех вещей.
*/ */
data class GroupStatistics( data class GroupStatistics(
val items: Int, val items: Int,
@@ -17,4 +17,5 @@ data class GroupStatistics(
val locations: Int, val locations: Int,
val totalValue: Double val totalValue: Double
) )
// [END_ENTITY: DataClass('GroupStatistics')]
// [END_FILE_GroupStatistics.kt] // [END_FILE_GroupStatistics.kt]

View File

@@ -2,17 +2,18 @@
// [FILE] Image.kt // [FILE] Image.kt
// [SEMANTICS] data_structure, entity, image // [SEMANTICS] data_structure, entity, image
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CORE-LOGIC]
// [ENTITY: DataClass('Image')]
/** /**
* [CONTRACT] * @summary Модель данных для представления изображения, привязанного к вещи.
* Модель данных для представления изображения, привязанного к вещи. * @param id Уникальный идентификатор изображения.
* @property id Уникальный идентификатор изображения. * @param path Путь к файлу изображения.
* @property path Путь к файлу изображения. * @param isPrimary Является ли это изображение основным для вещи.
* @property isPrimary Является ли это изображение основным для вещи.
*/ */
data class Image( data class Image(
val id: String, val id: String,
val path: String, val path: String,
val isPrimary: Boolean val isPrimary: Boolean
) )
// [END_ENTITY: DataClass('Image')]
// [END_FILE_Image.kt] // [END_FILE_Image.kt]

View File

@@ -1,32 +1,55 @@
// [PACKAGE] com.homebox.lens.domain.model // [PACKAGE] com.homebox.lens.domain.model
// [FILE] Item.kt // [FILE] Item.kt
// [SEMANTICS] domain, model
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
import java.math.BigDecimal // [IMPORTS]
// [END_IMPORTS]
// [CONTRACT] // [ENTITY: DataClass('Item')]
// [RELATION: DataClass('Item')] -> [DEPENDS_ON] -> [DataClass('Location')]
// [RELATION: DataClass('Item')] -> [DEPENDS_ON] -> [DataClass('Label')]
/** /**
* [ENTITY: DataClass('Item')] * @summary Представляет собой вещь в инвентаре.
* [PURPOSE] Представляет собой вещь в инвентаре. * @param id Уникальный идентификатор вещи.
* @property id Уникальный идентификатор вещи. * @param name Название вещи.
* @property name Название вещи. * @param description Описание вещи.
* @property description Описание вещи. * @param image Url изображения.
* @property image Url изображения. * @param location Местоположение вещи.
* @property location Местоположение вещи. * @param labels Список меток, присвоенных вещи.
* @property labels Список меток, присвоенных вещи. * @param value Стоимость вещи.
* @property value Стоимость вещи. * @param createdAt Дата создания.
* @property createdAt Дата создания.
*/ */
data class Item( data class Item(
val id: String, val id: String,
val name: String, val name: String,
val description: String?, val description: String?,
val quantity: Int,
val image: String?, val image: String?,
val location: Location?, val location: Location?,
val labels: List<Label>, val labels: List<Label>,
val value: BigDecimal?, val value: Double?,
val createdAt: String? val createdAt: String?,
val assetId: String?,
val notes: String?,
val serialNumber: String?,
val purchasePrice: Double?,
val purchaseDate: String?,
val warrantyUntil: String?,
val parentId: String?,
val isArchived: Boolean?,
val insured: Boolean?,
val lifetimeWarranty: Boolean?,
val manufacturer: String?,
val modelNumber: String?,
val purchaseFrom: String?,
val soldNotes: String?,
val soldPrice: Double?,
val soldTime: String?,
val soldTo: String?,
val syncChildItemsLocations: Boolean?,
val warrantyDetails: String?
) )
// [END_ENTITY: DataClass('Item')]
// [END_FILE_Item.kt] // [END_FILE_Item.kt]

View File

@@ -2,16 +2,16 @@
// [FILE] ItemAttachment.kt // [FILE] ItemAttachment.kt
// [SEMANTICS] data_structure, entity, attachment // [SEMANTICS] data_structure, entity, attachment
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemAttachment')]
/** /**
* [CONTRACT] * @summary Модель данных для представления вложения (файла), привязанного к вещи.
* Модель данных для представления вложения (файла), привязанного к вещи. * @param id Уникальный идентификатор вложения.
* @property id Уникальный идентификатор вложения. * @param name Имя файла.
* @property name Имя файла. * @param path Путь к файлу.
* @property path Путь к файлу. * @param type MIME-тип файла.
* @property type MIME-тип файла. * @param createdAt Дата и время создания.
* @property createdAt Дата и время создания. * @param updatedAt Дата и время последнего обновления.
* @property updatedAt Дата и время последнего обновления.
*/ */
data class ItemAttachment( data class ItemAttachment(
val id: String, val id: String,
@@ -21,4 +21,5 @@ data class ItemAttachment(
val createdAt: String, val createdAt: String,
val updatedAt: String val updatedAt: String
) )
// [END_ENTITY: DataClass('ItemAttachment')]
// [END_FILE_ItemAttachment.kt] // [END_FILE_ItemAttachment.kt]

View File

@@ -2,23 +2,23 @@
// [FILE] ItemCreate.kt // [FILE] ItemCreate.kt
// [SEMANTICS] data_structure, entity, input, create // [SEMANTICS] data_structure, entity, input, create
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemCreate')]
/** /**
* [CONTRACT] * @summary Модель данных для создания новой "Вещи".
* Модель данных для создания новой "Вещи". * @param name Название вещи (обязательно).
* @property name Название вещи (обязательно). * @param assetId Идентификатор актива.
* @property assetId Идентификатор актива. * @param description Описание.
* @property description Описание. * @param notes Заметки.
* @property notes Заметки. * @param serialNumber Серийный номер.
* @property serialNumber Серийный номер. * @param quantity Количество.
* @property quantity Количество. * @param value Стоимость.
* @property value Стоимость. * @param purchasePrice Цена покупки.
* @property purchasePrice Цена покупки. * @param purchaseDate Дата покупки.
* @property purchaseDate Дата покупки. * @param warrantyUntil Гарантия до.
* @property warrantyUntil Гарантия до. * @param locationId ID местоположения.
* @property locationId ID местоположения. * @param parentId ID родительской вещи.
* @property parentId ID родительской вещи. * @param labelIds Список ID меток.
* @property labelIds Список ID меток.
*/ */
data class ItemCreate( data class ItemCreate(
val name: String, val name: String,
@@ -35,4 +35,5 @@ data class ItemCreate(
val parentId: String?, val parentId: String?,
val labelIds: List<String>? val labelIds: List<String>?
) )
// [END_ENTITY: DataClass('ItemCreate')]
// [END_FILE_ItemCreate.kt] // [END_FILE_ItemCreate.kt]

View File

@@ -2,32 +2,32 @@
// [FILE] ItemOut.kt // [FILE] ItemOut.kt
// [SEMANTICS] data_structure, entity, detailed // [SEMANTICS] data_structure, entity, detailed
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemOut')]
/** /**
* [CONTRACT] * @summary Полная модель данных для представления "Вещи" со всеми полями.
* Полная модель данных для представления "Вещи" со всеми полями. * @param id Уникальный идентификатор.
* @property id Уникальный идентификатор. * @param name Название.
* @property name Название. * @param assetId Идентификатор актива.
* @property assetId Идентификатор актива. * @param description Описание.
* @property description Описание. * @param notes Заметки.
* @property notes Заметки. * @param serialNumber Серийный номер.
* @property serialNumber Серийный номер. * @param quantity Количество.
* @property quantity Количество. * @param isArchived Флаг архивации.
* @property isArchived Флаг архивации. * @param value Стоимость.
* @property value Стоимость. * @param purchasePrice Цена покупки.
* @property purchasePrice Цена покупки. * @param purchaseDate Дата покупки.
* @property purchaseDate Дата покупки. * @param warrantyUntil Гарантия до.
* @property warrantyUntil Гарантия до. * @param location Местоположение.
* @property location Местоположение. * @param parent Родительская вещь (если есть).
* @property parent Родительская вещь (если есть). * @param children Дочерние вещи.
* @property children Дочерние вещи. * @param labels Список меток.
* @property labels Список меток. * @param attachments Список вложений.
* @property attachments Список вложений. * @param images Список изображений.
* @property images Список изображений. * @param fields Список кастомных полей.
* @property fields Список кастомных полей. * @param maintenance Список записей об обслуживании.
* @property maintenance Список записей об обслуживании. * @param createdAt Дата и время создания.
* @property createdAt Дата и время создания. * @param updatedAt Дата и время последнего обновления.
* @property updatedAt Дата и время последнего обновления.
*/ */
data class ItemOut( data class ItemOut(
val id: String, val id: String,
@@ -51,6 +51,18 @@ data class ItemOut(
val fields: List<CustomField>, val fields: List<CustomField>,
val maintenance: List<MaintenanceEntry>, val maintenance: List<MaintenanceEntry>,
val createdAt: String, val createdAt: String,
val updatedAt: String val updatedAt: String,
val insured: Boolean?,
val lifetimeWarranty: Boolean?,
val manufacturer: String?,
val modelNumber: String?,
val purchaseFrom: String?,
val soldNotes: String?,
val soldPrice: Double?,
val soldTime: String?,
val soldTo: String?,
val syncChildItemsLocations: Boolean?,
val warrantyDetails: String?
) )
// [END_ENTITY: DataClass('ItemOut')]
// [END_FILE_ItemOut.kt] // [END_FILE_ItemOut.kt]

View File

@@ -2,20 +2,20 @@
// [FILE] ItemSummary.kt // [FILE] ItemSummary.kt
// [SEMANTICS] data_structure, entity, summary // [SEMANTICS] data_structure, entity, summary
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemSummary')]
/** /**
* [CONTRACT] * @summary Сокращенная модель данных для представления "Вещи" в списках.
* Сокращенная модель данных для представления "Вещи" в списках. * @param id Уникальный идентификатор вещи.
* @property id Уникальный идентификатор вещи. * @param name Название вещи.
* @property name Название вещи. * @param assetId Идентификатор актива.
* @property assetId Идентификатор актива. * @param image Основное изображение. Может быть null.
* @property image Основное изображение. Может быть null. * @param isArchived Флаг архивации.
* @property isArchived Флаг архивации. * @param labels Список меток.
* @property labels Список меток. * @param location Местоположение. Может быть null.
* @property location Местоположение. Может быть null. * @param value Стоимость.
* @property value Стоимость. * @param createdAt Дата и время создания.
* @property createdAt Дата и время создания. * @param updatedAt Дата и время последнего обновления.
* @property updatedAt Дата и время последнего обновления.
*/ */
data class ItemSummary( data class ItemSummary(
val id: String, val id: String,
@@ -29,4 +29,5 @@ data class ItemSummary(
val createdAt: String, val createdAt: String,
val updatedAt: String val updatedAt: String
) )
// [END_ENTITY: DataClass('ItemSummary')]
// [END_FILE_ItemSummary.kt] // [END_FILE_ItemSummary.kt]

View File

@@ -2,24 +2,24 @@
// [FILE] ItemUpdate.kt // [FILE] ItemUpdate.kt
// [SEMANTICS] data_structure, entity, input, update // [SEMANTICS] data_structure, entity, input, update
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemUpdate')]
/** /**
* [CONTRACT] * @summary Модель данных для обновления существующей "Вещи".
* Модель данных для обновления существующей "Вещи". * @param name Название вещи.
* @property name Название вещи. * @param assetId Идентификатор актива.
* @property assetId Идентификатор актива. * @param description Описание.
* @property description Описание. * @param notes Заметки.
* @property notes Заметки. * @param serialNumber Серийный номер.
* @property serialNumber Серийный номер. * @param quantity Количество.
* @property quantity Количество. * @param isArchived Флаг архивации.
* @property isArchived Флаг архивации. * @param value Стоимость.
* @property value Стоимость. * @param purchasePrice Цена покупки.
* @property purchasePrice Цена покупки. * @param purchaseDate Дата покупки.
* @property purchaseDate Дата покупки. * @param warrantyUntil Гарантия до.
* @property warrantyUntil Гарантия до. * @param locationId ID местоположения.
* @property locationId ID местоположения. * @param parentId ID родительской вещи.
* @property parentId ID родительской вещи. * @param labelIds Список ID меток для полной замены.
* @property labelIds Список ID меток для полной замены.
*/ */
data class ItemUpdate( data class ItemUpdate(
val name: String?, val name: String?,
@@ -37,4 +37,5 @@ data class ItemUpdate(
val parentId: String?, val parentId: String?,
val labelIds: List<String>? val labelIds: List<String>?
) )
// [END_ENTITY: DataClass('ItemUpdate')]
// [END_FILE_ItemUpdate.kt] // [END_FILE_ItemUpdate.kt]

View File

@@ -1,18 +1,18 @@
// [PACKAGE] com.homebox.lens.domain.model // [PACKAGE] com.homebox.lens.domain.model
// [FILE] Label.kt // [FILE] Label.kt
// [SEMANTICS] domain, model
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CONTRACT] // [ENTITY: DataClass('Label')]
/** /**
* [ENTITY: DataClass('Label')] * @summary Представляет собой метку (тег), которую можно присвоить вещи.
* [PURPOSE] Представляет собой метку (тег), которую можно присвоить вещи. * @param id Уникальный идентификатор метки.
* @property id Уникальный идентификатор метки. * @param name Название метки.
* @property name Название метки.
*/ */
data class Label( data class Label(
val id: String, val id: String,
val name: String val name: String
) )
// [END_ENTITY: DataClass('Label')]
// [END_FILE_Label.kt] // [END_FILE_Label.kt]

View File

@@ -0,0 +1,18 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] LabelCreate.kt
// [SEMANTICS] data_structure, contract, label, create
package com.homebox.lens.domain.model
// [ENTITY: DataClass('LabelCreate')]
/**
* @summary Модель с данными, необходимыми для создания новой метки.
* @param name Название новой метки. Обязательное поле.
* @param color Цвет метки в формате HEX. Необязательное поле.
* @invariant name не может быть пустым.
*/
data class LabelCreate(
val name: String,
val color: String?
)
// [END_ENTITY: DataClass('LabelCreate')]
// [END_FILE_LabelCreate.kt]

View File

@@ -2,16 +2,16 @@
// [FILE] LabelOut.kt // [FILE] LabelOut.kt
// [SEMANTICS] data_structure, entity, label // [SEMANTICS] data_structure, entity, label
package com.homebox.lens.domain.model package com.homebox.lens.domain.model
// [CORE-LOGIC]
// [ENTITY: DataClass('LabelOut')]
/** /**
* [CONTRACT] * @summary Модель данных для представления метки (тега).
* Модель данных для представления метки (тега). * @param id Уникальный идентификатор.
* @property id Уникальный идентификатор. * @param name Название метки.
* @property name Название метки. * @param color Цвет метки в формате HEX (например, "#FF0000").
* @property color Цвет метки в формате HEX (например, "#FF0000"). * @param isArchived Флаг, указывающий, заархивирована ли метка.
* @property isArchived Флаг, указывающий, заархивирована ли метка. * @param createdAt Дата и время создания.
* @property createdAt Дата и время создания. * @param updatedAt Дата и время последнего обновления.
* @property updatedAt Дата и время последнего обновления.
*/ */
data class LabelOut( data class LabelOut(
val id: String, val id: String,
@@ -21,4 +21,5 @@ data class LabelOut(
val createdAt: String, val createdAt: String,
val updatedAt: String val updatedAt: String
) )
// [END_ENTITY: DataClass('LabelOut')]
// [END_FILE_LabelOut.kt] // [END_FILE_LabelOut.kt]

Some files were not shown because too many files have changed in this diff Show More