From 3a37eac107e51eed0a477bc49763b3b58323610d Mon Sep 17 00:00:00 2001 From: Tinsae Date: Thu, 25 Dec 2025 13:45:27 +0100 Subject: [PATCH] ViewModel for dashboard --- app/build.gradle.kts | 1 + .../clocked/dashboard/DashboardScreen.kt | 31 ++++++------- .../clocked/dashboard/DashboardViewModel.kt | 43 +++++++++++++++++++ .../tinsae/clocked/history/HistoryScreen.kt | 20 +-------- .../net/tinsae/clocked/service/LogService.kt | 14 ++++++ gradle/libs.versions.toml | 2 + 6 files changed, 74 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ef6e938..1706494 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.compose.material3.adaptive.navigation.suite) implementation(libs.androidx.compose.material3.window.size.class1) + implementation(libs.androidx.lifecycle.viewmodel.compose) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt index b5be018..a4a302b 100644 --- a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt +++ b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt @@ -15,9 +15,8 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel 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.cyan import net.tinsae.clocked.ui.theme.green import net.tinsae.clocked.ui.theme.red @Composable -fun DashboardScreen(modifier: Modifier = Modifier) { - +fun DashboardScreen( + modifier: Modifier = Modifier, + viewModel: DashboardViewModel = viewModel() +) { + val uiState by viewModel.uiState.collectAsState() Column( modifier = modifier .fillMaxSize() - .padding(16.dp,0.dp) + .padding(16.dp, 0.dp) ) { Row( modifier = Modifier.fillMaxWidth(), 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)) - 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)) - NetBalanceCard(value = "+6h 30m") + NetBalanceCard(value = uiState.netBalance) - // ADDED: Manual spacer Spacer(modifier = Modifier.height(16.dp)) Row( @@ -69,13 +67,10 @@ fun DashboardScreen(modifier: Modifier = Modifier) { ActionButton(text = "− Time Off", modifier = Modifier.weight(1f)) } - // ADDED: Manual spacer Spacer(modifier = Modifier.height(16.dp)) - // This section now works as expected - val recentLogs by remember { mutableStateOf(getRecentLogs()) } ActivityList( - items = recentLogs, + items = uiState.recentActivities, modifier = Modifier.weight(1f) ) } @@ -131,6 +126,6 @@ fun ActionButton(text: String, modifier: Modifier = Modifier) { @Composable fun DashboardScreenPreview() { ClockedTheme { - DashboardScreen() + DashboardScreen(viewModel = DashboardViewModel()) } } diff --git a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt new file mode 100644 index 0000000..41529cd --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardViewModel.kt @@ -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 = emptyList() +) + +class DashboardViewModel : ViewModel() { + private val _uiState = MutableStateFlow(DashboardUiState()) + val uiState: StateFlow = _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 + ) + } +} diff --git a/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt b/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt index 5f03c5f..58c40eb 100644 --- a/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt +++ b/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt @@ -6,15 +6,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow -import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.getTimeOffLogs import net.tinsae.clocked.ui.theme.ClockedTheme -import java.time.Instant -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) -} - +import net.tinsae.clocked.util.Util.formatTimestampToMonthYear @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/app/src/main/java/net/tinsae/clocked/service/LogService.kt b/app/src/main/java/net/tinsae/clocked/service/LogService.kt index 909aa18..1cde3ef 100644 --- a/app/src/main/java/net/tinsae/clocked/service/LogService.kt +++ b/app/src/main/java/net/tinsae/clocked/service/LogService.kt @@ -69,6 +69,20 @@ fun getRecentLogs(): List { 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 { logs.filter { it.id != log.id } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0dd5d2b..78cdac8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ espressoCore = "3.7.0" lifecycleRuntimeKtx = "2.10.0" activityCompose = "1.12.2" composeBom = "2025.12.01" +lifecycle = "2.8.0" desugarJdkLibs = "2.1.5" 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-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-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-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }