Refactor code structure for improved readability and maintainability
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 53s

This commit is contained in:
Torsten Schulz (local)
2026-05-27 23:53:41 +02:00
parent 2e7cf0c28d
commit e57cdc6ad8
25 changed files with 156689 additions and 171 deletions

View File

@@ -0,0 +1,51 @@
package de.tsschulz.tt_tagebuch.shared.api.models
import kotlinx.serialization.json.Json
import org.junit.Test
import kotlin.test.assertEquals
class DiaryDrawingSerializationTest {
private val json = Json { ignoreUnknownKeys = true }
@Test
fun predefinedActivity_deserializesDrawingDataAndImageLink() {
val raw = """{"id":7,"name":"Aufschlag","imageLink":"/images/7.png","drawingData":{"targetPosition":"5"}}"""
val activity = json.decodeFromString(PredefinedActivityDto.serializer(), raw)
assertEquals("/images/7.png", activity.imageLink)
assertEquals("""{"targetPosition":"5"}""", activity.drawingData)
}
@Test
fun planActivity_deserializesNestedDrawingAndGroupScope() {
val raw = """{"id":1,"drawingData":{"selectedStartPosition":"AS1"},"groupActivities":[{"id":2,"groupId":4,"drawingData":{"targetPosition":"4"},"groupPredefinedActivity":{"id":9,"drawingData":{"strokeType":"RH"}}}]}"""
val activity = json.decodeFromString(DiaryDateActivityItem.serializer(), raw)
val nested = activity.groupActivities.single()
assertEquals("""{"selectedStartPosition":"AS1"}""", activity.drawingData)
assertEquals(4, nested.groupId)
assertEquals("""{"targetPosition":"4"}""", nested.drawingData)
assertEquals("""{"strokeType":"RH"}""", nested.groupPredefinedActivity?.drawingData)
}
@Test
fun planActivity_findsDrawingStoredOnActivityImage() {
val raw = """{"id":3,"predefinedActivity":{"id":10,"images":[{"id":12,"drawingData":{"strokeType":"VH","targetPosition":"5"}}]}}"""
val activity = json.decodeFromString(DiaryDateActivityItem.serializer(), raw)
assertEquals("""{"strokeType":"VH","targetPosition":"5"}""", activity.predefinedActivity.drawingDataForDisplay())
}
@Test
fun predefinedActivity_keepsStringDrawingDataCompatible() {
val raw = """{"id":8,"drawingData":"{\"strokeType\":\"VH\"}"}"""
val activity = json.decodeFromString(PredefinedActivityDto.serializer(), raw)
assertEquals("""{"strokeType":"VH"}""", activity.drawingData)
}
}

View File

@@ -13,6 +13,8 @@ data class DiaryDateActivityItem(
val groupId: Int? = null,
val planGroup: DiaryPlanGroupSummary? = null,
val predefinedActivity: PredefinedActivitySummary? = null,
@Serializable(with = FlexibleNullableDrawingDataSerializer::class)
val drawingData: String? = null,
val groupActivities: List<GroupActivitySummary> = emptyList(),
)
@@ -30,15 +32,29 @@ data class PredefinedActivitySummary(
/** Wie Backend: z. B. `/api/predefined-activities/…/image/…` */
val imageLink: String? = null,
val imageUrl: String? = null,
@Serializable(with = FlexibleNullableDrawingDataSerializer::class)
val drawingData: String? = null,
val images: List<PredefinedActivityImageSummary> = emptyList(),
)
@Serializable
data class PredefinedActivityImageSummary(
val id: Int? = null,
val imagePath: String? = null,
@Serializable(with = FlexibleNullableDrawingDataSerializer::class)
val drawingData: String? = null,
)
@Serializable
data class GroupActivitySummary(
val id: Int? = null,
val orderId: Int? = null,
val groupId: Int? = null,
val duration: Int? = null,
val durationText: String? = null,
val groupPredefinedActivity: PredefinedActivitySummary? = null,
@Serializable(with = FlexibleNullableDrawingDataSerializer::class)
val drawingData: String? = null,
)
fun PredefinedActivitySummary?.displayLabel(): String {
@@ -55,3 +71,9 @@ fun DiaryDateActivityItem.displayTitle(fallbackTimeblock: String): String {
if (label.isNotEmpty()) return label
return if (isTimeblock) fallbackTimeblock else ""
}
fun PredefinedActivitySummary?.drawingDataForDisplay(): String? {
if (this == null) return null
return drawingData?.takeIf { it.isNotBlank() }
?: images.firstNotNullOfOrNull { image -> image.drawingData?.takeIf { it.isNotBlank() } }
}

View File

@@ -0,0 +1,43 @@
package de.tsschulz.tt_tagebuch.shared.api.models
import kotlinx.serialization.KSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
/**
* The API may expose drawing data as stored JSON text or as an expanded JSON object.
* The app keeps one canonical text representation for the existing drawing parser.
*/
object FlexibleNullableDrawingDataSerializer : KSerializer<String?> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("FlexibleNullableDrawingData", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): String? {
val input = decoder as? JsonDecoder
?: error("FlexibleNullableDrawingDataSerializer requires JsonDecoder")
return when (val element = input.decodeJsonElement()) {
is JsonNull -> null
is JsonPrimitive -> element.contentOrNull
else -> element.toString()
}
}
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: String?) {
when (encoder) {
is JsonEncoder -> encoder.encodeJsonElement(
if (value == null) JsonNull else JsonPrimitive(value),
)
else -> if (value == null) encoder.encodeNull() else encoder.encodeString(value)
}
}
}

View File

@@ -10,6 +10,9 @@ data class PredefinedActivityDto(
val description: String? = null,
val duration: Int? = null,
val durationText: String? = null,
val imageLink: String? = null,
@Serializable(with = FlexibleNullableDrawingDataSerializer::class)
val drawingData: String? = null,
val excludeFromStats: Boolean? = null,
)

View File

@@ -16,6 +16,7 @@ import de.tsschulz.tt_tagebuch.shared.api.models.DiaryMemberTagLinkDto
import de.tsschulz.tt_tagebuch.shared.api.models.DiaryMemberTagMutationBody
import de.tsschulz.tt_tagebuch.shared.api.models.DiaryTag
import de.tsschulz.tt_tagebuch.shared.api.models.PredefinedActivityDto
import de.tsschulz.tt_tagebuch.shared.api.models.PredefinedActivityUpsertBody
import de.tsschulz.tt_tagebuch.shared.api.models.AddDiaryPlanGroupActivityRequest
import de.tsschulz.tt_tagebuch.shared.api.models.CreateDiaryPlanActivityRequest
import de.tsschulz.tt_tagebuch.shared.api.models.CreateTrainingGroupBody
@@ -121,6 +122,14 @@ class DiaryManager(
return predefinedActivitiesApi.getById(id)
}
suspend fun createPredefinedActivity(body: PredefinedActivityUpsertBody): PredefinedActivityDto {
return predefinedActivitiesApi.create(body)
}
suspend fun updatePredefinedActivity(id: Int, body: PredefinedActivityUpsertBody): PredefinedActivityDto {
return predefinedActivitiesApi.update(id, body)
}
suspend fun listAccidents(clubId: Int, diaryDateId: Int): List<AccidentReportDto> {
return accidentApi.list(clubId, diaryDateId)
}