feat(TODO): update phases for orders, billing, and calendar features
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s

- Marked orders and billing tasks as complete in the TODO list, detailing the associated components and APIs.
- Introduced a new phase for calendar features, outlining tasks for navigation, data loading, and event management.
- Enhanced the AppDependencies to include BillingApi and MemberOrdersApi for improved billing and order management.
- Updated AppRoot and SettingsScreen to incorporate billing and orders sections, enhancing user navigation and functionality.
This commit is contained in:
Torsten Schulz (local)
2026-05-13 00:19:30 +02:00
parent ea46a6d4f9
commit 9be5f50ede
8 changed files with 1348 additions and 7 deletions

View File

@@ -0,0 +1,88 @@
package de.tt_tagebuch.shared.api
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
import de.tt_tagebuch.shared.api.models.BillingCreateRunBody
import de.tt_tagebuch.shared.api.models.BillingCreateRunEnvelope
import de.tt_tagebuch.shared.api.models.BillingGenerateBody
import de.tt_tagebuch.shared.api.models.BillingGenerateEnvelope
import de.tt_tagebuch.shared.api.models.BillingHoursPreviewEnvelope
import de.tt_tagebuch.shared.api.models.BillingRunsEnvelope
import de.tt_tagebuch.shared.api.models.BillingSettingsEnvelope
import de.tt_tagebuch.shared.api.models.BillingTemplatesEnvelope
import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.readBytes
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import io.ktor.http.contentType
class BillingApi(
private val client: AuthedHttpClient,
) {
suspend fun listTemplates(clubId: Int): BillingTemplatesEnvelope =
client.http.get("/api/billing/templates/$clubId").body()
suspend fun uploadTemplate(clubId: Int, name: String, description: String, pdfBytes: ByteArray, filename: String = "template.pdf") {
client.http.post("/api/billing/templates/$clubId") {
contentType(ContentType.MultiPart.FormData)
setBody(
MultiPartFormDataContent(
formData {
append("name", name)
append("description", description)
append(
"templatePdf",
pdfBytes,
Headers.build {
append(HttpHeaders.ContentType, "application/pdf")
append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
},
)
},
),
)
}
}
suspend fun deleteTemplate(templateId: Int) {
client.http.delete("/api/billing/templates/$templateId")
}
suspend fun listRuns(clubId: Int): BillingRunsEnvelope =
client.http.get("/api/billing/runs/$clubId").body()
suspend fun getSettings(clubId: Int): BillingSettingsEnvelope =
client.http.get("/api/billing/settings/$clubId").body()
suspend fun hoursPreview(clubId: Int, monthFrom: String, monthTo: String): BillingHoursPreviewEnvelope =
client.http.get("/api/billing/hours-preview/$clubId") {
parameter("monthFrom", monthFrom)
parameter("monthTo", monthTo)
}.body()
suspend fun createRun(clubId: Int, body: BillingCreateRunBody): BillingCreateRunEnvelope =
client.http.post("/api/billing/runs/$clubId") {
contentType(ContentType.Application.Json)
setBody(body)
}.body()
suspend fun generateRun(runId: Int, locale: String = "de-DE"): BillingGenerateEnvelope =
client.http.post("/api/billing/runs/$runId/generate") {
contentType(ContentType.Application.Json)
setBody(BillingGenerateBody(locale = locale))
}.body()
suspend fun downloadRunPdf(runId: Int): ByteArray =
client.http.get("/api/billing/runs/$runId/download").readBytes()
suspend fun deleteRun(runId: Int) {
client.http.delete("/api/billing/runs/$runId")
}
}

View File

@@ -0,0 +1,36 @@
package de.tt_tagebuch.shared.api
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
import de.tt_tagebuch.shared.api.models.MemberOrderCreateBody
import de.tt_tagebuch.shared.api.models.MemberOrderEnvelope
import de.tt_tagebuch.shared.api.models.MemberOrderPatchBody
import de.tt_tagebuch.shared.api.models.MemberOrdersListEnvelope
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.patch
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.contentType
class MemberOrdersApi(
private val client: AuthedHttpClient,
) {
suspend fun listGlobal(): MemberOrdersListEnvelope =
client.http.get("/api/member-orders/global").body()
suspend fun listForMember(clubId: Int, memberId: Int): MemberOrdersListEnvelope =
client.http.get("/api/member-orders/$clubId/$memberId").body()
suspend fun create(clubId: Int, memberId: Int, body: MemberOrderCreateBody): MemberOrderEnvelope =
client.http.post("/api/member-orders/$clubId/$memberId") {
contentType(ContentType.Application.Json)
setBody(body)
}.body()
suspend fun update(clubId: Int, memberId: Int, orderId: Int, body: MemberOrderPatchBody): MemberOrderEnvelope =
client.http.patch("/api/member-orders/$clubId/$memberId/$orderId") {
contentType(ContentType.Application.Json)
setBody(body)
}.body()
}

