9 Commits

Author SHA1 Message Date
78b827f29e Fix: Handle missing 'color', 'isArchived' and 'value' fields in DTOs and mappers to prevent JsonDataException 2025-10-06 09:40:47 +03:00
9500d747b1 12 2025-10-06 08:11:43 +03:00
8cfad121b2 build: Устранены предупреждения и ошибки сборки Gradle
- Обновлены версии AGP, Kotlin и Compose Compiler для совместимости.
- Версия Java обновлена до 17 во всех модулях.
- Выполнена миграция Moshi с Kapt на KSP.
- Удален устаревший атрибут 'package' из AndroidManifest.xml.
2025-10-05 15:23:21 +03:00
e3f52fca52 Убрали // [PACKAGE] из разметки, чтобы было меньше шума 2025-10-05 14:52:07 +03:00
9286e041da TokenResponse rework 2025-10-05 14:46:02 +03:00
556b7f7c7d feat(enrichment): apply semantic markup 2025-10-04 09:53:10 +03:00
eccc7ee970 feat: Refactor login screen - fix compilation error 2025-10-02 13:11:49 +03:00
8816377361 fix: Resolve build and runtime errors 2025-10-02 10:34:00 +03:00
5eb23eed5b feat: Refactor Item Edit Screen with all API fields and user-friendly UI 2025-09-28 11:33:57 +03:00
167 changed files with 3698 additions and 1920 deletions

View File

