32 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
206 changed files with 13945 additions and 7418 deletions

3
.gitignore vendored
View File

@@ -35,4 +35,5 @@ output.json
# 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>

View File

@@ -6,11 +6,35 @@
</PROJECT_INFO>
<TECHNICAL_DECISIONS>
<DECISION id="tech_logging">
<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="defined">
<DECISION id="tech_i18n" status="implemented">
<summary>Интернационализация (Мультиязычность)</summary>
<description>
Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории.
@@ -21,135 +45,207 @@
- В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`).
</description>
</DECISION>
<DECISION id="tech_ui_framework" status="defined">
<DECISION id="tech_ui_framework" status="implemented">
<summary>UI Framework</summary>
<description>Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных.</description>
</DECISION>
<DECISION id="tech_di" status="defined">
<DECISION id="tech_di" status="implemented">
<summary>Внедрение зависимостей (Dependency Injection)</summary>
<description>Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях.</description>
</DECISION>
<DECISION id="tech_navigation" status="defined">
<DECISION id="tech_navigation" status="implemented">
<summary>Навигация</summary>
<description>Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation.</description>
</DECISION>
<DECISION id="tech_async" status="defined">
<DECISION id="tech_async" status="implemented">
<summary>Асинхронные операции</summary>
<description>Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока.</description>
</DECISION>
<DECISION id="tech_networking" status="defined">
<DECISION id="tech_networking" status="implemented">
<summary>Сетевое взаимодействие</summary>
<description>Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON.</description>
</DECISION>
<DECISION id="tech_database" status="defined">
<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="бэкенд_реализован">
<FEATURE id="feat_dashboard" status="implemented">
<summary>Экран панели управления</summary>
<description>Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам.</description>
<UI_COMPONENT ref_id="screen_dashboard" />
<FUNCTIONALITY>
<FUNCTION id="func_get_stats" status="реализовано">
<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="бэкенд_реализован">
<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="реализовано">
<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="реализовано">
<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="бэкенд_реализован">
<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="реализовано">
<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="бэкенд_реализован">
<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="реализовано">
<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="реализовано">
<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="реализовано">
<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="бэкенд_реализован">
<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="реализовано">
<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="реализовано">
<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="бэкенд_реализован">
<FEATURE id="feat_search" status="implemented">
<summary>Экран поиска</summary>
<description>Предоставляет специальный пользовательский интерфейс для поиска товаров.</description>
<UI_COMPONENT ref_id="screen_search" />
<FUNCTIONALITY>
<FUNCTION id="func_search_items_dedicated" status="реализовано">
<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="defined">
<SCREEN id="screen_dashboard" status="implemented">
<summary>Главный экран "Панель управления"</summary>
<description>
Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox.
@@ -186,10 +282,19 @@
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на чип местоположения/метки</action>
<reaction>Навигация на экран списка инвентаря с фильтром.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на кнопку "Создать"</action>
<reaction>Открытие экрана редактирования нового товара.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<!-- [ЯКОРЬ] Начало спецификации UI для экрана Локаций. Добавлено на основе референса HomeBox. -->
<SCREEN id="screen_locations_list" status="defined">
<SCREEN id="screen_locations_list" status="implemented">
<summary>Экран "Локации"</summary>
<description>
Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer).
@@ -230,10 +335,8 @@
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<!-- [ЯКОРЬ] Конец спецификации UI для экрана Локаций. -->
<!-- [ЯКОРЬ] Начало спецификации UI для экрана Меток. -->
<SCREEN id="screen_labels_list" status="defined">
<SCREEN id="screen_labels_list" status="implemented">
<summary>Экран "Метки"</summary>
<description>
Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
@@ -268,10 +371,192 @@
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<!-- [ЯКОРЬ] Конец спецификации UI для экрана Меток. -->
<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" />
@@ -295,4 +580,4 @@
<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>
</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 {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
@@ -30,7 +31,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
}
}
@@ -45,9 +46,7 @@ android {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.composeCompiler
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -60,6 +59,18 @@ dependencies {
implementation(project(":data"))
// [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity)
implementation(project(":domain"))
implementation(project(":feature:scan"))
implementation(project(":feature:dashboard"))
implementation(project(":feature:inventorylist"))
implementation(project(":feature:itemdetails"))
implementation(project(":feature:itemedit"))
implementation(project(":feature:labeledit"))
implementation(project(":feature:labelslist"))
implementation(project(":feature:locationedit"))
implementation(project(":feature:locationslist"))
implementation(project(":feature:search"))
implementation(project(":feature:settings"))
implementation(project(":feature:setup"))
// [DEPENDENCY] AndroidX
implementation(Libs.coreKtx)
@@ -67,11 +78,12 @@ dependencies {
implementation(Libs.activityCompose)
// [DEPENDENCY] Compose
implementation(platform(Libs.composeBom))
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
@@ -84,9 +96,13 @@ dependencies {
// [DEPENDENCY] Testing
testImplementation(Libs.junit)
testImplementation(Libs.kotestRunnerJunit5)
testImplementation(Libs.kotestAssertionsCore)
testImplementation(Libs.mockk)
testImplementation("app.cash.turbine:turbine:1.1.0")
androidTestImplementation(Libs.extJunit)
androidTestImplementation(Libs.espressoCore)
androidTestImplementation(platform(Libs.composeBom))
androidTestImplementation(Libs.composeUiTestJunit4)
debugImplementation(Libs.composeUiTooling)
debugImplementation(Libs.composeUiTestManifest)

View File

@@ -1,8 +1,8 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainActivity.kt
// [FILE] app/src/main/java/com/homebox/lens/MainActivity.kt
// [SEMANTICS] ui, activity, entrypoint
package com.homebox.lens
// [IMPORTS]
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -13,50 +13,80 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.homebox.lens.navigation.NavGraph
import com.homebox.lens.ui.theme.HomeboxLensTheme
import com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme
import com.homebox.lens.feature.dashboard.navigation.navGraph
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
// [END_IMPORTS]
// [ENTITY: Activity('MainActivity')]
// [CONTRACT]
/**
* [ENTITY: Activity('MainActivity')]
* [PURPOSE] Главная и единственная Activity в приложении.
* @summary Главная и единственная Activity в приложении.
*/
// [ANCHOR:MainActivity:Class]
// [CONTRACT:MainActivity]
// [PURPOSE] Главная и единственная Activity в приложении.
// [END_CONTRACT:MainActivity]
@AndroidEntryPoint
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?) {
super.onCreate(savedInstanceState)
Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.")
setContent {
HomeboxLensTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
color = MaterialTheme.colorScheme.background,
) {
NavGraph()
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
fun Greeting(name: String, modifier: Modifier = Modifier) {
fun greeting(
name: String,
modifier: Modifier = Modifier,
) {
Text(
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)
@Composable
fun GreetingPreview() {
fun greetingPreview() {
HomeboxLensTheme {
Greeting("Android")
greeting("Android")
}
}
// [END_FILE_MainActivity.kt]
// [END_ANCHOR:greetingPreview]
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]

View File

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

View File

@@ -1,88 +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(
onNavigateToLocations = { navController.navigate(Screen.LocationsList.route) },
onNavigateToSearch = { navController.navigate(Screen.Search.route) },
onNavigateToCreateItem = { navController.navigate(Screen.ItemEdit.createRoute("new")) },
onLogout = {
navController.navigate(Screen.Setup.route) {
popUpTo(Screen.Dashboard.route) { inclusive = true }
}
}
)
}
composable(route = Screen.InventoryList.route) {
InventoryListScreen()
}
composable(route = Screen.ItemDetails.route) {
ItemDetailsScreen()
}
composable(route = Screen.ItemEdit.route) {
ItemEditScreen()
}
composable(route = Screen.LabelsList.route) {
LabelsListScreen(
onNavigateBack = { navController.popBackStack() },
onLabelClick = { labelId ->
// TODO: Navigate to a pre-filtered inventory list screen
navController.navigate(Screen.InventoryList.route)
},
onAddNewLabelClick = {
// TODO: Navigate to a screen for creating a new label
}
)
}
composable(route = Screen.LocationsList.route) {
LocationsListScreen(
onNavigateBack = { navController.popBackStack() },
onLocationClick = { locationId ->
// TODO: Navigate to a pre-filtered inventory list screen
navController.navigate(Screen.InventoryList.route)
},
onAddNewLocationClick = {
// TODO: Navigate to a screen for creating a new location
}
)
}
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,385 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardScreen.kt
package com.homebox.lens.ui.screen.dashboard
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.domain.model.*
import com.homebox.lens.ui.theme.HomeboxLensTheme
import kotlinx.coroutines.launch
// [ANCHOR] Главная точка входа для экрана Dashboard
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel(),
onNavigateToLocations: () -> Unit,
onNavigateToSearch: () -> Unit,
onNavigateToCreateItem: () -> Unit,
onLogout: () -> Unit
) {
// [ACTION] Собираем состояние из ViewModel
val uiState by viewModel.uiState.collectAsState()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
// [ANCHOR] Определяем навигационное меню
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
DrawerContent(
onNavigateToLocations = onNavigateToLocations,
onNavigateToSearch = onNavigateToSearch,
onNavigateToCreateItem = onNavigateToCreateItem,
onLogout = onLogout,
onCloseDrawer = { scope.launch { drawerState.close() } }
)
}
) {
// [ANCHOR] Основной Scaffold экрана
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.dashboard_title)) },
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Default.Menu, contentDescription = stringResource(id = R.string.cd_open_navigation_drawer))
}
},
actions = {
IconButton(onClick = { /* TODO: Handle scanner click */ }) {
Icon(Icons.Default.Search, contentDescription = stringResource(id = R.string.cd_scan_qr_code))
}
}
)
}
) { paddingValues ->
// [ANCHOR] Основной контент экрана
DashboardContent(
modifier = Modifier.padding(paddingValues),
uiState = uiState,
onLocationClick = { /* TODO */ },
onLabelClick = { /* TODO */ }
)
}
}
}
// [ANCHOR] Компонент основного контента
@Composable
private fun DashboardContent(
modifier: Modifier = Modifier,
uiState: DashboardUiState,
onLocationClick: (LocationOutCount) -> Unit,
onLabelClick: (LabelOut) -> Unit
) {
// [FIX] Based on the UiState, we decide what to show
when (uiState) {
is DashboardUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is DashboardUiState.Error -> {
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
Text(
text = uiState.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
)
}
}
is DashboardUiState.Success -> {
LazyColumn(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
// [ANCHOR] Секция "Быстрая статистика"
item {
StatisticsSection(statistics = uiState.statistics)
}
// [ANCHOR] Секция "Недавно добавлено"
item {
// TODO: Add recently added items to UiState and display them here
// RecentlyAddedSection(items = uiState.recentlyAddedItems)
}
// [ANCHOR] Секция "Места хранения"
item {
LocationsSection(locations = uiState.locations, onLocationClick = onLocationClick)
}
// [ANCHOR] Секция "Метки"
item {
LabelsSection(labels = uiState.labels, onLabelClick = onLabelClick)
}
item { Spacer(modifier = Modifier.height(16.dp)) }
}
}
}
}
// [ANCHOR] Секция статистики
@Composable
private fun StatisticsSection(statistics: GroupStatistics) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_quick_stats),
style = MaterialTheme.typography.titleMedium
)
Card {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.height(120.dp).fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) }
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) }
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) }
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) }
}
}
}
}
@Composable
private fun StatisticCard(title: String, value: String) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center)
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
}
}
// [ANCHOR] Секция недавно добавленных
@Composable
private fun RecentlyAddedSection(items: List<ItemSummary>) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_recently_added),
style = MaterialTheme.typography.titleMedium
)
if (items.isEmpty()) {
Text(
text = stringResource(id = R.string.items_not_found),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
textAlign = TextAlign.Center
)
} else {
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
items(items) { item ->
ItemCard(item = item)
}
}
}
}
}
@Composable
private fun ItemCard(item: ItemSummary) {
Card(modifier = Modifier.width(150.dp)) {
Column(modifier = Modifier.padding(8.dp)) {
// TODO: Add image here from item.image
Spacer(modifier = Modifier.height(80.dp).fillMaxWidth().background(MaterialTheme.colorScheme.secondaryContainer))
Spacer(modifier = Modifier.height(8.dp))
Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1)
}
}
}
// [ANCHOR] Секция местоположений
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick: (LocationOutCount) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_locations),
style = MaterialTheme.typography.titleMedium
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
locations.forEach { location ->
SuggestionChip(
onClick = { onLocationClick(location) },
label = { Text("${location.name} (${location.itemCount})") }
)
}
}
}
}
// [ANCHOR] Секция меток
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_labels),
style = MaterialTheme.typography.titleMedium
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
labels.forEach { label ->
SuggestionChip(
onClick = { onLabelClick(label) },
label = { Text(label.name) }
)
}
}
}
}
// [ANCHOR] Контент бокового меню
@Composable
private fun DrawerContent(
onNavigateToLocations: () -> Unit,
onNavigateToSearch: () -> Unit,
onNavigateToCreateItem: () -> Unit,
onLogout: () -> Unit,
onCloseDrawer: () -> Unit
) {
ModalDrawerSheet {
Spacer(Modifier.height(12.dp))
Button(
onClick = {
onNavigateToCreateItem()
onCloseDrawer()
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text(stringResource(id = R.string.create))
}
Spacer(Modifier.height(12.dp))
Divider()
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.dashboard_title)) },
selected = true,
onClick = { onCloseDrawer() }
)
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.nav_locations)) },
selected = false,
onClick = {
onNavigateToLocations()
onCloseDrawer()
}
)
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.search)) },
selected = false,
onClick = {
onNavigateToSearch()
onCloseDrawer()
}
)
// TODO: Add Profile and Tools items
Divider()
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.logout)) },
selected = false,
onClick = {
onLogout()
onCloseDrawer()
}
)
}
}
// [ANCHOR] Preview для DashboardContent
@Preview(showBackground = true, name = "Dashboard Success State")
@Composable
fun DashboardContentSuccessPreview() {
val previewState = DashboardUiState.Success(
statistics = GroupStatistics(
items = 123,
totalValue = 9999.99,
locations = 5,
labels = 8
),
locations = listOf(
LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""),
LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""),
LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""),
LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""),
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
),
labels = listOf(
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
)
)
HomeboxLensTheme {
DashboardContent(
uiState = previewState,
onLocationClick = {},
onLabelClick = {}
)
}
}
@Preview(showBackground = true, name = "Dashboard Loading State")
@Composable
fun DashboardContentLoadingPreview() {
HomeboxLensTheme {
DashboardContent(
uiState = DashboardUiState.Loading,
onLocationClick = {},
onLabelClick = {}
)
}
}
@Preview(showBackground = true, name = "Dashboard Error State")
@Composable
fun DashboardContentErrorPreview() {
HomeboxLensTheme {
DashboardContent(
uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)),
onLocationClick = {},
onLabelClick = {}
)
}
}

View File

@@ -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,172 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt
// [SEMANTICS] ui, screen, labels_list, compose
package com.homebox.lens.ui.screen.labelslist
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Add
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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.ui.theme.HomeboxLensTheme
// [ENTRYPOINT]
/**
* [CONTRACT]
* @summary Главная Composable-функция для экрана списка меток.
* @param onNavigateBack Функция для навигации на предыдущий экран.
* @param onLabelClick Функция, вызываемая при нажатии на метку.
* @param onAddNewLabelClick Функция, вызываемая при нажатии на FAB для добавления новой метки.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LabelsListScreen(
viewModel: LabelsListViewModel = hiltViewModel(),
onNavigateBack: () -> Unit,
onLabelClick: (String) -> Unit,
onAddNewLabelClick: () -> Unit
) {
// [STATE]
val uiState by viewModel.uiState.collectAsState()
// [UI]
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.nav_labels)) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.cd_navigate_back)
)
}
}
)
},
floatingActionButton = {
FloatingActionButton(onClick = onAddNewLabelClick) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(id = R.string.cd_add_new_label)
)
}
}
) { paddingValues ->
// [ANCHOR] Основной контент в зависимости от состояния
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (val state = uiState) {
is LabelsListUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is LabelsListUiState.Error -> {
Text(
text = state.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.Center).padding(16.dp)
)
}
is LabelsListUiState.Success -> {
LabelsList(
labels = state.labels,
onLabelClick = onLabelClick
)
}
}
}
}
}
// [HELPER]
/**
* [CONTRACT]
* @summary Отображает список меток.
*/
@Composable
private fun LabelsList(
labels: List<LabelOut>,
onLabelClick: (String) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(labels) { label ->
LabelListItem(
label = label,
onClick = { onLabelClick(label.id) }
)
}
}
}
// [HELPER]
/**
* [CONTRACT]
* @summary Отображает один элемент списка меток.
*/
@Composable
private fun LabelListItem(
label: LabelOut,
onClick: () -> Unit
) {
ListItem(
headlineContent = { Text(label.name) },
leadingContent = {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null
)
},
modifier = Modifier.clickable(onClick = onClick)
)
}
// [PREVIEW]
@Preview(showBackground = true, name = "Labels List Success")
@Composable
private fun LabelsListScreenSuccessPreview() {
val labels = listOf(
LabelOut("1", "Electronics", "#FF0000", false, "", ""),
LabelOut("2", "Books", "#00FF00", false, "", ""),
LabelOut("3", "Winter Clothes", "#0000FF", false, "", "")
)
HomeboxLensTheme {
LabelsList(labels = labels, onLabelClick = {})
}
}
// [PREVIEW]
@Preview(showBackground = true, name = "Labels List Empty")
@Composable
private fun LabelsListScreenEmptyPreview() {
HomeboxLensTheme {
LabelsList(labels = emptyList(), onLabelClick = {})
}
}
// [END_FILE_LabelsListScreen.kt]

View File

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

View File

@@ -1,73 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListViewModel.kt
// [SEMANTICS] ui_logic, labels_list, state_management
package com.homebox.lens.ui.screen.labelslist
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
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('LabelsListViewModel')]
/**
* [CONTRACT]
* @summary ViewModel для экрана со списком меток.
* @description Управляет состоянием экрана, загружает список меток и обрабатывает ошибки.
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
*/
@HiltViewModel
class LabelsListViewModel @Inject constructor(
private val getAllLabelsUseCase: GetAllLabelsUseCase
) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
val uiState = _uiState.asStateFlow()
// [LIFECYCLE_HANDLER]
init {
loadLabels()
}
/**
* [CONTRACT]
* @summary Загружает список меток.
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error`.
* @sideeffect Асинхронно обновляет `_uiState`.
*/
fun loadLabels() {
// [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = LabelsListUiState.Loading
Timber.i("[ACTION] Starting labels list load. State -> Loading.")
// [CORE-LOGIC]
val result = runCatching {
getAllLabelsUseCase()
}
// [RESULT_HANDLER]
result.fold(
onSuccess = { labels ->
Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labels.size}. State -> Success.")
_uiState.value = LabelsListUiState.Success(labels)
},
onFailure = { exception ->
Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.")
_uiState.value = LabelsListUiState.Error(
message = exception.message ?: "Could not load labels."
)
}
)
}
}
// [END_CLASS_LabelsListViewModel]
}
// [END_FILE_LabelsListViewModel.kt]

View File

@@ -1,175 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListScreen.kt
// [SEMANTICS] ui, screen, locations_list, compose
package com.homebox.lens.ui.screen.locationslist
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Place
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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.ui.theme.HomeboxLensTheme
// [ENTRYPOINT]
/**
* [CONTRACT]
* @summary Главная Composable-функция для экрана списка локаций.
* @param onNavigateBack Функция для навигации на предыдущий экран.
* @param onLocationClick Функция, вызываемая при нажатии на локацию.
* @param onAddNewLocationClick Функция, вызываемая при нажатии на FAB для добавления новой локации.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LocationsListScreen(
viewModel: LocationsListViewModel = hiltViewModel(),
onNavigateBack: () -> Unit,
onLocationClick: (String) -> Unit,
onAddNewLocationClick: () -> Unit
) {
// [STATE]
val uiState by viewModel.uiState.collectAsState()
// [UI]
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.nav_locations)) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.cd_navigate_back)
)
}
}
)
},
floatingActionButton = {
FloatingActionButton(onClick = onAddNewLocationClick) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(id = R.string.cd_add_new_location)
)
}
}
) { paddingValues ->
// [ANCHOR] Основной контент в зависимости от состояния
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (val state = uiState) {
is LocationsListUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is LocationsListUiState.Error -> {
Text(
text = state.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.Center).padding(16.dp)
)
}
is LocationsListUiState.Success -> {
LocationsList(
locations = state.locations,
onLocationClick = onLocationClick
)
}
}
}
}
}
// [HELPER]
/**
* [CONTRACT]
* @summary Отображает список локаций.
*/
@Composable
private fun LocationsList(
locations: List<LocationOutCount>,
onLocationClick: (String) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(locations) { location ->
LocationListItem(
location = location,
onClick = { onLocationClick(location.id) }
)
}
}
}
// [HELPER]
/**
* [CONTRACT]
* @summary Отображает один элемент списка локаций.
*/
@Composable
private fun LocationListItem(
location: LocationOutCount,
onClick: () -> Unit
) {
ListItem(
headlineContent = { Text(location.name) },
leadingContent = {
Icon(
imageVector = Icons.Default.Place,
contentDescription = null
)
},
trailingContent = {
Text(text = location.itemCount.toString())
},
modifier = Modifier.clickable(onClick = onClick)
)
}
// [PREVIEW]
@Preview(showBackground = true, name = "Locations List Success")
@Composable
private fun LocationsListScreenSuccessPreview() {
val locations = listOf(
LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""),
LocationOutCount("2", "Kitchen", "#00FF00", false, 3, "", ""),
LocationOutCount("3", "Office", "#0000FF", false, 25, "", "")
)
HomeboxLensTheme {
LocationsList(locations = locations, onLocationClick = {})
}
}
// [PREVIEW]
@Preview(showBackground = true, name = "Locations List Empty")
@Composable
private fun LocationsListScreenEmptyPreview() {
HomeboxLensTheme {
LocationsList(locations = emptyList(), onLocationClick = {})
}
}
// [END_FILE_LocationsListScreen.kt]

