Add PDF invoice generation: implement backend API, introduce PdfService, update frontend repository and UI with download logic, and mark roadmap task complete.

This commit is contained in:
2026-04-13 17:18:45 +02:00
parent 9b9c068e7f
commit 76d7019d30
12 changed files with 204 additions and 7 deletions
@@ -49,5 +49,6 @@ object ApiRoutes {
fun buche(kontoId: String) = "$ROOT/konten/$kontoId/buche"
fun veranstaltungKonten(veranstaltungId: String) = "$ROOT/veranstaltungen/$veranstaltungId/konten"
fun personKonto(veranstaltungId: String, personId: String) = "$ROOT/veranstaltungen/$veranstaltungId/personen/$personId"
fun rechnung(kontoId: String) = "$ROOT/konten/$kontoId/rechnung"
}
}
@@ -38,4 +38,8 @@ class DefaultBillingRepository(
setBody(request)
}.body()
}
override suspend fun getRechnungPdf(kontoId: String): Result<ByteArray> = runCatching {
client.get(ApiRoutes.Billing.rechnung(kontoId)).body()
}
}
@@ -57,4 +57,8 @@ class FakeBillingRepository : BillingRepository {
konten[index] = updatedKonto
return Result.success(updatedKonto)
}
override suspend fun getRechnungPdf(kontoId: String): Result<ByteArray> {
return Result.success("MOCK PDF CONTENT".encodeToByteArray())
}
}
@@ -31,4 +31,11 @@ interface BillingRepository {
kontoId: String,
request: BuchungRequest
): Result<TeilnehmerKontoDto>
/**
* Holt das PDF für eine Rechnung.
*/
suspend fun getRechnungPdf(
kontoId: String
): Result<ByteArray>
}
@@ -5,10 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Receipt
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -85,6 +82,16 @@ fun BillingScreen(
Text("Buchungen", fontWeight = FontWeight.Bold, fontSize = 16.sp)
Spacer(Modifier.weight(1f))
if (state.selectedKonto != null) {
OutlinedButton(
onClick = { viewModel.downloadRechnung() },
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
modifier = Modifier.height(32.dp)
) {
Icon(Icons.Default.PictureAsPdf, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Rechnung", fontSize = 12.sp)
}
Spacer(Modifier.width(8.dp))
Button(
onClick = { showBuchungsDialog = true },
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
@@ -126,6 +133,19 @@ fun BillingScreen(
}
)
}
if (state.pdfData != null) {
AlertDialog(
onDismissRequest = { viewModel.clearPdf() },
title = { Text("Rechnung bereit") },
text = { Text("Die Rechnung für ${state.selectedKonto?.personName} wurde generiert (${state.pdfData?.size} Bytes).") },
confirmButton = {
TextButton(onClick = { viewModel.clearPdf() }) {
Text("Schließen")
}
}
)
}
}
@Composable
@@ -16,6 +16,7 @@ data class BillingUiState(
val konten: List<TeilnehmerKontoDto> = emptyList(),
val selectedKonto: TeilnehmerKontoDto? = null,
val buchungen: List<BuchungDto> = emptyList(),
val pdfData: ByteArray? = null,
val error: String? = null
)
@@ -103,7 +104,28 @@ class BillingViewModel(
// Für Abwärtskompatibilität oder Listenansicht (optional)
fun selectKonto(konto: TeilnehmerKontoDto) {
_uiState.value = _uiState.value.copy(selectedKonto = konto)
_uiState.value = _uiState.value.copy(selectedKonto = konto, pdfData = null)
loadBuchungen(konto.id)
}
fun downloadRechnung() {
val konto = _uiState.value.selectedKonto ?: return
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
repository.getRechnungPdf(konto.id)
.onSuccess { data ->
_uiState.value = _uiState.value.copy(pdfData = data, isLoading = false)
}
.onFailure {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = "Fehler beim Laden der Rechnung: ${it.message}"
)
}
}
}
fun clearPdf() {
_uiState.value = _uiState.value.copy(pdfData = null)
}
}