View File

@@ -0,0 +1,104 @@
package de.tt_tagebuch.shared.api.models
import kotlinx.serialization.Serializable
@Serializable
data class BillingTemplateDto(
val id: Int,
val clubId: Int? = null,
val name: String = "",
val description: String? = null,
val version: Int = 1,
)
@Serializable
data class BillingTemplatesEnvelope(
val success: Boolean = false,
val templates: List<BillingTemplateDto> = emptyList(),
)
@Serializable
data class BillingRunDto(
val id: Int,
val clubId: Int? = null,
val templateId: Int? = null,
val name: String? = null,
val periodStart: String? = null,
val periodEnd: String? = null,
val selfRecipientName: String? = null,
val hourlyRate: Double = 0.0,
val computedHoursTotal: Double? = null,
val status: String? = null,
)
@Serializable
data class BillingRunsEnvelope(
val success: Boolean = false,
val runs: List<BillingRunDto> = emptyList(),
)
@Serializable
data class BillingUserSettingsDto(
val lastHourlyRate: Double? = null,
val lastSelfRecipientName: String? = null,
val lastLocationText: String? = null,
)
@Serializable
data class BillingSettingsEnvelope(
val success: Boolean = false,
val settings: BillingUserSettingsDto? = null,
)
@Serializable
data class BillingSessionPreviewDto(
val date: String? = null,
val startTime: String? = null,
val endTime: String? = null,
val durationHours: Double? = null,
)
@Serializable
data class BillingHoursPreviewEnvelope(
val success: Boolean = false,
val computedHoursTotal: Double? = null,
val sessions: List<BillingSessionPreviewDto> = emptyList(),
)
@Serializable
data class BillingCreateRunBody(
val templateId: Int,
val monthFrom: String,
val monthTo: String,
val selfRecipientName: String? = null,
val iban: String? = null,
val ibanWithoutCountry: Boolean = false,
val hourlyRate: Double,
val sessionLabel: String? = null,
val sameAccountCheckbox: Boolean = false,
val omitSelfRecipientName: Boolean = false,
val omitIban: Boolean = false,
val omitLocationText: Boolean = false,
val omitDocumentDate: Boolean = false,
val omitSessionLabel: Boolean = false,
val locationText: String? = null,
val documentDate: String? = null,
)
@Serializable
data class BillingCreateRunEnvelope(
val success: Boolean = false,
val run: BillingRunDto? = null,
val error: String? = null,
)
@Serializable
data class BillingGenerateBody(
val locale: String = "de-DE",
)
@Serializable
data class BillingGenerateEnvelope(
val success: Boolean = false,
val error: String? = null,
)

View File

@@ -0,0 +1,80 @@
package de.tt_tagebuch.shared.api.models
import kotlinx.serialization.Serializable
@Serializable
data class OrderMemberSnippetDto(
val id: Int? = null,
val firstName: String? = null,
val lastName: String? = null,
)
@Serializable
data class OrderClubSnippetDto(
val id: Int? = null,
val name: String? = null,
)
@Serializable
data class MemberOrderHistoryEntryDto(
val id: Int? = null,
val status: String? = null,
val changedAt: String? = null,
val cost: Double? = null,
val paidAmount: Double? = null,
val budget: Double? = null,
val paidConfirmed: Boolean? = null,
)
@Serializable
data class MemberOrderDto(
val id: Int,
val memberId: Int? = null,
val clubId: Int? = null,
val item: String = "",
val status: String = "requested",
val orderDate: String? = null,
val statusDate: String? = null,
val createdAt: String? = null,
val updatedAt: String? = null,
val cost: Double = 0.0,
val paidAmount: Double = 0.0,
val budget: Double = 0.0,
val paidConfirmed: Boolean = false,
val openAmount: Double = 0.0,
val member: OrderMemberSnippetDto? = null,
val club: OrderClubSnippetDto? = null,
val historyEntries: List<MemberOrderHistoryEntryDto> = emptyList(),
)
@Serializable
data class MemberOrdersListEnvelope(
val success: Boolean = false,
val orders: List<MemberOrderDto> = emptyList(),
)
@Serializable
data class MemberOrderEnvelope(
val success: Boolean = false,
val order: MemberOrderDto? = null,
)
@Serializable
data class MemberOrderCreateBody(
val item: String,
val status: String = "requested",
val cost: Double = 0.0,
val paidAmount: Double = 0.0,
val budget: Double = 0.0,
val paidConfirmed: Boolean = false,
)
@Serializable
data class MemberOrderPatchBody(
val item: String,
val status: String,
val cost: Double,
val paidAmount: Double,
val budget: Double,
val paidConfirmed: Boolean,
)