View File

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

View File

@@ -1,73 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListViewModel.kt
// [SEMANTICS] ui_logic, locations_list, state_management
package com.homebox.lens.ui.screen.locationslist
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
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('LocationsListViewModel')]
/**
* [CONTRACT]
* @summary ViewModel для экрана со списком локаций.
* @description Управляет состоянием экрана, загружает список локаций и обрабатывает ошибки.
* @invariant `uiState` всегда является одним из состояний, определенных в `LocationsListUiState`.
*/
@HiltViewModel
class LocationsListViewModel @Inject constructor(
private val getAllLocationsUseCase: GetAllLocationsUseCase
) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
val uiState = _uiState.asStateFlow()
// [LIFECYCLE_HANDLER]
init {
loadLocations()
}
/**
* [CONTRACT]
* @summary Загружает список локаций.
* @description Выполняет `GetAllLocationsUseCase` и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error`.
* @sideeffect Асинхронно обновляет `_uiState`.
*/
fun loadLocations() {
// [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = LocationsListUiState.Loading
Timber.i("[ACTION] Starting locations list load. State -> Loading.")
// [CORE-LOGIC]
val result = runCatching {
getAllLocationsUseCase()
}
// [RESULT_HANDLER]
result.fold(
onSuccess = { locations ->
Timber.i("[SUCCESS] Locations loaded successfully. Count: ${locations.size}. State -> Success.")
_uiState.value = LocationsListUiState.Success(locations)
},
onFailure = { exception ->
Timber.e(exception, "[ERROR] Failed to load locations. State -> Error.")
_uiState.value = LocationsListUiState.Error(
message = exception.message ?: "Could not load locations."
)
}
)
}
}
// [END_CLASS_LocationsListViewModel]
}
// [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

@@ -3,6 +3,8 @@
<!-- 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>
@@ -12,9 +14,11 @@
<!-- 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="cd_add_new_label">Add new label</string>
<string name="content_desc_add_label">Add new label</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string>
@@ -22,6 +26,7 @@
<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>
@@ -33,4 +38,109 @@
<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

@@ -3,6 +3,8 @@
<!-- 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>
@@ -11,10 +13,34 @@
<!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Открыть боковое меню</string>
<string name="cd_scan_qr_code">Сканировать QR-код</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="cd_add_new_label">Добавить новую метку</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>
@@ -22,6 +48,7 @@
<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>
@@ -33,4 +60,81 @@
<string name="nav_locations">Локации</string>
<string name="nav_labels">Метки</string>
</resources>
<!-- 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>

View File

