Skip to content

Commit 8003887

Browse files
committed
ui improvements
1 parent 85a556a commit 8003887

20 files changed

Lines changed: 2337 additions & 605 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
- **Encrypted Storage** - Secure token storage using Android DataStore
2121
- **Offline-First Architecture** - Clean architecture with reactive state management
2222

23+
## Quirks
24+
25+
- State of tile will not sync correctly with what is online unless you have the application open, this is not a problem if you exclusively use the tile and app instead of using the website & desktop app.
26+
2327
## Screenshots
2428

2529
<p align="center">

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ android {
3434
versionName = "1.0"
3535

3636
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
37+
38+
resourceConfigurations += listOf("en", "nl", "ja")
3739
}
3840

3941
buildTypes {

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
android:allowBackup="false"
1010
android:icon="@mipmap/ic_launcher"
1111
android:label="@string/app_name"
12+
android:localeConfig="@xml/locales_config"
1213
android:supportsRtl="true"
1314
android:theme="@style/AppTheme">
1415
<activity

app/src/main/java/dev/tricked/solidverdant/MainActivity.kt

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.compose.runtime.collectAsState
1616
import androidx.compose.runtime.getValue
1717
import androidx.compose.ui.Modifier
1818
import dagger.hilt.android.AndroidEntryPoint
19+
import dev.tricked.solidverdant.data.model.TimeEntry
1920
import dev.tricked.solidverdant.ui.auth.AuthViewModel
2021
import dev.tricked.solidverdant.ui.login.LoginScreen
2122
import dev.tricked.solidverdant.ui.theme.SolidVerdantTheme
@@ -104,17 +105,21 @@ fun SolidVerdantApp(
104105
val isLoggedIn by authViewModel.isLoggedIn.collectAsState()
105106
val trackingUiState by trackingViewModel.uiState.collectAsState()
106107

107-
// Load user data and tracking state when logged in
108+
// Load user data when logged in
108109
LaunchedEffect(isLoggedIn) {
109110
if (isLoggedIn) {
110111
authViewModel.loadUserData()
111112
}
112113
}
113114

114-
// Load tracking state when user and membership are available
115-
LaunchedEffect(authUiState.currentMembership) {
116-
authUiState.currentMembership?.let {
117-
trackingViewModel.loadActiveTimeEntry()
115+
// Load all tracking data when user and membership are available
116+
LaunchedEffect(authUiState.currentMembership, authUiState.user) {
117+
val membership = authUiState.currentMembership
118+
if (membership != null) {
119+
trackingViewModel.loadAllData(
120+
organizationId = membership.organizationId,
121+
memberId = membership.id
122+
)
118123
}
119124
}
120125

@@ -124,8 +129,11 @@ fun SolidVerdantApp(
124129
user = authUiState.user,
125130
uiState = trackingUiState,
126131
onRefresh = {
127-
authUiState.currentMembership?.let {
128-
trackingViewModel.loadActiveTimeEntry()
132+
authUiState.currentMembership?.let { membership ->
133+
trackingViewModel.loadAllData(
134+
organizationId = membership.organizationId,
135+
memberId = membership.id
136+
)
129137
}
130138
},
131139
onLogout = {
@@ -149,8 +157,59 @@ fun SolidVerdantApp(
149157
organizationId = membership.organizationId,
150158
userId = user.id
151159
)
160+
// Reload data to show the stopped entry in history
161+
trackingViewModel.loadAllData(
162+
organizationId = membership.organizationId,
163+
memberId = membership.id
164+
)
152165
}
153166
}
167+
},
168+
onDescriptionChange = { description ->
169+
trackingViewModel.updateDescription(description)
170+
},
171+
onProjectChange = { projectId ->
172+
trackingViewModel.updateProject(projectId)
173+
},
174+
onTaskChange = { taskId ->
175+
trackingViewModel.updateTask(taskId)
176+
},
177+
onTagsChange = { tags ->
178+
trackingViewModel.updateTags(tags)
179+
},
180+
onBillableChange = { billable ->
181+
trackingViewModel.updateBillable(billable)
182+
},
183+
onUpdateCurrentEntry = {
184+
authUiState.currentMembership?.let { membership ->
185+
trackingViewModel.updateCurrentTimeEntry(
186+
organizationId = membership.organizationId
187+
)
188+
}
189+
},
190+
onUpdatePastEntry = { entry: TimeEntry, description: String?, projectId: String?, taskId: String?, tags: List<String>, billable: Boolean ->
191+
authUiState.currentMembership?.let { membership ->
192+
trackingViewModel.updatePastTimeEntry(
193+
organizationId = membership.organizationId,
194+
timeEntry = entry,
195+
description = description,
196+
projectId = projectId,
197+
taskId = taskId,
198+
tags = tags,
199+
billable = billable
200+
)
201+
}
202+
},
203+
onDeleteEntry = { timeEntryId ->
204+
authUiState.currentMembership?.let { membership ->
205+
trackingViewModel.deleteTimeEntry(
206+
organizationId = membership.organizationId,
207+
timeEntryId = timeEntryId
208+
)
209+
}
210+
},
211+
getGroupedEntries = {
212+
trackingViewModel.getGroupedTimeEntries()
154213
}
155214
)
156215
}

app/src/main/java/dev/tricked/solidverdant/data/local/CacheDataStore.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
99
import dev.tricked.solidverdant.data.model.Project
1010
import dev.tricked.solidverdant.data.model.Task
1111
import kotlinx.coroutines.flow.first
12-
import kotlinx.serialization.encodeToString
1312
import kotlinx.serialization.json.Json
1413
import timber.log.Timber
1514
import javax.inject.Inject

app/src/main/java/dev/tricked/solidverdant/data/model/TimeEntry.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,37 @@ data class ProjectsResponse(
180180
data class TasksResponse(
181181
val data: List<Task>
182182
)
183+
184+
/**
185+
* Response wrapper for tags API calls
186+
*/
187+
@Serializable
188+
data class TagsResponse(
189+
val data: List<Tag>
190+
)
191+
192+
/**
193+
* Response wrapper for multiple time entries
194+
*/
195+
@Serializable
196+
data class TimeEntriesResponse(
197+
val data: List<TimeEntry>
198+
)
199+
200+
/**
201+
* Request to update an existing time entry
202+
*/
203+
@Serializable
204+
data class UpdateTimeEntryRequest(
205+
@SerialName("user_id")
206+
val userId: String,
207+
val start: String,
208+
val end: String? = null,
209+
val description: String? = null,
210+
@SerialName("project_id")
211+
val projectId: String? = null,
212+
@SerialName("task_id")
213+
val taskId: String? = null,
214+
val billable: Boolean = false,
215+
val tags: List<String> = emptyList()
216+
)

app/src/main/java/dev/tricked/solidverdant/data/remote/SolidtimeApi.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@ import dev.tricked.solidverdant.data.model.MembershipsResponse
44
import dev.tricked.solidverdant.data.model.ProjectsResponse
55
import dev.tricked.solidverdant.data.model.StartTimeEntryRequest
66
import dev.tricked.solidverdant.data.model.StopTimeEntryRequest
7+
import dev.tricked.solidverdant.data.model.TagsResponse
78
import dev.tricked.solidverdant.data.model.TasksResponse
9+
import dev.tricked.solidverdant.data.model.TimeEntriesResponse
810
import dev.tricked.solidverdant.data.model.TimeEntryResponse
911
import dev.tricked.solidverdant.data.model.TokenResponse
12+
import dev.tricked.solidverdant.data.model.UpdateTimeEntryRequest
1013
import dev.tricked.solidverdant.data.model.UserResponse
14+
import retrofit2.Response
1115
import retrofit2.http.Body
16+
import retrofit2.http.DELETE
1217
import retrofit2.http.Field
1318
import retrofit2.http.FormUrlEncoded
1419
import retrofit2.http.GET
1520
import retrofit2.http.POST
1621
import retrofit2.http.PUT
1722
import retrofit2.http.Path
23+
import retrofit2.http.Query
1824

1925
/**
2026
* Retrofit API interface for Solidtime API
@@ -97,4 +103,41 @@ interface SolidtimeApi {
97103
suspend fun getTasks(
98104
@Path("organization") organizationId: String
99105
): TasksResponse
106+
107+
/**
108+
* Get all tags for an organization
109+
*/
110+
@GET("api/v1/organizations/{organization}/tags")
111+
suspend fun getTags(
112+
@Path("organization") organizationId: String
113+
): TagsResponse
114+
115+
/**
116+
* Get time entries for an organization with optional filters
117+
*/
118+
@GET("api/v1/organizations/{organization}/time-entries")
119+
suspend fun getTimeEntries(
120+
@Path("organization") organizationId: String,
121+
@Query("member_id") memberId: String,
122+
@Query("only_full_dates") onlyFullDates: Boolean = true
123+
): TimeEntriesResponse
124+
125+
/**
126+
* Update an existing time entry
127+
*/
128+
@PUT("api/v1/organizations/{organization}/time-entries/{id}")
129+
suspend fun updateTimeEntry(
130+
@Path("organization") organizationId: String,
131+
@Path("id") timeEntryId: String,
132+
@Body request: UpdateTimeEntryRequest
133+
): TimeEntryResponse
134+
135+
/**
136+
* Delete a time entry
137+
*/
138+
@DELETE("api/v1/organizations/{organization}/time-entries/{id}")
139+
suspend fun deleteTimeEntry(
140+
@Path("organization") organizationId: String,
141+
@Path("id") timeEntryId: String
142+
): Response<Unit>
100143
}

app/src/main/java/dev/tricked/solidverdant/data/repository/AuthRepository.kt

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package dev.tricked.solidverdant.data.repository
22

33
import dev.tricked.solidverdant.data.local.AuthDataStore
44
import dev.tricked.solidverdant.data.model.Membership
5+
import dev.tricked.solidverdant.data.model.Project
6+
import dev.tricked.solidverdant.data.model.Tag
7+
import dev.tricked.solidverdant.data.model.Task
58
import dev.tricked.solidverdant.data.model.TimeEntry
9+
import dev.tricked.solidverdant.data.model.UpdateTimeEntryRequest
610
import dev.tricked.solidverdant.data.model.User
711
import dev.tricked.solidverdant.data.remote.ApiClientFactory
812
import dev.tricked.solidverdant.util.PKCEUtil
@@ -289,4 +293,112 @@ class AuthRepository @Inject constructor(
289293
Result.failure(e)
290294
}
291295
}
296+
297+
/**
298+
* Get time entries for an organization
299+
*/
300+
suspend fun getTimeEntries(organizationId: String, memberId: String): Result<List<TimeEntry>> {
301+
return try {
302+
val endpoint = authDataStore.getEndpoint()
303+
val api = apiClientFactory.createApi(endpoint)
304+
val response = api.getTimeEntries(organizationId, memberId, onlyFullDates = true)
305+
Result.success(response.data)
306+
} catch (e: Exception) {
307+
Timber.e(e, "Failed to get time entries")
308+
Result.failure(e)
309+
}
310+
}
311+
312+
/**
313+
* Get all tags for an organization
314+
*/
315+
suspend fun getTags(organizationId: String): Result<List<Tag>> {
316+
return try {
317+
val endpoint = authDataStore.getEndpoint()
318+
val api = apiClientFactory.createApi(endpoint)
319+
val response = api.getTags(organizationId)
320+
Result.success(response.data)
321+
} catch (e: Exception) {
322+
Timber.e(e, "Failed to get tags")
323+
Result.failure(e)
324+
}
325+
}
326+
327+
/**
328+
* Get all projects for an organization
329+
*/
330+
suspend fun getProjects(organizationId: String): Result<List<Project>> {
331+
return try {
332+
val endpoint = authDataStore.getEndpoint()
333+
val api = apiClientFactory.createApi(endpoint)
334+
val response = api.getProjects(organizationId)
335+
Result.success(response.data)
336+
} catch (e: Exception) {
337+
Timber.e(e, "Failed to get projects")
338+
Result.failure(e)
339+
}
340+
}
341+
342+
/**
343+
* Get all tasks for an organization
344+
*/
345+
suspend fun getTasks(organizationId: String): Result<List<Task>> {
346+
return try {
347+
val endpoint = authDataStore.getEndpoint()
348+
val api = apiClientFactory.createApi(endpoint)
349+
val response = api.getTasks(organizationId)
350+
Result.success(response.data)
351+
} catch (e: Exception) {
352+
Timber.e(e, "Failed to get tasks")
353+
Result.failure(e)
354+
}
355+
}
356+
357+
/**
358+
* Update an existing time entry
359+
*/
360+
suspend fun updateTimeEntry(
361+
organizationId: String,
362+
timeEntry: TimeEntry,
363+
tags: List<String> = emptyList()
364+
): Result<TimeEntry> {
365+
return try {
366+
val endpoint = authDataStore.getEndpoint()
367+
val api = apiClientFactory.createApi(endpoint)
368+
369+
val request = UpdateTimeEntryRequest(
370+
userId = timeEntry.userId,
371+
start = timeEntry.start,
372+
end = timeEntry.end,
373+
description = timeEntry.description,
374+
projectId = timeEntry.projectId,
375+
taskId = timeEntry.taskId,
376+
billable = timeEntry.billable,
377+
tags = tags
378+
)
379+
380+
val response = api.updateTimeEntry(organizationId, timeEntry.id, request)
381+
Timber.d("Time entry updated: ${response.data?.id}")
382+
Result.success(response.data!!)
383+
} catch (e: Exception) {
384+
Timber.e(e, "Failed to update time entry")
385+
Result.failure(e)
386+
}
387+
}
388+
389+
/**
390+
* Delete a time entry
391+
*/
392+
suspend fun deleteTimeEntry(organizationId: String, timeEntryId: String): Result<Unit> {
393+
return try {
394+
val endpoint = authDataStore.getEndpoint()
395+
val api = apiClientFactory.createApi(endpoint)
396+
api.deleteTimeEntry(organizationId, timeEntryId)
397+
Timber.d("Time entry deleted: $timeEntryId")
398+
Result.success(Unit)
399+
} catch (e: Exception) {
400+
Timber.e(e, "Failed to delete time entry")
401+
Result.failure(e)
402+
}
403+
}
292404
}

0 commit comments

Comments
 (0)