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:
+1
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -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>
|
||||
}
|
||||
|
||||
+24
-4
@@ -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
|
||||
|
||||
+23
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user