@@ -12,10 +12,9 @@
<Rationale>Заголовок служит 'паспортом' файла, позволяя инструментам мгновенно понять его расположение, имя и назначение.</Rationale> <Rationale>Заголовок служит 'паспортом' файла, позволяя инструментам мгновенно понять его расположение, имя и назначение.</Rationale>
<Definition type="regex"> <Definition type="regex">
<!-- CDATA используется для того, чтобы символы вроде '<' или '>' не были интерпретированы как XML --> <!-- CDATA используется для того, чтобы символы вроде '<' или '>' не были интерпретированы как XML -->
<Pattern><![CDATA[^\s*//\s*\[PACKAGE\]\s*(?P<package>.*?)\n//\s*\[FILE\]\s*(?P<file>.*?)\n//\s*\[SEMANTICS\]\s*(?P<semantics>.*)]]></Pattern> <Pattern><![CDATA[^\s*//\s*\[FILE\]\s*(?P<file>.*?)\n//\s*\[SEMANTICS\]\s*(?P<semantics>.*)]]></Pattern>
</Definition> </Definition>
<Example><![CDATA[ <Example><![CDATA[
// [PACKAGE] com.example.your.package.name
// [FILE] YourFileName.kt // [FILE] YourFileName.kt
// [SEMANTICS] ui, viewmodel, state_management // [SEMANTICS] ui, viewmodel, state_management
package com.example.your.package.name package com.example.your.package.name

View File

@@ -5,8 +5,8 @@
</META> </META>
<INCLUDES> <INCLUDES>
<INCLUDE from="../knowledge_base/semantic_linting.xml"/> <INCLUDE from="../knowledge_base/semantic_linting.xml"/>
<INCLUDE from="../knowledge_base/graphrag_optimization.md"/> <INCLUDE from="../knowledge_base/graphrag_optimization.xml"/>
<INCLUDE from="../knowledge_base/design_by_contract.md"/> <INCLUDE from="../knowledge_base/design_by_contract.xml"/>
<INCLUDE from="../knowledge_base/ai_friendly_logging.md"/> <INCLUDE from="../knowledge_base/ai_friendly_logging.xml"/>
</INCLUDES> </INCLUDES>
</SEMANTIC_ENRICHMENT_PROTOCOL> </SEMANTIC_ENRICHMENT_PROTOCOL>

View File

@@ -18,7 +18,7 @@
</META> </META>
<ROLE_DEFINITION> <ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через выбранный канал задач.</SPECIALIZATION> <SPECIALIZATION>При исполнении этой роли, я действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через выбранный канал задач.</SPECIALIZATION>
<CORE_GOAL>Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.</CORE_GOAL> <CORE_GOAL>Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.</CORE_GOAL>
</ROLE_DEFINITION> </ROLE_DEFINITION>

View File

@@ -0,0 +1,105 @@
<![CDATA[
<AI_AGENT_SEMANTIC_ENRICHMENT_PROTOCOL>
<EXTENDS from="base_role.xml"/>
<META>
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента Семантического Обогащения'**. Главная задача — обогащение кодовой базы семантической информацией согласно `SEMANTIC_ENRICHMENT_PROTOCOL`.</PURPOSE>
<VERSION>1.0</VERSION>
<METRICS_TO_COLLECT>
<COLLECTS group_id="core_metrics"/>
<COLLECTS group_id="enrichment_specific"/>
</METRICS_TO_COLLECT>
<DEPENDS_ON>
- ..agent_promts/interfaces/task_channel_interface.xml
- ..agent_promts/protocols/semantic_enrichment_protocol.xml
</DEPENDS_ON>
</META>
<ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я действую как агент семантического обогащения. Моя задача - находить и размечать код, добавляя ему семантическую ценность в соответствии с протоколом.</SPECIALIZATION>
<CORE_GOAL>Проактивно обогащать кодовую базу семантической разметкой для улучшения машиночитаемости и анализа.</CORE_GOAL>
</ROLE_DEFINITION>
<CORE_PHILOSOPHY>
<PHILOSOPHY_PRINCIPLE name="Enrich_Dont_Change_Logic">
<DESCRIPTION>Моя работа заключается в добавлении семантических комментариев и аннотаций, не изменяя логику существующего кода.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
<PHILOSOPHY_PRINCIPLE name="Traceable_And_Reviewable">
<DESCRIPTION>Все изменения должны быть доступны для просмотра, например, через Pull Request.</DESCRIPTION>
</PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY>
<BOOTSTRAP_PROTOCOL name="Initialization">
<ACTION>Загрузить и полностью проанализировать `agent_promts/protocols/semantic_enrichment_protocol.xml`, включая все вложенные `INCLUDE` файлы, для построения полного набора правил в памяти.</ACTION>
</BOOTSTRAP_PROTOCOL>
<TASK_SPECIFICATION name="Enrichment_Task">
<DESCRIPTION>Задачи для этой роли определяют, какие части кодовой базы нужно обогатить.</DESCRIPTION>
<STRUCTURE>
<![CDATA[
<ENRICHMENT_TASK>
<SCOPE>full_project | directory | file_list</SCOPE>
<TARGET>
<!-- Для directory: path/to/dir -->
<!-- Для file_list: список файлов -->
</TARGET>
</ENRICHMENT_TASK>
]]>
</STRUCTURE>
</TASK_SPECIFICATION>
<MASTER_WORKFLOW name="Enrich_Code_And_Create_PR">
<WORKFLOW_STEP id="1" name="Acknowledge_Task">
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-enrichment', TaskType='type::enrichment')"/>
<IF condition="WorkOrder IS NULL">
<TERMINATE/>
</IF>
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, NewStatus='status::in-progress')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="2" name="Execute_Enrichment">
<ACTION>Извлечь `<ENRICHMENT_TASK>` из `WorkOrder`.</ACTION>
<LET name="BranchName">feature/{WorkOrder.ID}/semantic-enrichment</LET>
<ACTION>CALL MyTaskChannel.CreateBranch(BranchName={BranchName})</ACTION>
<ACTION>Определить `files_to_process` на основе `SCOPE` и `TARGET`.</ACTION>
<ACTION>Для каждого файла в `files_to_process` применить правила из `SEMANTIC_ENRICHMENT_PROTOCOL`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="3" name="Commit_And_PR">
<IF condition="есть_изменения">
<ACTION>Сделать коммит с сообщением: `feat(enrichment): apply semantic markup`.</ACTION>
<ACTION>CALL MyTaskChannel.CommitChanges(...)</ACTION>
<LET name="PrID" value="CALL MyTaskChannel.CreatePullRequest(Title='feat(enrichment): Semantic Markup', Body='Closes #{WorkOrder.ID}', HeadBranch={BranchName}, BaseBranch='main')"/>
<ACTION>CALL MyTaskChannel.AddComment(IssueID={WorkOrder.ID}, CommentBody='Enrichment complete. PR #{PrID} is ready for review.')</ACTION>
</IF>
<ELSE>
<ACTION>CALL MyTaskChannel.AddComment(IssueID={WorkOrder.ID}, CommentBody='Enrichment complete. No new semantic markup was added.')</ACTION>
</ELSE>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="4" name="Finalize">
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, NewStatus='status::completed')</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="5" name="Log_Metrics">
<ACTION>Отправить метрики через `MyMetricsSink`.</ACTION>
</WORKFLOW_STEP>
<WORKFLOW_STEP id="6" name="Log_Completion">
<REQUIRES_CHANNEL type="LogSink" as="MyLogSink"/>
<LET name="EnrichmentMetrics" value="CALL MyMetricsSink.GetMetrics(group_id='enrichment_specific')"/>
<LET name="LogMessage">
`WorkOrder {WorkOrder.ID} completed.
- Files Processed: {EnrichmentMetrics.files_processed}
- Entities Enriched: {EnrichmentMetrics.entities_enriched}
- Relations Added: {EnrichmentMetrics.relations_added}
- Contracts Added: {EnrichmentMetrics.contracts_added}
- Logs Added: {EnrichmentMetrics.logs_added}`
</LET>
<ACTION>CALL MyLogSink.Log(FileName="logs/enrichment_agent_log.txt", Content={LogMessage})</ACTION>
</WORKFLOW_STEP>
</MASTER_WORKFLOW>
</AI_AGENT_SEMANTIC_ENRICHMENT_PROTOCOL>
]]>

View File

@@ -11,13 +11,13 @@
</METRICS_TO_COLLECT> </METRICS_TO_COLLECT>
<DEPENDS_ON> <DEPENDS_ON>
- ../interfaces/task_channel_interface.xml - ..agent_promts/interfaces/task_channel_interface.xml
- ../protocols/semantic_enrichment_protocol.xml - ..agent_promts/protocols/semantic_enrichment_protocol.xml
</DEPENDS_ON> </DEPENDS_ON>
</META> </META>
<ROLE_DEFINITION> <ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный хранитель чистоты кода. Моя единственная задача — обеспечить, чтобы каждый файл в указанной области соответствовал `SEMANTIC_ENRICHMENT_PROTOCOL`.</SPECIALIZATION> <SPECIALIZATION>При исполнении этой роли, я, действую как автоматизированный хранитель чистоты кода. Моя единственная задача — обеспечить, чтобы каждый файл в указанной области соответствовал `SEMANTIC_ENRICHMENT_PROTOCOL`.</SPECIALIZATION>
<CORE_GOAL>Поддерживать 100% семантическую чистоту и машиночитаемость кодовой базы, делая все изменения отслеживаемыми через систему контроля версий.</CORE_GOAL> <CORE_GOAL>Поддерживать 100% семантическую чистоту и машиночитаемость кодовой базы, делая все изменения отслеживаемыми через систему контроля версий.</CORE_GOAL>
</ROLE_DEFINITION> </ROLE_DEFINITION>
@@ -30,33 +30,6 @@
</PHILOSOPHY_PRINCIPLE> </PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY> </CORE_PHILOSOPHY>
<TOOLS_FOR_ROLE>
<TOOL name="CodeEditor">
<COMMANDS><COMMAND name="ReadFile"/><COMMAND name="WriteFile"/></COMMANDS>
</TOOL>
<TOOL name="Shell">
<ALLOWED_COMMANDS>
<COMMAND>find . -name "*.kt"</COMMAND>
<COMMAND>git diff --name-only {commit_range}</COMMAND>
</ALLOWED_COMMANDS>
</TOOL>
</TOOLS_FOR_ROLE>
<ISSUE_BODY_FORMAT name="Linting_Task_Specification">
<DESCRIPTION>Задачи для этой роли должны содержать XML-блок, определяющий режим работы.</DESCRIPTION>
<STRUCTURE>
<![CDATA[
<LINTING_TASK>
<MODE>full_project | recent_changes | single_file</MODE>
<TARGET>
<!-- Для recent_changes: commit range, e.g., HEAD~1..HEAD -->
<!-- Для single_file: path/to/file.kt -->
</TARGET>
</LINTING_TASK>
]]>
</STRUCTURE>
</ISSUE_BODY_FORMAT>
<MASTER_WORKFLOW name="Lint_And_Create_Pull_Request_Cycle"> <MASTER_WORKFLOW name="Lint_And_Create_Pull_Request_Cycle">
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task"> <WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-linter', TaskType='type::linting')"/> <LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-linter', TaskType='type::linting')"/>

View File

@@ -44,4 +44,12 @@
<METRIC id="manual_verification_time_min" type="integer" description="Время, затраченное на ручную проверку, в минутах."/> <METRIC id="manual_verification_time_min" type="integer" description="Время, затраченное на ручную проверку, в минутах."/>
</METRIC_GROUP> </METRIC_GROUP>
<METRIC_GROUP id="enrichment_specific">
<METRIC name="files_processed" type="integer" unit="files">Количество обработанных файлов.</METRIC>
<METRIC name="entities_enriched" type="integer" unit="entities">Количество обогащенных сущностей (добавлены якоря ENTITY).</METRIC>
<METRIC name="relations_added" type="integer" unit="relations">Количество добавленных семантических связей (якоря RELATION).</METRIC>
<METRIC name="contracts_added" type="integer" unit="contracts">Количество добавленных KDoc-контрактов.</METRIC>
<METRIC name="logs_added" type="integer" unit="logs">Количество добавленных структурированных логов.</METRIC>
</METRIC_GROUP>
</METRICS_CATALOG> </METRICS_CATALOG>

View File

@@ -36,15 +36,19 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "17"
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true buildConfig = true
aidl = false
renderScript = false
resValues = true
shaders = false
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = Versions.composeCompiler kotlinCompilerExtensionVersion = Versions.composeCompiler
@@ -54,6 +58,10 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
lint {
checkReleaseBuilds = false
abortOnError = false
}
} }
dependencies { dependencies {
@@ -61,6 +69,8 @@ dependencies {
implementation(project(":data")) implementation(project(":data"))
// [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity) // [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity)
implementation(project(":domain")) implementation(project(":domain"))
implementation(project(":ui"))
implementation(project(":feature:inventory"))
// [DEPENDENCY] AndroidX // [DEPENDENCY] AndroidX
implementation(Libs.coreKtx) implementation(Libs.coreKtx)

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.homebox.lens">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainActivity.kt // [FILE] MainActivity.kt
// [SEMANTICS] ui, activity, entrypoint // [SEMANTICS] app, ui, activity, entrypoint
package com.homebox.lens package com.homebox.lens
// [IMPORTS] // [IMPORTS]
@@ -22,7 +21,7 @@ import timber.log.Timber
// [ENTITY: Activity('MainActivity')] // [ENTITY: Activity('MainActivity')]
/** /**
* @summary Главная и единственная Activity в приложении. * @summary The main and only Activity in the application.
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainApplication.kt // [FILE] MainApplication.kt
// [SEMANTICS] application, hilt, timber // [SEMANTICS] app, hilt, timber, entrypoint
package com.homebox.lens package com.homebox.lens
// [IMPORTS] // [IMPORTS]
@@ -11,7 +10,7 @@ import timber.log.Timber
// [ENTITY: Application('MainApplication')] // [ENTITY: Application('MainApplication')]
/** /**
* @summary Точка входа в приложение. Инициализирует Hilt и Timber. * @summary The entry point of the application. Initializes Hilt and Timber.
*/ */
@HiltAndroidApp @HiltAndroidApp
class MainApplication : Application() { class MainApplication : Application() {

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] NavGraph.kt // [FILE] NavGraph.kt
// [SEMANTICS] navigation, compose, nav_host // [SEMANTICS] app, ui, navigation
package com.homebox.lens.navigation package com.homebox.lens.navigation
@@ -16,7 +15,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.homebox.lens.ui.screen.dashboard.DashboardScreen import com.homebox.lens.ui.screen.dashboard.DashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen import com.homebox.lens.feature.inventory.ui.InventoryScreen
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
@@ -25,17 +24,22 @@ import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.search.SearchScreen import com.homebox.lens.ui.screen.search.SearchScreen
import com.homebox.lens.ui.screen.setup.SetupScreen import com.homebox.lens.ui.screen.setup.SetupScreen
import com.homebox.lens.ui.screen.settings.SettingsScreen
import com.homebox.lens.ui.screen.splash.SplashScreen
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.navigation.Screen
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Function('NavGraph')] // [ENTITY: Function('NavGraph')]
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')] // [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')] // [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
// [RELATION: Function('NavGraph')] -> [USES] -> [Screen('SplashScreen')]
/** /**
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation. * @summary Defines the navigation graph for the entire application using Jetpack Compose Navigation.
* @param navController Контроллер навигации. * @param navController The navigation controller.
* @see Screen * @see Screen
* @sideeffect Регистрирует все экраны и управляет состоянием навигации. * @sideeffect Registers all screens and manages the navigation state.
* @invariant Стартовый экран - `Screen.Setup`. * @invariant The start screen is `Screen.Splash`.
*/ */
@Composable @Composable
fun NavGraph( fun NavGraph(
@@ -47,15 +51,19 @@ fun NavGraph(
val navigationActions = remember(navController) { val navigationActions = remember(navController) {
NavigationActions(navController) NavigationActions(navController)
} }
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.Setup.route startDestination = Screen.Splash.route
) { ) {
composable(route = Screen.Splash.route) {
SplashScreen(navController = navController)
}
composable(route = Screen.Setup.route) { composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = { SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) { navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Setup.route) { inclusive = true } popUpTo(Screen.Setup.route) {
inclusive = true
}
} }
}) })
} }
@@ -65,8 +73,8 @@ fun NavGraph(
navigationActions = navigationActions navigationActions = navigationActions
) )
} }
composable(route = Screen.InventoryList.route) { composable(route = Screen.Inventory.route) {
InventoryListScreen( InventoryScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions
) )
@@ -101,7 +109,7 @@ fun NavGraph(
navigationActions = navigationActions, navigationActions = navigationActions,
onLocationClick = { locationId -> onLocationClick = { locationId ->
// [AI_NOTE]: Navigate to a pre-filtered inventory list screen // [AI_NOTE]: Navigate to a pre-filtered inventory list screen
navController.navigate(Screen.InventoryList.route) navController.navigate(Screen.Inventory.route)
}, },
onAddNewLocationClick = { onAddNewLocationClick = {
navController.navigate(Screen.LocationEdit.createRoute("new")) navController.navigate(Screen.LocationEdit.createRoute("new"))
@@ -137,6 +145,12 @@ fun NavGraph(
navigationActions = navigationActions navigationActions = navigationActions
) )
} }
composable(route = Screen.Settings.route) {
SettingsScreen(
currentRoute = currentRoute,
navigationActions = navigationActions
)
}
} }
} }
// [END_ENTITY: Function('NavGraph')] // [END_ENTITY: Function('NavGraph')]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.components
// [FILE] ColorPicker.kt // [FILE] ColorPicker.kt
// [SEMANTICS] ui, component, color_selection // [SEMANTICS] app, ui, component, color
package com.homebox.lens.ui.components package com.homebox.lens.ui.components
@@ -25,10 +24,10 @@ import com.homebox.lens.R
// [ENTITY: Function('ColorPicker')] // [ENTITY: Function('ColorPicker')]
/** /**
* @summary Компонент для выбора цвета. * @summary A component for color selection.
* @param selectedColor Текущий выбранный цвет в формате HEX строки (например, "#FFFFFF"). * @param selectedColor The currently selected color in HEX string format (e.g., "#FFFFFF").
* @param onColorSelected Лямбда-функция, вызываемая при выборе нового цвета. * @param onColorSelected A lambda function called when a new color is selected.
* @param modifier Модификатор для настройки внешнего вида. * @param modifier A modifier for customizing the appearance.
*/ */
@Composable @Composable
fun ColorPicker( fun ColorPicker(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.components
// [FILE] LoadingOverlay.kt // [FILE] LoadingOverlay.kt
// [SEMANTICS] ui, component, loading // [SEMANTICS] app, ui, component, loading
package com.homebox.lens.ui.components package com.homebox.lens.ui.components
@@ -18,7 +17,7 @@ import androidx.compose.ui.graphics.Color
// [ENTITY: Function('LoadingOverlay')] // [ENTITY: Function('LoadingOverlay')]
/** /**
* @summary Полноэкранный оверлей с индикатором загрузки. * @summary A full-screen overlay with a loading indicator.
*/ */
@Composable @Composable
fun LoadingOverlay() { fun LoadingOverlay() {

View File

@@ -0,0 +1,62 @@
// [FILE] ItemMapper.kt
// [SEMANTICS] app, ui, mapper, item
package com.homebox.lens.ui.mapper
import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.model.Label
import com.homebox.lens.domain.model.Location
import javax.inject.Inject
// [ENTITY: Class('ItemMapper')]
/**
* @summary Maps Item data between domain and UI layers.
* @invariant This class is stateless and its methods are pure functions.
*/
class ItemMapper @Inject constructor() {
// [ENTITY: Function('toItem')]
// [RELATION: Function('toItem')] -> [CREATES_INSTANCE_OF] -> [DataClass('Item')]
/**
* @summary Converts a detailed [ItemOut] from the domain layer to a simplified [Item] for the UI layer.
* @param itemOut The [ItemOut] object to convert.
* @return The resulting [Item] object.
* @precondition itemOut MUST NOT be null.
* @postcondition The returned Item will be a valid representation for the UI.
*/
fun toItem(itemOut: ItemOut): Item {
return Item(
id = itemOut.id,
name = itemOut.name,
description = itemOut.description,
quantity = itemOut.quantity,
image = itemOut.images.firstOrNull { it.isPrimary }?.path,
location = itemOut.location?.let { Location(it.id, it.name) },
labels = itemOut.labels.map { Label(it.id, it.name) },
purchasePrice = itemOut.purchasePrice,
createdAt = itemOut.createdAt,
archived = itemOut.isArchived,
assetId = itemOut.assetId,
fields = itemOut.fields.map { com.homebox.lens.domain.model.CustomField(it.name, it.value, it.type) },
insured = itemOut.insured ?: false,
lifetimeWarranty = itemOut.lifetimeWarranty ?: false,
manufacturer = itemOut.manufacturer,
modelNumber = itemOut.modelNumber,
notes = itemOut.notes,
parentId = itemOut.parent?.id,
purchaseFrom = itemOut.purchaseFrom,
purchaseTime = itemOut.purchaseTime,
serialNumber = itemOut.serialNumber,
soldNotes = itemOut.soldNotes,
soldPrice = itemOut.soldPrice,
soldTime = itemOut.soldTime,
soldTo = itemOut.soldTo,
syncChildItemsLocations = itemOut.syncChildItemsLocations ?: false,
warrantyDetails = itemOut.warrantyDetails,
warrantyExpires = itemOut.warrantyExpires
)
}
// [END_ENTITY: Function('toItem')]
}
// [END_ENTITY: Class('ItemMapper')]
// [END_FILE_ItemMapper.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardScreen.kt // [FILE] DashboardScreen.kt
// [SEMANTICS] ui, screen, dashboard, compose, navigation // [SEMANTICS] app, ui, screen, dashboard
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS] // [IMPORTS]
@@ -37,11 +36,11 @@ import timber.log.Timber
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')]
/** /**
* @summary Главная Composable-функция для экрана "Панель управления". * @summary The main Composable function for the "Dashboard" screen.
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt. * @param viewModel The ViewModel for this screen, provided by Hilt.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. * @param currentRoute The current route to highlight the active item in the Drawer.
* @param navigationActions Объект с навигационными действиями. * @param navigationActions The object with navigation actions.
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI. * @sideeffect Calls navigation lambdas upon UI interaction.
*/ */
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
@@ -82,11 +81,11 @@ fun DashboardScreen(
// [ENTITY: Function('DashboardContent')] // [ENTITY: Function('DashboardContent')]
// [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')] // [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')]
/** /**
* @summary Отображает основной контент экрана в зависимости от uiState. * @summary Displays the main content of the screen depending on the uiState.
* @param modifier Модификатор для стилизации. * @param modifier A modifier for styling.
* @param uiState Текущее состояние UI экрана. * @param uiState The current UI state of the screen.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение. * @param onLocationClick A lambda handler for clicking on a location.
* @param onLabelClick Лямбда-обработчик нажатия на метку. * @param onLabelClick A lambda handler for clicking on a label.
*/ */
@Composable @Composable
private fun DashboardContent( private fun DashboardContent(
@@ -132,8 +131,8 @@ private fun DashboardContent(
// [ENTITY: Function('StatisticsSection')] // [ENTITY: Function('StatisticsSection')]
// [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')] // [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
/** /**
* @summary Секция для отображения общей статистики. * @summary Section for displaying general statistics.
* @param statistics Объект со статистическими данными. * @param statistics The object with statistical data.
*/ */
@Composable @Composable
private fun StatisticsSection(statistics: GroupStatistics) { private fun StatisticsSection(statistics: GroupStatistics) {
@@ -164,9 +163,9 @@ private fun StatisticsSection(statistics: GroupStatistics) {
// [ENTITY: Function('StatisticCard')] // [ENTITY: Function('StatisticCard')]
/** /**
* @summary Карточка для отображения одного статистического показателя. * @summary Card for displaying a single statistical indicator.
* @param title Название показателя. * @param title The name of the indicator.
* @param value Значение показателя. * @param value The value of the indicator.
*/ */
@Composable @Composable
private fun StatisticCard(title: String, value: String) { private fun StatisticCard(title: String, value: String) {
@@ -180,8 +179,8 @@ private fun StatisticCard(title: String, value: String) {
// [ENTITY: Function('RecentlyAddedSection')] // [ENTITY: Function('RecentlyAddedSection')]
// [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] // [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
/** /**
* @summary Секция для отображения недавно добавленных элементов. * @summary Section for displaying recently added items.
* @param items Список элементов для отображения. * @param items The list of items to display.
*/ */
@Composable @Composable
private fun RecentlyAddedSection(items: List<ItemSummary>) { private fun RecentlyAddedSection(items: List<ItemSummary>) {
@@ -213,8 +212,8 @@ private fun RecentlyAddedSection(items: List<ItemSummary>) {
// [ENTITY: Function('ItemCard')] // [ENTITY: Function('ItemCard')]
// [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] // [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
/** /**
* @summary Карточка для отображения краткой информации об элементе. * @summary Card for displaying brief information about an item.
* @param item Элемент для отображения. * @param item The item to display.
*/ */
@Composable @Composable
private fun ItemCard(item: ItemSummary) { private fun ItemCard(item: ItemSummary) {
@@ -236,9 +235,9 @@ private fun ItemCard(item: ItemSummary) {
// [ENTITY: Function('LocationsSection')] // [ENTITY: Function('LocationsSection')]
// [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] // [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
/** /**
* @summary Секция для отображения местоположений в виде чипсов. * @summary Section for displaying locations as chips.
* @param locations Список местоположений. * @param locations The list of locations.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение. * @param onLocationClick A lambda handler for clicking on a location.
*/ */
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
@@ -265,9 +264,9 @@ private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick:
// [ENTITY: Function('LabelsSection')] // [ENTITY: Function('LabelsSection')]
// [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')] // [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
/** /**
* @summary Секция для отображения меток в виде чипсов. * @summary Section for displaying labels as chips.
* @param labels Список меток. * @param labels The list of labels.
* @param onLabelClick Лямбда-обработчик нажатия на метку. * @param onLabelClick A lambda handler for clicking on a label.
*/ */
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
@@ -310,10 +309,10 @@ fun DashboardContentSuccessPreview() {
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "") LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
), ),
labels = listOf( labels = listOf(
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""), LabelOut(id="1", name="electronics", description = null, color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""), LabelOut(id="2", name="important", description = null, color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""), LabelOut(id="3", name="seasonal", description = null, color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "") LabelOut(id="4", name="hobby", description = null, color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
), ),
recentlyAddedItems = emptyList() recentlyAddedItems = emptyList()
) )

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardUiState.kt // [FILE] DashboardUiState.kt
// [SEMANTICS] ui, state, dashboard // [SEMANTICS] app, ui, state, dashboard
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS] // [IMPORTS]
@@ -12,8 +11,8 @@ import com.homebox.lens.domain.model.LocationOutCount
// [ENTITY: SealedInterface('DashboardUiState')] // [ENTITY: SealedInterface('DashboardUiState')]
/** /**
* @summary Определяет все возможные состояния для экрана "Дэшборд". * @summary Defines all possible states for the "Dashboard" screen.
* @invariant В любой момент времени экран может находиться только в одном из этих состояний. * @invariant At any given time, the screen can only be in one of these states.
*/ */
sealed interface DashboardUiState { sealed interface DashboardUiState {
// [ENTITY: DataClass('Success')] // [ENTITY: DataClass('Success')]
@@ -22,11 +21,11 @@ sealed interface DashboardUiState {
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
/** /**
* @summary Состояние успешной загрузки данных. * @summary The state of a successful data load.
* @param statistics Статистика по инвентарю. * @param statistics The inventory statistics.
* @param locations Список локаций со счетчиками. * @param locations The list of locations with counters.
* @param labels Список всех меток. * @param labels The list of all labels.
* @param recentlyAddedItems Список недавно добавленных товаров. * @param recentlyAddedItems The list of recently added items.
*/ */
data class Success( data class Success(
val statistics: GroupStatistics, val statistics: GroupStatistics,
@@ -38,15 +37,15 @@ sealed interface DashboardUiState {
// [ENTITY: DataClass('Error')] // [ENTITY: DataClass('Error')]
/** /**
* @summary Состояние ошибки во время загрузки данных. * @summary The state of an error during data loading.
* @param message Человекочитаемое сообщение об ошибке. * @param message A human-readable error message.
*/ */
data class Error(val message: String) : DashboardUiState data class Error(val message: String) : DashboardUiState
// [END_ENTITY: DataClass('Error')] // [END_ENTITY: DataClass('Error')]
// [ENTITY: Object('Loading')] // [ENTITY: Object('Loading')]
/** /**
* @summary Состояние, когда данные для экрана загружаются. * @summary The state when data for the screen is being loaded.
*/ */
data object Loading : DashboardUiState data object Loading : DashboardUiState
// [END_ENTITY: Object('Loading')] // [END_ENTITY: Object('Loading')]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardViewModel.kt // [FILE] DashboardViewModel.kt
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging // [SEMANTICS] app, ui, viewmodel, dashboard
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS] // [IMPORTS]
@@ -24,10 +23,10 @@ import javax.inject.Inject
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')] // [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')]
// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')] // [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')]
/** /**
* @summary ViewModel для главного экрана (Dashboard). * @summary ViewModel for the main screen (Dashboard).
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний * @description Orchestrates the loading of data for the Dashboard, using a strict state model
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки. * (`DashboardUiState`), and handles parallel requests without race conditions.
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`. * @invariant `uiState` is always one of the states defined in `DashboardUiState`.
*/ */
@HiltViewModel @HiltViewModel
class DashboardViewModel @Inject constructor( class DashboardViewModel @Inject constructor(
@@ -46,10 +45,10 @@ class DashboardViewModel @Inject constructor(
// [ENTITY: Function('loadDashboardData')] // [ENTITY: Function('loadDashboardData')]
/** /**
* @summary Загружает все необходимые данные для экрана Dashboard. * @summary Loads all necessary data for the Dashboard screen.
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его * @description Executes UseCases in parallel and updates the UI by switching it
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`. * between the `Loading`, `Success`, and `Error` states from `DashboardUiState`.
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. * @sideeffect Asynchronously updates `_uiState` with one of the `DashboardUiState` states.
*/ */
fun loadDashboardData() { fun loadDashboardData() {
viewModelScope.launch { viewModelScope.launch {

View File

@@ -1,39 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListScreen.kt
// [SEMANTICS] ui, screen, inventory, list
package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [END_IMPORTS]
// [ENTITY: Function('InventoryListScreen')]
// [RELATION: Function('InventoryListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('InventoryListScreen')] -> [CALLS] -> [Function('MainScaffold')]
/**
* @summary Composable-функция для экрана "Список инвентаря".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
*/
@Composable
fun InventoryListScreen(
currentRoute: String?,
navigationActions: NavigationActions
) {
MainScaffold(
topBarTitle = stringResource(id = R.string.inventory_list_title),
currentRoute = currentRoute,
navigationActions = navigationActions
) {
// [AI_NOTE]: Implement Inventory List Screen UI
Text(text = "Inventory List Screen")
}
}
// [END_ENTITY: Function('InventoryListScreen')]
// [END_FILE_InventoryListScreen.kt]

View File

@@ -1,21 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListViewModel.kt
// [SEMANTICS] ui, viewmodel, inventory_list
package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: ViewModel('InventoryListViewModel')]
/**
* @summary ViewModel for the inventory list screen.
*/
@HiltViewModel
class InventoryListViewModel @Inject constructor() : ViewModel() {
// [AI_NOTE]: Implement UI state
}
// [END_ENTITY: ViewModel('InventoryListViewModel')]
// [END_FILE_InventoryListViewModel.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsScreen.kt // [FILE] ItemDetailsScreen.kt
// [SEMANTICS] ui, screen, item, details // [SEMANTICS] app, ui, screen, details
package com.homebox.lens.ui.screen.itemdetails package com.homebox.lens.ui.screen.itemdetails
@@ -17,9 +16,9 @@ import com.homebox.lens.ui.common.MainScaffold
// [RELATION: Function('ItemDetailsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('ItemDetailsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('ItemDetailsScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION: Function('ItemDetailsScreen')] -> [CALLS] -> [Function('MainScaffold')]
/** /**
* @summary Composable-функция для экрана "Детали элемента". * @summary Composable function for the "Item Details" screen.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. * @param currentRoute The current route to highlight the active item in the Drawer.
* @param navigationActions Объект с навигационными действиями. * @param navigationActions The object with navigation actions.
*/ */
@Composable @Composable
fun ItemDetailsScreen( fun ItemDetailsScreen(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsViewModel.kt // [FILE] ItemDetailsViewModel.kt
// [SEMANTICS] ui, viewmodel, item_details // [SEMANTICS] app, ui, viewmodel, details
package com.homebox.lens.ui.screen.itemdetails package com.homebox.lens.ui.screen.itemdetails
// [IMPORTS] // [IMPORTS]

View File

@@ -1,32 +1,56 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditScreen.kt // [FILE] ItemEditScreen.kt
// [SEMANTICS] ui, screen, item, edit // [SEMANTICS] app, ui, screen, edit
package com.homebox.lens.ui.screen.itemedit package com.homebox.lens.ui.screen.itemedit
// [IMPORTS] // [IMPORTS]
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@@ -36,21 +60,25 @@ import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold import com.homebox.lens.ui.common.MainScaffold
import timber.log.Timber import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Function('ItemEditScreen')] // [ENTITY: Composable('ItemEditScreen')]
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Composable('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')] // [RELATION: Composable('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')]
// [RELATION: Function('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')] // [RELATION: Composable('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')]
// [RELATION: Function('ItemEditScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION: Composable('ItemEditScreen')] -> [CALLS] -> [Composable('MainScaffold')]
/** /**
* @summary Composable-функция для экрана "Редактирование элемента". * @summary Composable function for the "Edit Item" screen.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. * @param currentRoute The current route to highlight the active item in the Drawer.
* @param navigationActions Объект с навигационными действиями. * @param navigationActions The object with navigation actions.
* @param itemId ID элемента для редактирования. Null, если создается новый элемент. * @param itemId The ID of the item to edit. Null if a new item is being created.
* @param viewModel ViewModel для управления состоянием экрана. * @param viewModel The ViewModel for managing the screen's state.
* @param onSaveSuccess Callback, вызываемый после успешного сохранения товара. * @param onSaveSuccess A callback invoked after the item is successfully saved.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ItemEditScreen( fun ItemEditScreen(
currentRoute: String?, currentRoute: String?,
@@ -85,7 +113,7 @@ fun ItemEditScreen(
topBarTitle = stringResource(id = R.string.item_edit_title), topBarTitle = stringResource(id = R.string.item_edit_title),
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions
) { ) { paddingValues ->
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = { floatingActionButton = {
@@ -100,40 +128,479 @@ fun ItemEditScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(it) .padding(paddingValues)
.padding(16.dp) .padding(16.dp)
.verticalScroll(rememberScrollState())
) { ) {
if (uiState.isLoading) { if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.fillMaxWidth()) CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
} else { } else {
uiState.item?.let { item -> uiState.item?.let { item ->
OutlinedTextField( // [AI_NOTE]: General Information section for basic item details.
value = item.name, Card(
onValueChange = { viewModel.updateName(it) }, modifier = Modifier.fillMaxWidth(),
label = { Text(stringResource(R.string.item_name)) }, elevation = CardDefaults.cardElevation(4.dp)
modifier = Modifier.fillMaxWidth() ) {
) Column(modifier = Modifier.padding(16.dp)) {
Spacer(modifier = Modifier.height(8.dp)) Text(
OutlinedTextField( text = stringResource(R.string.item_edit_general_information),
value = item.description ?: "", style = MaterialTheme.typography.headlineSmall
onValueChange = { viewModel.updateDescription(it) }, )
label = { Text(stringResource(R.string.item_description)) }, Spacer(modifier = Modifier.height(16.dp))
modifier = Modifier.fillMaxWidth() OutlinedTextField(
) value = item.name,
Spacer(modifier = Modifier.height(8.dp)) onValueChange = { viewModel.updateName(it) },
OutlinedTextField( label = { Text(stringResource(R.string.item_name)) },
value = item.quantity.toString(), modifier = Modifier.fillMaxWidth()
onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) }, )
label = { Text(stringResource(R.string.item_quantity)) }, Spacer(modifier = Modifier.height(8.dp))
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), OutlinedTextField(
modifier = Modifier.fillMaxWidth() value = item.description ?: "",
) onValueChange = { viewModel.updateDescription(it) },
// Add more fields as needed label = { Text(stringResource(R.string.item_description)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.quantity.toString(),
onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) },
label = { Text(stringResource(R.string.item_quantity)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Location Dropdown
var locationExpanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = locationExpanded,
onExpandedChange = { locationExpanded = !locationExpanded }
) {
OutlinedTextField(
value = item.location?.name ?: "",
onValueChange = { },
label = { Text(stringResource(R.string.item_edit_location)) },
readOnly = true,
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = locationExpanded)
},
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
)
ExposedDropdownMenu(
expanded = locationExpanded,
onDismissRequest = { locationExpanded = false }
) {
uiState.allLocations.forEach { location ->
DropdownMenuItem(
text = { Text(location.name) },
onClick = {
viewModel.updateLocation(location)
locationExpanded = false
}
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
// Labels Dialog
var showLabelsDialog by remember { mutableStateOf(false) }
OutlinedTextField(
value = item.labels.joinToString { it.name },
onValueChange = { },
label = { Text(stringResource(R.string.item_edit_labels)) },
readOnly = true,
modifier = Modifier
.fillMaxWidth()
.clickable { showLabelsDialog = true },
trailingIcon = {
Icon(Icons.Filled.ArrowDropDown, contentDescription = stringResource(R.string.item_edit_select_labels))
}
)
if (showLabelsDialog) {
// This state will hold the temporary selections within the dialog
val tempSelectedLabels = remember { mutableStateOf(item.labels.toSet()) }
AlertDialog(
onDismissRequest = { showLabelsDialog = false },
title = { Text(stringResource(R.string.item_edit_select_labels)) },
text = {
Column {
uiState.allLabels.forEach { label ->
val isChecked = tempSelectedLabels.value.contains(label)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
val currentSelection = tempSelectedLabels.value.toMutableSet()
if (isChecked) {
currentSelection.remove(label)
} else {
currentSelection.add(label)
}
tempSelectedLabels.value = currentSelection
}
.padding(vertical = 8.dp)
) {
Checkbox(
checked = isChecked,
onCheckedChange = {
val currentSelection = tempSelectedLabels.value.toMutableSet()
if (it) {
currentSelection.add(label)
} else {
currentSelection.remove(label)
}
tempSelectedLabels.value = currentSelection
}
)
Text(
text = label.name,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
},
confirmButton = {
TextButton(
onClick = {
// Update the ViewModel with the final selection
viewModel.updateLabels(tempSelectedLabels.value.toList())
showLabelsDialog = false
}
) {
Text(stringResource(R.string.dialog_ok))
}
},
dismissButton = {
TextButton(onClick = { showLabelsDialog = false }) {
Text(stringResource(R.string.dialog_cancel))
}
}
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Purchase Information section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_purchase_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.purchasePrice?.toString() ?: "",
onValueChange = { viewModel.updatePurchasePrice(it.toDoubleOrNull()) },
label = { Text(stringResource(R.string.item_edit_purchase_price)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.purchaseFrom ?: "",
onValueChange = { viewModel.updatePurchaseFrom(it) },
label = { Text(stringResource(R.string.item_edit_purchase_from)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Date picker for purchase time.
var showPurchaseDatePicker by remember { mutableStateOf(false) }
val purchaseDateState = rememberDatePickerState()
OutlinedTextField(
value = item.purchaseTime ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_edit_purchase_time)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showPurchaseDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showPurchaseDatePicker = true }
)
if (showPurchaseDatePicker) {
DatePickerDialog(
onDismissRequest = { showPurchaseDatePicker = false },
confirmButton = {
Text(stringResource(R.string.dialog_ok), modifier = Modifier.clickable {
val selectedDate = purchaseDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
if (selectedDate != null) {
viewModel.updatePurchaseTime(selectedDate)
}
showPurchaseDatePicker = false
})
},
dismissButton = {
Text(stringResource(R.string.dialog_cancel), modifier = Modifier.clickable { showPurchaseDatePicker = false })
}
) {
DatePicker(state = purchaseDateState)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Warranty Information section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_warranty_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_edit_lifetime_warranty))
Switch(
checked = item.lifetimeWarranty,
onCheckedChange = { viewModel.updateLifetimeWarranty(it) }
)
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.warrantyDetails ?: "",
onValueChange = { viewModel.updateWarrantyDetails(it) },
label = { Text(stringResource(R.string.item_edit_warranty_details)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Date picker for warranty expiration.
var showWarrantyDatePicker by remember { mutableStateOf(false) }
val warrantyDateState = rememberDatePickerState()
OutlinedTextField(
value = item.warrantyExpires ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_edit_warranty_expires)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showWarrantyDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showWarrantyDatePicker = true }
)
if (showWarrantyDatePicker) {
DatePickerDialog(
onDismissRequest = { showWarrantyDatePicker = false },
confirmButton = {
Text(stringResource(R.string.dialog_ok), modifier = Modifier.clickable {
val selectedDate = warrantyDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
if (selectedDate != null) {
viewModel.updateWarrantyExpires(selectedDate)
}
showWarrantyDatePicker = false
})
},
dismissButton = {
Text(stringResource(R.string.dialog_cancel), modifier = Modifier.clickable { showWarrantyDatePicker = false })
}
) {
DatePicker(state = warrantyDateState)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Identification section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_identification),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.assetId ?: "",
onValueChange = { viewModel.updateAssetId(it) },
label = { Text(stringResource(R.string.item_edit_asset_id)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.serialNumber ?: "",
onValueChange = { viewModel.updateSerialNumber(it) },
label = { Text(stringResource(R.string.item_edit_serial_number)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.manufacturer ?: "",
onValueChange = { viewModel.updateManufacturer(it) },
label = { Text(stringResource(R.string.item_edit_manufacturer)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.modelNumber ?: "",
onValueChange = { viewModel.updateModelNumber(it) },
label = { Text(stringResource(R.string.item_edit_model_number)) },
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Status & Notes section.
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_status_notes),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_edit_archived))
Switch(
checked = item.archived,
onCheckedChange = { viewModel.updateArchived(it) }
)
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.item_edit_insured))
Switch(
checked = item.insured,
onCheckedChange = { viewModel.updateInsured(it) }
)
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.notes ?: "",
onValueChange = { viewModel.updateNotes(it) },
label = { Text(stringResource(R.string.item_edit_notes)) },
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// [AI_NOTE]: Sold Information section (conditionally displayed).
if (item.soldTime != null || item.soldPrice != null || item.soldTo != null || item.soldNotes != null) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.item_edit_sold_information),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = item.soldPrice?.toString() ?: "",
onValueChange = { viewModel.updateSoldPrice(it.toDoubleOrNull()) },
label = { Text(stringResource(R.string.item_edit_sold_price)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.soldTo ?: "",
onValueChange = { viewModel.updateSoldTo(it) },
label = { Text(stringResource(R.string.item_edit_sold_to)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.soldNotes ?: "",
onValueChange = { viewModel.updateSoldNotes(it) },
label = { Text(stringResource(R.string.item_edit_sold_notes)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// [AI_NOTE]: Date picker for sold time.
var showSoldDatePicker by remember { mutableStateOf(false) }
val soldDateState = rememberDatePickerState()
OutlinedTextField(
value = item.soldTime ?: "",
onValueChange = { }, // Read-only, handled by date picker
label = { Text(stringResource(R.string.item_edit_sold_time)) },
readOnly = true,
trailingIcon = {
IconButton(onClick = { showSoldDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = stringResource(R.string.item_edit_select_date))
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showSoldDatePicker = true }
)
if (showSoldDatePicker) {
DatePickerDialog(
onDismissRequest = { showSoldDatePicker = false },
confirmButton = {
Text(stringResource(R.string.dialog_ok), modifier = Modifier.clickable {
val selectedDate = soldDateState.selectedDateMillis?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it))
}
if (selectedDate != null) {
viewModel.updateSoldTime(selectedDate)
}
showSoldDatePicker = false
})
},
dismissButton = {
Text(stringResource(R.string.dialog_cancel), modifier = Modifier.clickable { showSoldDatePicker = false })
}
) {
DatePicker(state = soldDateState)
}
}
}
}
} }
} }
} }}
} }
} }
} }
// [END_ENTITY: Function('ItemEditScreen')] // [END_ENTITY: Composable('ItemEditScreen')]
// [END_FILE_ItemEditScreen.kt] // [END_FILE_ItemEditScreen.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditViewModel.kt // [FILE] ItemEditViewModel.kt
// [SEMANTICS] ui, viewmodel, item_edit // [SEMANTICS] app, ui, viewmodel, edit
package com.homebox.lens.ui.screen.itemedit package com.homebox.lens.ui.screen.itemedit
@@ -9,11 +8,16 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.Item import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemCreate import com.homebox.lens.domain.model.ItemCreate
import com.homebox.lens.domain.model.Label import com.homebox.lens.domain.model.ItemUpdate
import com.homebox.lens.domain.model.Location import com.homebox.lens.domain.model.Location
import com.homebox.lens.domain.model.Label
import com.homebox.lens.domain.usecase.CreateItemUseCase import com.homebox.lens.domain.usecase.CreateItemUseCase
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
import com.homebox.lens.data.mapper.toDomain
import com.homebox.lens.domain.usecase.UpdateItemUseCase import com.homebox.lens.domain.usecase.UpdateItemUseCase
import com.homebox.lens.ui.mapper.ItemMapper
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -32,11 +36,15 @@ import javax.inject.Inject
* @param item The item being edited, or null if creating a new item. * @param item The item being edited, or null if creating a new item.
* @param isLoading Whether data is currently being loaded or saved. * @param isLoading Whether data is currently being loaded or saved.
* @param error An error message if an operation failed. * @param error An error message if an operation failed.
* @param allLocations A list of all available locations.
* @param allLabels A list of all available labels.
*/ */
data class ItemEditUiState( data class ItemEditUiState(
val item: Item? = null, val item: Item? = null,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null val error: String? = null,
val allLocations: List<Location> = emptyList(),
val allLabels: List<Label> = emptyList()
) )
// [END_ENTITY: DataClass('ItemEditUiState')] // [END_ENTITY: DataClass('ItemEditUiState')]
@@ -44,15 +52,25 @@ data class ItemEditUiState(
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')] // [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')] // [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')] // [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [Class('ItemMapper')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')] // [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')]
/** /**
* @summary ViewModel for the item edit screen. * @summary ViewModel for the item edit screen.
* @param createItemUseCase Use case for creating a new item.
* @param updateItemUseCase Use case for updating an existing item.
* @param getItemDetailsUseCase Use case for fetching item details.
* @param getAllLocationsUseCase Use case for fetching all locations.
* @param getAllLabelsUseCase Use case for fetching all labels.
* @param itemMapper Mapper for converting between domain and UI item models.
*/ */
@HiltViewModel @HiltViewModel
class ItemEditViewModel @Inject constructor( class ItemEditViewModel @Inject constructor(
private val createItemUseCase: CreateItemUseCase, private val createItemUseCase: CreateItemUseCase,
private val updateItemUseCase: UpdateItemUseCase, private val updateItemUseCase: UpdateItemUseCase,
private val getItemDetailsUseCase: GetItemDetailsUseCase private val getItemDetailsUseCase: GetItemDetailsUseCase,
private val getAllLocationsUseCase: GetAllLocationsUseCase,
private val getAllLabelsUseCase: GetAllLabelsUseCase,
private val itemMapper: ItemMapper
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(ItemEditUiState()) private val _uiState = MutableStateFlow(ItemEditUiState())
@@ -73,34 +91,93 @@ class ItemEditViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(isLoading = true, error = null) _uiState.value = _uiState.value.copy(isLoading = true, error = null)
if (itemId == null) { if (itemId == null) {
Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.") Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.")
_uiState.value = _uiState.value.copy(isLoading = false, item = Item(id = "", name = "", description = null, quantity = 0, image = null, location = null, labels = emptyList(), value = null, createdAt = null)) _uiState.value = _uiState.value.copy(
isLoading = false,
item = Item(
id = "",
name = "",
description = null,
quantity = 1,
image = null,
location = null,
labels = emptyList(),
purchasePrice = null,
createdAt = null,
archived = false,
assetId = null,
fields = emptyList(),
insured = false,
lifetimeWarranty = false,
manufacturer = null,
modelNumber = null,
notes = null,
parentId = null,
purchaseFrom = null,
purchaseTime = null,
serialNumber = null,
soldNotes = null,
soldPrice = null,
soldTime = null,
soldTo = null,
syncChildItemsLocations = false,
warrantyDetails = null,
warrantyExpires = null
)
)
} else { } else {
try { try {
Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId) Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId)
val itemOut = getItemDetailsUseCase(itemId) val itemOut = getItemDetailsUseCase(itemId)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.") Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
val item = Item( val item = itemMapper.toItem(itemOut)
id = itemOut.id,
name = itemOut.name,
description = itemOut.description,
quantity = itemOut.quantity,
image = itemOut.images.firstOrNull()?.path, // Assuming first image is the main one
location = itemOut.location?.let { Location(it.id, it.name) }, // Simplified mapping
labels = itemOut.labels.map { Label(it.id, it.name) }, // Simplified mapping
value = itemOut.value?.toBigDecimal(),
createdAt = itemOut.createdAt
)
_uiState.value = _uiState.value.copy(isLoading = false, item = item) _uiState.value = _uiState.value.copy(isLoading = false, item = item)
Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched item details for ID: %s", itemId) Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched and mapped item details for ID: %s", itemId)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId) Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId)
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage) _uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
} }
} }
// Load all locations and labels
try {
Timber.i("[INFO][ACTION][fetching_all_locations] Fetching all locations.")
val allLocations = getAllLocationsUseCase().map { Location(it.id, it.name) }
Timber.i("[INFO][ACTION][fetching_all_labels] Fetching all labels.")
val allLabels = getAllLabelsUseCase().map { it.toDomain() }
_uiState.value = _uiState.value.copy(allLocations = allLocations, allLabels = allLabels)
Timber.i("[INFO][ACTION][all_locations_labels_fetched] Successfully fetched all locations and labels.")
} catch (e: Exception) {
Timber.e(e, "[ERROR][FALLBACK][locations_labels_load_failed] Failed to load locations or labels.")
_uiState.value = _uiState.value.copy(error = e.localizedMessage)
}
} }
} }
// [END_ENTITY: Function('loadItem')] // [END_ENTITY: Function('loadItem')]
// [ENTITY: Function('updateLocation')]
/**
* @summary Updates the location of the item in the UI state.
* @param location The new location for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateLocation(location: Location) {
Timber.d("[DEBUG][ACTION][updating_item_location] Updating item location to: %s", location.name)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(location = location))
}
// [END_ENTITY: Function('updateLocation')]
// [ENTITY: Function('updateLabels')]
/**
* @summary Updates the labels of the item in the UI state.
* @param labels The new list of labels for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateLabels(labels: List<Label>) {
Timber.d("[DEBUG][ACTION][updating_item_labels] Updating item labels to: %s", labels.map { it.name }.joinToString())
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = labels))
}
// [END_ENTITY: Function('updateLabels')]
// [ENTITY: Function('saveItem')] // [ENTITY: Function('saveItem')]
/** /**
* @summary Saves the current item, either creating a new one or updating an existing one. * @summary Saves the current item, either creating a new one or updating an existing one.
@@ -117,53 +194,48 @@ class ItemEditViewModel @Inject constructor(
try { try {
if (currentItem.id.isBlank()) { if (currentItem.id.isBlank()) {
Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name) Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name)
val createdItemSummary = createItemUseCase(ItemCreate( val createdItemSummary = createItemUseCase(
name = currentItem.name, ItemCreate(
description = currentItem.description, name = currentItem.name,
quantity = currentItem.quantity, description = currentItem.description,
assetId = null, // Item does not have assetId quantity = currentItem.quantity,
notes = null, // Item does not have notes archived = currentItem.archived,
serialNumber = null, // Item does not have serialNumber assetId = currentItem.assetId,
value = currentItem.value?.toDouble(), // Convert BigDecimal to Double insured = currentItem.insured,
purchasePrice = null, // Item does not have purchasePrice lifetimeWarranty = currentItem.lifetimeWarranty,
purchaseDate = null, // Item does not have purchaseDate manufacturer = currentItem.manufacturer,
warrantyUntil = null, // Item does not have warrantyUntil modelNumber = currentItem.modelNumber,
locationId = currentItem.location?.id, notes = currentItem.notes,
parentId = null, // Item does not have parentId parentId = currentItem.parentId,
labelIds = currentItem.labels.map { it.id } purchaseFrom = currentItem.purchaseFrom,
)) purchasePrice = currentItem.purchasePrice,
Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.") purchaseTime = currentItem.purchaseTime,
val createdItem = Item( serialNumber = currentItem.serialNumber,
id = createdItemSummary.id, soldNotes = currentItem.soldNotes,
name = createdItemSummary.name, soldPrice = currentItem.soldPrice,
description = null, // ItemSummary does not have description soldTime = currentItem.soldTime,
quantity = 0, // ItemSummary does not have quantity soldTo = currentItem.soldTo,
image = null, // ItemSummary does not have image syncChildItemsLocations = currentItem.syncChildItemsLocations,
location = null, // ItemSummary does not have location warrantyDetails = currentItem.warrantyDetails,
labels = emptyList(), // ItemSummary does not have labels warrantyExpires = currentItem.warrantyExpires,
value = null, // ItemSummary does not have value locationId = currentItem.location?.id,
createdAt = null // ItemSummary does not have createdAt labelIds = currentItem.labels.map { it.id }
)
) )
_uiState.value = _uiState.value.copy(isLoading = false, item = createdItem) Timber.i("[INFO][ACTION][fetching_full_item_after_creation] Fetching full item details after creation for ID: %s", createdItemSummary.id)
Timber.i("[INFO][ACTION][new_item_created] Successfully created new item with ID: %s", createdItem.id) val createdItemOut = getItemDetailsUseCase(createdItemSummary.id)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping created ItemOut to Item for UI state.")
val item = itemMapper.toItem(createdItemOut)
_uiState.value = _uiState.value.copy(isLoading = false, item = item)
Timber.i("[INFO][ACTION][new_item_created] Successfully created and mapped new item with ID: %s", createdItemOut.id)
_saveCompleted.emit(Unit) _saveCompleted.emit(Unit)
} else { } else {
Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id) Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id)
val updatedItemOut = updateItemUseCase(currentItem) val updatedItemOut = updateItemUseCase(currentItem)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.") Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping updated ItemOut to Item for UI state.")
val updatedItem = Item( val item = itemMapper.toItem(updatedItemOut)
id = updatedItemOut.id, _uiState.value = _uiState.value.copy(isLoading = false, item = item)
name = updatedItemOut.name, Timber.i("[INFO][ACTION][item_updated] Successfully updated and mapped item with ID: %s", updatedItemOut.id)
description = updatedItemOut.description,
quantity = updatedItemOut.quantity,
image = updatedItemOut.images.firstOrNull()?.path,
location = updatedItemOut.location?.let { Location(it.id, it.name) },
labels = updatedItemOut.labels.map { Label(it.id, it.name) },
value = updatedItemOut.value.toBigDecimal(),
createdAt = updatedItemOut.createdAt
)
_uiState.value = _uiState.value.copy(isLoading = false, item = updatedItem)
Timber.i("[INFO][ACTION][item_updated] Successfully updated item with ID: %s", updatedItem.id)
_saveCompleted.emit(Unit) _saveCompleted.emit(Unit)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -209,6 +281,234 @@ class ItemEditViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity)) _uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity))
} }
// [END_ENTITY: Function('updateQuantity')] // [END_ENTITY: Function('updateQuantity')]
}
// [END_ENTITY: ViewModel('ItemEditViewModel')] // [ENTITY: Function('updateArchived')]
/**
* @summary Updates the archived status of the item in the UI state.
* @param newArchived The new archived status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateArchived(newArchived: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_archived] Updating item archived status to: %s", newArchived)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(archived = newArchived))
}
// [END_ENTITY: Function('updateArchived')]
// [ENTITY: Function('updateAssetId')]
/**
* @summary Updates the asset ID of the item in the UI state.
* @param newAssetId The new asset ID for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateAssetId(newAssetId: String) {
Timber.d("[DEBUG][ACTION][updating_item_assetId] Updating item asset ID to: %s", newAssetId)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(assetId = newAssetId))
}
// [END_ENTITY: Function('updateAssetId')]
// [ENTITY: Function('updateInsured')]
/**
* @summary Updates the insured status of the item in the UI state.
* @param newInsured The new insured status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateInsured(newInsured: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_insured] Updating item insured status to: %s", newInsured)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(insured = newInsured))
}
// [END_ENTITY: Function('updateInsured')]
// [ENTITY: Function('updateLifetimeWarranty')]
/**
* @summary Updates the lifetime warranty status of the item in the UI state.
* @param newLifetimeWarranty The new lifetime warranty status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateLifetimeWarranty(newLifetimeWarranty: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_lifetime_warranty] Updating item lifetime warranty status to: %s", newLifetimeWarranty)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(lifetimeWarranty = newLifetimeWarranty))
}
// [END_ENTITY: Function('updateLifetimeWarranty')]
// [ENTITY: Function('updateManufacturer')]
/**
* @summary Updates the manufacturer of the item in the UI state.
* @param newManufacturer The new manufacturer for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateManufacturer(newManufacturer: String) {
Timber.d("[DEBUG][ACTION][updating_item_manufacturer] Updating item manufacturer to: %s", newManufacturer)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(manufacturer = newManufacturer))
}
// [END_ENTITY: Function('updateManufacturer')]
// [ENTITY: Function('updateModelNumber')]
/**
* @summary Updates the model number of the item in the UI state.
* @param newModelNumber The new model number for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateModelNumber(newModelNumber: String) {
Timber.d("[DEBUG][ACTION][updating_item_model_number] Updating item model number to: %s", newModelNumber)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(modelNumber = newModelNumber))
}
// [END_ENTITY: Function('updateModelNumber')]
// [ENTITY: Function('updateNotes')]
/**
* @summary Updates the notes of the item in the UI state.
* @param newNotes The new notes for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateNotes(newNotes: String) {
Timber.d("[DEBUG][ACTION][updating_item_notes] Updating item notes to: %s", newNotes)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(notes = newNotes))
}
// [END_ENTITY: Function('updateNotes')]
// [ENTITY: Function('updateParentId')]
/**
* @summary Updates the parent ID of the item in the UI state.
* @param newParentId The new parent ID for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateParentId(newParentId: String) {
Timber.d("[DEBUG][ACTION][updating_item_parent_id] Updating item parent ID to: %s", newParentId)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(parentId = newParentId))
}
// [END_ENTITY: Function('updateParentId')]
// [ENTITY: Function('updatePurchaseFrom')]
/**
* @summary Updates the purchase source of the item in the UI state.
* @param newPurchaseFrom The new purchase source for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchaseFrom(newPurchaseFrom: String) {
Timber.d("[DEBUG][ACTION][updating_item_purchase_from] Updating item purchase from to: %s", newPurchaseFrom)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseFrom = newPurchaseFrom))
}
// [END_ENTITY: Function('updatePurchaseFrom')]
// [ENTITY: Function('updatePurchasePrice')]
/**
* @summary Updates the purchase price of the item in the UI state.
* @param newPurchasePrice The new purchase price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchasePrice(newPurchasePrice: Double?) {
Timber.d("[DEBUG][ACTION][updating_item_purchase_price] Updating item purchase price to: %s", newPurchasePrice)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchasePrice = newPurchasePrice))
}
// [END_ENTITY: Function('updatePurchasePrice')]
// [ENTITY: Function('updatePurchaseTime')]
/**
* @summary Updates the purchase time of the item in the UI state.
* @param newPurchaseTime The new purchase time for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchaseTime(newPurchaseTime: String) {
Timber.d("[DEBUG][ACTION][updating_item_purchase_time] Updating item purchase time to: %s", newPurchaseTime)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseTime = newPurchaseTime))
}
// [END_ENTITY: Function('updatePurchaseTime')]
// [ENTITY: Function('updateSerialNumber')]
/**
* @summary Updates the serial number of the item in the UI state.
* @param newSerialNumber The new serial number for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSerialNumber(newSerialNumber: String) {
Timber.d("[DEBUG][ACTION][updating_item_serial_number] Updating item serial number to: %s", newSerialNumber)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(serialNumber = newSerialNumber))
}
// [END_ENTITY: Function('updateSerialNumber')]
// [ENTITY: Function('updateSoldNotes')]
/**
* @summary Updates the sold notes of the item in the UI state.
* @param newSoldNotes The new sold notes for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldNotes(newSoldNotes: String) {
Timber.d("[DEBUG][ACTION][updating_item_sold_notes] Updating item sold notes to: %s", newSoldNotes)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldNotes = newSoldNotes))
}
// [END_ENTITY: Function('updateSoldNotes')]
// [ENTITY: Function('updateSoldPrice')]
/**
* @summary Updates the sold price of the item in the UI state.
* @param newSoldPrice The new sold price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldPrice(newSoldPrice: Double?) {
Timber.d("[DEBUG][ACTION][updating_item_sold_price] Updating item sold price to: %s", newSoldPrice)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldPrice = newSoldPrice))
}
// [END_ENTITY: Function('updateSoldPrice')]
// [ENTITY: Function('updateSoldTime')]
/**
* @summary Updates the sold time of the item in the UI state.
* @param newSoldTime The new sold time for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldTime(newSoldTime: String) {
Timber.d("[DEBUG][ACTION][updating_item_sold_time] Updating item sold time to: %s", newSoldTime)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTime = newSoldTime))
}
// [END_ENTITY: Function('updateSoldTime')]
// [ENTITY: Function('updateSoldTo')]
/**
* @summary Updates the sold to field of the item in the UI state.
* @param newSoldTo The new sold to for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldTo(newSoldTo: String) {
Timber.d("[DEBUG][ACTION][updating_item_sold_to] Updating item sold to to: %s", newSoldTo)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTo = newSoldTo))
}
// [END_ENTITY: Function('updateSoldTo')]
// [ENTITY: Function('updateSyncChildItemsLocations')]
/**
* @summary Updates the sync child items locations status of the item in the UI state.
* @param newSyncChildItemsLocations The new sync child items locations status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSyncChildItemsLocations(newSyncChildItemsLocations: Boolean) {
Timber.d("[DEBUG][ACTION][updating_item_sync_child_items_locations] Updating item sync child items locations status to: %s", newSyncChildItemsLocations)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(syncChildItemsLocations = newSyncChildItemsLocations))
}
// [END_ENTITY: Function('updateSyncChildItemsLocations')]
// [ENTITY: Function('updateWarrantyDetails')]
/**
* @summary Updates the warranty details of the item in the UI state.
* @param newWarrantyDetails The new warranty details for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateWarrantyDetails(newWarrantyDetails: String) {
Timber.d("[DEBUG][ACTION][updating_item_warranty_details] Updating item warranty details to: %s", newWarrantyDetails)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyDetails = newWarrantyDetails))
}
// [END_ENTITY: Function('updateWarrantyDetails')]
// [ENTITY: Function('updateWarrantyExpires')]
/**
* @summary Updates the warranty expires date of the item in the UI state.
* @param newWarrantyExpires The new warranty expires date for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateWarrantyExpires(newWarrantyExpires: String) {
Timber.d("[DEBUG][ACTION][updating_item_warranty_expires] Updating item warranty expires date to: %s", newWarrantyExpires)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyExpires = newWarrantyExpires))
}
// [END_ENTITY: Function('updateWarrantyExpires')]
}
// [END_ENTITY: ViewModel('ItemEditViewModel')]
// [END_FILE_ItemEditViewModel.kt] // [END_FILE_ItemEditViewModel.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
// [FILE] LabelEditScreen.kt // [FILE] LabelEditScreen.kt
// [SEMANTICS] ui, screen, label, edit // [SEMANTICS] app, ui, screen, edit, label
package com.homebox.lens.ui.screen.labeledit package com.homebox.lens.ui.screen.labeledit
@@ -24,10 +23,10 @@ import com.homebox.lens.ui.components.LoadingOverlay
// [ENTITY: Function('LabelEditScreen')] // [ENTITY: Function('LabelEditScreen')]
// [RELATION: Function('LabelEditScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelEditViewModel')] // [RELATION: Function('LabelEditScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelEditViewModel')]
/** /**
* @summary Composable-функция для экрана "Редактирование метки". * @summary Composable function for the "Edit Label" screen.
* @param labelId ID метки для редактирования или null для создания новой. * @param labelId The ID of the label to edit, or null to create a new one.
* @param onBack Навигация назад. * @param onBack Navigation back.
* @param onLabelSaved Действие после сохранения метки. * @param onLabelSaved Action after the label is saved.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -97,6 +96,13 @@ fun LabelEditScreen(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = uiState.description.orEmpty(),
onValueChange = viewModel::onDescriptionChange,
label = { Text(stringResource(R.string.label_description)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
ColorPicker( ColorPicker(
selectedColor = uiState.color, selectedColor = uiState.color,
onColorSelected = viewModel::onColorChange, onColorSelected = viewModel::onColorChange,

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
// [FILE] LabelEditViewModel.kt // [FILE] LabelEditViewModel.kt
// [SEMANTICS] ui, viewmodel, label_management // [SEMANTICS] app, ui, viewmodel, edit, label
package com.homebox.lens.ui.screen.labeledit package com.homebox.lens.ui.screen.labeledit
@@ -50,6 +49,10 @@ class LabelEditViewModel @Inject constructor(
uiState = uiState.copy(name = newName, nameError = null) uiState = uiState.copy(name = newName, nameError = null)
} }
fun onDescriptionChange(newDescription: String) {
uiState = uiState.copy(description = newDescription)
}
fun onColorChange(newColor: String) { fun onColorChange(newColor: String) {
uiState = uiState.copy(color = newColor) uiState = uiState.copy(color = newColor)
} }
@@ -63,35 +66,41 @@ class LabelEditViewModel @Inject constructor(
uiState = uiState.copy(isLoading = true, error = null) uiState = uiState.copy(isLoading = true, error = null)
try { try {
if (labelId == null) { val result = if (labelId == null) {
// Create new label // [LOG_EVENT] [EVENT_TYPE: LabelCreationAttempt] [DATA: { "labelName": "${uiState.name}" }]
val newLabel = LabelCreate(name = uiState.name, color = uiState.color) val newLabel = LabelCreate(name = uiState.name, color = uiState.color, description = uiState.description)
createLabelUseCase(newLabel) createLabelUseCase(newLabel)
} else { } else {
// Update existing label // [LOG_EVENT] [EVENT_TYPE: LabelUpdateAttempt] [DATA: { "labelId": "$labelId", "labelName": "${uiState.name}" }]
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color) val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color, description = uiState.description)
updateLabelUseCase(labelId, updatedLabel) updateLabelUseCase(labelId, updatedLabel)
} }
// [LOG_EVENT] [EVENT_TYPE: LabelSaveSuccess] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
uiState = uiState.copy(isSaved = true) uiState = uiState.copy(isSaved = true)
} catch (e: Exception) { } catch (e: Exception) {
// [LOG_EVENT] [EVENT_TYPE: LabelSaveFailure] [ERROR: "${e.message}"] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
uiState = uiState.copy(error = e.message, isLoading = false) uiState = uiState.copy(error = e.message, isLoading = false)
} finally { } finally {
uiState = uiState.copy(isLoading = false) uiState = uiState.copy(isLoading = false)
} }
} }
} }
private fun loadLabelDetails(id: String) { private fun loadLabelDetails(id: String) {
viewModelScope.launch { viewModelScope.launch {
uiState = uiState.copy(isLoading = true, error = null) uiState = uiState.copy(isLoading = true, error = null)
try { try {
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchAttempt] [DATA: { "labelId": "$id" }]
val label = getLabelDetailsUseCase(id) val label = getLabelDetailsUseCase(id)
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchSuccess] [DATA: { "labelId": "$id", "labelName": "${label.name}" }]
uiState = uiState.copy( uiState = uiState.copy(
name = label.name, name = label.name,
color = label.color, color = label.color,
isLoading = false description = label.description,
isLoading = false,
originalLabel = label
) )
} catch (e: Exception) { } catch (e: Exception) {
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchFailure] [ERROR: "${e.message}"] [DATA: { "labelId": "$id" }]
uiState = uiState.copy(error = e.message, isLoading = false) uiState = uiState.copy(error = e.message, isLoading = false)
} }
} }
@@ -104,6 +113,7 @@ class LabelEditViewModel @Inject constructor(
*/ */
data class LabelEditUiState( data class LabelEditUiState(
val name: String = "", val name: String = "",
val description: String? = null,
val color: String = "#FFFFFF", // Default color val color: String = "#FFFFFF", // Default color
val nameError: String? = null, val nameError: String? = null,
val isLoading: Boolean = false, val isLoading: Boolean = false,

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt // [FILE] LabelsListScreen.kt
// [SEMANTICS] ui, labels_list, state_management, compose, dialog // [SEMANTICS] app, ui, screen, list, label
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
// [IMPORTS] // [IMPORTS]
@@ -17,24 +16,18 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Label import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -43,7 +36,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R import com.homebox.lens.R
import com.homebox.lens.domain.model.Label import com.homebox.lens.domain.model.Label
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.navigation.Screen import com.homebox.lens.ui.navigation.Screen
import com.homebox.lens.ui.common.MainScaffold import com.homebox.lens.ui.common.MainScaffold
import timber.log.Timber import timber.log.Timber
// [END_IMPORTS] // [END_IMPORTS]
@@ -52,10 +45,12 @@ import timber.log.Timber
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelsListViewModel')] // [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelsListViewModel')]
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')] // [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
/** /**
* @summary Отображает экран со списком всех меток. * @summary Displays the screen with a list of all labels.
* @param navController Контроллер навигации для перемещения между экранами. * @param currentRoute The current navigation route.
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток. * @param navigationActions The object containing navigation actions.
* @param viewModel The ViewModel providing the UI state for the labels screen.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LabelsListScreen( fun LabelsListScreen(
currentRoute: String?, currentRoute: String?,
@@ -90,19 +85,19 @@ fun LabelsListScreen(
.padding(innerPaddingValues), // Use innerPaddingValues here .padding(innerPaddingValues), // Use innerPaddingValues here
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
when (currentState) { when (val state = uiState) {
is LabelsListUiState.Loading -> { is LabelsListUiState.Loading -> {
CircularProgressIndicator() CircularProgressIndicator()
} }
is LabelsListUiState.Error -> { is LabelsListUiState.Error -> {
Text(text = currentState.message) Text(text = state.message)
} }
is LabelsListUiState.Success -> { is LabelsListUiState.Success -> {
if (currentState.labels.isEmpty()) { if (state.labels.isEmpty()) {
Text(text = stringResource(id = R.string.no_labels_found)) Text(text = stringResource(id = R.string.no_labels_found))
} else { } else {
LabelsList( LabelsList(
labels = currentState.labels, labels = state.labels,
onLabelClick = { label -> onLabelClick = { label ->
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.") Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
navigationActions.navigateToLabelEdit(label.id) navigationActions.navigateToLabelEdit(label.id)
@@ -120,10 +115,10 @@ fun LabelsListScreen(
// [ENTITY: Function('LabelsList')] // [ENTITY: Function('LabelsList')]
// [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')] // [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')]
/** /**
* @summary Composable-функция для отображения списка меток. * @summary Composable function for displaying a list of labels.
* @param labels Список объектов `Label` для отображения. * @param labels The list of `Label` objects to display.
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка. * @param onLabelClick A lambda function called when a list item is clicked.
* @param modifier Модификатор для настройки внешнего вида. * @param modifier A modifier for customizing the appearance.
*/ */
@Composable @Composable
private fun LabelsList( private fun LabelsList(
@@ -149,9 +144,9 @@ private fun LabelsList(
// [ENTITY: Function('LabelListItem')] // [ENTITY: Function('LabelListItem')]
// [RELATION: Function('LabelListItem')] -> [DEPENDS_ON] -> [DataClass('Label')] // [RELATION: Function('LabelListItem')] -> [DEPENDS_ON] -> [DataClass('Label')]
/** /**
* @summary Composable-функция для отображения одного элемента в списке меток. * @summary Composable function for displaying a single item in the list of labels.
* @param label Объект `Label`, который нужно отобразить. * @param label The `Label` object to display.
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент. * @param onClick A lambda function called when the item is clicked.
*/ */
@Composable @Composable
private fun LabelListItem( private fun LabelListItem(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListUiState.kt // [FILE] LabelsListUiState.kt
// [SEMANTICS] ui_state, sealed_interface, contract // [SEMANTICS] app, ui, state, list, label
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
// [IMPORTS] // [IMPORTS]
@@ -9,17 +8,17 @@ import com.homebox.lens.domain.model.Label
// [ENTITY: SealedInterface('LabelsListUiState')] // [ENTITY: SealedInterface('LabelsListUiState')]
/** /**
* @summary Определяет все возможные состояния для UI экрана со списком меток. * @summary Defines all possible states for the UI of the screen with a list of labels.
* @description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях. * @description Using a sealed interface allows for exhaustive handling of all states in Composable functions.
*/ */
sealed interface LabelsListUiState { sealed interface LabelsListUiState {
// [ENTITY: DataClass('Success')] // [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')]
/** /**
* @summary Состояние успеха, содержит список меток и состояние диалога. * @summary The success state, contains the list of labels and the state of the dialog.
* @param labels Список меток для отображения. * @param labels The list of labels to display.
* @param isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки. * @param isShowingCreateDialog A flag indicating whether the label creation dialog should be displayed.
* @invariant labels не может быть null. * @invariant labels cannot be null.
*/ */
data class Success( data class Success(
val labels: List<Label>, val labels: List<Label>,
@@ -29,17 +28,17 @@ sealed interface LabelsListUiState {
// [ENTITY: DataClass('Error')] // [ENTITY: DataClass('Error')]
/** /**
* @summary Состояние ошибки. * @summary The error state.
* @param message Текст ошибки для отображения пользователю. * @param message The error text to display to the user.
* @invariant message не может быть пустой. * @invariant message cannot be empty.
*/ */
data class Error(val message: String) : LabelsListUiState data class Error(val message: String) : LabelsListUiState
// [END_ENTITY: DataClass('Error')] // [END_ENTITY: DataClass('Error')]
// [ENTITY: Object('Loading')] // [ENTITY: Object('Loading')]
/** /**
* @summary Состояние загрузки данных. * @summary The data loading state.
* @description Указывает, что идет процесс загрузки меток. * @description Indicates that the process of loading labels is in progress.
*/ */
data object Loading : LabelsListUiState data object Loading : LabelsListUiState
// [END_ENTITY: Object('Loading')] // [END_ENTITY: Object('Loading')]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListViewModel.kt // [FILE] LabelsListViewModel.kt
// [SEMANTICS] ui_logic, labels_list, state_management, dialog_management // [SEMANTICS] app, ui, viewmodel, list, label
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
// [IMPORTS] // [IMPORTS]
@@ -21,9 +20,9 @@ import javax.inject.Inject
// [RELATION: ViewModel('LabelsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')] // [RELATION: ViewModel('LabelsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
// [RELATION: ViewModel('LabelsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LabelsListUiState')] // [RELATION: ViewModel('LabelsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LabelsListUiState')]
/** /**
* @summary ViewModel для экрана со списком меток. * @summary ViewModel for the screen with a list of labels.
* @description Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки. * @description Manages the screen state, loads the list of labels, handles errors, and manages the dialog for creating a new label.
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`. * @invariant `uiState` is always one of the states defined in `LabelsListUiState`.
*/ */
@HiltViewModel @HiltViewModel
class LabelsListViewModel @Inject constructor( class LabelsListViewModel @Inject constructor(
@@ -39,10 +38,10 @@ class LabelsListViewModel @Inject constructor(
// [ENTITY: Function('loadLabels')] // [ENTITY: Function('loadLabels')]
/** /**
* @summary Загружает список меток. * @summary Loads the list of labels.
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его * @description Executes `GetAllLabelsUseCase` and updates the UI by switching it
* между состояниями `Loading`, `Success` и `Error`. * between the `Loading`, `Success`, and `Error` states.
* @sideeffect Асинхронно обновляет `_uiState`. * @sideeffect Asynchronously updates `_uiState`.
*/ */
fun loadLabels() { fun loadLabels() {
viewModelScope.launch { viewModelScope.launch {
@@ -77,9 +76,9 @@ class LabelsListViewModel @Inject constructor(
// [ENTITY: Function('onShowCreateDialog')] // [ENTITY: Function('onShowCreateDialog')]
/** /**
* @summary Инициирует отображение диалога для создания метки. * @summary Initiates the display of the dialog for creating a label.
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`. * @description Updates the `uiState` by setting `isShowingCreateDialog` to `true`.
* @sideeffect Обновляет `_uiState`. * @sideeffect Updates `_uiState`.
*/ */
fun onShowCreateDialog() { fun onShowCreateDialog() {
Timber.i("[INFO][ACTION][show_create_dialog] Show create label dialog requested.") Timber.i("[INFO][ACTION][show_create_dialog] Show create label dialog requested.")
@@ -93,9 +92,9 @@ class LabelsListViewModel @Inject constructor(
// [ENTITY: Function('onDismissCreateDialog')] // [ENTITY: Function('onDismissCreateDialog')]
/** /**
* @summary Скрывает диалог создания метки. * @summary Hides the label creation dialog.
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`. * @description Updates the `uiState` by setting `isShowingCreateDialog` to `false`.
* @sideeffect Обновляет `_uiState`. * @sideeffect Updates `_uiState`.
*/ */
fun onDismissCreateDialog() { fun onDismissCreateDialog() {
Timber.i("[INFO][ACTION][dismiss_create_dialog] Dismiss create label dialog requested.") Timber.i("[INFO][ACTION][dismiss_create_dialog] Dismiss create label dialog requested.")
@@ -109,12 +108,12 @@ class LabelsListViewModel @Inject constructor(
// [ENTITY: Function('createLabel')] // [ENTITY: Function('createLabel')]
/** /**
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА. * @summary Creates a new label. [MVP_SCOPE] STUB.
* @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие * @description In the current implementation (Plan B, Stage 1), this function only logs the action
* и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе. * and hides the dialog. The actual save logic will be added in the next stage.
* @param name Название новой метки. * @param name The name of the new label.
* @precondition `name` не должен быть пустым. * @precondition `name` must not be blank.
* @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог. * @sideeffect Logs the action, updates `_uiState` to hide the dialog.
*/ */
fun createLabel(name: String) { fun createLabel(name: String) {
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." } require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationedit
// [FILE] LocationEditScreen.kt // [FILE] LocationEditScreen.kt
// [SEMANTICS] ui, screen, location, edit // [SEMANTICS] app, ui, screen, edit, location
package com.homebox.lens.ui.screen.locationedit package com.homebox.lens.ui.screen.locationedit
@@ -19,8 +18,8 @@ import com.homebox.lens.R
// [ENTITY: Function('LocationEditScreen')] // [ENTITY: Function('LocationEditScreen')]
/** /**
* @summary Composable-функция для экрана "Редактирование местоположения". * @summary Composable function for the "Edit Location" screen.
* @param locationId ID местоположения для редактирования или "new" для создания. * @param locationId The ID of the location to edit, or "new" to create one.
*/ */
@Composable @Composable
fun LocationEditScreen( fun LocationEditScreen(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListScreen.kt // [FILE] LocationsListScreen.kt
// [SEMANTICS] ui, screen, locations, list // [SEMANTICS] app, ui, screen, list, location
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
@@ -56,12 +55,12 @@ import com.homebox.lens.ui.theme.HomeboxLensTheme
// [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('LocationsListScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION: Function('LocationsListScreen')] -> [CALLS] -> [Function('MainScaffold')]
/** /**
* @summary Composable-функция для экрана "Список местоположений". * @summary Composable function for the "List of Locations" screen.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. * @param currentRoute The current route to highlight the active item in the Drawer.
* @param navigationActions Объект с навигационными действиями. * @param navigationActions The object with navigation actions.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение. * @param onLocationClick A lambda handler for clicking on a location.
* @param onAddNewLocationClick Лямбда-обработчик нажатия на кнопку добавления нового местоположения. * @param onAddNewLocationClick A lambda handler for clicking the button to add a new location.
* @param viewModel ViewModel для этого экрана. * @param viewModel The ViewModel for this screen.
*/ */
@Composable @Composable
fun LocationsListScreen( fun LocationsListScreen(
@@ -104,12 +103,12 @@ fun LocationsListScreen(
// [ENTITY: Function('LocationsListContent')] // [ENTITY: Function('LocationsListContent')]
// [RELATION: Function('LocationsListContent')] -> [CONSUMES_STATE] -> [SealedInterface('LocationsListUiState')] // [RELATION: Function('LocationsListContent')] -> [CONSUMES_STATE] -> [SealedInterface('LocationsListUiState')]
/** /**
* @summary Отображает основной контент экрана в зависимости от `uiState`. * @summary Displays the main content of the screen depending on the `uiState`.
* @param modifier Модификатор для стилизации. * @param modifier A modifier for styling.
* @param uiState Текущее состояние UI. * @param uiState The current UI state.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение. * @param onLocationClick A lambda handler for clicking on a location.
* @param onEditLocation Лямбда-обработчик для редактирования местоположения. * @param onEditLocation A lambda handler for editing a location.
* @param onDeleteLocation Лямбда-обработчик для удаления местоположения. * @param onDeleteLocation A lambda handler for deleting a location.
*/ */
@Composable @Composable
private fun LocationsListContent( private fun LocationsListContent(
@@ -167,11 +166,11 @@ private fun LocationsListContent(
// [ENTITY: Function('LocationCard')] // [ENTITY: Function('LocationCard')]
// [RELATION: Function('LocationCard')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] // [RELATION: Function('LocationCard')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
/** /**
* @summary Карточка для отображения одного местоположения. * @summary Card for displaying a single location.
* @param location Данные о местоположении. * @param location The data about the location.
* @param onClick Лямбда-обработчик нажатия на карточку. * @param onClick A lambda handler for clicking on the card.
* @param onEditClick Лямбда-обработчик нажатия на "Редактировать". * @param onEditClick A lambda handler for clicking "Edit".
* @param onDeleteClick Лямбда-обработчик нажатия на "Удалить". * @param onDeleteClick A lambda handler for clicking "Delete".
*/ */
@Composable @Composable
private fun LocationCard( private fun LocationCard(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListUiState.kt // [FILE] LocationsListUiState.kt
// [SEMANTICS] ui, state, locations // [SEMANTICS] app, ui, state, list, location
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
@@ -10,30 +9,30 @@ import com.homebox.lens.domain.model.LocationOutCount
// [ENTITY: SealedInterface('LocationsListUiState')] // [ENTITY: SealedInterface('LocationsListUiState')]
/** /**
* @summary Определяет возможные состояния UI для экрана списка местоположений. * @summary Defines the possible UI states for the list of locations screen.
* @see LocationsListViewModel * @see LocationsListViewModel
*/ */
sealed interface LocationsListUiState { sealed interface LocationsListUiState {
// [ENTITY: DataClass('Success')] // [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
/** /**
* @summary Состояние успешной загрузки данных. * @summary The state of a successful data load.
* @param locations Список местоположений для отображения. * @param locations The list of locations to display.
*/ */
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
// [END_ENTITY: DataClass('Success')] // [END_ENTITY: DataClass('Success')]
// [ENTITY: DataClass('Error')] // [ENTITY: DataClass('Error')]
/** /**
* @summary Состояние ошибки. * @summary The error state.
* @param message Сообщение об ошибке. * @param message The error message.
*/ */
data class Error(val message: String) : LocationsListUiState data class Error(val message: String) : LocationsListUiState
// [END_ENTITY: DataClass('Error')] // [END_ENTITY: DataClass('Error')]
// [ENTITY: Object('Loading')] // [ENTITY: Object('Loading')]
/** /**
* @summary Состояние загрузки данных. * @summary The data loading state.
*/ */
object Loading : LocationsListUiState object Loading : LocationsListUiState
// [END_ENTITY: Object('Loading')] // [END_ENTITY: Object('Loading')]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListViewModel.kt // [FILE] LocationsListViewModel.kt
// [SEMANTICS] ui, viewmodel, locations, hilt // [SEMANTICS] app, ui, viewmodel, list, location
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
@@ -21,10 +20,10 @@ import javax.inject.Inject
// [RELATION: ViewModel('LocationsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')] // [RELATION: ViewModel('LocationsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
// [RELATION: ViewModel('LocationsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LocationsListUiState')] // [RELATION: ViewModel('LocationsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LocationsListUiState')]
/** /**
* @summary ViewModel для экрана списка местоположений. * @summary ViewModel for the list of locations screen.
* @param getAllLocationsUseCase Use case для получения всех местоположений. * @param getAllLocationsUseCase Use case for getting all locations.
* @property uiState Поток, содержащий текущее состояние UI. * @property uiState A flow containing the current UI state.
* @invariant `uiState` всегда отражает результат последней операции загрузки. * @invariant `uiState` always reflects the result of the last load operation.
*/ */
@HiltViewModel @HiltViewModel
class LocationsListViewModel @Inject constructor( class LocationsListViewModel @Inject constructor(
@@ -40,8 +39,8 @@ class LocationsListViewModel @Inject constructor(
// [ENTITY: Function('loadLocations')] // [ENTITY: Function('loadLocations')]
/** /**
* @summary Загружает список местоположений из репозитория. * @summary Loads the list of locations from the repository.
* @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error. * @sideeffect Updates `_uiState` depending on the result: Loading -> Success/Error.
*/ */
fun loadLocations() { fun loadLocations() {
Timber.d("[DEBUG][ENTRYPOINT][loading_locations] Starting to load locations.") Timber.d("[DEBUG][ENTRYPOINT][loading_locations] Starting to load locations.")

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchScreen.kt // [FILE] SearchScreen.kt
// [SEMANTICS] ui, screen, search // [SEMANTICS] app, ui, screen, search
package com.homebox.lens.ui.screen.search package com.homebox.lens.ui.screen.search
@@ -17,9 +16,9 @@ import com.homebox.lens.ui.common.MainScaffold
// [RELATION: Function('SearchScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')] // [RELATION: Function('SearchScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('SearchScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION: Function('SearchScreen')] -> [CALLS] -> [Function('MainScaffold')]
/** /**
* @summary Composable-функция для экрана "Поиск". * @summary Composable function for the "Search" screen.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. * @param currentRoute The current route to highlight the active item in the Drawer.
* @param navigationActions Объект с навигационными действиями. * @param navigationActions The object with navigation actions.
*/ */
@Composable @Composable
fun SearchScreen( fun SearchScreen(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchViewModel.kt // [FILE] SearchViewModel.kt
// [SEMANTICS] ui, viewmodel, search // [SEMANTICS] app, ui, viewmodel, search
package com.homebox.lens.ui.screen.search package com.homebox.lens.ui.screen.search
// [IMPORTS] // [IMPORTS]

View File

@@ -0,0 +1,52 @@
// [FILE] SettingsScreen.kt
// [SEMANTICS] app, ui, screen, settings
package com.homebox.lens.ui.screen.settings
// [IMPORTS]
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [END_IMPORTS]
// [ENTITY: Function('SettingsScreen')]
// [RELATION: Function('SettingsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
/**
* @summary Composable function for the settings screen.
* @param currentRoute The current navigation route.
* @param navigationActions The object containing navigation actions.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
currentRoute: String?,
navigationActions: NavigationActions
) {
MainScaffold(
topBarTitle = stringResource(id = R.string.screen_title_settings),
currentRoute = currentRoute,
navigationActions = navigationActions
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Text(text = "Settings Screen (Under Construction)")
}
}
}
// [END_ENTITY: Function('SettingsScreen')]
// [END_FILE_SettingsScreen.kt]

View File

@@ -1,23 +1,27 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt // [FILE] SetupScreen.kt
// [SEMANTICS] ui, screen, setup, compose // [SEMANTICS] app, ui, screen, setup
@file:OptIn(ExperimentalMaterial3Api::class) @file:OptIn(ExperimentalMaterial3Api::class)
package com.homebox.lens.ui.screen.setup package com.homebox.lens.ui.screen.setup
// [IMPORTS] // [IMPORTS]
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R import com.homebox.lens.R
// [END_IMPORTS] // [END_IMPORTS]
@@ -26,10 +30,10 @@ import com.homebox.lens.R
// [RELATION: Function('SetupScreen')] -> [DEPENDS_ON] -> [ViewModel('SetupViewModel')] // [RELATION: Function('SetupScreen')] -> [DEPENDS_ON] -> [ViewModel('SetupViewModel')]
// [RELATION: Function('SetupScreen')] -> [CALLS] -> [Function('SetupScreenContent')] // [RELATION: Function('SetupScreen')] -> [CALLS] -> [Function('SetupScreenContent')]
/** /**
* @summary Главная Composable-функция для экрана настройки соединения с сервером. * @summary The main Composable function for the server connection setup screen.
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt. * @param viewModel The ViewModel for this screen, provided by Hilt.
* @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа. * @param onSetupComplete A lambda invoked after successful setup and login.
* @sideeffect Вызывает `onSetupComplete` при изменении `uiState.isSetupComplete`. * @sideeffect Calls `onSetupComplete` when `uiState.isSetupComplete` changes.
*/ */
@Composable @Composable
fun SetupScreen( fun SetupScreen(
@@ -55,12 +59,12 @@ fun SetupScreen(
// [ENTITY: Function('SetupScreenContent')] // [ENTITY: Function('SetupScreenContent')]
// [RELATION: Function('SetupScreenContent')] -> [CONSUMES_STATE] -> [DataClass('SetupUiState')] // [RELATION: Function('SetupScreenContent')] -> [CONSUMES_STATE] -> [DataClass('SetupUiState')]
/** /**
* @summary Отображает контент экрана настройки: поля ввода и кнопку. * @summary Displays the content of the setup screen: input fields and a button.
* @param uiState Текущее состояние UI. * @param uiState The current UI state.
* @param onServerUrlChange Лямбда-обработчик изменения URL сервера. * @param onServerUrlChange A lambda handler for changing the server URL.
* @param onUsernameChange Лямбда-обработчик изменения имени пользователя. * @param onUsernameChange A lambda handler for changing the username.
* @param onPasswordChange Лямбда-обработчик изменения пароля. * @param onPasswordChange A lambda handler for changing the password.
* @param onConnectClick Лямбда-обработчик нажатия на кнопку "Подключиться". * @param onConnectClick A lambda handler for clicking the "Connect" button.
*/ */
@Composable @Composable
private fun SetupScreenContent( private fun SetupScreenContent(
@@ -70,11 +74,7 @@ private fun SetupScreenContent(
onPasswordChange: (String) -> Unit, onPasswordChange: (String) -> Unit,
onConnectClick: () -> Unit onConnectClick: () -> Unit
) { ) {
Scaffold( Scaffold { paddingValues ->
topBar = {
TopAppBar(title = { Text(stringResource(id = R.string.setup_title)) })
}
) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -83,42 +83,76 @@ private fun SetupScreenContent(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
OutlinedTextField( Image(
value = uiState.serverUrl, imageVector = Icons.Default.Lock,
onValueChange = onServerUrlChange, contentDescription = stringResource(id = R.string.app_name),
label = { Text(stringResource(id = R.string.setup_server_url_label)) }, modifier = Modifier.size(128.dp)
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.username,
onValueChange = onUsernameChange,
label = { Text(stringResource(id = R.string.setup_username_label)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.password,
onValueChange = onPasswordChange,
label = { Text(stringResource(id = R.string.setup_password_label)) },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.setup_title),
style = MaterialTheme.typography.headlineLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Enter your Homebox server details to connect.",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
OutlinedTextField(
value = uiState.serverUrl,
onValueChange = onServerUrlChange,
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.username,
onValueChange = onUsernameChange,
label = { Text(stringResource(id = R.string.setup_username_label)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.password,
onValueChange = onPasswordChange,
label = { Text(stringResource(id = R.string.setup_password_label)) },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(24.dp))
Button( Button(
onClick = onConnectClick, onClick = onConnectClick,
enabled = !uiState.isLoading, enabled = !uiState.isLoading,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) { ) {
if (uiState.isLoading) { if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp)) CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
)
} else { } else {
Text(stringResource(id = R.string.setup_connect_button)) Text(stringResource(id = R.string.setup_connect_button))
} }
} }
uiState.error?.let { uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text(text = it, color = MaterialTheme.colorScheme.error) Text(
text = it,
color = MaterialTheme.colorScheme.error
)
} }
} }
} }

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupUiState.kt // [FILE] SetupUiState.kt
// [SEMANTICS] ui_state, data_model, immutable // [SEMANTICS] ui_state, data_model, immutable

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupViewModel.kt // [FILE] SetupViewModel.kt
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow // [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
package com.homebox.lens.ui.screen.setup package com.homebox.lens.ui.screen.setup
@@ -74,40 +73,61 @@ class SetupViewModel @Inject constructor(
// [END_ENTITY: Function('onUsernameChange')] // [END_ENTITY: Function('onUsernameChange')]
// [ENTITY: Function('onPasswordChange')] // [ENTITY: Function('onPasswordChange')]
/**
* @summary Updates the password in the UI state.
* @param newPassword The new password.
* @sideeffect Updates the `password` in `_uiState`.
*/
fun onPasswordChange(newPassword: String) { fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) } _uiState.update { it.copy(password = newPassword) }
} }
// [END_ENTITY: Function('onPasswordChange')] // [END_ENTITY: Function('onPasswordChange')]
// [ENTITY: Function('connect')] // [ENTITY: Function('areCredentialsSaved')]
fun connect() { /**
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.") * @summary Checks synchronously if credentials are saved.
viewModelScope.launch { * @return true if credentials are saved, false otherwise.
_uiState.update { it.copy(isLoading = true, error = null) } * @sideeffect None.
*/
fun areCredentialsSaved(): Boolean {
Timber.d("[DEBUG][ENTRYPOINT][checking_credentials_saved] Checking if credentials are saved.")
return credentialsRepository.areCredentialsSavedSync()
}
// [END_ENTITY: Function('areCredentialsSaved')]
val credentials = Credentials( // [ENTITY: Function('connect')]
serverUrl = _uiState.value.serverUrl.trim(), /**
username = _uiState.value.username.trim(), * @summary Initiates the connection process, saving credentials and attempting to log in.
password = _uiState.value.password * @sideeffect Updates `_uiState` with loading, error, and completion states.
) */
fun connect() {
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.") val credentials = Credentials(
credentialsRepository.saveCredentials(credentials) serverUrl = _uiState.value.serverUrl.trim(),
username = _uiState.value.username.trim(),
password = _uiState.value.password
)
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.") Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
loginUseCase(credentials).fold( credentialsRepository.saveCredentials(credentials)
onSuccess = {
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.") Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) } loginUseCase(credentials).fold(
}, onSuccess = {
onFailure = { exception -> Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.") _uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") } },
} onFailure = { exception ->
) Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
} _uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
} }
// [END_ENTITY: Function('connect')] )
}
}
// [END_ENTITY: Function('connect')]
} }
// [END_ENTITY: ViewModel('SetupViewModel')] // [END_ENTITY: ViewModel('SetupViewModel')]
// [END_FILE_SetupViewModel.kt] // [END_FILE_SetupViewModel.kt]

View File

@@ -0,0 +1,60 @@
// [FILE] SplashScreen.kt
// [SEMANTICS] app, ui, screen, splash
package com.homebox.lens.ui.screen.splash
// [IMPORTS]
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.homebox.lens.ui.navigation.Screen
import com.homebox.lens.ui.screen.setup.SetupViewModel
import timber.log.Timber
// [END_IMPORTS]
// [ENTITY: Composable('SplashScreen')]
// [RELATION: Composable('SplashScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
// [RELATION: Composable('SplashScreen')] -> [DEPENDS_ON] -> [ViewModel('SetupViewModel')]
/**
* @summary A splash screen that checks for saved credentials and navigates accordingly.
* @param navController The navigation controller for navigating to the next screen.
* @param viewModel The view model for checking credentials.
* @sideeffect Navigates to either the Setup or Dashboard screen.
*/
@Composable
fun SplashScreen(
navController: NavController,
viewModel: SetupViewModel = hiltViewModel()
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
LaunchedEffect(Unit) {
Timber.d("[DEBUG][ACTION][checking_credentials] Checking for saved credentials on splash screen.")
val areCredentialsSaved = viewModel.areCredentialsSaved()
val destination = if (areCredentialsSaved) {
Timber.d("[DEBUG][SUCCESS][credentials_found] Credentials found, navigating to Dashboard.")
Screen.Dashboard.route
} else {
Timber.d("[DEBUG][FALLBACK][no_credentials] No credentials found, navigating to Setup.")
Screen.Setup.route
}
navController.navigate(destination) {
popUpTo(Screen.Splash.route) {
inclusive = true
}
}
}
}
// [END_ENTITY: Composable('SplashScreen')]
// [END_FILE_SplashScreen.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Color.kt // [FILE] Color.kt
// [SEMANTICS] ui, theme, color // [SEMANTICS] app, ui, theme, color
package com.homebox.lens.ui.theme package com.homebox.lens.ui.theme
// [IMPORTS] // [IMPORTS]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Theme.kt // [FILE] Theme.kt
// [SEMANTICS] ui, theme // [SEMANTICS] app, ui, theme
package com.homebox.lens.ui.theme package com.homebox.lens.ui.theme
// [IMPORTS] // [IMPORTS]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Typography.kt // [FILE] Typography.kt
// [SEMANTICS] ui, theme, typography // [SEMANTICS] app, ui, theme, typography
package com.homebox.lens.ui.theme package com.homebox.lens.ui.theme
// [IMPORTS] // [IMPORTS]

View File

@@ -1,121 +0,0 @@
<resources>
<string name="app_name">Homebox Lens</string>
<!-- Common -->
<string name="create">Create</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="search">Search</string>
<string name="logout">Logout</string>
<string name="no_location">No location</string>
<string name="items_not_found">Items not found</string>
<string name="error_loading_failed">Failed to load data. Please try again.</string>
<!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
<string name="cd_scan_qr_code">Scan QR code</string>
<string name="cd_navigate_back">Navigate back</string>
<string name="cd_add_new_location">Add new location</string>
<string name="content_desc_add_label">Add new label</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string>
<string name="dashboard_section_quick_stats">Quick Stats</string>
<string name="dashboard_section_recently_added">Recently Added</string>
<string name="dashboard_section_locations">Locations</string>
<string name="dashboard_section_labels">Labels</string>
<string name="location_chip_label">%1$s (%2$d)</string>
<!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Total Items</string>
<string name="dashboard_stat_total_value">Total Value</string>
<string name="dashboard_stat_total_labels">Total Labels</string>
<string name="dashboard_stat_total_locations">Total Locations</string>
<!-- Navigation -->
<string name="nav_locations">Locations</string>
<string name="nav_labels">Labels</string>
<!-- Screen Titles -->
<string name="inventory_list_title">Inventory</string>
<!-- Screen Titles -->
<string name="item_details_title">Details</string>
<string name="item_edit_title">Edit Item</string>
<string name="labels_list_title">Labels</string>
<string name="locations_list_title">Locations</string>
<string name="search_title">Search</string>
<string name="save_item">Save</string>
<string name="item_name">Name</string>
<string name="item_description">Description</string>
<string name="item_quantity">Quantity</string>
<!-- Location Edit Screen -->
<string name="location_edit_title_create">Create Location</string>
<string name="location_edit_title_edit">Edit Location</string>
<!-- Locations List Screen -->
<string name="locations_not_found">Locations not found. Press + to add a new one.</string>
<string name="item_count">Items: %1$d</string>
<string name="cd_more_options">More options</string>
<!-- Setup Screen -->
<string name="setup_title">Server Setup</string>
<string name="setup_server_url_label">Server URL</string>
<string name="setup_username_label">Username</string>
<string name="setup_password_label">Password</string>
<string name="setup_connect_button">Connect</string>
<!-- Labels List Screen -->
<string name="screen_title_labels">Labels</string>
<string name="content_desc_navigate_back">Navigate back</string>
<string name="content_desc_create_label">Create new label</string>
<string name="content_desc_label_icon">Label icon</string>
<string name="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>
</resources>

View File

@@ -2,114 +2,149 @@
<string name="app_name">Homebox Lens</string> <string name="app_name">Homebox Lens</string>
<!-- Common --> <!-- Common -->
<string name="create">Создать</string> <string name="create">Create</string>
<string name="edit">Редактировать</string> <string name="edit">Edit</string>
<string name="delete">Удалить</string> <string name="delete">Delete</string>
<string name="search">Поиск</string> <string name="search">Search</string>
<string name="logout">Выйти</string> <string name="logout">Logout</string>
<string name="no_location">Нет локации</string> <string name="no_location">No location</string>
<string name="items_not_found">Элементы не найдены</string> <string name="items_not_found">Items not found</string>
<string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string> <string name="error_loading_failed">Failed to load data. Please try again.</string>
<!-- Content Descriptions --> <!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Открыть боковое меню</string> <string name="cd_open_navigation_drawer">Open navigation drawer</string>
<string name="cd_scan_qr_code">Сканировать QR-код</string> <string name="cd_scan_qr_code">Scan QR code</string>
<string name="cd_navigate_back">Вернуться назад</string> <string name="cd_navigate_back">Navigate back</string>
<string name="cd_add_new_location">Добавить новую локацию</string> <string name="cd_add_new_location">Add new location</string>
<string name="content_desc_add_label">Добавить новую метку</string> <string name="content_desc_add_label">Add new 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 --> <!-- Dashboard Screen -->
<string name="dashboard_title">Главная</string> <string name="dashboard_title">Dashboard</string>
<string name="dashboard_section_quick_stats">Быстрая статистика</string> <string name="dashboard_section_quick_stats">Quick Stats</string>
<string name="dashboard_section_recently_added">Недавно добавлено</string> <string name="dashboard_section_recently_added">Recently Added</string>
<string name="dashboard_section_locations">Места хранения</string> <string name="dashboard_section_locations">Locations</string>
<string name="dashboard_section_labels">Метки</string> <string name="dashboard_section_labels">Labels</string>
<string name="location_chip_label">%1$s (%2$d)</string> <string name="location_chip_label">%1$s (%2$d)</string>
<!-- Dashboard Statistics --> <!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Всего вещей</string> <string name="dashboard_stat_total_items">Total Items</string>
<string name="dashboard_stat_total_value">Общая стоимость</string> <string name="dashboard_stat_total_value">Total Value</string>
<string name="dashboard_stat_total_labels">Всего меток</string> <string name="dashboard_stat_total_labels">Total Labels</string>
<string name="dashboard_stat_total_locations">Всего локаций</string> <string name="dashboard_stat_total_locations">Total Locations</string>
<!-- Navigation --> <!-- Navigation -->
<string name="nav_locations">Локации</string> <string name="nav_locations">Locations</string>
<string name="nav_labels">Метки</string> <string name="nav_labels">Labels</string>
<!-- Screen Titles --> <!-- Screen Titles -->
<string name="inventory_list_title">Инвентарь</string> <string name="inventory_list_title">Inventory</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> <!-- Screen Titles -->
<string name="item_name">Название</string> <string name="item_details_title">Details</string>
<string name="item_description">Описание</string> <string name="item_edit_title">Edit Item</string>
<string name="item_quantity">Количество</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 --> <!-- Location Edit Screen -->
<string name="location_edit_title_create">Создать локацию</string> <string name="location_edit_title_create">Create Location</string>
<string name="location_edit_title_edit">Редактировать локацию</string> <string name="location_edit_title_edit">Edit Location</string>
<!-- Locations List Screen --> <!-- Locations List Screen -->
<string name="locations_not_found">Местоположения не найдены. Нажмите +, чтобы добавить новое.</string> <string name="locations_not_found">Locations not found. Press + to add a new one.</string>
<string name="item_count">Предметов: %1$d</string> <string name="item_count">Items: %1$d</string>
<string name="cd_more_options">Больше опций</string> <string name="cd_more_options">More options</string>
<!-- Setup Screen --> <!-- Setup Screen -->
<string name="screen_title_setup">Настройка</string> <string name="setup_title">Server Setup</string>
<string name="setup_title">Настройка сервера</string> <string name="setup_server_url_label">Server URL</string>
<string name="setup_server_url_label">URL сервера</string> <string name="setup_username_label">Username</string>
<string name="setup_username_label">Имя пользователя</string> <string name="setup_password_label">Password</string>
<string name="setup_password_label">Пароль</string> <string name="setup_connect_button">Connect</string>
<string name="setup_connect_button">Подключиться</string>
<!-- Labels List Screen --> <!-- Labels List Screen -->
<string name="screen_title_labels">Метки</string> <string name="screen_title_labels">Labels</string>
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string> <string name="content_desc_navigate_back">Navigate back</string>
<string name="content_desc_create_label">Создать новую метку</string> <string name="content_desc_create_label">Create new label</string>
<string name="content_desc_label_icon">Иконка метки</string> <string name="content_desc_label_icon">Label icon</string>
<string name="no_labels_found">Метки не найдены.</string> <string name="no_labels_found">No labels found.</string>
<string name="dialog_title_create_label">Создать метку</string> <string name="dialog_title_create_label">Create Label</string>
<string name="dialog_field_label_name">Название метки</string> <string name="dialog_field_label_name">Label Name</string>
<string name="dialog_button_create">Создать</string> <string name="dialog_button_create">Create</string>
<string name="dialog_button_cancel">Отмена</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>
<string name="item_edit_general_information">General Information</string>
<string name="item_edit_location">Location</string>
<string name="item_edit_labels">Labels</string>
<string name="item_edit_select_labels">Select Labels</string>
<string name="dialog_ok">OK</string>
<string name="dialog_cancel">Cancel</string>
<string name="item_edit_purchase_information">Purchase Information</string>
<string name="item_edit_purchase_price">Purchase Price</string>
<string name="item_edit_purchase_from">Purchase From</string>
<string name="item_edit_purchase_time">Purchase Time</string>
<string name="item_edit_select_date">Select Date</string>
<string name="item_edit_warranty_information">Warranty Information</string>
<string name="item_edit_lifetime_warranty">Lifetime Warranty</string>
<string name="item_edit_warranty_details">Warranty Details</string>
<string name="item_edit_warranty_expires">Warranty Expires</string>
<string name="item_edit_identification">Identification</string>
<string name="item_edit_asset_id">Asset ID</string>
<string name="item_edit_serial_number">Serial Number</string>
<string name="item_edit_manufacturer">Manufacturer</string>
<string name="item_edit_model_number">Model Number</string>
<string name="item_edit_status_notes">Status &amp; Notes</string>
<string name="item_edit_archived">Archived</string>
<string name="item_edit_insured">Insured</string>
<string name="item_edit_notes">Notes</string>
<string name="item_edit_sold_information">Sold Information</string>
<string name="item_edit_sold_price">Sold Price</string>
<string name="item_edit_sold_to">Sold To</string>
<string name="item_edit_sold_notes">Sold Notes</string>
<string name="item_edit_sold_time">Sold Time</string>
<!-- Search Screen -->
<string name="placeholder_search_items">Search items...</string>
<!-- Setup Screen -->
<string name="screen_title_setup">Setup</string>
<string name="screen_title_settings">Settings</string>
<!-- Label Edit Screen --> <!-- Label Edit Screen -->
<string name="label_edit_title_create">Создать метку</string> <string name="label_edit_title_create">Create label</string>
<string name="label_edit_title_edit">Редактировать метку</string> <string name="label_edit_title_edit">Edit label</string>
<string name="label_name_edit">Название метки</string> <string name="label_name_edit">Label name</string>
<!-- Common Actions --> <!-- Common Actions -->
<string name="back">Назад</string> <string name="back">Back</string>
<string name="save">Сохранить</string> <string name="save">Save</string>
<!-- Common Actions -->
<!-- Color Picker --> <!-- Color Picker -->
<string name="label_color">Цвет</string> <string name="label_color">Color</string>
<string name="label_hex_color">HEX-код цвета</string> <string name="label_hex_color">HEX color code</string>
</resources> </resources>

View File

@@ -1,129 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditViewModelTest.kt
// [SEMANTICS] ui, viewmodel, testing
package com.homebox.lens.ui.screen.itemedit
import app.cash.turbine.test
import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemCreate
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.model.ItemSummary
import com.homebox.lens.domain.usecase.CreateItemUseCase
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
import com.homebox.lens.domain.usecase.UpdateItemUseCase
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.util.UUID
@ExperimentalCoroutinesApi
class ItemEditViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private lateinit var createItemUseCase: CreateItemUseCase
private lateinit var updateItemUseCase: UpdateItemUseCase
private lateinit var getItemDetailsUseCase: GetItemDetailsUseCase
private lateinit var viewModel: ItemEditViewModel
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
createItemUseCase = mockk()
updateItemUseCase = mockk()
getItemDetailsUseCase = mockk()
viewModel = ItemEditViewModel(createItemUseCase, updateItemUseCase, getItemDetailsUseCase)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `loadItem with valid id should update uiState with item`() = runTest {
val itemId = UUID.randomUUID().toString()
val itemOut = ItemOut(id = itemId, name = "Test Item", description = "Description", quantity = 1, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
coEvery { getItemDetailsUseCase(itemId) } returns itemOut
viewModel.loadItem(itemId)
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(itemId, uiState.item?.id)
assertEquals("Test Item", uiState.item?.name)
}
@Test
fun `loadItem with null id should prepare a new item`() = runTest {
viewModel.loadItem(null)
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals("", uiState.item?.id)
assertEquals("", uiState.item?.name)
}
@Test
fun `saveItem should call createItemUseCase for new item`() = runTest {
val createdItemSummary = ItemSummary(id = UUID.randomUUID().toString(), name = "New Item", assetId = null, image = null, isArchived = false, labels = emptyList(), location = null, value = 0.0, createdAt = "2025-08-28T12:00:00Z", updatedAt = "2025-08-28T12:00:00Z")
coEvery { createItemUseCase(any()) } returns createdItemSummary
viewModel.loadItem(null)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.updateName("New Item")
viewModel.updateDescription("New Description")
viewModel.updateQuantity(2)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.saveItem()
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(createdItemSummary.id, uiState.item?.id)
}
@Test
fun `saveItem should call updateItemUseCase for existing item`() = runTest {
val itemId = UUID.randomUUID().toString()
val updatedItemOut = ItemOut(id = itemId, name = "Updated Item", description = "Updated Description", quantity = 4, images = emptyList(), location = null, labels = emptyList(), value = 12.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
coEvery { getItemDetailsUseCase(itemId) } returns ItemOut(id = itemId, name = "Existing Item", description = "Existing Description", quantity = 3, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
coEvery { updateItemUseCase(any()) } returns updatedItemOut
viewModel.loadItem(itemId)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.updateName("Updated Item")
viewModel.updateDescription("Updated Description")
viewModel.updateQuantity(4)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.saveItem()
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(itemId, uiState.item?.id)
assertEquals("Updated Item", uiState.item?.name)
assertEquals(4, uiState.item?.quantity)
}
}

View File

@@ -3,11 +3,13 @@
plugins { plugins {
// [PLUGIN] Android Application plugin // [PLUGIN] Android Application plugin
id("com.android.application") version "8.12.2" apply false id("com.android.application") version "8.4.0" apply false
// [PLUGIN] Kotlin Android plugin // [PLUGIN] Kotlin Android plugin
id("org.jetbrains.kotlin.android") version "1.9.22" apply false id("org.jetbrains.kotlin.android") version "1.9.23" apply false
// [PLUGIN] Hilt Android plugin // [PLUGIN] Hilt Android plugin
id("com.google.dagger.hilt.android") version "2.48.1" apply false id("com.google.dagger.hilt.android") version "2.48.1" apply false
// [PLUGIN] KSP plugin
id("com.google.devtools.ksp") version "1.9.23-1.0.19" apply false
} }
// [END_FILE_build.gradle.kts] // [END_FILE_build.gradle.kts]

View File

@@ -1,4 +1,3 @@
// [PACKAGE] buildsrc.dependencies
// [FILE] Dependencies.kt // [FILE] Dependencies.kt
// [SEMANTICS] build, dependencies // [SEMANTICS] build, dependencies
@@ -16,7 +15,7 @@ object Versions {
const val coroutines = "1.7.3" const val coroutines = "1.7.3"
// Jetpack Compose // Jetpack Compose
const val composeCompiler = "1.5.8" const val composeCompiler = "1.5.11"
const val composeBom = "2023.10.01" const val composeBom = "2023.10.01"
const val activityCompose = "1.8.2" const val activityCompose = "1.8.2"
const val navigationCompose = "2.7.6" const val navigationCompose = "2.7.6"

View File

@@ -6,6 +6,7 @@ plugins {
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android") id("com.google.dagger.hilt.android")
id("kotlin-kapt") id("kotlin-kapt")
id("com.google.devtools.ksp")
} }
android { android {
@@ -27,11 +28,11 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "17"
} }
} }
@@ -51,7 +52,7 @@ dependencies {
implementation(Libs.okhttp) implementation(Libs.okhttp)
implementation(Libs.okhttpLoggingInterceptor) implementation(Libs.okhttpLoggingInterceptor)
implementation(Libs.moshiKotlin) implementation(Libs.moshiKotlin)
kapt(Libs.moshiCodegen) ksp(Libs.moshiCodegen)
// [DEPENDENCY] Database (Room) // [DEPENDENCY] Database (Room)
implementation(Libs.roomRuntime) implementation(Libs.roomRuntime)

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] ExampleInstrumentedTest.kt // [FILE] ExampleInstrumentedTest.kt
// [SEMANTICS] testing, android, ktlint, rules // [SEMANTICS] testing, android, ktlint, rules

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] CustomRuleSetProvider.kt // [FILE] CustomRuleSetProvider.kt
// [SEMANTICS] ktlint, rules, provider // [SEMANTICS] ktlint, rules, provider
package com.busya.ktlint.rules package com.busya.ktlint.rules

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] FileHeaderRule.kt // [FILE] FileHeaderRule.kt
// [SEMANTICS] ktlint, rules, file_header // [SEMANTICS] ktlint, rules, file_header
package com.busya.ktlint.rules package com.busya.ktlint.rules

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] MandatoryEntityDeclarationRule.kt // [FILE] MandatoryEntityDeclarationRule.kt
// [SEMANTICS] ktlint, rules, entity_declaration // [SEMANTICS] ktlint, rules, entity_declaration
package com.busya.ktlint.rules package com.busya.ktlint.rules

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] NoStrayCommentsRule.kt // [FILE] NoStrayCommentsRule.kt
// [SEMANTICS] ktlint, rules, comments // [SEMANTICS] ktlint, rules, comments
package com.busya.ktlint.rules package com.busya.ktlint.rules

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] ExampleUnitTest.kt // [FILE] ExampleUnitTest.kt
// [SEMANTICS] testing, ktlint, rules // [SEMANTICS] testing, ktlint, rules

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.api
// [FILE] HomeboxApiService.kt // [FILE] HomeboxApiService.kt
// [SEMANTICS] data, api, retrofit // [SEMANTICS] data, api, retrofit
package com.homebox.lens.data.api package com.homebox.lens.data.api
@@ -11,7 +10,7 @@ import retrofit2.http.*
// [ENTITY: Interface('HomeboxApiService')] // [ENTITY: Interface('HomeboxApiService')]
/** /**
* @summary Определяет эндпоинты для взаимодействия с Homebox API, используя DTO. * @summary Defines the endpoints for interacting with the Homebox API using DTOs.
*/ */
interface HomeboxApiService { interface HomeboxApiService {

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] CustomFieldDto.kt // [FILE] CustomFieldDto.kt
// [SEMANTICS] data_transfer_object, custom_field // [SEMANTICS] data, dto, custom_field
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -12,7 +11,7 @@ import com.homebox.lens.domain.model.CustomField
// [ENTITY: DataClass('CustomFieldDto')] // [ENTITY: DataClass('CustomFieldDto')]
/** /**
* @summary DTO для кастомного поля. * @summary DTO for a custom field.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class CustomFieldDto( data class CustomFieldDto(
@@ -25,7 +24,7 @@ data class CustomFieldDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('CustomField')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('CustomField')]
/** /**
* @summary Маппер из CustomFieldDto в доменную модель CustomField. * @summary Mapper from CustomFieldDto to the CustomField domain model.
*/ */
fun CustomFieldDto.toDomain(): CustomField { fun CustomFieldDto.toDomain(): CustomField {
return CustomField( return CustomField(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] GroupStatisticsDto.kt // [FILE] GroupStatisticsDto.kt
// [SEMANTICS] data_transfer_object, statistics // [SEMANTICS] data, dto, statistics
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -12,7 +11,7 @@ import com.homebox.lens.domain.model.GroupStatistics
// [ENTITY: DataClass('GroupStatisticsDto')] // [ENTITY: DataClass('GroupStatisticsDto')]
/** /**
* @summary DTO для статистики. * @summary DTO for statistics.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GroupStatisticsDto( data class GroupStatisticsDto(
@@ -28,7 +27,7 @@ data class GroupStatisticsDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('GroupStatistics')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('GroupStatistics')]
/** /**
* @summary Маппер из GroupStatisticsDto в доменную модель GroupStatistics. * @summary Mapper from GroupStatisticsDto to the GroupStatistics domain model.
*/ */
fun GroupStatisticsDto.toDomain(): GroupStatistics { fun GroupStatisticsDto.toDomain(): GroupStatistics {
return GroupStatistics( return GroupStatistics(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ImageDto.kt // [FILE] ImageDto.kt
// [SEMANTICS] data_transfer_object, image // [SEMANTICS] data, dto, image
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -12,10 +11,10 @@ import com.homebox.lens.domain.model.Image
// [ENTITY: DataClass('ImageDto')] // [ENTITY: DataClass('ImageDto')]
/** /**
* @summary DTO для изображения. * @summary DTO for an image.
* @param id Уникальный идентификатор. * @param id The unique identifier.
* @param path Путь к файлу. * @param path The path to the file.
* @param isPrimary Является ли основным. * @param isPrimary Whether it is the primary image.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ImageDto( data class ImageDto(
@@ -28,7 +27,7 @@ data class ImageDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('Image')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('Image')]
/** /**
* @summary Маппер из ImageDto в доменную модель Image. * @summary Mapper from ImageDto to the Image domain model.
*/ */
fun ImageDto.toDomain(): Image { fun ImageDto.toDomain(): Image {
return Image( return Image(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemAttachmentDto.kt // [FILE] ItemAttachmentDto.kt
// [SEMANTICS] data_transfer_object, attachment // [SEMANTICS] data, dto, attachment
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -12,7 +11,7 @@ import com.homebox.lens.domain.model.ItemAttachment
// [ENTITY: DataClass('ItemAttachmentDto')] // [ENTITY: DataClass('ItemAttachmentDto')]
/** /**
* @summary DTO для вложения. * @summary DTO for an attachment.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemAttachmentDto( data class ItemAttachmentDto(
@@ -28,7 +27,7 @@ data class ItemAttachmentDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemAttachment')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemAttachment')]
/** /**
* @summary Маппер из ItemAttachmentDto в доменную модель ItemAttachment. * @summary Mapper from ItemAttachmentDto to the ItemAttachment domain model.
*/ */
fun ItemAttachmentDto.toDomain(): ItemAttachment { fun ItemAttachmentDto.toDomain(): ItemAttachment {
return ItemAttachment( return ItemAttachment(

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemCreateDto.kt // [FILE] ItemCreateDto.kt
// [SEMANTICS] data_transfer_object, item_creation // [SEMANTICS] data, dto, item_creation
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -12,22 +11,33 @@ import com.homebox.lens.domain.model.ItemCreate
// [ENTITY: DataClass('ItemCreateDto')] // [ENTITY: DataClass('ItemCreateDto')]
/** /**
* @summary DTO для создания вещи. * @summary DTO for creating an item.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemCreateDto( data class ItemCreateDto(
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "assetId") val assetId: String?,
@Json(name = "description") val description: String?, @Json(name = "description") val description: String?,
@Json(name = "notes") val notes: String?,
@Json(name = "serialNumber") val serialNumber: String?,
@Json(name = "quantity") val quantity: Int?, @Json(name = "quantity") val quantity: Int?,
@Json(name = "value") val value: Double?, @Json(name = "archived") val archived: Boolean?,
@Json(name = "purchasePrice") val purchasePrice: Double?, @Json(name = "assetId") val assetId: String?,
@Json(name = "purchaseDate") val purchaseDate: String?, @Json(name = "insured") val insured: Boolean?,
@Json(name = "warrantyUntil") val warrantyUntil: String?, @Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
@Json(name = "locationId") val locationId: String?, @Json(name = "manufacturer") val manufacturer: String?,
@Json(name = "modelNumber") val modelNumber: String?,
@Json(name = "notes") val notes: String?,
@Json(name = "parentId") val parentId: String?, @Json(name = "parentId") val parentId: String?,
@Json(name = "purchaseFrom") val purchaseFrom: String?,
@Json(name = "purchasePrice") val purchasePrice: Double?,
@Json(name = "purchaseTime") val purchaseTime: String?,
@Json(name = "serialNumber") val serialNumber: 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?,
@Json(name = "warrantyExpires") val warrantyExpires: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>? @Json(name = "labelIds") val labelIds: List<String>?
) )
// [END_ENTITY: DataClass('ItemCreateDto')] // [END_ENTITY: DataClass('ItemCreateDto')]
@@ -35,22 +45,33 @@ data class ItemCreateDto(
// [ENTITY: Function('toDto')] // [ENTITY: Function('toDto')]
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')] // [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')]
/** /**
* @summary Маппер из доменной модели ItemCreate в ItemCreateDto. * @summary Mapper from the ItemCreate domain model to ItemCreateDto.
*/ */
fun ItemCreate.toDto(): ItemCreateDto { fun ItemCreate.toItemCreateDto(): ItemCreateDto {
return ItemCreateDto( return ItemCreateDto(
name = this.name, name = this.name,
assetId = this.assetId,
description = this.description, description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity, quantity = this.quantity,
value = this.value, archived = this.archived,
purchasePrice = this.purchasePrice, assetId = this.assetId,
purchaseDate = this.purchaseDate, insured = this.insured,
warrantyUntil = this.warrantyUntil, lifetimeWarranty = this.lifetimeWarranty,
locationId = this.locationId, manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId, parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchasePrice = this.purchasePrice,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires,
locationId = this.locationId,
labelIds = this.labelIds labelIds = this.labelIds
) )
} }

View File

@@ -1,74 +0,0 @@
// [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]
// [ENTITY: DataClass('ItemOut')]
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LabelOutDto')]
/**
* @summary DTO для полной информации о вещи (GET /v1/items/{id}).
*/
@JsonClass(generateAdapter = true)
data class ItemOut(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "description") val description: String?,
@Json(name = "image") val image: String?,
@Json(name = "location") val location: LocationOut?,
@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')]
/**
* @summary DTO для краткой информации о вещи в списках (GET /v1/items).
*/
@JsonClass(generateAdapter = true)
data class ItemSummary(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "image") val image: String?,
@Json(name = "location") val location: LocationOut?,
@Json(name = "createdAt") val createdAt: String?
)
// [END_ENTITY: DataClass('ItemSummary')]
// [ENTITY: DataClass('ItemCreate')]
/**
* @summary DTO для создания новой вещи (POST /v1/items).
*/
@JsonClass(generateAdapter = true)
data class ItemCreate(
@Json(name = "name") val name: String,
@Json(name = "description") val description: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal?
)
// [END_ENTITY: DataClass('ItemCreate')]
// [ENTITY: DataClass('ItemUpdate')]
/**
* @summary DTO для обновления вещи (PUT /v1/items/{id}).
*/
@JsonClass(generateAdapter = true)
data class ItemUpdate(
@Json(name = "name") val name: String,
@Json(name = "description") val description: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal?
)
// [END_ENTITY: DataClass('ItemUpdate')]
// [END_FILE_ItemDto.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemOutDto.kt // [FILE] ItemOutDto.kt
// [SEMANTICS] data_transfer_object, item_detailed // [SEMANTICS] data, dto, item_detailed
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -24,10 +23,20 @@ data class ItemOutDto(
@Json(name = "serialNumber") val serialNumber: String?, @Json(name = "serialNumber") val serialNumber: String?,
@Json(name = "quantity") val quantity: Int, @Json(name = "quantity") val quantity: Int,
@Json(name = "isArchived") val isArchived: Boolean, @Json(name = "isArchived") val isArchived: Boolean,
@Json(name = "value") val value: Double,
@Json(name = "purchasePrice") val purchasePrice: Double?, @Json(name = "purchasePrice") val purchasePrice: Double?,
@Json(name = "purchaseDate") val purchaseDate: String?, @Json(name = "purchaseTime") val purchaseTime: String?,
@Json(name = "warrantyUntil") val warrantyUntil: String?, @Json(name = "purchaseFrom") val purchaseFrom: String?,
@Json(name = "warrantyExpires") val warrantyExpires: String?,
@Json(name = "warrantyDetails") val warrantyDetails: String?,
@Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
@Json(name = "insured") val insured: Boolean?,
@Json(name = "manufacturer") val manufacturer: String?,
@Json(name = "modelNumber") val modelNumber: String?,
@Json(name = "soldPrice") val soldPrice: Double?,
@Json(name = "soldTime") val soldTime: String?,
@Json(name = "soldTo") val soldTo: String?,
@Json(name = "soldNotes") val soldNotes: String?,
@Json(name = "syncChildItemsLocations") val syncChildItemsLocations: Boolean?,
@Json(name = "location") val location: LocationOutDto?, @Json(name = "location") val location: LocationOutDto?,
@Json(name = "parent") val parent: ItemSummaryDto?, @Json(name = "parent") val parent: ItemSummaryDto?,
@Json(name = "children") val children: List<ItemSummaryDto>, @Json(name = "children") val children: List<ItemSummaryDto>,
@@ -44,7 +53,7 @@ data class ItemOutDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemOut')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemOut')]
/** /**
* @summary Маппер из ItemOutDto в доменную модель ItemOut. * @summary Mapper from ItemOutDto to the ItemOut domain model.
*/ */
fun ItemOutDto.toDomain(): ItemOut { fun ItemOutDto.toDomain(): ItemOut {
return ItemOut( return ItemOut(
@@ -56,10 +65,20 @@ fun ItemOutDto.toDomain(): ItemOut {
serialNumber = this.serialNumber, serialNumber = this.serialNumber,
quantity = this.quantity, quantity = this.quantity,
isArchived = this.isArchived, isArchived = this.isArchived,
value = this.value,
purchasePrice = this.purchasePrice, purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate, purchaseTime = this.purchaseTime,
warrantyUntil = this.warrantyUntil, purchaseFrom = this.purchaseFrom,
warrantyExpires = this.warrantyExpires,
warrantyDetails = this.warrantyDetails,
lifetimeWarranty = this.lifetimeWarranty,
insured = this.insured,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
soldNotes = this.soldNotes,
syncChildItemsLocations = this.syncChildItemsLocations,
location = this.location?.toDomain(), location = this.location?.toDomain(),
parent = this.parent?.toDomain(), parent = this.parent?.toDomain(),
children = this.children.map { it.toDomain() }, children = this.children.map { it.toDomain() },

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemSummaryDto.kt // [FILE] ItemSummaryDto.kt
// [SEMANTICS] data_transfer_object, item_summary // [SEMANTICS] data, dto, item_summary
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -20,10 +19,10 @@ data class ItemSummaryDto(
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "assetId") val assetId: String?, @Json(name = "assetId") val assetId: String?,
@Json(name = "image") val image: ImageDto?, @Json(name = "image") val image: ImageDto?,
@Json(name = "isArchived") val isArchived: Boolean, @Json(name = "isArchived") val isArchived: Boolean? = false,
@Json(name = "labels") val labels: List<LabelOutDto>, @Json(name = "labels") val labels: List<LabelOutDto>,
@Json(name = "location") val location: LocationOutDto?, @Json(name = "location") val location: LocationOutDto?,
@Json(name = "value") val value: Double, @Json(name = "value") val value: Double? = 0.0,
@Json(name = "createdAt") val createdAt: String, @Json(name = "createdAt") val createdAt: String,
@Json(name = "updatedAt") val updatedAt: String @Json(name = "updatedAt") val updatedAt: String
) )
@@ -32,7 +31,7 @@ data class ItemSummaryDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
/** /**
* @summary Маппер из ItemSummaryDto в доменную модель ItemSummary. * @summary Mapper from ItemSummaryDto to the ItemSummary domain model.
*/ */
fun ItemSummaryDto.toDomain(): ItemSummary { fun ItemSummaryDto.toDomain(): ItemSummary {
return ItemSummary( return ItemSummary(
@@ -40,10 +39,10 @@ fun ItemSummaryDto.toDomain(): ItemSummary {
name = this.name, name = this.name,
assetId = this.assetId, assetId = this.assetId,
image = this.image?.toDomain(), image = this.image?.toDomain(),
isArchived = this.isArchived, isArchived = this.isArchived ?: false,
labels = this.labels.map { it.toDomain() }, labels = this.labels.map { it.toDomain() },
location = this.location?.toDomain(), location = this.location?.toDomain(),
value = this.value, value = this.value ?: 0.0,
createdAt = this.createdAt, createdAt = this.createdAt,
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemUpdateDto.kt // [FILE] ItemUpdateDto.kt
// [SEMANTICS] data_transfer_object, item_update // [SEMANTICS] data, dto, item_update
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -17,18 +16,28 @@ import com.homebox.lens.domain.model.ItemUpdate
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ItemUpdateDto( data class ItemUpdateDto(
@Json(name = "name") val name: String?, @Json(name = "name") val name: String?,
@Json(name = "assetId") val assetId: String?,
@Json(name = "description") val description: String?, @Json(name = "description") val description: String?,
@Json(name = "notes") val notes: String?,
@Json(name = "serialNumber") val serialNumber: String?,
@Json(name = "quantity") val quantity: Int?, @Json(name = "quantity") val quantity: Int?,
@Json(name = "isArchived") val isArchived: Boolean?, @Json(name = "archived") val archived: Boolean?,
@Json(name = "value") val value: Double?, @Json(name = "assetId") val assetId: String?,
@Json(name = "purchasePrice") val purchasePrice: Double?, @Json(name = "insured") val insured: Boolean?,
@Json(name = "purchaseDate") val purchaseDate: String?, @Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
@Json(name = "warrantyUntil") val warrantyUntil: String?, @Json(name = "manufacturer") val manufacturer: String?,
@Json(name = "locationId") val locationId: String?, @Json(name = "modelNumber") val modelNumber: String?,
@Json(name = "notes") val notes: String?,
@Json(name = "parentId") val parentId: String?, @Json(name = "parentId") val parentId: String?,
@Json(name = "purchaseFrom") val purchaseFrom: String?,
@Json(name = "purchasePrice") val purchasePrice: Double?,
@Json(name = "purchaseTime") val purchaseTime: String?,
@Json(name = "serialNumber") val serialNumber: 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?,
@Json(name = "warrantyExpires") val warrantyExpires: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>? @Json(name = "labelIds") val labelIds: List<String>?
) )
// [END_ENTITY: DataClass('ItemUpdateDto')] // [END_ENTITY: DataClass('ItemUpdateDto')]
@@ -38,21 +47,31 @@ data class ItemUpdateDto(
/** /**
* @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto. * @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto.
*/ */
fun ItemUpdate.toDto(): ItemUpdateDto { fun ItemUpdate.toItemUpdateDto(): ItemUpdateDto {
return ItemUpdateDto( return ItemUpdateDto(
name = this.name, name = this.name,
assetId = this.assetId,
description = this.description, description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity, quantity = this.quantity,
isArchived = this.isArchived, archived = this.archived,
value = this.value, assetId = this.assetId,
purchasePrice = this.purchasePrice, insured = this.insured,
purchaseDate = this.purchaseDate, lifetimeWarranty = this.lifetimeWarranty,
warrantyUntil = this.warrantyUntil, manufacturer = this.manufacturer,
locationId = this.locationId, modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId, parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchasePrice = this.purchasePrice,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires,
locationId = this.locationId,
labelIds = this.labelIds labelIds = this.labelIds
) )
} }

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelCreateDto.kt // [FILE] LabelCreateDto.kt
// [SEMANTICS] data_transfer_object, label, create, api // [SEMANTICS] data, dto, label, create
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS] // [IMPORTS]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelOutDto.kt // [FILE] LabelOutDto.kt
// [SEMANTICS] data_transfer_object, label // [SEMANTICS] data, dto, label
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -29,13 +28,14 @@ data class LabelOutDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
/** /**
* @summary Маппер из LabelOutDto в доменную модель LabelOut. * @summary Mapper from LabelOutDto to the LabelOut domain model.
*/ */
fun LabelOutDto.toDomain(): LabelOut { fun LabelOutDto.toDomain(): LabelOut {
return LabelOut( return LabelOut(
id = this.id, id = this.id,
name = this.name, name = this.name,
color = this.color ?: "", description = this.description,
color = this.color ?: "#000000",
isArchived = this.isArchived ?: false, isArchived = this.isArchived ?: false,
createdAt = this.createdAt, createdAt = this.createdAt,
updatedAt = this.updatedAt updatedAt = this.updatedAt

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelSummaryDto.kt // [FILE] LabelSummaryDto.kt
// [SEMANTICS] data_transfer_object, label, summary, api, mapper // [SEMANTICS] data, dto, label, summary
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS] // [IMPORTS]
@@ -35,7 +34,8 @@ data class LabelSummaryDto(
fun LabelSummaryDto.toDomain(): LabelSummary { fun LabelSummaryDto.toDomain(): LabelSummary {
return LabelSummary( return LabelSummary(
id = this.id, id = this.id,
name = this.name name = this.name,
color = this.color ?: ""
) )
} }
// [END_ENTITY: Function('toDomain')] // [END_ENTITY: Function('toDomain')]

View File

@@ -1,31 +1,24 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelUpdateDto.kt // [FILE] LabelUpdateDto.kt
// [SEMANTICS] data_transfer_object, label, update // [SEMANTICS] data, dto, label, update
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS] // [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.LabelUpdate
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: DataClass('LabelUpdateDto')] // [ENTITY: DataClass('LabelUpdateDto')]
/**
* @summary DTO for updating a label.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LabelUpdateDto( data class LabelUpdateDto(
@Json(name = "name") @Json(name = "name")
val name: String?, val name: String?,
@Json(name = "color") @Json(name = "color")
val color: String? val color: String?,
@Json(name = "description")
val description: String?
) )
// [END_ENTITY: DataClass('LabelUpdateDto')] // [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] // [END_FILE_LabelUpdateDto.kt]

View File

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

View File

@@ -1,34 +0,0 @@
// [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]
// [ENTITY: DataClass('LocationOut')]
/**
* @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')]
/**
* @summary DTO для информации о местоположении со счетчиком вещей.
*/
@JsonClass(generateAdapter = true)
data class LocationOutCount(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "itemCount") val itemCount: Int
)
// [END_ENTITY: DataClass('LocationOutCount')]
// [END_FILE_LocationDto.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationOutCountDto.kt // [FILE] LocationOutCountDto.kt
// [SEMANTICS] data_transfer_object, location, count // [SEMANTICS] data, dto, location, count
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -12,7 +11,7 @@ import com.homebox.lens.domain.model.LocationOutCount
// [ENTITY: DataClass('LocationOutCountDto')] // [ENTITY: DataClass('LocationOutCountDto')]
/** /**
* @summary DTO для местоположения со счетчиком. * @summary DTO for a location with an item count.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationOutCountDto( data class LocationOutCountDto(
@@ -30,13 +29,13 @@ data class LocationOutCountDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOutCount')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOutCount')]
/** /**
* @summary Маппер из LocationOutCountDto в доменную модель LocationOutCount. * @summary Mapper from LocationOutCountDto to the LocationOutCount domain model.
*/ */
fun LocationOutCountDto.toDomain(): LocationOutCount { fun LocationOutCountDto.toDomain(): LocationOutCount {
return LocationOutCount( return LocationOutCount(
id = this.id, id = this.id,
name = this.name, name = this.name,
color = this.color ?: "", color = this.color ?: "#000000",
isArchived = this.isArchived ?: false, isArchived = this.isArchived ?: false,
itemCount = this.itemCount, itemCount = this.itemCount,
createdAt = this.createdAt, createdAt = this.createdAt,

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationOutDto.kt // [FILE] LocationOutDto.kt
// [SEMANTICS] data_transfer_object, location, output // [SEMANTICS] data, dto, location
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS] // [IMPORTS]
@@ -17,9 +16,9 @@ data class LocationOutDto(
@Json(name = "name") @Json(name = "name")
val name: String, val name: String,
@Json(name = "color") @Json(name = "color")
val color: String, val color: String? = "#000000",
@Json(name = "isArchived") @Json(name = "isArchived")
val isArchived: Boolean, val isArchived: Boolean? = false,
@Json(name = "createdAt") @Json(name = "createdAt")
val createdAt: String, val createdAt: String,
@Json(name = "updatedAt") @Json(name = "updatedAt")
@@ -29,12 +28,15 @@ data class LocationOutDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOut')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOut')]
/**
* @summary Mapper from LocationOutDto to the LocationOut domain model.
*/
fun LocationOutDto.toDomain(): LocationOut { fun LocationOutDto.toDomain(): LocationOut {
return LocationOut( return LocationOut(
id = this.id, id = this.id,
name = this.name, name = this.name,
color = this.color, color = this.color ?: "#000000",
isArchived = this.isArchived, isArchived = this.isArchived ?: false,
createdAt = this.createdAt, createdAt = this.createdAt,
updatedAt = this.updatedAt updatedAt = this.updatedAt
) )

View File

@@ -1,31 +1,25 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationUpdateDto.kt // [FILE] LocationUpdateDto.kt
// [SEMANTICS] data_transfer_object, location, update // [SEMANTICS] data, dto, location, update
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS] // [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.homebox.lens.domain.model.LocationUpdate
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: DataClass('LocationUpdateDto')] // [ENTITY: DataClass('LocationUpdateDto')]
/**
* @summary DTO for updating a location.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationUpdateDto( data class LocationUpdateDto(
@Json(name = "name") @Json(name = "name")
val name: String?, val name: String?,
@Json(name = "color") @Json(name = "color")
val color: String? val color: String?,
@Json(name = "description")
val description: String?
) )
// [END_ENTITY: DataClass('LocationUpdateDto')] // [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] // [END_FILE_LocationUpdateDto.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LoginFormDto.kt // [FILE] LoginFormDto.kt
// [SEMANTICS] data, dto, api, login // [SEMANTICS] data, dto, login
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS] // [IMPORTS]
@@ -9,6 +8,9 @@ import com.squareup.moshi.JsonClass
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: DataClass('LoginFormDto')] // [ENTITY: DataClass('LoginFormDto')]
/**
* @summary DTO for the login form.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LoginFormDto( data class LoginFormDto(
@Json(name = "username") val username: String, @Json(name = "username") val username: String,

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] MaintenanceEntryDto.kt // [FILE] MaintenanceEntryDto.kt
// [SEMANTICS] data_transfer_object, maintenance // [SEMANTICS] data, dto, maintenance
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -12,7 +11,7 @@ import com.homebox.lens.domain.model.MaintenanceEntry
// [ENTITY: DataClass('MaintenanceEntryDto')] // [ENTITY: DataClass('MaintenanceEntryDto')]
/** /**
* @summary DTO для записи об обслуживании. * @summary DTO for a maintenance entry.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MaintenanceEntryDto( data class MaintenanceEntryDto(
@@ -30,7 +29,7 @@ data class MaintenanceEntryDto(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('MaintenanceEntry')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('MaintenanceEntry')]
/** /**
* @summary Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry. * @summary Mapper from MaintenanceEntryDto to the MaintenanceEntry domain model.
*/ */
fun MaintenanceEntryDto.toDomain(): MaintenanceEntry { fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
return MaintenanceEntry( return MaintenanceEntry(

View File

@@ -1,25 +0,0 @@
// [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]
// [ENTITY: DataClass('PaginationResult')]
/**
* @summary DTO для пагинированных результатов от API.
*/
@JsonClass(generateAdapter = true)
data class PaginationResult<T>(
@Json(name = "items") val items: List<T>,
@Json(name = "page") val page: Int,
@Json(name = "pages") val pages: Int,
@Json(name = "total") val total: Int,
@Json(name = "pageSize") val pageSize: Int
)
// [END_ENTITY: DataClass('PaginationResult')]
// [END_FILE_PaginationDto.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] PaginationResultDto.kt // [FILE] PaginationResultDto.kt
// [SEMANTICS] data_transfer_object, pagination // [SEMANTICS] data, dto, pagination
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
@@ -26,12 +25,11 @@ data class PaginationResultDto<T>(
// [ENTITY: Function('toDomain')] // [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('PaginationResult')] // [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('PaginationResult')]
/** /**
* @summary Маппер из PaginationResultDto в доменную модель PaginationResult. * @summary Mapper from PaginationResultDto to the PaginationResult domain model.
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
*/ */
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> { fun <T, R> PaginationResultDto<T>.toDomain(mapper: (T) -> R): PaginationResult<R> {
return PaginationResult( return PaginationResult(
items = this.items.map(transform), items = this.items.map(mapper),
page = this.page, page = this.page,
pageSize = this.pageSize, pageSize = this.pageSize,
total = this.total total = this.total

View File

@@ -1,25 +0,0 @@
// [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]
// [ENTITY: DataClass('GroupStatistics')]
/**
* @summary DTO для статистической информации.
*/
@JsonClass(generateAdapter = true)
data class GroupStatistics(
@Json(name = "totalValue") val totalValue: BigDecimal,
@Json(name = "totalItems") val totalItems: Int,
@Json(name = "locations") val locations: Int,
@Json(name = "labels") val labels: Int
)
// [END_ENTITY: DataClass('GroupStatistics')]
// [END_FILE_StatisticsDto.kt]

View File

@@ -1,14 +1,17 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] TokenResponseDto.kt // [FILE] TokenResponseDto.kt
// [SEMANTICS] data, dto, api, token // [SEMANTICS] data, dto, token
package com.homebox.lens.data.api.dto package com.homebox.lens.data.api.dto
// [IMPORTS] // [IMPORTS]
import com.homebox.lens.domain.model.TokenResponse
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: DataClass('TokenResponseDto')] // [ENTITY: DataClass('TokenResponseDto')]
/**
* @summary DTO for the token response.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class TokenResponseDto( data class TokenResponseDto(
@Json(name = "token") val token: String, @Json(name = "token") val token: String,
@@ -16,4 +19,18 @@ data class TokenResponseDto(
@Json(name = "expiresAt") val expiresAt: String @Json(name = "expiresAt") val expiresAt: String
) )
// [END_ENTITY: DataClass('TokenResponseDto')] // [END_ENTITY: DataClass('TokenResponseDto')]
// [ENTITY: Function('toDomain')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('TokenResponse')]
/**
* @summary Mapper from TokenResponseDto to the TokenResponse domain model.
*/
fun TokenResponseDto.toDomain(): TokenResponse {
return TokenResponse(
token = this.token,
attachmentToken = this.attachmentToken,
expiresAt = this.expiresAt
)
}
// [END_ENTITY: Function('toDomain')]
// [END_FILE_TokenResponseDto.kt] // [END_FILE_TokenResponseDto.kt]

View File

@@ -1,30 +0,0 @@
// [PACKAGE] com.homebox.lens.data.api.mapper
// [FILE] TokenMapper.kt
// [SEMANTICS] mapper, data_conversion, clean_architecture
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')]
/**
* @summary Преобразует DTO-объект токена в доменную модель.
* @receiver [TokenResponseDto] объект из слоя данных.
* @return [TokenResponse] объект для доменного слоя.
* @throws IllegalArgumentException если токен в DTO пустой.
*/
fun TokenResponseDto.toDomain(): TokenResponse {
require(this.token.isNotBlank()) { "DTO token is blank, cannot map to domain model." }
val domainModel = TokenResponse(token = this.token)
check(domainModel.token.isNotBlank()) { "Domain model token is blank after mapping." }
return domainModel
}
// [END_ENTITY: Function('toDomain')]
// [END_FILE_TokenMapper.kt]

View File

@@ -1,19 +1,19 @@
// [PACKAGE] com.homebox.lens.data.api.model
// [FILE] LoginRequest.kt // [FILE] LoginRequest.kt
// [SEMANTICS] dto, network, serialization, authentication // [SEMANTICS] data, dto, login
package com.homebox.lens.data.api.model package com.homebox.lens.data.api.model
// [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
// [END_IMPORTS]
// [ENTITY: DataClass('LoginRequest')]
/** /**
* [ENTITY: DataClass('LoginRequest')] * @summary DTO for the authentication request.
* [CONTRACT] * @property username The user's name.
* DTO (Data Transfer Object) для запроса на аутентификацию. * @property password The user's password.
* @property username Имя пользователя. * @invariant The properties must not be blank.
* @property password Пароль пользователя.
* @invariant Свойства не должны быть пустыми.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LoginRequest( data class LoginRequest(
@@ -21,9 +21,9 @@ data class LoginRequest(
@Json(name = "password") val password: String @Json(name = "password") val password: String
) { ) {
init { init {
// [INVARIANT_CHECK] require(username.isNotBlank()) { "Username cannot be blank." }
require(username.isNotBlank()) { "[INVARIANT_FAILED] Username cannot be blank." } require(password.isNotBlank()) { "Password cannot be blank." }
require(password.isNotBlank()) { "[INVARIANT_FAILED] Password cannot be blank." }
} }
} }
// [END_ENTITY: DataClass('LoginRequest')]
// [END_FILE_LoginRequest.kt] // [END_FILE_LoginRequest.kt]

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db
// [FILE] Converters.kt // [FILE] Converters.kt
// [SEMANTICS] data, database, room, converter // [SEMANTICS] data, database, room, converter
package com.homebox.lens.data.db package com.homebox.lens.data.db
@@ -10,7 +9,7 @@ import java.math.BigDecimal
// [ENTITY: Class('Converters')] // [ENTITY: Class('Converters')]
/** /**
* @summary Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию. * @summary Provides TypeConverters for Room for types not supported by default.
*/ */
class Converters { class Converters {
// [ENTITY: Function('fromString')] // [ENTITY: Function('fromString')]

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db
// [FILE] HomeboxDatabase.kt // [FILE] HomeboxDatabase.kt
// [SEMANTICS] data, database, room // [SEMANTICS] data, database, room
package com.homebox.lens.data.db package com.homebox.lens.data.db
@@ -15,7 +14,7 @@ import com.homebox.lens.data.db.entity.*
// [ENTITY: Database('HomeboxDatabase')] // [ENTITY: Database('HomeboxDatabase')]
/** /**
* @summary Основной класс для работы с локальной базой данных Room. * @summary The main class for working with the local Room database.
*/ */
@Database( @Database(
entities = [ entities = [
@@ -24,7 +23,7 @@ import com.homebox.lens.data.db.entity.*
LocationEntity::class, LocationEntity::class,
ItemLabelCrossRef::class ItemLabelCrossRef::class
], ],
version = 1, version = 2,
exportSchema = false exportSchema = false
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] ItemDao.kt // [FILE] ItemDao.kt
// [SEMANTICS] data, database, dao, item // [SEMANTICS] data, database, dao, item
package com.homebox.lens.data.db.dao package com.homebox.lens.data.db.dao
@@ -13,7 +12,7 @@ import kotlinx.coroutines.flow.Flow
// [ENTITY: Interface('ItemDao')] // [ENTITY: Interface('ItemDao')]
/** /**
* @summary Предоставляет методы для работы с 'items' в локальной БД. * @summary Provides methods for working with 'items' in the local DB.
*/ */
@Dao @Dao
interface ItemDao { interface ItemDao {

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] LabelDao.kt // [FILE] LabelDao.kt
// [SEMANTICS] data, database, dao, label // [SEMANTICS] data, database, dao, label
package com.homebox.lens.data.db.dao package com.homebox.lens.data.db.dao
@@ -13,7 +12,7 @@ import com.homebox.lens.data.db.entity.LabelEntity
// [ENTITY: Interface('LabelDao')] // [ENTITY: Interface('LabelDao')]
/** /**
* @summary Предоставляет методы для работы с 'labels' в локальной БД. * @summary Provides methods for working with 'labels' in the local DB.
*/ */
@Dao @Dao
interface LabelDao { interface LabelDao {

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] LocationDao.kt // [FILE] LocationDao.kt
// [SEMANTICS] data, database, dao, location // [SEMANTICS] data, database, dao, location
package com.homebox.lens.data.db.dao package com.homebox.lens.data.db.dao
@@ -13,7 +12,7 @@ import com.homebox.lens.data.db.entity.LocationEntity
// [ENTITY: Interface('LocationDao')] // [ENTITY: Interface('LocationDao')]
/** /**
* @summary Предоставляет методы для работы с 'locations' в локальной БД. * @summary Provides methods for working with 'locations' in the local DB.
*/ */
@Dao @Dao
interface LocationDao { interface LocationDao {

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemEntity.kt // [FILE] ItemEntity.kt
// [SEMANTICS] data, database, entity, item // [SEMANTICS] data, database, entity, item
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
@@ -6,22 +5,40 @@ package com.homebox.lens.data.db.entity
// [IMPORTS] // [IMPORTS]
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.math.BigDecimal
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: DatabaseTable('ItemEntity')] // [ENTITY: DatabaseTable('ItemEntity')]
/** /**
* @summary Представляет собой строку в таблице 'items' в локальной БД. * @summary Represents a row in the 'items' table in the local DB.
*/ */
@Entity(tableName = "items") @Entity(tableName = "items")
data class ItemEntity( data class ItemEntity(
@PrimaryKey val id: String, @PrimaryKey val id: String,
val name: String, val name: String,
val description: String?, val description: String?,
val quantity: Int,
val image: String?, val image: String?,
val locationId: String?, val locationId: String?,
val value: BigDecimal?, val purchasePrice: Double?,
val createdAt: String? val createdAt: String?,
val archived: Boolean,
val assetId: String?,
val insured: Boolean,
val lifetimeWarranty: Boolean,
val manufacturer: String?,
val modelNumber: String?,
val notes: String?,
val parentId: String?,
val purchaseFrom: String?,
val purchaseTime: String?,
val serialNumber: String?,
val soldNotes: String?,
val soldPrice: Double?,
val soldTime: String?,
val soldTo: String?,
val syncChildItemsLocations: Boolean,
val warrantyDetails: String?,
val warrantyExpires: String?
) )
// [END_ENTITY: DatabaseTable('ItemEntity')] // [END_ENTITY: DatabaseTable('ItemEntity')]

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemLabelCrossRef.kt // [FILE] ItemLabelCrossRef.kt
// [SEMANTICS] data, database, entity, relation // [SEMANTICS] data, database, entity, relation
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
@@ -10,7 +9,7 @@ import androidx.room.Index
// [ENTITY: DatabaseTable('ItemLabelCrossRef')] // [ENTITY: DatabaseTable('ItemLabelCrossRef')]
/** /**
* @summary Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity. * @summary Table for the many-to-many relationship between ItemEntity and LabelEntity.
*/ */
@Entity( @Entity(
primaryKeys = ["itemId", "labelId"], primaryKeys = ["itemId", "labelId"],

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemWithLabels.kt // [FILE] ItemWithLabels.kt
// [SEMANTICS] data, database, entity, relation // [SEMANTICS] data, database, entity, relation
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
@@ -13,7 +12,7 @@ import androidx.room.Relation
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('ItemEntity')] // [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('ItemEntity')]
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('LabelEntity')] // [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('LabelEntity')]
/** /**
* @summary POJO для получения ItemEntity вместе со связанными LabelEntity. * @summary POJO for retrieving an ItemEntity with its associated LabelEntity objects.
*/ */
data class ItemWithLabels( data class ItemWithLabels(
@Embedded val item: ItemEntity, @Embedded val item: ItemEntity,

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] LabelEntity.kt // [FILE] LabelEntity.kt
// [SEMANTICS] data, database, entity, label // [SEMANTICS] data, database, entity, label
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
@@ -10,7 +9,7 @@ import androidx.room.PrimaryKey
// [ENTITY: DatabaseTable('LabelEntity')] // [ENTITY: DatabaseTable('LabelEntity')]
/** /**
* @summary Представляет собой строку в таблице 'labels' в локальной БД. * @summary Represents a row in the 'labels' table in the local DB.
*/ */
@Entity(tableName = "labels") @Entity(tableName = "labels")
data class LabelEntity( data class LabelEntity(

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] LocationEntity.kt // [FILE] LocationEntity.kt
// [SEMANTICS] data, database, entity, location // [SEMANTICS] data, database, entity, location
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
@@ -10,7 +9,7 @@ import androidx.room.PrimaryKey
// [ENTITY: DatabaseTable('LocationEntity')] // [ENTITY: DatabaseTable('LocationEntity')]
/** /**
* @summary Представляет собой строку в таблице 'locations' в локальной БД. * @summary Represents a row in the 'locations' table in the local DB.
*/ */
@Entity(tableName = "locations") @Entity(tableName = "locations")
data class LocationEntity( data class LocationEntity(

View File

@@ -1,49 +1,175 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] Mapper.kt // [FILE] Mapper.kt
// [SEMANTICS] data, database, mapper // [SEMANTICS] data, database, mapper
package com.homebox.lens.data.db.entity package com.homebox.lens.data.db.entity
// [IMPORTS] // [IMPORTS]
import com.homebox.lens.domain.model.Image import com.homebox.lens.data.mapper.toDomain
import com.homebox.lens.domain.model.ItemSummary import com.homebox.lens.domain.model.*
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LocationOut
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Function('toDomain')] // [ENTITY: Function('ItemWithLabels.toDomainItemSummary')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')] // [RELATION: Function('ItemWithLabels.toDomainItemSummary')] -> [RETURNS] -> [DataClass('ItemSummary')]
/** /**
* @summary Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель). * @summary Converts [ItemWithLabels] (DB entity) to [ItemSummary] (domain model).
*/ */
fun ItemWithLabels.toDomain(): ItemSummary { fun ItemWithLabels.toDomainItemSummary(): ItemSummary {
return ItemSummary( return ItemSummary(
id = this.item.id, id = this.item.id,
name = this.item.name, name = this.item.name,
image = this.item.image?.let { Image(id = "", path = it, isPrimary = true) }, 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 = "") }, location = this.item.locationId?.let { LocationOut(id = it, name = "", color = "", isArchived = false, createdAt = "", updatedAt = "") },
labels = this.labels.map { it.toDomain() }, labels = this.labels.map { it.toDomainLabelOut() },
assetId = null, assetId = this.item.assetId,
isArchived = false, isArchived = this.item.archived,
value = this.item.value?.toDouble() ?: 0.0, value = this.item.purchasePrice ?: 0.0,
createdAt = this.item.createdAt ?: "", createdAt = this.item.createdAt ?: "",
updatedAt = "" updatedAt = "" // ItemEntity does not have updatedAt
) )
} }
// [END_ENTITY: Function('toDomain')] // [END_ENTITY: Function('ItemWithLabels.toDomainItemSummary')]
// [ENTITY: Function('toDomain')] // [ENTITY: Function('ItemEntity.toDomainItem')]
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')] // [RELATION: Function('ItemEntity.toDomainItem')] -> [RETURNS] -> [DataClass('Item')]
/** /**
* @summary Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель). * @summary Converts [ItemEntity] (DB entity) to [Item] (domain model).
*/ */
fun LabelEntity.toDomain(): LabelOut { fun ItemEntity.toDomainItem(): Item {
return Item(
id = this.id,
name = this.name,
description = this.description,
quantity = this.quantity,
image = this.image,
location = this.locationId?.let { Location(it, "") }, // Simplified, name is not in ItemEntity
labels = emptyList(), // Labels are handled via ItemWithLabels
purchasePrice = this.purchasePrice,
createdAt = this.createdAt,
archived = this.archived,
assetId = this.assetId,
fields = emptyList(), // Custom fields are not stored in ItemEntity
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires
)
}
// [END_ENTITY: Function('ItemEntity.toDomainItem')]
// [ENTITY: Function('Item.toItemEntity')]
// [RELATION: Function('Item.toItemEntity')] -> [RETURNS] -> [DataClass('ItemEntity')]
/**
* @summary Converts [Item] (domain model) to [ItemEntity] (DB entity).
*/
fun Item.toItemEntity(): ItemEntity {
return ItemEntity(
id = this.id,
name = this.name,
description = this.description,
quantity = this.quantity,
image = this.image,
locationId = this.location?.id,
purchasePrice = this.purchasePrice,
createdAt = this.createdAt,
archived = this.archived,
assetId = this.assetId,
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires
)
}
// [END_ENTITY: Function('Item.toItemEntity')]
// [ENTITY: Function('ItemOut.toItemEntity')]
// [RELATION: Function('ItemOut.toItemEntity')] -> [RETURNS] -> [DataClass('ItemEntity')]
fun ItemOut.toItemEntity(): ItemEntity {
return ItemEntity(
id = this.id,
name = this.name,
description = this.description,
quantity = this.quantity,
image = this.images.firstOrNull()?.path,
locationId = this.location?.id,
purchasePrice = this.purchasePrice,
createdAt = this.createdAt,
archived = this.isArchived,
assetId = this.assetId,
insured = this.insured ?: false,
lifetimeWarranty = this.lifetimeWarranty ?: false,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parent?.id,
purchaseFrom = this.purchaseFrom,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations ?: false,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires
)
}
// [END_ENTITY: Function('ItemOut.toItemEntity')]
// [ENTITY: Function('LabelEntity.toDomain')]
// [RELATION: Function('LabelEntity.toDomain')] -> [RETURNS] -> [DataClass('Label')]
fun LabelEntity.toDomain(): Label {
return Label(
id = this.id,
name = this.name
)
}
// [END_ENTITY: Function('LabelEntity.toDomain')]
// [ENTITY: Function('LabelEntity.toDomainLabelOut')]
// [RELATION: Function('LabelEntity.toDomainLabelOut')] -> [RETURNS] -> [DataClass('LabelOut')]
fun LabelEntity.toDomainLabelOut(): LabelOut {
return LabelOut( return LabelOut(
id = this.id, id = this.id,
name = this.name, name = this.name,
color = "#CCCCCC", description = null, // Not available in LabelEntity
isArchived = false, color = "", // Not available in LabelEntity
createdAt = "", isArchived = false, // Not available in LabelEntity
updatedAt = "" createdAt = "", // Not available in LabelEntity
updatedAt = "" // Not available in LabelEntity
) )
} }
// [END_ENTITY: Function('toDomain')] // [END_ENTITY: Function('LabelEntity.toDomainLabelOut')]
// [ENTITY: Function('Label.toEntity')]
// [RELATION: Function('Label.toEntity')] -> [RETURNS] -> [DataClass('LabelEntity')]
fun Label.toEntity(): LabelEntity {
return LabelEntity(
id = this.id,
name = this.name
)
}
// [END_ENTITY: Function('Label.toEntity')]
// [END_FILE_Mapper.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] ApiModule.kt // [FILE] ApiModule.kt
// [SEMANTICS] di, networking // [SEMANTICS] data, di, networking
package com.homebox.lens.data.di package com.homebox.lens.data.di
// [IMPORTS] // [IMPORTS]
@@ -25,8 +24,8 @@ import javax.inject.Singleton
// [ENTITY: Module('ApiModule')] // [ENTITY: Module('ApiModule')]
/** /**
* @summary Hilt-модуль, отвечающий за создание и предоставление всех зависимостей, * @summary Hilt module responsible for creating and providing all dependencies
* необходимых для сетевого взаимодействия. * necessary for network interaction.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] DatabaseModule.kt // [FILE] DatabaseModule.kt
// [SEMANTICS] di, hilt, database // [SEMANTICS] data, di, database
package com.homebox.lens.data.di package com.homebox.lens.data.di
// [IMPORTS] // [IMPORTS]
@@ -18,7 +17,7 @@ import javax.inject.Singleton
// [ENTITY: Module('DatabaseModule')] // [ENTITY: Module('DatabaseModule')]
/** /**
* @summary Предоставляет зависимости для работы с базой данных Room. * @summary Provides dependencies for working with the Room database.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@@ -34,7 +33,7 @@ object DatabaseModule {
context, context,
HomeboxDatabase::class.java, HomeboxDatabase::class.java,
HomeboxDatabase.DATABASE_NAME HomeboxDatabase.DATABASE_NAME
).build() ).fallbackToDestructiveMigration().build()
} }
// [END_ENTITY: Function('provideHomeboxDatabase')] // [END_ENTITY: Function('provideHomeboxDatabase')]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] RepositoryModule.kt // [FILE] RepositoryModule.kt
// [SEMANTICS] dependency_injection, hilt, module, binding // [SEMANTICS] data, di, repository
package com.homebox.lens.data.di package com.homebox.lens.data.di
@@ -20,8 +19,8 @@ import javax.inject.Singleton
// [ENTITY: Module('RepositoryModule')] // [ENTITY: Module('RepositoryModule')]
/** /**
* @summary Hilt-модуль для предоставления реализаций репозиториев. * @summary Hilt module for providing repository implementations.
* @description Использует `@Binds` для эффективного связывания интерфейсов с их реализациями. * @description Uses `@Binds` for efficient binding of interfaces to their implementations.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@@ -30,7 +29,7 @@ abstract class RepositoryModule {
// [ENTITY: Function('bindItemRepository')] // [ENTITY: Function('bindItemRepository')]
// [RELATION: Function('bindItemRepository')] -> [PROVIDES] -> [Interface('ItemRepository')] // [RELATION: Function('bindItemRepository')] -> [PROVIDES] -> [Interface('ItemRepository')]
/** /**
* @summary Связывает интерфейс ItemRepository с его реализацией. * @summary Binds the ItemRepository interface to its implementation.
*/ */
@Binds @Binds
@Singleton @Singleton
@@ -42,7 +41,7 @@ abstract class RepositoryModule {
// [ENTITY: Function('bindCredentialsRepository')] // [ENTITY: Function('bindCredentialsRepository')]
// [RELATION: Function('bindCredentialsRepository')] -> [PROVIDES] -> [Interface('CredentialsRepository')] // [RELATION: Function('bindCredentialsRepository')] -> [PROVIDES] -> [Interface('CredentialsRepository')]
/** /**
* @summary Связывает интерфейс CredentialsRepository с его реализацией. * @summary Binds the CredentialsRepository interface to its implementation.
*/ */
@Binds @Binds
@Singleton @Singleton
@@ -54,7 +53,7 @@ abstract class RepositoryModule {
// [ENTITY: Function('bindAuthRepository')] // [ENTITY: Function('bindAuthRepository')]
// [RELATION: Function('bindAuthRepository')] -> [PROVIDES] -> [Interface('AuthRepository')] // [RELATION: Function('bindAuthRepository')] -> [PROVIDES] -> [Interface('AuthRepository')]
/** /**
* @summary Связывает интерфейс AuthRepository с его реализацией. * @summary Binds the AuthRepository interface to its implementation.
*/ */
@Binds @Binds
@Singleton @Singleton

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] StorageModule.kt // [FILE] StorageModule.kt
// [SEMANTICS] di, hilt, storage // [SEMANTICS] data, di, storage
package com.homebox.lens.data.di package com.homebox.lens.data.di
// [IMPORTS] // [IMPORTS]

View File

@@ -0,0 +1,129 @@
// [FILE] DomainToDto.kt
// [SEMANTICS] data, mapper
package com.homebox.lens.data.mapper
// [IMPORTS]
import com.homebox.lens.data.api.dto.ItemCreateDto
import com.homebox.lens.data.api.dto.ItemUpdateDto
import com.homebox.lens.data.api.dto.LabelCreateDto
import com.homebox.lens.data.api.dto.LabelUpdateDto
import com.homebox.lens.data.api.dto.LocationCreateDto
import com.homebox.lens.data.api.dto.LocationUpdateDto
import com.homebox.lens.domain.model.ItemCreate as DomainItemCreate
import com.homebox.lens.domain.model.ItemUpdate as DomainItemUpdate
import com.homebox.lens.domain.model.LabelCreate as DomainLabelCreate
import com.homebox.lens.domain.model.LabelUpdate as DomainLabelUpdate
import com.homebox.lens.domain.model.LocationCreate as DomainLocationCreate
import com.homebox.lens.domain.model.LocationUpdate as DomainLocationUpdate
// [END_IMPORTS]
// [ENTITY: Function('DomainItemCreate.toDto')]
// [RELATION: Function('DomainItemCreate.toDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')]
fun DomainItemCreate.toDto(): ItemCreateDto {
return ItemCreateDto(
name = this.name,
description = this.description,
quantity = this.quantity,
archived = this.archived,
assetId = this.assetId,
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchasePrice = this.purchasePrice?.toDouble(),
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice?.toDouble(),
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires,
locationId = this.locationId,
labelIds = this.labelIds
)
}
// [END_ENTITY: Function('ItemCreate.toDto')]
// [ENTITY: Function('DomainItemUpdate.toDto')]
// [RELATION: Function('DomainItemUpdate.toDto')] -> [RETURNS] -> [DataClass('ItemUpdateDto')]
fun DomainItemUpdate.toDto(): ItemUpdateDto {
return ItemUpdateDto(
name = this.name,
description = this.description,
quantity = this.quantity,
archived = this.archived,
assetId = this.assetId,
insured = this.insured,
lifetimeWarranty = this.lifetimeWarranty,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parentId,
purchaseFrom = this.purchaseFrom,
purchasePrice = this.purchasePrice?.toDouble(),
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice?.toDouble(),
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires,
locationId = this.locationId,
labelIds = this.labelIds
)
}
// [END_ENTITY: Function('ItemUpdate.toDto')]
// [ENTITY: Function('DomainLabelCreate.toDto')]
// [RELATION: Function('DomainLabelCreate.toDto')] -> [RETURNS] -> [DataClass('LabelCreateDto')]
fun DomainLabelCreate.toDto(): LabelCreateDto {
return LabelCreateDto(
name = this.name,
color = this.color,
description = this.description
)
}
// [END_ENTITY: Function('LabelCreate.toDto')]
// [ENTITY: Function('DomainLabelUpdate.toDto')]
// [RELATION: Function('DomainLabelUpdate.toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
fun DomainLabelUpdate.toDto(): LabelUpdateDto {
return LabelUpdateDto(
name = this.name,
color = this.color,
description = this.description
)
}
// [END_ENTITY: Function('DomainLabelUpdate.toDto')]
// [ENTITY: Function('DomainLocationCreate.toDto')]
// [RELATION: Function('DomainLocationCreate.toDto')] -> [RETURNS] -> [DataClass('LocationCreateDto')]
fun DomainLocationCreate.toDto(): LocationCreateDto {
return LocationCreateDto(
name = this.name,
parentId = this.parentId,
color = null,
description = this.description
)
}
// [END_ENTITY: Function('DomainLocationCreate.toDto')]
// [ENTITY: Function('DomainLocationUpdate.toDto')]
// [RELATION: Function('DomainLocationUpdate.toDto')] -> [RETURNS] -> [DataClass('LocationUpdateDto')]
fun DomainLocationUpdate.toDto(): LocationUpdateDto {
return LocationUpdateDto(
name = this.name,
color = this.color,
description = this.description
)
}
// [END_ENTITY: Function('DomainLocationUpdate.toDto')]
// [END_FILE_DomainToDto.kt]

View File

@@ -0,0 +1,269 @@
// [FILE] DtoToDomain.kt
// [SEMANTICS] data, mapper
package com.homebox.lens.data.mapper
// [IMPORTS]
import com.homebox.lens.data.api.dto.*
import com.homebox.lens.domain.model.CustomField as DomainCustomField
import com.homebox.lens.domain.model.GroupStatistics as DomainGroupStatistics
import com.homebox.lens.domain.model.Image as DomainImage
import com.homebox.lens.domain.model.Item as DomainItem
import com.homebox.lens.domain.model.ItemAttachment as DomainItemAttachment
import com.homebox.lens.domain.model.ItemOut as DomainItemOut
import com.homebox.lens.domain.model.ItemSummary as DomainItemSummary
import com.homebox.lens.domain.model.Label as DomainLabel
import com.homebox.lens.domain.model.LabelOut as DomainLabelOut
import com.homebox.lens.domain.model.LabelSummary as DomainLabelSummary
import com.homebox.lens.domain.model.Location as DomainLocation
import com.homebox.lens.domain.model.LocationOut as DomainLocationOut
import com.homebox.lens.domain.model.LocationOutCount as DomainLocationOutCount
import com.homebox.lens.domain.model.MaintenanceEntry as DomainMaintenanceEntry
import com.homebox.lens.domain.model.PaginationResult as DomainPaginationResult
import com.homebox.lens.domain.model.TokenResponse as DomainTokenResponse
// [END_IMPORTS]
// [ENTITY: Function('ItemOutDto.toDomain')]
// [RELATION: Function('ItemOutDto.toDomain')] -> [RETURNS] -> [DataClass('DomainItemOut')]
fun ItemOutDto.toDomain(): DomainItemOut {
return DomainItemOut(
id = this.id,
name = this.name,
assetId = this.assetId,
description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity,
isArchived = this.isArchived,
purchasePrice = this.purchasePrice,
purchaseTime = this.purchaseTime,
purchaseFrom = this.purchaseFrom,
warrantyExpires = this.warrantyExpires,
warrantyDetails = this.warrantyDetails,
lifetimeWarranty = this.lifetimeWarranty,
insured = this.insured,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
soldNotes = this.soldNotes,
syncChildItemsLocations = this.syncChildItemsLocations,
location = this.location?.toDomain(),
parent = this.parent?.toDomain(),
children = this.children.map { it.toDomain() },
labels = this.labels.map { it.toDomain() },
attachments = this.attachments.map { it.toDomain() },
images = this.images.map { it.toDomain() },
fields = this.fields.map { it.toDomain() },
maintenance = this.maintenance.map { it.toDomain() },
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
fun ItemOutDto.toDomainItem(): DomainItem {
return DomainItem(
id = this.id,
name = this.name,
description = this.description,
quantity = this.quantity,
image = this.images.firstOrNull { it.isPrimary }?.path,
location = this.location?.toDomainLocation(),
labels = this.labels.map { it.toDomainLabel() },
purchasePrice = this.purchasePrice,
createdAt = this.createdAt,
archived = this.isArchived,
assetId = this.assetId,
fields = this.fields.map { it.toDomain() },
insured = this.insured ?: false,
lifetimeWarranty = this.lifetimeWarranty ?: false,
manufacturer = this.manufacturer,
modelNumber = this.modelNumber,
notes = this.notes,
parentId = this.parent?.id,
purchaseFrom = this.purchaseFrom,
purchaseTime = this.purchaseTime,
serialNumber = this.serialNumber,
soldNotes = this.soldNotes,
soldPrice = this.soldPrice,
soldTime = this.soldTime,
soldTo = this.soldTo,
syncChildItemsLocations = this.syncChildItemsLocations ?: false,
warrantyDetails = this.warrantyDetails,
warrantyExpires = this.warrantyExpires
)
}
// [END_ENTITY: Function('ItemOutDto.toDomain')]
// [ENTITY: Function('ItemSummaryDto.toDomain')]
// [RELATION: Function('ItemSummaryDto.toDomain')] -> [RETURNS] -> [DataClass('DomainItemSummary')]
fun ItemSummaryDto.toDomain(): DomainItemSummary {
return DomainItemSummary(
id = this.id,
name = this.name,
assetId = this.assetId,
image = this.image?.toDomain(),
isArchived = this.isArchived ?: false,
labels = this.labels.map { it.toDomain() },
location = this.location?.toDomain(),
value = this.value ?: 0.0,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
// [END_ENTITY: Function('ItemSummaryDto.toDomain')]
// [ENTITY: Function('LabelOutDto.toDomain')]
// [RELATION: Function('LabelOutDto.toDomain')] -> [RETURNS] -> [DataClass('DomainLabelOut')]
fun LabelOutDto.toDomain(): DomainLabelOut {
return DomainLabelOut(
id = this.id,
name = this.name,
description = this.description,
color = this.color ?: "",
isArchived = this.isArchived ?: false,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
fun LabelOutDto.toDomainLabel(): DomainLabel {
return DomainLabel(
id = this.id,
name = this.name
)
}
fun com.homebox.lens.domain.model.LabelOut.toDomain(): DomainLabel {
return DomainLabel(
id = this.id,
name = this.name
)
}
// [END_ENTITY: Function('LabelOutDto.toDomain')]
// [ENTITY: Function('LocationOutDto.toDomain')]
// [RELATION: Function('LocationOutDto.toDomain')] -> [RETURNS] -> [DataClass('DomainLocationOut')]
fun LocationOutDto.toDomain(): DomainLocationOut {
return DomainLocationOut(
id = this.id,
name = this.name,
color = this.color,
isArchived = this.isArchived,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
fun LocationOutDto.toDomainLocation(): DomainLocation {
return DomainLocation(
id = this.id,
name = this.name
)
}
// [END_ENTITY: Function('LocationOutDto.toDomain')]
// [ENTITY: Function('LocationOutCountDto.toDomain')]
// [RELATION: Function('LocationOutCountDto.toDomain')] -> [RETURNS] -> [DataClass('DomainLocationOutCount')]
fun LocationOutCountDto.toDomain(): DomainLocationOutCount {
return DomainLocationOutCount(
id = this.id,
name = this.name,
color = this.color ?: "",
isArchived = this.isArchived ?: false,
itemCount = this.itemCount,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
// [END_ENTITY: Function('LocationOutCountDto.toDomain')]
// [ENTITY: Function('PaginationResultDto.toDomain')]
// [RELATION: Function('PaginationResultDto.toDomain')] -> [RETURNS] -> [DataClass('DomainPaginationResult')]
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): DomainPaginationResult<R> {
return DomainPaginationResult(
items = this.items.map(transform),
page = this.page,
pageSize = this.pageSize,
total = this.total
)
}
// [END_ENTITY: Function('PaginationResultDto.toDomain')]
// [ENTITY: Function('ImageDto.toDomain')]
// [RELATION: Function('ImageDto.toDomain')] -> [RETURNS] -> [DataClass('DomainImage')]
fun ImageDto.toDomain(): DomainImage {
return DomainImage(
id = this.id,
path = this.path,
isPrimary = this.isPrimary
)
}
// [END_ENTITY: Function('ImageDto.toDomain')]
// [ENTITY: Function('CustomFieldDto.toDomain')]
// [RELATION: Function('CustomFieldDto.toDomain')] -> [RETURNS] -> [DataClass('DomainCustomField')]
fun CustomFieldDto.toDomain(): DomainCustomField {
return DomainCustomField(
name = this.name,
value = this.value,
type = this.type
)
}
// [END_ENTITY: Function('CustomFieldDto.toDomain')]
// [ENTITY: Function('ItemAttachmentDto.toDomain')]
// [RELATION: Function('ItemAttachmentDto.toDomain')] -> [RETURNS] -> [DataClass('DomainItemAttachment')]
fun ItemAttachmentDto.toDomain(): DomainItemAttachment {
return DomainItemAttachment(
id = this.id,
name = this.name,
path = this.path,
type = this.type,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
// [END_ENTITY: Function('ItemAttachmentDto.toDomain')]
// [ENTITY: Function('MaintenanceEntryDto.toDomain')]
// [RELATION: Function('MaintenanceEntryDto.toDomain')] -> [RETURNS] -> [DataClass('DomainMaintenanceEntry')]
fun MaintenanceEntryDto.toDomain(): DomainMaintenanceEntry {
return DomainMaintenanceEntry(
id = this.id,
itemId = this.itemId,
title = this.title,
details = this.details,
dueAt = this.dueAt,
completedAt = this.completedAt,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}
// [END_ENTITY: Function('MaintenanceEntryDto.toDomain')]
// [ENTITY: Function('GroupStatisticsDto.toDomain')]
// [RELATION: Function('GroupStatisticsDto.toDomain')] -> [RETURNS] -> [DataClass('DomainGroupStatistics')]
fun GroupStatisticsDto.toDomain(): DomainGroupStatistics {
return DomainGroupStatistics(
items = this.totalItems,
labels = this.totalLabels,
locations = this.totalLocations,
totalValue = this.totalItemPrice
)
}
// [END_ENTITY: Function('GroupStatisticsDto.toDomain')]
// [ENTITY: Function('LabelSummaryDto.toDomain')]
// [RELATION: Function('LabelSummaryDto.toDomain')] -> [RETURNS] -> [DataClass('DomainLabelSummary')]
fun LabelSummaryDto.toDomain(): DomainLabelSummary {
return DomainLabelSummary(
id = this.id,
name = this.name,
color = this.color ?: ""
)
}
// [END_ENTITY: Function('LabelSummaryDto.toDomain')]
// [END_FILE_DtoToDomain.kt]

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] AuthRepositoryImpl.kt // [FILE] AuthRepositoryImpl.kt
// [SEMANTICS] data_implementation, authentication, repository // [SEMANTICS] data, repository, authentication
package com.homebox.lens.data.repository package com.homebox.lens.data.repository
@@ -8,8 +7,9 @@ package com.homebox.lens.data.repository
import android.content.SharedPreferences import android.content.SharedPreferences
import com.homebox.lens.data.api.HomeboxApiService import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.LoginFormDto import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.mapper.toDomain import com.homebox.lens.data.api.dto.toDomain
import com.homebox.lens.domain.model.Credentials import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.model.Result
import com.homebox.lens.domain.model.TokenResponse import com.homebox.lens.domain.model.TokenResponse
import com.homebox.lens.domain.repository.AuthRepository import com.homebox.lens.domain.repository.AuthRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -30,10 +30,10 @@ import javax.inject.Inject
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('OkHttpClient')] // [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('OkHttpClient')]
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('MoshiConverterFactory')] // [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('MoshiConverterFactory')]
/** /**
* @summary Реализация репозитория для управления аутентификацией. * @summary Implementation of the repository for managing authentication.
* @param encryptedPrefs Защищенное хранилище для токена. * @param encryptedPrefs The secure storage for the token.
* @param okHttpClient Общий OkHttp клиент для переиспользования. * @param okHttpClient The shared OkHttp client for reuse.
* @param moshiConverterFactory Общий конвертер Moshi для переиспользования. * @param moshiConverterFactory The shared Moshi converter for reuse.
*/ */
class AuthRepositoryImpl @Inject constructor( class AuthRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences, private val encryptedPrefs: SharedPreferences,
@@ -47,16 +47,16 @@ class AuthRepositoryImpl @Inject constructor(
// [ENTITY: Function('login')] // [ENTITY: Function('login')]
/** /**
* @summary Реализует вход пользователя. Создает временный API сервис для выполнения запроса * @summary Implements user login. Creates a temporary API service to execute a request
* на указанный пользователем URL сервера. * to the server URL specified by the user.
* @param credentials Учетные данные пользователя, включая URL сервера. * @param credentials The user's credentials, including the server URL.
* @return [Result] с доменной моделью [TokenResponse] при успехе или [Exception] при ошибке. * @return A [Result] with a [TokenResponse] domain model on success or an [Exception] on failure.
*/ */
override suspend fun login(credentials: Credentials): Result<TokenResponse> { override suspend fun login(credentials: Credentials): Result<TokenResponse> {
require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." } require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." }
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
runCatching { try {
Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}") Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}")
val tempApiService = Retrofit.Builder() val tempApiService = Retrofit.Builder()
.baseUrl(credentials.serverUrl) .baseUrl(credentials.serverUrl)
@@ -70,7 +70,10 @@ class AuthRepositoryImpl @Inject constructor(
val tokenResponseDto = tempApiService.login(loginForm) val tokenResponseDto = tempApiService.login(loginForm)
Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.") Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.")
tokenResponseDto.toDomain() Result.Success(tokenResponseDto.toDomain())
} catch (e: Exception) {
Timber.e(e, "[ERROR][FAILURE][login_failed] Login failed.")
Result.Error(e)
} }
} }
} }

View File

@@ -1,4 +1,3 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] CredentialsRepositoryImpl.kt // [FILE] CredentialsRepositoryImpl.kt
// [SEMANTICS] data, repository, credentials, security // [SEMANTICS] data, repository, credentials, security
package com.homebox.lens.data.repository package com.homebox.lens.data.repository
@@ -20,10 +19,10 @@ import javax.inject.Inject
// [RELATION: Class('CredentialsRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('CredentialsRepository')] // [RELATION: Class('CredentialsRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('CredentialsRepository')]
// [RELATION: Class('CredentialsRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')] // [RELATION: Class('CredentialsRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
/** /**
* @summary Реализует репозиторий для управления учетными данными пользователя. * @summary Implements the repository for managing user credentials.
* @description Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных. * @description Interacts with encrypted SharedPreferences to save and retrieve data.
* @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt. * @param encryptedPrefs The encrypted key-value store provided by Hilt.
* @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`. * @invariant The state of this repository is entirely dependent on the contents of `encryptedPrefs`.
*/ */
class CredentialsRepositoryImpl @Inject constructor( class CredentialsRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences private val encryptedPrefs: SharedPreferences
@@ -38,9 +37,9 @@ class CredentialsRepositoryImpl @Inject constructor(
// [ENTITY: Function('saveCredentials')] // [ENTITY: Function('saveCredentials')]
/** /**
* @summary Сохраняет основные учетные данные пользователя. * @summary Saves the user's primary credentials.
* @param credentials Объект с учетными данными для сохранения. * @param credentials The credentials object to save.
* @sideeffect Перезаписывает существующие учетные данные в SharedPreferences. * @sideeffect Overwrites existing credentials in SharedPreferences.
*/ */
override suspend fun saveCredentials(credentials: Credentials) { override suspend fun saveCredentials(credentials: Credentials) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -56,8 +55,8 @@ class CredentialsRepositoryImpl @Inject constructor(
// [ENTITY: Function('getCredentials')] // [ENTITY: Function('getCredentials')]
/** /**
* @summary Извлекает сохраненные учетные данные пользователя в виде потока. * @summary Retrieves the saved user credentials as a Flow.
* @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют. * @return A Flow that emits a [Credentials] object or null if no data is present.
*/ */
override fun getCredentials(): Flow<Credentials?> = flow { override fun getCredentials(): Flow<Credentials?> = flow {
Timber.d("[DEBUG][ACTION][getting_credentials] Getting user credentials.") Timber.d("[DEBUG][ACTION][getting_credentials] Getting user credentials.")
@@ -77,9 +76,9 @@ class CredentialsRepositoryImpl @Inject constructor(
// [ENTITY: Function('saveToken')] // [ENTITY: Function('saveToken')]
/** /**
* @summary Сохраняет токен авторизации. * @summary Saves the authorization token.
* @param token Токен для сохранения. * @param token The token to save.
* @sideeffect Перезаписывает существующий токен в SharedPreferences. * @sideeffect Overwrites the existing token in SharedPreferences.
*/ */
override suspend fun saveToken(token: String) { override suspend fun saveToken(token: String) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -93,16 +92,51 @@ class CredentialsRepositoryImpl @Inject constructor(
// [ENTITY: Function('getToken')] // [ENTITY: Function('getToken')]
/** /**
* @summary Извлекает сохраненный токен авторизации. * @summary Retrieves the saved authorization token.
* @return Строка с токеном или null, если он не найден. * @return A string with the token or null if it is not found.
*/ */
override suspend fun getToken(): String? { override suspend fun getToken(): String? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.") val token = encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
encryptedPrefs.getString(KEY_AUTH_TOKEN, null) if (token != null) {
Timber.i("[INFO][ACTION][token_retrieved] Auth token retrieved successfully.")
} else {
Timber.w("[WARN][FALLBACK][no_token_found] No auth token found.")
}
token
} }
} }
// [END_ENTITY: Function('getToken')] // [END_ENTITY: Function('getToken')]
// [ENTITY: Function('clearAllCredentials')]
/**
* @summary Clears all saved credentials and tokens.
* @sideeffect Removes all records related to credentials from SharedPreferences.
*/
override suspend fun clearAllCredentials() {
withContext(Dispatchers.IO) {
Timber.i("[INFO][ACTION][clearing_all_credentials] Clearing all saved credentials and tokens.")
encryptedPrefs.edit()
.remove(KEY_SERVER_URL)
.remove(KEY_USERNAME)
.remove(KEY_PASSWORD)
.remove(KEY_AUTH_TOKEN)
.apply()
}
}
// [END_ENTITY: Function('clearAllCredentials')]
// [ENTITY: Function('areCredentialsSavedSync')]
/**
* @summary Synchronously checks if user credentials are saved.
* @return true if all essential credentials (URL, username, password) are present, false otherwise.
*/
override fun areCredentialsSavedSync(): Boolean {
return encryptedPrefs.contains(KEY_SERVER_URL) &&
encryptedPrefs.contains(KEY_USERNAME) &&
encryptedPrefs.contains(KEY_PASSWORD)
}
// [END_ENTITY: Function('areCredentialsSavedSync')]
} }
// [END_ENTITY: Class('CredentialsRepositoryImpl')] // [END_ENTITY: Class('CredentialsRepositoryImpl')]
// [END_FILE_CredentialsRepositoryImpl.kt] // [END_FILE_CredentialsRepositoryImpl.kt]

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