@@ -1,13 +1,13 @@
// [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 {
// [PLUGIN] Android Application plugin
id("com.android.application") version "8.11.0" apply false
// [PLUGIN] Kotlin Android plugin
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
// [PLUGIN] Hilt Android plugin
id("com.google.dagger.hilt.android") version "2.48.1" apply false
id("com.android.application") version "8.12.3" apply false
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
id("com.google.dagger.hilt.android") version "2.51.1" apply false
id("com.google.devtools.ksp") version "2.0.0-1.0.24" apply false
}
// [END_FILE_build.gradle.kts]

View File

@@ -1,72 +1,56 @@
// [PACKAGE] buildsrc.dependencies
// [FILE] Dependencies.kt
// [PURPOSE] Centralized dependency management for the entire project.
// [SEMANTICS] build, dependencies
// [ENTITY: Object('Versions')]
object Versions {
// Build
const val compileSdk = 34
const val minSdk = 26
const val minSdk = 24
const val targetSdk = 34
const val versionCode = 1
const val versionName = "1.0"
// Kotlin
const val kotlin = "1.9.22"
const val kotlin = "1.9.10"
const val coroutines = "1.7.3"
// Jetpack Compose
const val composeCompiler = "1.5.8"
const val composeBom = "2023.10.01"
const val composeCompiler = "1.5.4"
const val composeBom = "2024.05.00"
const val activityCompose = "1.8.2"
const val navigationCompose = "2.7.6"
const val navigationCompose = "2.7.7"
const val hiltNavigationCompose = "1.1.0"
// AndroidX
const val coreKtx = "1.12.0"
const val lifecycle = "2.6.2"
const val lifecycle = "2.7.0"
const val appcompat = "1.6.1"
// Networking
const val retrofit = "2.9.0"
const val okhttp = "4.12.0"
const val moshi = "1.15.0"
// Database
const val moshi = "1.15.1"
const val room = "2.6.1"
// DI
const val hilt = "2.48.1"
const val hiltCompiler = "1.1.0"
// Logging
const val hilt = "2.51.1"
const val hiltCompiler = "1.2.0"
const val timber = "5.0.1"
// Testing
const val junit = "4.13.2"
const val extJunit = "1.1.5"
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 {
// Kotlin
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${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 lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
// Compose
const val composeBom = "androidx.compose:compose-bom:${Versions.composeBom}"
const val composeUi = "androidx.compose.ui:ui"
const val composeUiGraphics = "androidx.compose.ui:ui-graphics"
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview"
const val composeMaterial3 = "androidx.compose.material3:material3"
const val composeUi = "androidx.compose.ui:ui:1.5.4"
const val composeUiGraphics = "androidx.compose.ui:ui-graphics:1.5.4"
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview:1.5.4"
const val composeMaterial3 = "androidx.compose.material3:material3:1.1.2"
const val composeFoundation = "androidx.compose.foundation:foundation:1.5.4"
const val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:1.5.4"
const val composeMaterialIconsExtended = "androidx.compose.material:material-icons-extended:1.5.4"
const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}"
const val navigationCompose = "androidx.navigation:navigation-compose:${Versions.navigationCompose}"
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 converterMoshi = "com.squareup.retrofit2:converter-moshi:${Versions.retrofit}"
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 moshiKotlin = "com.squareup.moshi:moshi-kotlin:${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 roomKtx = "androidx.room:room-ktx:${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 hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}"
// Logging
const val timber = "com.jakewharton.timber:timber:${Versions.timber}"
// Testing
const val junit = "junit:junit:${Versions.junit}"
const val extJunit = "androidx.test.ext:junit:${Versions.extJunit}"
const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}"
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4"
const val composeUiTooling = "androidx.compose.ui:ui-tooling"
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.5.4"
const val composeUiTooling = "androidx.compose.ui:ui-tooling:1.5.4"
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)
kapt(Libs.hiltCompiler)
// [DEPENDENCY] Logging
implementation(Libs.timber)
// [DEPENDENCY] Testing
testImplementation(Libs.junit)
androidTestImplementation(Libs.extJunit)

View File

