Statistik: scrollbar trendgraf med läsbara etiketter + Totalt-period; auth-luckor
All checks were successful
release / build-release (push) Successful in 7m34s
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:
@@ -92,7 +92,10 @@ class AuthRepository(
|
||||
suspend fun restoreSession() {
|
||||
val saved = store.read()
|
||||
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
|
||||
}
|
||||
try {
|
||||
@@ -112,12 +115,14 @@ class AuthRepository(
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 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()
|
||||
_state.value = if (stillValid) {
|
||||
AuthState.LoggedIn(saved.username.orEmpty())
|
||||
} else {
|
||||
AuthState.LoggedOut
|
||||
_state.value = when {
|
||||
stillValid -> AuthState.LoggedIn(saved.username.orEmpty())
|
||||
runCatching { trySilentRelogin() }.getOrNull() != null ->
|
||||
AuthState.LoggedIn(store.read().username.orEmpty())
|
||||
else -> AuthState.LoggedOut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,8 @@ fun SettingsScreen(
|
||||
Text("Automatisk återinloggning", style = MaterialTheme.typography.bodyLarge)
|
||||
Text(
|
||||
"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,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Button
|
||||
@@ -44,6 +45,7 @@ val PERIODS = listOf(
|
||||
PeriodOption("month", "Månad"),
|
||||
PeriodOption("halfyear", "Halvår"),
|
||||
PeriodOption("year", "År"),
|
||||
PeriodOption("all", "Totalt"),
|
||||
)
|
||||
|
||||
sealed interface StatsUiState {
|
||||
@@ -96,8 +98,8 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
|
||||
Text("Statistik", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PERIODS.forEach { p ->
|
||||
androidx.compose.foundation.lazy.LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(PERIODS, key = { it.id }) { p ->
|
||||
FilterChip(
|
||||
selected = period == p.id,
|
||||
onClick = { viewModel.setPeriod(p.id) },
|
||||
@@ -151,7 +153,14 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -278,23 +287,44 @@ private fun KpiCard(label: String, value: String, deltaText: String?, modifier:
|
||||
@Composable
|
||||
private fun TrendBars(stats: StatsBundle) {
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.height(96.dp)
|
||||
.height(112.dp)
|
||||
.padding(top = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
stats.trend.forEach { bucket ->
|
||||
items(stats.trend.size) { i ->
|
||||
val bucket = stats.trend[i]
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier.width(38.dp),
|
||||
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(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(((bucket.volumeKg / max) * 72).dp.coerceAtLeast(2.dp))
|
||||
.height(((bucket.volumeKg / max) * 64).dp.coerceAtLeast(2.dp))
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = if (bucket.volumeKg > 0) 0.85f else 0.25f,
|
||||
@@ -303,7 +333,7 @@ private fun TrendBars(stats: StatsBundle) {
|
||||
),
|
||||
)
|
||||
Text(
|
||||
bucket.label.takeLast(5),
|
||||
bucket.label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
|
||||
Reference in New Issue
Block a user