Add start dashboard

This commit is contained in:
2025-08-09 11:34:40 +03:00
parent 07a8d82a4d
commit 8db12a7599
5 changed files with 549 additions and 73 deletions

View File

@@ -36,7 +36,16 @@ fun NavGraph() {
})
}
composable(route = Screen.Dashboard.route) {
DashboardScreen()
DashboardScreen(
onNavigateToLocations = { navController.navigate(Screen.LocationsList.route) },
onNavigateToSearch = { navController.navigate(Screen.Search.route) },
onNavigateToCreateItem = { navController.navigate(Screen.ItemEdit.createRoute("new")) },
onLogout = {
navController.navigate(Screen.Setup.route) {
popUpTo(Screen.Dashboard.route) { inclusive = true }
}
}
)
}
composable(route = Screen.InventoryList.route) {
InventoryListScreen()

View File

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