ViewModel for dashboard

This commit is contained in:
2025-12-25 13:45:27 +01:00
parent 167d18124a
commit 3a37eac107
6 changed files with 74 additions and 37 deletions

View File

@@ -57,6 +57,7 @@ dependencies {
implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.material3.adaptive.navigation.suite) implementation(libs.androidx.compose.material3.adaptive.navigation.suite)
implementation(libs.androidx.compose.material3.window.size.class1) implementation(libs.androidx.compose.material3.window.size.class1)
implementation(libs.androidx.lifecycle.viewmodel.compose)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -15,9 +15,8 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -25,39 +24,38 @@ import androidx.compose.ui.text.font.FontWeight
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.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import net.tinsae.clocked.components.ActivityList import net.tinsae.clocked.components.ActivityList
import net.tinsae.clocked.service.deleteLog
import net.tinsae.clocked.service.editLog
import net.tinsae.clocked.service.getRecentLogs
import net.tinsae.clocked.ui.theme.ClockedTheme import net.tinsae.clocked.ui.theme.ClockedTheme
import net.tinsae.clocked.ui.theme.cyan import net.tinsae.clocked.ui.theme.cyan
import net.tinsae.clocked.ui.theme.green import net.tinsae.clocked.ui.theme.green
import net.tinsae.clocked.ui.theme.red import net.tinsae.clocked.ui.theme.red
@Composable @Composable
fun DashboardScreen(modifier: Modifier = Modifier) { fun DashboardScreen(
modifier: Modifier = Modifier,
viewModel: DashboardViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Column( Column(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp,0.dp) .padding(16.dp, 0.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
SummaryCard(title = "Overtime", value = "+12h 30m", color = green, modifier = Modifier.weight(1f)) SummaryCard(title = "Overtime", value = uiState.overtime, color = green, modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
SummaryCard(title = "Time Off", value = "6h 00m", color = red, modifier = Modifier.weight(1f)) SummaryCard(title = "Time Off", value = uiState.timeOff, color = red, modifier = Modifier.weight(1f))
} }
// ADDED: Manual spacer
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
NetBalanceCard(value = "+6h 30m") NetBalanceCard(value = uiState.netBalance)
// ADDED: Manual spacer
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Row( Row(
@@ -69,13 +67,10 @@ fun DashboardScreen(modifier: Modifier = Modifier) {
ActionButton(text = " Time Off", modifier = Modifier.weight(1f)) ActionButton(text = " Time Off", modifier = Modifier.weight(1f))
} }
// ADDED: Manual spacer
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// This section now works as expected
val recentLogs by remember { mutableStateOf(getRecentLogs()) }
ActivityList( ActivityList(
items = recentLogs, items = uiState.recentActivities,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
@@ -131,6 +126,6 @@ fun ActionButton(text: String, modifier: Modifier = Modifier) {
@Composable @Composable
fun DashboardScreenPreview() { fun DashboardScreenPreview() {
ClockedTheme { ClockedTheme {
DashboardScreen() DashboardScreen(viewModel = DashboardViewModel())
} }
} }

View File

@@ -0,0 +1,43 @@
package net.tinsae.clocked.dashboard
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import net.tinsae.clocked.data.Log
import net.tinsae.clocked.service.getRecentLogs
import net.tinsae.clocked.service.getTotalOvertimeDuration
import net.tinsae.clocked.service.getTotalTimeOffDuration
import net.tinsae.clocked.util.Util.formatDuration
data class DashboardUiState(
val overtime: String = "0m",
val timeOff: String = "0m",
val netBalance: String = "0m",
val recentActivities: List<Log> = emptyList()
)
class DashboardViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DashboardUiState())
val uiState: StateFlow<DashboardUiState> = _uiState
init {
loadDashboardData()
}
private fun loadDashboardData() {
val recentLogs = getRecentLogs()
val overtimeDuration = getTotalOvertimeDuration()
val timeOffDuration = getTotalTimeOffDuration()
// timeOffDuration is negative, so we add it to get the difference
val netBalanceDuration = overtimeDuration.plus(timeOffDuration)
_uiState.value = DashboardUiState(
overtime = formatDuration(overtimeDuration),
timeOff = formatDuration(timeOffDuration),
netBalance = formatDuration(netBalanceDuration),
recentActivities = recentLogs
)
}
}

View File

@@ -6,15 +6,12 @@ import androidx.compose.foundation.layout.Column
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
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.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -35,22 +32,7 @@ import net.tinsae.clocked.service.getAllLogs
import net.tinsae.clocked.service.getOvertimeLogs import net.tinsae.clocked.service.getOvertimeLogs
import net.tinsae.clocked.service.getTimeOffLogs import net.tinsae.clocked.service.getTimeOffLogs
import net.tinsae.clocked.ui.theme.ClockedTheme import net.tinsae.clocked.ui.theme.ClockedTheme
import java.time.Instant import net.tinsae.clocked.util.Util.formatTimestampToMonthYear
import java.time.ZoneId
import java.time.format.DateTimeFormatter
// Helper to format the Instant into a readable string like "Tue 18"
fun formatTimestampToDayAndDate(instant: Instant): String {
val formatter = DateTimeFormatter.ofPattern("E d").withZone(ZoneId.systemDefault())
return formatter.format(instant)
}
// Helper to format the Instant into a month and year like "March 2025"
fun formatTimestampToMonthYear(instant: Instant): String {
val formatter = DateTimeFormatter.ofPattern("MMMM yyyy").withZone(ZoneId.systemDefault())
return formatter.format(instant)
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable

View File

@@ -69,6 +69,20 @@ fun getRecentLogs(): List<Log> {
return logs.take(7) return logs.take(7)
} }
fun getTotalOvertimeDuration(): Duration {
return logs
.filter { it.type == EntryType.OVERTIME }
.map { it.duration }
.fold(Duration.ZERO, Duration::plus)
}
fun getTotalTimeOffDuration(): Duration {
return logs
.filter { it.type == EntryType.TIME_OFF }
.map { it.duration }
.fold(Duration.ZERO, Duration::plus)
}
fun deleteLog(log: Log): Boolean { fun deleteLog(log: Log): Boolean {
logs.filter { it.id != log.id } logs.filter { it.id != log.id }

View File

@@ -8,6 +8,7 @@ espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.2" activityCompose = "1.12.2"
composeBom = "2025.12.01" composeBom = "2025.12.01"
lifecycle = "2.8.0"
desugarJdkLibs = "2.1.5" desugarJdkLibs = "2.1.5"
material3WindowSizeClass = "1.4.0" material3WindowSizeClass = "1.4.0"
@@ -21,6 +22,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }