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() {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user