Statistik: scrollbar trendgraf med läsbara etiketter + Totalt-period; auth-luckor
All checks were successful
release / build-release (push) Successful in 7m34s

- Trendstaplarna i en horisontellt scrollbar rad (38dp per stapel) med
  volymsiffra ovanför och hel etikett under — startar på senaste perioden.
  Fixar etikettröran på månad/halvår
- Ny period 'Totalt' (period=all, stöds redan av API:t; delta saknas per design)
- Tyst återinloggning provas nu även när tokens saknas helt och när JWT:n
  hunnit gå ut vid nätfel under appstart
- Inställningstext: förtydligar att auto-återinloggning aktiveras vid
  nästa inloggning (uppgifter sparas bara vid login)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP
This commit is contained in:
2026-07-25 16:42:45 +02:00
parent df4d122562
commit 34c2a6c4da
3 changed files with 53 additions and 17 deletions

View File

@@ -92,7 +92,10 @@ class AuthRepository(
suspend fun restoreSession() { suspend fun restoreSession() {
val saved = store.read() val saved = store.read()
if (saved.sessionId.isNullOrEmpty() || saved.refreshToken.isNullOrEmpty()) { if (saved.sessionId.isNullOrEmpty() || saved.refreshToken.isNullOrEmpty()) {
_state.value = AuthState.LoggedOut // Inga tokens alls — men kanske sparade uppgifter (t.ex. efter
// att servern rensat sessioner). Prova tyst innan login-skärmen.
val relogged = runCatching { trySilentRelogin() }.getOrNull()
if (relogged == null) _state.value = AuthState.LoggedOut
return return
} }
try { try {
@@ -112,12 +115,14 @@ class AuthRepository(
} }
} catch (e: Exception) { } catch (e: Exception) {
// Nätverksfel vid uppstart: släpp in användaren om JWT:n inte hunnit // Nätverksfel vid uppstart: släpp in användaren om JWT:n inte hunnit
// gå ut, annars visa inloggningen igen. // gå ut. Har den gått ut: prova tyst återinloggning (kan också
// faila på nätet — då login-skärmen, synken tar det sen).
val stillValid = (saved.expirationEpochMs ?: 0) > System.currentTimeMillis() val stillValid = (saved.expirationEpochMs ?: 0) > System.currentTimeMillis()
_state.value = if (stillValid) { _state.value = when {
AuthState.LoggedIn(saved.username.orEmpty()) stillValid -> AuthState.LoggedIn(saved.username.orEmpty())
} else { runCatching { trySilentRelogin() }.getOrNull() != null ->
AuthState.LoggedOut AuthState.LoggedIn(store.read().username.orEmpty())
else -> AuthState.LoggedOut
} }
} }
} }

View File

@@ -188,7 +188,8 @@ fun SettingsScreen(
Text("Automatisk återinloggning", style = MaterialTheme.typography.bodyLarge) Text("Automatisk återinloggning", style = MaterialTheme.typography.bodyLarge)
Text( Text(
"Sparar inloggningen krypterat (Android Keystore) och loggar " + "Sparar inloggningen krypterat (Android Keystore) och loggar " +
"in igen tyst om servern kastat ut dig", "in igen tyst om servern kastat ut dig. " +
"Aktiveras vid nästa inloggning.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )

View File

@@ -9,6 +9,7 @@ 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.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -44,6 +45,7 @@ val PERIODS = listOf(
PeriodOption("month", "Månad"), PeriodOption("month", "Månad"),
PeriodOption("halfyear", "Halvår"), PeriodOption("halfyear", "Halvår"),
PeriodOption("year", "År"), PeriodOption("year", "År"),
PeriodOption("all", "Totalt"),
) )
sealed interface StatsUiState { sealed interface StatsUiState {
@@ -96,8 +98,8 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
Text("Statistik", style = MaterialTheme.typography.headlineSmall) Text("Statistik", style = MaterialTheme.typography.headlineSmall)
} }
item { item {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { androidx.compose.foundation.lazy.LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
PERIODS.forEach { p -> items(PERIODS, key = { it.id }) { p ->
FilterChip( FilterChip(
selected = period == p.id, selected = period == p.id,
onClick = { viewModel.setPeriod(p.id) }, onClick = { viewModel.setPeriod(p.id) },
@@ -151,7 +153,14 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
item { item {
Card(modifier = Modifier.fillMaxWidth()) { Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(14.dp)) { Column(modifier = Modifier.padding(14.dp)) {
Text("Volym per ${if (period == "week") "dag" else "period"}", style = MaterialTheme.typography.titleSmall) Text(
"Volym per " + when (period) {
"week", "month" -> "dag"
"halfyear" -> "vecka"
else -> "månad"
},
style = MaterialTheme.typography.titleSmall,
)
TrendBars(stats) TrendBars(stats)
} }
} }
@@ -278,23 +287,44 @@ private fun KpiCard(label: String, value: String, deltaText: String?, modifier:
@Composable @Composable
private fun TrendBars(stats: StatsBundle) { private fun TrendBars(stats: StatsBundle) {
val max = stats.trend.maxOf { it.volumeKg }.takeIf { it > 0 } ?: 1.0 val max = stats.trend.maxOf { it.volumeKg }.takeIf { it > 0 } ?: 1.0
Row( val listState = androidx.compose.foundation.lazy.rememberLazyListState()
// Börja längst till höger — senaste perioden är den intressanta.
androidx.compose.runtime.LaunchedEffect(stats.trend.size) {
if (stats.trend.isNotEmpty()) listState.scrollToItem(stats.trend.size - 1)
}
androidx.compose.foundation.lazy.LazyRow(
state = listState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(96.dp) .height(112.dp)
.padding(top = 10.dp), .padding(top = 10.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
stats.trend.forEach { bucket -> items(stats.trend.size) { i ->
val bucket = stats.trend[i]
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.width(38.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
if (bucket.volumeKg > 0) {
Text(
if (bucket.volumeKg >= 1000) {
"${(bucket.volumeKg / 1000).toInt()}t"
} else {
"${bucket.volumeKg.toInt()}"
},
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
)
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(((bucket.volumeKg / max) * 72).dp.coerceAtLeast(2.dp)) .height(((bucket.volumeKg / max) * 64).dp.coerceAtLeast(2.dp))
.background( .background(
color = MaterialTheme.colorScheme.primary.copy( color = MaterialTheme.colorScheme.primary.copy(
alpha = if (bucket.volumeKg > 0) 0.85f else 0.25f, alpha = if (bucket.volumeKg > 0) 0.85f else 0.25f,
@@ -303,7 +333,7 @@ private fun TrendBars(stats: StatsBundle) {
), ),
) )
Text( Text(
bucket.label.takeLast(5), bucket.label,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1, maxLines = 1,