@@ -1,73 +1,97 @@
// [PACKAGE] com.homebox.lens.data.api
// [FILE] HomeboxApiService.kt
// [SEMANTICS] data, api, retrofit
package com.homebox.lens.data.api
import com.homebox.lens.data.api.dto.GroupStatisticsDto
import com.homebox.lens.data.api.dto.ItemCreateDto
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
// [IMPORTS]
import com.homebox.lens.data.api.dto.*
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.*
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Interface('HomeboxApiService')]
/**
* [ENTITY: Interface('HomeboxApiService')]
* [PURPOSE] Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
* @summary Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
*/
interface HomeboxApiService {
// [ENDPOINT] Auth
// [FIX] Явно указываем заголовок Content-Type, чтобы переопределить
// значение по умолчанию от Moshi, которое содержит "; charset=UTF-8".
// [ENTITY: ApiEndpoint('login')]
@Headers("Content-Type: application/json")
@POST("v1/users/login")
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
// [END_ENTITY: ApiEndpoint('login')]
// [ENDPOINT] Items
// [ENTITY: ApiEndpoint('getItems')]
@GET("v1/items")
suspend fun getItems(
@Query("q") query: String? = null,
@Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int? = null
): PaginationResultDto<ItemSummaryDto>
// [END_ENTITY: ApiEndpoint('getItems')]
// [ENTITY: ApiEndpoint('createItem')]
@POST("v1/items")
suspend fun createItem(@Body item: ItemCreateDto): ItemSummaryDto
// [END_ENTITY: ApiEndpoint('createItem')]
// [ENTITY: ApiEndpoint('getItem')]
@GET("v1/items/{id}")
suspend fun getItem(@Path("id") itemId: String): ItemOutDto
// [END_ENTITY: ApiEndpoint('getItem')]
// [ENTITY: ApiEndpoint('updateItem')]
@PUT("v1/items/{id}")
suspend fun updateItem(@Path("id") itemId: String, @Body item: ItemUpdateDto): ItemOutDto
// [END_ENTITY: ApiEndpoint('updateItem')]
// [ENTITY: ApiEndpoint('deleteItem')]
@DELETE("v1/items/{id}")
suspend fun deleteItem(@Path("id") itemId: String): Response<Unit>
// [END_ENTITY: ApiEndpoint('deleteItem')]
// [ENDPOINT] Locations
// [ENTITY: ApiEndpoint('getLocations')]
@GET("v1/locations")
suspend fun getLocations(): List<LocationOutCountDto>
// [END_ENTITY: ApiEndpoint('getLocations')]
// [ENDPOINT] Labels
// [ENTITY: ApiEndpoint('getLabels')]
@GET("v1/labels")
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")
suspend fun getStatistics(): GroupStatisticsDto
// [END_ENTITY: ApiEndpoint('getStatistics')]
}
// [END_ENTITY: Interface('HomeboxApiService')]
// [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.JsonClass
import com.homebox.lens.domain.model.CustomField
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('CustomFieldDto')]
/**
* [CONTRACT]
* DTO для кастомного поля.
* @summary DTO для кастомного поля.
*/
@JsonClass(generateAdapter = true)
data class CustomFieldDto(
@@ -20,10 +20,12 @@ data class CustomFieldDto(
@Json(name = "value") val value: String,
@Json(name = "type") val type: String
)
// [END_ENTITY: DataClass('CustomFieldDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('CustomField')]
/**
* [CONTRACT]
* Маппер из CustomFieldDto в доменную модель CustomField.
* @summary Маппер из CustomFieldDto в доменную модель CustomField.
*/
fun CustomFieldDto.toDomain(): CustomField {
return CustomField(
@@ -32,3 +34,4 @@ fun CustomFieldDto.toDomain(): CustomField {
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.JsonClass
import com.homebox.lens.domain.model.GroupStatistics
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('GroupStatisticsDto')]
/**
* [CONTRACT]
* DTO для статистики.
* [COHERENCE_NOTE] Этот DTO был исправлен, чтобы точно соответствовать JSON-ответу от сервера.
* Поля `items`, `labels`, `locations`, `totalValue` были заменены на `totalItems`, `totalLabels`,
* `totalLocations`, `totalItemPrice` и т.д., чтобы устранить ошибку парсинга `JsonDataException`.
* @summary DTO для статистики.
*/
@JsonClass(generateAdapter = true)
data class GroupStatisticsDto(
@@ -23,19 +20,17 @@ data class GroupStatisticsDto(
@Json(name = "totalLabels") val totalLabels: Int,
@Json(name = "totalLocations") val totalLocations: Int,
@Json(name = "totalItemPrice") val totalItemPrice: Double,
// [FIX] Добавляем недостающие поля, которые присутствуют в JSON, но отсутствовали в DTO.
// Делаем их nullable на случай, если API перестанет их присылать в будущем.
@Json(name = "totalUsers") val totalUsers: Int? = null,
@Json(name = "totalWithWarranty") val totalWithWarranty: Int? = null
)
// [END_ENTITY: DataClass('GroupStatisticsDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('GroupStatistics')]
/**
* [CONTRACT]
* Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
* [COHERENCE_NOTE] Маппер обновлен для использования правильных полей из исправленного DTO.
* @summary Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
*/
fun GroupStatisticsDto.toDomain(): GroupStatistics {
// [ACTION] Маппим данные из DTO в доменную модель.
return GroupStatistics(
items = this.totalItems,
labels = this.totalLabels,
@@ -43,4 +38,5 @@ fun GroupStatisticsDto.toDomain(): GroupStatistics {
totalValue = this.totalItemPrice
)
}
// [END_FILE_GroupStatisticsDto.kt]
// [END_ENTITY: Function('toDomain')]
// [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.JsonClass
import com.homebox.lens.domain.model.Image
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('ImageDto')]
/**
* [CONTRACT]
* DTO для изображения.
* @property id Уникальный идентификатор.
* @property path Путь к файлу.
* @property isPrimary Является ли основным.
* @summary DTO для изображения.
* @param id Уникальный идентификатор.
* @param path Путь к файлу.
* @param isPrimary Является ли основным.
*/
@JsonClass(generateAdapter = true)
data class ImageDto(
@@ -23,10 +23,12 @@ data class ImageDto(
@Json(name = "path") val path: String,
@Json(name = "isPrimary") val isPrimary: Boolean
)
// [END_ENTITY: DataClass('ImageDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('Image')]
/**
* [CONTRACT]
* Маппер из ImageDto в доменную модель Image.
* @summary Маппер из ImageDto в доменную модель Image.
*/
fun ImageDto.toDomain(): Image {
return Image(
@@ -35,3 +37,4 @@ fun ImageDto.toDomain(): Image {
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.JsonClass
import com.homebox.lens.domain.model.ItemAttachment
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemAttachmentDto')]
/**
* [CONTRACT]
* DTO для вложения.
* @summary DTO для вложения.
*/
@JsonClass(generateAdapter = true)
data class ItemAttachmentDto(
@@ -23,10 +23,12 @@ data class ItemAttachmentDto(
@Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String
)
// [END_ENTITY: DataClass('ItemAttachmentDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemAttachment')]
/**
* [CONTRACT]
* Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
* @summary Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
*/
fun ItemAttachmentDto.toDomain(): ItemAttachment {
return ItemAttachment(
@@ -38,3 +40,4 @@ fun ItemAttachmentDto.toDomain(): ItemAttachment {
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.JsonClass
import com.homebox.lens.domain.model.ItemCreate
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemCreateDto')]
/**
* [CONTRACT]
* DTO для создания вещи.
* @summary DTO для создания вещи.
*/
@JsonClass(generateAdapter = true)
data class ItemCreateDto(
@@ -30,10 +30,12 @@ data class ItemCreateDto(
@Json(name = "parentId") val parentId: String?,
@Json(name = "labelIds") val labelIds: List<String>?
)
// [END_ENTITY: DataClass('ItemCreateDto')]
// [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')]
/**
* [CONTRACT]
* Маппер из доменной модели ItemCreate в ItemCreateDto.
* @summary Маппер из доменной модели ItemCreate в ItemCreateDto.
*/
fun ItemCreate.toDto(): ItemCreateDto {
return ItemCreateDto(
@@ -52,3 +54,4 @@ fun ItemCreate.toDto(): ItemCreateDto {
labelIds = this.labelIds
)
}
// [END_ENTITY: Function('toDto')]

View File

@@ -1,16 +1,19 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemDto.kt
// [SEMANTICS] data, dto, api
package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
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')]
* [PURPOSE] DTO для полной информации о вещи (GET /v1/items/{id}).
* @summary DTO для полной информации о вещи (GET /v1/items/{id}).
*/
@JsonClass(generateAdapter = true)
data class ItemOut(
@@ -19,14 +22,16 @@ data class ItemOut(
@Json(name = "description") val description: String?,
@Json(name = "image") val image: String?,
@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 = "createdAt") val createdAt: String?
)
// [END_ENTITY: DataClass('ItemOut')]
// [ENTITY: DataClass('ItemSummary')]
// [RELATION: DataClass('ItemSummary')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
/**
* [ENTITY: DataClass('ItemSummary')]
* [PURPOSE] DTO для краткой информации о вещи в списках (GET /v1/items).
* @summary DTO для краткой информации о вещи в списках (GET /v1/items).
*/
@JsonClass(generateAdapter = true)
data class ItemSummary(
@@ -36,10 +41,11 @@ data class ItemSummary(
@Json(name = "location") val location: LocationOut?,
@Json(name = "createdAt") val createdAt: String?
)
// [END_ENTITY: DataClass('ItemSummary')]
// [ENTITY: DataClass('ItemCreate')]
/**
* [ENTITY: DataClass('ItemCreate')]
* [PURPOSE] DTO для создания новой вещи (POST /v1/items).
* @summary DTO для создания новой вещи (POST /v1/items).
*/
@JsonClass(generateAdapter = true)
data class ItemCreate(
@@ -49,10 +55,11 @@ data class ItemCreate(
@Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal?
)
// [END_ENTITY: DataClass('ItemCreate')]
// [ENTITY: DataClass('ItemUpdate')]
/**
* [ENTITY: DataClass('ItemUpdate')]
* [PURPOSE] DTO для обновления вещи (PUT /v1/items/{id}).
* @summary DTO для обновления вещи (PUT /v1/items/{id}).
*/
@JsonClass(generateAdapter = true)
data class ItemUpdate(
@@ -62,5 +69,6 @@ data class ItemUpdate(
@Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal?
)
// [END_ENTITY: DataClass('ItemUpdate')]
// [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.JsonClass
import com.homebox.lens.domain.model.ItemOut
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemOutDto')]
/**
* [CONTRACT]
* DTO для полной модели вещи.
* @summary DTO для полной модели вещи.
*/
@JsonClass(generateAdapter = true)
data class ItemOutDto(
@@ -37,12 +37,25 @@ data class ItemOutDto(
@Json(name = "fields") val fields: List<CustomFieldDto>,
@Json(name = "maintenance") val maintenance: List<MaintenanceEntryDto>,
@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]
* Маппер из ItemOutDto в доменную модель ItemOut.
* @summary Маппер из ItemOutDto в доменную модель ItemOut.
*/
fun ItemOutDto.toDomain(): ItemOut {
return ItemOut(
@@ -67,6 +80,18 @@ fun ItemOutDto.toDomain(): ItemOut {
fields = this.fields.map { it.toDomain() },
maintenance = this.maintenance.map { it.toDomain() },
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.JsonClass
import com.homebox.lens.domain.model.ItemSummary
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemSummaryDto')]
/**
* [CONTRACT]
* DTO для сокращенной модели вещи.
* @summary DTO для сокращенной модели вещи.
*/
@JsonClass(generateAdapter = true)
data class ItemSummaryDto(
@@ -27,10 +27,12 @@ data class ItemSummaryDto(
@Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String
)
// [END_ENTITY: DataClass('ItemSummaryDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
/**
* [CONTRACT]
* Маппер из ItemSummaryDto в доменную модель ItemSummary.
* @summary Маппер из ItemSummaryDto в доменную модель ItemSummary.
*/
fun ItemSummaryDto.toDomain(): ItemSummary {
return ItemSummary(
@@ -46,3 +48,4 @@ fun ItemSummaryDto.toDomain(): ItemSummary {
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.JsonClass
import com.homebox.lens.domain.model.ItemUpdate
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('ItemUpdateDto')]
/**
* [CONTRACT]
* DTO для обновления вещи.
* @summary DTO для обновления вещи.
*/
@JsonClass(generateAdapter = true)
data class ItemUpdateDto(
@@ -31,10 +31,12 @@ data class ItemUpdateDto(
@Json(name = "parentId") val parentId: String?,
@Json(name = "labelIds") val labelIds: List<String>?
)
// [END_ENTITY: DataClass('ItemUpdateDto')]
// [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemUpdateDto')]
/**
* [CONTRACT]
* Маппер из доменной модели ItemUpdate в ItemUpdateDto.
* @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto.
*/
fun ItemUpdate.toDto(): ItemUpdateDto {
return ItemUpdateDto(
@@ -54,3 +56,4 @@ fun ItemUpdate.toDto(): ItemUpdateDto {
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.JsonClass
import com.homebox.lens.domain.model.LabelOut
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('LabelOutDto')]
/**
* [CONTRACT]
* DTO для метки.
* [COHERENCE_NOTE] Поле `isArchived` сделано nullable (`Boolean?`),
* так как оно отсутствует в JSON-ответе от сервера. Это исправляет ошибку парсинга
* `JsonDataException: Required value 'isArchived' missing`.
* @summary DTO для метки.
*/
@JsonClass(generateAdapter = true)
data class LabelOutDto(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
// [COHERENCE_NOTE] Поле `color` может быть null или отсутствовать, делаем его nullable для безопасности.
@Json(name = "color") val color: String?,
// [FIX] Поле отсутствует в JSON, делаем nullable.
@Json(name = "isArchived") val isArchived: Boolean?,
@Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String,
// [COHERENCE_NOTE] Поле `description` также может отсутствовать, добавляем его как nullable.
@Json(name = "description") val description: String?
)
// [END_ENTITY: DataClass('LabelOutDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
/**
* [CONTRACT]
* Маппер из LabelOutDto в доменную модель LabelOut.
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
* и предоставления non-nullable значений по умолчанию для доменной модели.
* @summary Маппер из LabelOutDto в доменную модель LabelOut.
*/
fun LabelOutDto.toDomain(): LabelOut {
return LabelOut(
id = this.id,
name = this.name,
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию.
color = this.color ?: "", // Пустая строка как дефолтный цвет
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
color = this.color ?: "",
isArchived = this.isArchived ?: false,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
// [END_FILE_LabelOutDto.kt]
// [END_ENTITY: Function('toDomain')]
// [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
// [FILE] LocationDto.kt
// [SEMANTICS] data, dto, api, location
package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: DataClass('LocationOut')]
/**
* [ENTITY: DataClass('LocationOut')]
* [PURPOSE] DTO для информации о местоположении.
* @summary DTO для информации о местоположении.
*/
@JsonClass(generateAdapter = true)
data class LocationOut(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String
)
// [END_ENTITY: DataClass('LocationOut')]
// [ENTITY: DataClass('LocationOutCount')]
/**
* [ENTITY: DataClass('LocationOutCount')]
* [PURPOSE] DTO для информации о местоположении со счетчиком вещей.
* @summary DTO для информации о местоположении со счетчиком вещей.
*/
@JsonClass(generateAdapter = true)
data class LocationOutCount(
@@ -27,5 +29,6 @@ data class LocationOutCount(
@Json(name = "name") val name: String,
@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.JsonClass
import com.homebox.lens.domain.model.LocationOutCount
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('LocationOutCountDto')]
/**
* [CONTRACT]
* DTO для местоположения со счетчиком.
* [COHERENCE_NOTE] Поля `color` и `isArchived` сделаны nullable (`String?`, `Boolean?`),
* так как они отсутствуют в JSON-ответе от сервера. Это исправляет ошибку парсинга
* `JsonDataException: Required value '...' missing`.
* @summary DTO для местоположения со счетчиком.
*/
@JsonClass(generateAdapter = true)
data class LocationOutCountDto(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
// [FIX] Поле отсутствует в JSON, делаем nullable.
@Json(name = "color") val color: String?,
// [FIX] Поле отсутствует в JSON, делаем nullable.
@Json(name = "isArchived") val isArchived: Boolean?,
@Json(name = "itemCount") val itemCount: Int,
@Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String,
// [COHERENCE_NOTE] Поле `description` также может отсутствовать или быть null,
// поэтому его тоже безопасно сделать nullable.
@Json(name = "description") val description: String?
)
// [END_ENTITY: DataClass('LocationOutCountDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOutCount')]
/**
* [CONTRACT]
* Маппер из LocationOutCountDto в доменную модель LocationOutCount.
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
* и предоставления non-nullable значений по умолчанию для доменной модели.
* @summary Маппер из LocationOutCountDto в доменную модель LocationOutCount.
*/
fun LocationOutCountDto.toDomain(): LocationOutCount {
return LocationOutCount(
id = this.id,
name = this.name,
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию, если поле null.
color = this.color ?: "", // Пустая строка как дефолтный цвет
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
color = this.color ?: "",
isArchived = this.isArchived ?: false,
itemCount = this.itemCount,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
// [END_FILE_LocationOutCountDto.kt]
// [END_ENTITY: Function('toDomain')]
// [END_FILE_LocationOutCountDto.kt]

View File

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

View File

@@ -1,15 +1,16 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] PaginationDto.kt
// [SEMANTICS] data, dto, api, pagination
package com.homebox.lens.data.api.dto
// [IMPORTS]
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: DataClass('PaginationResult')]
/**
* [ENTITY: DataClass('PaginationResult')]
* [PURPOSE] DTO для пагинированных результатов от API.
* @summary DTO для пагинированных результатов от API.
*/
@JsonClass(generateAdapter = true)
data class PaginationResult<T>(
@@ -19,5 +20,6 @@ data class PaginationResult<T>(
@Json(name = "total") val total: 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.JsonClass
import com.homebox.lens.domain.model.PaginationResult
// [END_IMPORTS]
// [CORE-LOGIC]
// [ENTITY: DataClass('PaginationResultDto')]
/**
* [CONTRACT]
* DTO для постраничных результатов.
* @summary DTO для постраничных результатов.
*/
@JsonClass(generateAdapter = true)
data class PaginationResultDto<T>(
@@ -21,10 +21,12 @@ data class PaginationResultDto<T>(
@Json(name = "pageSize") val pageSize: Int,
@Json(name = "total") val total: Int
)
// [END_ENTITY: DataClass('PaginationResultDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('PaginationResult')]
/**
* [CONTRACT]
* Маппер из PaginationResultDto в доменную модель PaginationResult.
* @summary Маппер из PaginationResultDto в доменную модель PaginationResult.
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
*/
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
)
}
// [END_ENTITY: Function('toDomain')]

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
// [PACKAGE] com.homebox.lens.data.db
// [FILE] HomeboxDatabase.kt
// [SEMANTICS] data, database, room
package com.homebox.lens.data.db
// [IMPORTS]
import androidx.room.Database
import androidx.room.RoomDatabase
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.LocationDao
import com.homebox.lens.data.db.entity.*
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Database('HomeboxDatabase')]
/**
* [ENTITY: RoomDatabase('HomeboxDatabase')]
* [PURPOSE] Основной класс для работы с локальной базой данных Room.
* @summary Основной класс для работы с локальной базой данных Room.
*/
@Database(
entities = [
@@ -37,5 +38,6 @@ abstract class HomeboxDatabase : RoomDatabase() {
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
// [FILE] ItemDao.kt
// [SEMANTICS] data, database, dao, item
package com.homebox.lens.data.db.dao
// [IMPORTS]
import androidx.room.*
import com.homebox.lens.data.db.entity.ItemEntity
import com.homebox.lens.data.db.entity.ItemLabelCrossRef
import com.homebox.lens.data.db.entity.ItemWithLabels
import kotlinx.coroutines.flow.Flow
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Interface('ItemDao')]
/**
* [ENTITY: RoomDao('ItemDao')]
* [PURPOSE] Предоставляет методы для работы с 'items' в локальной БД.
* @summary Предоставляет методы для работы с 'items' в локальной БД.
*/
@Dao
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
@Query("SELECT * FROM items")
suspend fun getItems(): List<ItemWithLabels>
// [END_ENTITY: Function('getItems')]
// [ENTITY: Function('getItem')]
@Transaction
@Query("SELECT * FROM items WHERE id = :itemId")
suspend fun getItem(itemId: String): ItemWithLabels?
// [END_ENTITY: Function('getItem')]
// [ENTITY: Function('insertItems')]
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItems(items: List<ItemEntity>)
// [END_ENTITY: Function('insertItems')]
// [ENTITY: Function('insertItem')]
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItem(item: ItemEntity)
// [END_ENTITY: Function('insertItem')]
// [ENTITY: Function('deleteItem')]
@Query("DELETE FROM items WHERE id = :itemId")
suspend fun deleteItem(itemId: String)
// [END_ENTITY: Function('deleteItem')]
// [ENTITY: Function('insertItemLabelCrossRefs')]
@Insert(onConflict = OnConflictStrategy.REPLACE)
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
// [FILE] LabelDao.kt
// [SEMANTICS] data, database, dao, label
package com.homebox.lens.data.db.dao
// [IMPORTS]
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.homebox.lens.data.db.entity.LabelEntity
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Interface('LabelDao')]
/**
* [ENTITY: RoomDao('LabelDao')]
* [PURPOSE] Предоставляет методы для работы с 'labels' в локальной БД.
* @summary Предоставляет методы для работы с 'labels' в локальной БД.
*/
@Dao
interface LabelDao {
// [ENTITY: Function('getLabels')]
@Query("SELECT * FROM labels")
suspend fun getLabels(): List<LabelEntity>
// [END_ENTITY: Function('getLabels')]
// [ENTITY: Function('insertLabels')]
@Insert(onConflict = OnConflictStrategy.REPLACE)
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]

View File

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

View File

@@ -1,16 +1,17 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemEntity.kt
// [SEMANTICS] data, database, entity, item
package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.math.BigDecimal
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: DatabaseTable('ItemEntity')]
/**
* [ENTITY: RoomEntity('ItemEntity')]
* [PURPOSE] Представляет собой строку в таблице 'items' в локальной БД.
* @summary Представляет собой строку в таблице 'items' в локальной БД.
*/
@Entity(tableName = "items")
data class ItemEntity(
@@ -22,5 +23,6 @@ data class ItemEntity(
val value: BigDecimal?,
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
// [FILE] ItemLabelCrossRef.kt
// [SEMANTICS] data, database, entity, relation
package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Entity
import androidx.room.Index
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: DatabaseTable('ItemLabelCrossRef')]
/**
* [ENTITY: RoomEntity('ItemLabelCrossRef')]
* [PURPOSE] Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
* @summary Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
*/
@Entity(
primaryKeys = ["itemId", "labelId"],
@@ -19,5 +20,6 @@ data class ItemLabelCrossRef(
val itemId: 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
// [FILE] ItemWithLabels.kt
// [SEMANTICS] data, database, entity, relation
package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Embedded
import androidx.room.Junction
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')]
* [PURPOSE] POJO для получения ItemEntity вместе со связанными LabelEntity.
* @summary POJO для получения ItemEntity вместе со связанными LabelEntity.
*/
data class ItemWithLabels(
@Embedded val item: ItemEntity,
@@ -25,5 +28,6 @@ data class ItemWithLabels(
)
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
// [FILE] LabelEntity.kt
// [SEMANTICS] data, database, entity, label
package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Entity
import androidx.room.PrimaryKey
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: DatabaseTable('LabelEntity')]
/**
* [ENTITY: RoomEntity('LabelEntity')]
* [PURPOSE] Представляет собой строку в таблице 'labels' в локальной БД.
* @summary Представляет собой строку в таблице 'labels' в локальной БД.
*/
@Entity(tableName = "labels")
data class LabelEntity(
@PrimaryKey val id: 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
// [FILE] LocationEntity.kt
// [SEMANTICS] data, database, entity, location
package com.homebox.lens.data.db.entity
// [IMPORTS]
import androidx.room.Entity
import androidx.room.PrimaryKey
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: DatabaseTable('LocationEntity')]
/**
* [ENTITY: RoomEntity('LocationEntity')]
* [PURPOSE] Представляет собой строку в таблице 'locations' в локальной БД.
* @summary Представляет собой строку в таблице 'locations' в локальной БД.
*/
@Entity(tableName = "locations")
data class LocationEntity(
@PrimaryKey val id: String,
val name: String
)
// [END_ENTITY: DatabaseTable('LocationEntity')]
// [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
// [FILE] ApiModule.kt
// [PURPOSE] Предоставляет синглтон-зависимости для работы с сетью, включая OkHttpClient, Retrofit и ApiService.
// [SEMANTICS] di, networking
package com.homebox.lens.data.di
// [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.domain.repository.CredentialsRepository
@@ -17,41 +18,34 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import timber.log.Timber
import javax.inject.Provider
import javax.inject.Singleton
// [END_IMPORTS]
// [ENTITY: Module('ApiModule')]
/**
* [ENTITY: Module('ApiModule')]
* [CONTRACT]
* Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
* @summary Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
* необходимых для сетевого взаимодействия.
*/
@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
// [HELPER] Временный базовый URL для API. В будущем должен стать динамическим.
private const val BASE_URL = "https://homebox.bebesh.ru/api/"
/**
* [PROVIDER]
* [CONTRACT]
* Предоставляет сконфигурированный OkHttpClient.
* @param credentialsRepositoryProvider Провайдер репозитория для доступа к токену авторизации.
* Используется Provider<T> для предотвращения циклов зависимостей.
* @return Синглтон-экземпляр OkHttpClient с настроенными перехватчиками.
*/
// [ENTITY: Function('provideOkHttpClient')]
// [RELATION: Function('provideOkHttpClient')] -> [PROVIDES] -> [Framework('OkHttpClient')]
@Provides
@Singleton
fun provideOkHttpClient(
credentialsRepositoryProvider: Provider<CredentialsRepository>
): OkHttpClient {
// [ACTION] Создаем перехватчик для логирования.
Timber.d("[DEBUG][PROVIDER][providing_okhttp_client] Providing OkHttpClient.")
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
// [ACTION] Создаем перехватчик для добавления заголовка 'Accept'.
val acceptHeaderInterceptor = Interceptor { chain ->
val request = chain.request().newBuilder()
.header("Accept", "application/json")
@@ -59,77 +53,71 @@ object ApiModule {
chain.proceed(request)
}
// [CORE-LOGIC] Создаем перехватчик для добавления токена авторизации.
val authInterceptor = Interceptor { chain ->
// [HELPER] Получаем токен из репозитория.
// runBlocking здесь допустим, т.к. чтение из SharedPreferences - быстрая I/O операция,
// а интерфейс Interceptor'а является синхронным.
val token = runBlocking { credentialsRepositoryProvider.get().getToken() }
val requestBuilder = chain.request().newBuilder()
// [ACTION] Если токен существует, добавляем его в заголовок.
if (token != null) {
// Сервер ожидает заголовок "Authorization: Bearer <token>"
// Предполагается, что `token` уже содержит префикс "Bearer ".
requestBuilder.addHeader("Authorization", token)
}
chain.proceed(requestBuilder.build())
}
// [ACTION] Собираем OkHttpClient с правильным порядком перехватчиков.
return OkHttpClient.Builder()
.addInterceptor(acceptHeaderInterceptor)
.addInterceptor(authInterceptor) // Добавляем перехватчик для токена
.addInterceptor(loggingInterceptor) // Логирование должно идти последним, чтобы видеть финальный запрос
.addInterceptor(authInterceptor)
.addInterceptor(loggingInterceptor)
.build()
}
// [END_ENTITY: Function('provideOkHttpClient')]
/**
* [PROVIDER]
* [CONTRACT] Предоставляет экземпляр Moshi для парсинга JSON.
*/
// [ENTITY: Function('provideMoshi')]
// [RELATION: Function('provideMoshi')] -> [PROVIDES] -> [Framework('Moshi')]
@Provides
@Singleton
fun provideMoshi(): Moshi {
Timber.d("[DEBUG][PROVIDER][providing_moshi] Providing Moshi.")
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
// [END_ENTITY: Function('provideMoshi')]
/**
* [PROVIDER]
* [CONTRACT] Предоставляет фабрику конвертеров для Retrofit.
*/
// [ENTITY: Function('provideMoshiConverterFactory')]
// [RELATION: Function('provideMoshiConverterFactory')] -> [PROVIDES] -> [Framework('MoshiConverterFactory')]
@Provides
@Singleton
fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
Timber.d("[DEBUG][PROVIDER][providing_moshi_converter] Providing MoshiConverterFactory.")
return MoshiConverterFactory.create(moshi)
}
// [END_ENTITY: Function('provideMoshiConverterFactory')]
/**
* [PROVIDER]
* [CONTRACT] Предоставляет сконфигурированный экземпляр Retrofit.
*/
// [ENTITY: Function('provideRetrofit')]
// [RELATION: Function('provideRetrofit')] -> [PROVIDES] -> [Framework('Retrofit')]
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
Timber.d("[DEBUG][PROVIDER][providing_retrofit] Providing Retrofit.")
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(moshiConverterFactory)
.build()
}
// [END_ENTITY: Function('provideRetrofit')]
/**
* [PROVIDER]
* [CONTRACT] Предоставляет реализацию интерфейса HomeboxApiService.
*/
// [ENTITY: Function('provideHomeboxApiService')]
// [RELATION: Function('provideHomeboxApiService')] -> [PROVIDES] -> [Interface('HomeboxApiService')]
@Provides
@Singleton
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
Timber.d("[DEBUG][PROVIDER][providing_api_service] Providing HomeboxApiService.")
return retrofit.create(HomeboxApiService::class.java)
}
// [END_ENTITY: Function('provideHomeboxApiService')]
}
// [END_ENTITY: Module('ApiModule')]
// [END_FILE_ApiModule.kt]

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] StorageModule.kt
// [SEMANTICS] di, hilt, storage
package com.homebox.lens.data.di
// [IMPORTS]
import android.content.Context
import android.content.SharedPreferences
import com.homebox.lens.data.repository.EncryptedPreferencesWrapper
@@ -12,30 +13,39 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import timber.log.Timber
import javax.inject.Singleton
// [END_IMPORTS]
// [ENTITY: Module('StorageModule')]
@Module
@InstallIn(SingletonComponent::class)
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
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
Timber.d("[DEBUG][PROVIDER][providing_shared_preferences] Providing SharedPreferences.")
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.
// Hilt will automatically provide SharedPreferences and CryptoManager to its constructor.
// [ENTITY: Function('provideEncryptedPreferencesWrapper')]
// [RELATION: Function('provideEncryptedPreferencesWrapper')] -> [PROVIDES] -> [Class('EncryptedPreferencesWrapper')]
@Provides
@Singleton
fun provideEncryptedPreferencesWrapper(
sharedPreferences: SharedPreferences,
cryptoManager: CryptoManager
): EncryptedPreferencesWrapper {
Timber.d("[DEBUG][PROVIDER][providing_encrypted_prefs_wrapper] Providing EncryptedPreferencesWrapper.")
return EncryptedPreferencesWrapper(sharedPreferences, cryptoManager)
}
// [END_ENTITY: Function('provideEncryptedPreferencesWrapper')]
}
// [END_ENTITY: Module('StorageModule')]
// [END_FILE_StorageModule.kt]

View File

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

View File

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

View File

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

View File

@@ -1,104 +1,196 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] ItemRepositoryImpl.kt
// [SEMANTICS] data_repository, implementation, items
// [SEMANTICS] data_repository, implementation, items, labels
package com.homebox.lens.data.repository
// [IMPORTS]
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.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.repository.ItemRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
// [END_IMPORTS]
// [CORE-LOGIC]
/**
* [CONTRACT]
* Реализация репозитория для работы с данными о вещах.
* @param apiService Сервис для взаимодействия с Homebox API.
* [COHERENCE_NOTE] Метод 'login' был полностью удален из этого класса, так как его ответственность
* была передана в AuthRepositoryImpl. Это устраняет ошибку компиляции "'login' overrides nothing".
*/
// [ENTITY: Repository('ItemRepositoryImpl')]
// [RELATION: Repository('ItemRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('ItemRepository')]
// [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [ApiEndpoint('HomeboxApiService')]
// [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [DatabaseTable('ItemDao')]
@Singleton
class ItemRepositoryImpl @Inject constructor(
private val apiService: HomeboxApiService,
private val itemDao: ItemDao,
private val labelDao: LabelDao
) : ItemRepository {
// [DELETED] Метод login был здесь, но теперь он удален.
/**
* [CONTRACT] @see ItemRepository.createItem
*/
// [ENTITY: Function('createItem')]
// [RELATION: Function('createItem')] -> [RETURNS] -> [DataClass('ItemSummary')]
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
val itemDto = newItemData.toDto()
val resultDto = apiService.createItem(itemDto)
return resultDto.toDomain()
}
// [END_ENTITY: Function('createItem')]
/**
* [CONTRACT] @see ItemRepository.getItemDetails
*/
// [ENTITY: Function('getItemDetails')]
// [RELATION: Function('getItemDetails')] -> [RETURNS] -> [DataClass('ItemOut')]
override suspend fun getItemDetails(itemId: String): ItemOut {
val resultDto = apiService.getItem(itemId)
return resultDto.toDomain()
}
// [END_ENTITY: Function('getItemDetails')]
/**
* [CONTRACT] @see ItemRepository.updateItem
*/
// [ENTITY: Function('updateItem')]
// [RELATION: Function('updateItem')] -> [RETURNS] -> [DataClass('ItemOut')]
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
val itemDto = item.toDto()
val resultDto = apiService.updateItem(itemId, itemDto)
return resultDto.toDomain()
}
// [END_ENTITY: Function('updateItem')]
/**
* [CONTRACT] @see ItemRepository.deleteItem
*/
// [ENTITY: Function('deleteItem')]
override suspend fun deleteItem(itemId: String) {
apiService.deleteItem(itemId)
}
// [END_ENTITY: Function('deleteItem')]
/**
* [CONTRACT] @see ItemRepository.syncInventory
*/
// [ENTITY: Function('syncInventory')]
// [RELATION: Function('syncInventory')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')]
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
return resultDto.toDomain { it.toDomain() }
}
// [END_ENTITY: Function('syncInventory')]
/**
* [CONTRACT] @see ItemRepository.getStatistics
*/
// [ENTITY: Function('getStatistics')]
// [RELATION: Function('getStatistics')] -> [RETURNS] -> [DataClass('GroupStatistics')]
override suspend fun getStatistics(): GroupStatistics {
val resultDto = apiService.getStatistics()
return resultDto.toDomain()
}
// [END_ENTITY: Function('getStatistics')]
/**
* [CONTRACT] @see ItemRepository.getAllLocations
*/
// [ENTITY: Function('getAllLocations')]
// [RELATION: Function('getAllLocations')] -> [RETURNS] -> [DataStructure('List<LocationOutCount>')]
override suspend fun getAllLocations(): List<LocationOutCount> {
val resultDto = apiService.getLocations()
return resultDto.map { it.toDomain() }
}
// [END_ENTITY: Function('getAllLocations')]
/**
* [CONTRACT] @see ItemRepository.getAllLabels
*/
// [ENTITY: Function('getAllLabels')]
// [RELATION: Function('getAllLabels')] -> [RETURNS] -> [DataStructure('List<LabelOut>')]
override suspend fun getAllLabels(): List<LabelOut> {
val resultDto = apiService.getLabels()
return resultDto.map { it.toDomain() }
}
// [END_ENTITY: Function('getAllLabels')]
/**
* [CONTRACT] @see ItemRepository.searchItems
*/
// [ENTITY: Function('getLabelDetails')]
// [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> {
val resultDto = apiService.getItems(query = query)
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]

View File

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

View File

@@ -20,6 +20,12 @@ dependencies {
// [DEPENDENCY] Javax Inject for DI annotations
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]

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,55 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Item.kt
// [SEMANTICS] 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')]
* [PURPOSE] Представляет собой вещь в инвентаре.
* @property id Уникальный идентификатор вещи.
* @property name Название вещи.
* @property description Описание вещи.
* @property image Url изображения.
* @property location Местоположение вещи.
* @property labels Список меток, присвоенных вещи.
* @property value Стоимость вещи.
* @property createdAt Дата создания.
* @summary Представляет собой вещь в инвентаре.
* @param id Уникальный идентификатор вещи.
* @param name Название вещи.
* @param description Описание вещи.
* @param image Url изображения.
* @param location Местоположение вещи.
* @param labels Список меток, присвоенных вещи.
* @param value Стоимость вещи.
* @param createdAt Дата создания.
*/
data class Item(
val id: String,
val name: String,
val description: String?,
val quantity: Int,
val image: String?,
val location: Location?,
val labels: List<Label>,
val value: BigDecimal?,
val createdAt: String?
val value: Double?,
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]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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