Formularformatierung

This commit is contained in:
2026-08-21 09:38:41 +02:00
parent b6e7ce999b
commit 34de5b5c5a
32 changed files with 1148 additions and 203 deletions
@@ -0,0 +1,108 @@
package de.medapp.wunddoku.ui.components.format
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import java.time.LocalDate
/**
* Zentrale, wiederverwendbare Formatierungs-/Transformationslösung für ziffernbasierte
* Eingabefelder (Geburtsdatum, Telefonnummern). Bewusst als EINE generische Klasse statt
* getrennter Lösungen pro Feldtyp, damit die Formatierungslogik nicht auf mehrere Screens
* verteilt wird.
*
* Funktionsweise: Der zugrunde liegende Wert (`value` im TextField) bleibt immer eine reine
* Ziffernfolge ohne Trennzeichen - das ist die Quelle der Wahrheit im ViewModel. Die
* [VisualTransformation] fügt Trennzeichen ausschließlich für die DARSTELLUNG ein, ohne den
* zugrunde liegenden Text zu verändern. Dadurch bleibt die Persistenzschicht vollständig
* unabhängig von der UI-Darstellung: Repository/DAO sehen nur die rohe Ziffernfolge
* (Telefonnummern) bzw. eine daraus geparste LocalDate (Geburtsdatum).
*
* [groupSizes] beschreibt die Länge jeder Zifferngruppe von links nach rechts (z. B. [2,2,4]
* für TT.MM.JJJJ). Ziffern, die über die Summe aller Gruppen hinausgehen, werden ohne weitere
* Gruppierung angehängt (z. B. [5] für Telefonnummern: nur nach der fünften Ziffer ein
* Leerzeichen, danach durchgehend).
*/
class DigitGroupVisualTransformation(
private val groupSizes: List<Int>,
private val separator: Char,
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val digits = text.text
val formatted = StringBuilder()
// separatorsBeforeDigit[i] = Anzahl der Trennzeichen, die in der formatierten Darstellung
// VOR der i-ten Ziffer stehen - wird für die Cursor-Rückrechnung (OffsetMapping) benötigt.
val separatorsBeforeDigit = IntArray(digits.length + 1)
var groupIndex = 0
var posInGroup = 0
for (i in digits.indices) {
val currentGroupSize = groupSizes.getOrNull(groupIndex)
if (currentGroupSize != null && posInGroup == currentGroupSize) {
formatted.append(separator)
groupIndex++
posInGroup = 0
}
separatorsBeforeDigit[i] = formatted.count { it == separator }
formatted.append(digits[i])
posInGroup++
}
separatorsBeforeDigit[digits.length] = formatted.count { it == separator }
val offsetMapping = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
val clamped = offset.coerceIn(0, digits.length)
return clamped + separatorsBeforeDigit[clamped]
}
override fun transformedToOriginal(offset: Int): Int {
val clamped = offset.coerceIn(0, formatted.length)
var digitCount = 0
for (i in 0 until clamped) {
if (formatted[i] != separator) digitCount++
}
return digitCount
}
}
return TransformedText(AnnotatedString(formatted.toString()), offsetMapping)
}
}
/** Filtert eine Roheingabe auf reine Ziffern und begrenzt sie auf [maxDigits] Zeichen. */
fun filterDigits(input: String, maxDigits: Int): String =
input.filter { it.isDigit() }.take(maxDigits)
/** Gruppierung TT.MM.JJJJ, maximal 8 Ziffern. */
val DATE_GROUP_SIZES = listOf(2, 2, 4)
const val DATE_MAX_DIGITS = 8
/** Gruppierung: erste 5 Ziffern, dann ein Leerzeichen, danach ungruppiert. */
val PHONE_GROUP_SIZES = listOf(5)
const val PHONE_MAX_DIGITS = 15 // großzügige, defensive Obergrenze (E.164-Maximallänge)
const val POSTAL_CODE_MAX_DIGITS = 5 // deutsche PLZ
/**
* Wandelt eine 8-stellige Ziffernfolge im Format TTMMJJJJ in ein [LocalDate] um, oder liefert
* `null`, wenn die Länge nicht stimmt oder das Datum ungültig ist (z. B. "31022000" für den
* nicht existierenden 31. Februar). `LocalDate.of(...)` wirft in diesem Fall eine
* `DateTimeException`, die hier zu `null` statt einer stillschweigenden Fehlerkorrektur führt.
*/
fun germanDateDigitsToLocalDateOrNull(digits: String): LocalDate? {
if (digits.length != DATE_MAX_DIGITS) return null
return try {
val day = digits.substring(0, 2).toInt()
val month = digits.substring(2, 4).toInt()
val year = digits.substring(4, 8).toInt()
LocalDate.of(year, month, day)
} catch (e: Exception) {
null
}
}
/** Kehrfunktion zu [germanDateDigitsToLocalDateOrNull]: liefert die TTMMJJJJ-Ziffernfolge. */
fun LocalDate.toGermanDateDigits(): String =
"%02d%02d%04d".format(dayOfMonth, monthValue, year)
@@ -111,7 +111,7 @@ fun DocumentationEditScreen(
modifier = Modifier.fillMaxSize().padding(padding),
block = currentBlock,
onCheckedChanged = viewModel::onCheckedChanged,
onFreeTextChanged = { text -> viewModel.onFreeTextChanged(currentBlock.blockStateId, text) },
onFreeTextChanged = viewModel::onFreeTextChanged,
canGoBack = uiState.currentBlockIndex > 0,
canGoNext = uiState.currentBlockIndex < uiState.blockCount - 1,
onPrevious = viewModel::onPreviousBlock,
@@ -128,7 +128,7 @@ private fun DocumentationBlockContent(
modifier: Modifier = Modifier,
block: DocumentationBlock,
onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit,
onFreeTextChanged: (String) -> Unit,
onFreeTextChanged: (checkItemStateId: String, text: String) -> Unit,
canGoBack: Boolean,
canGoNext: Boolean,
onPrevious: () -> Unit,
@@ -142,15 +142,17 @@ private fun DocumentationBlockContent(
) {
Text(block.title, style = MaterialTheme.typography.headlineSmall)
block.items.forEach { item -> CheckItemRow(item = item, onCheckedChanged = onCheckedChanged) }
OutlinedTextField(
value = block.freeText,
onValueChange = onFreeTextChanged,
label = { Text(stringResource(R.string.documentation_edit_free_text_label)) },
modifier = Modifier.fillMaxWidth(),
minLines = 3,
)
block.items.forEach { item ->
if (item.isFreeText) {
FreeTextCheckItemRow(
item = item,
onCheckedChanged = onCheckedChanged,
onFreeTextChanged = onFreeTextChanged,
)
} else {
CheckItemRow(item = item, onCheckedChanged = onCheckedChanged)
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
@@ -179,3 +181,34 @@ private fun CheckItemRow(
Text(item.label)
}
}
/**
* Die Freitext-Option eines Blocks (Änderungswunsch: Freitext als weitere auswählbare Option
* statt eigenes Feld am Block). Das Textfeld ist deaktiviert, solange die zugehörige Checkbox
* nicht angehakt ist - so kann kein Freitext eingegeben werden, ohne dass die Option auch als
* "bearbeitet" gilt (siehe BlockEditedPolicy: `items.any { it.checked }`).
*/
@Composable
private fun FreeTextCheckItemRow(
item: CheckItem,
onCheckedChanged: (checkItemStateId: String, checked: Boolean) -> Unit,
onFreeTextChanged: (checkItemStateId: String, text: String) -> Unit,
) {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = item.checked,
onCheckedChange = { checked -> onCheckedChanged(item.checkItemStateId, checked) },
)
Text(item.label)
}
OutlinedTextField(
value = item.freeTextValue,
onValueChange = { text -> onFreeTextChanged(item.checkItemStateId, text) },
enabled = item.checked,
label = { Text(stringResource(R.string.documentation_edit_free_text_label)) },
modifier = Modifier.fillMaxWidth().padding(start = 40.dp),
minLines = 2,
)
}
}
@@ -51,7 +51,8 @@ class DocumentationEditViewModel(
private val freeTextChanges = MutableSharedFlow<Pair<String, String>>(extraBufferCapacity = 64)
/** Letzter noch nicht persistierter Freitext-Wert je BlockState-ID, für [flushPendingChanges]. */
/** Letzter noch nicht persistierter Freitext-Wert je CheckItemState-ID (Freitext-Option
* eines Blocks), für [flushPendingChanges]. */
private val pendingFreeText = mutableMapOf<String, String>()
init {
@@ -74,29 +75,37 @@ class DocumentationEditViewModel(
@OptIn(FlowPreview::class)
private fun observeFreeTextChanges() {
viewModelScope.launch {
freeTextChanges.debounce(FREE_TEXT_DEBOUNCE_MS).collect { (blockStateId, text) ->
persistFreeText(blockStateId, text)
freeTextChanges.debounce(FREE_TEXT_DEBOUNCE_MS).collect { (checkItemStateId, text) ->
persistFreeText(checkItemStateId, text)
}
}
}
private suspend fun persistFreeText(blockStateId: String, text: String) {
repository.updateFreeText(blockStateId, text)
private suspend fun persistFreeText(checkItemStateId: String, text: String) {
repository.updateFreeText(checkItemStateId, text)
// Nur entfernen, wenn zwischenzeitlich kein neuerer Wert eingetroffen ist.
if (pendingFreeText[blockStateId] == text) pendingFreeText.remove(blockStateId)
if (pendingFreeText[checkItemStateId] == text) pendingFreeText.remove(checkItemStateId)
}
fun onFreeTextChanged(blockStateId: String, text: String) {
updateLocalFreeText(blockStateId, text)
pendingFreeText[blockStateId] = text
freeTextChanges.tryEmit(blockStateId to text)
/**
* [checkItemStateId] referenziert die Freitext-Option eines Blocks (CheckItem.isFreeText =
* true), NICHT den Block selbst - der Freitext ist seit der Umstellung auf eine
* Checkbox-Option kein eigenes Feld am Block mehr.
*/
fun onFreeTextChanged(checkItemStateId: String, text: String) {
updateLocalFreeText(checkItemStateId, text)
pendingFreeText[checkItemStateId] = text
freeTextChanges.tryEmit(checkItemStateId to text)
}
private fun updateLocalFreeText(blockStateId: String, text: String) {
private fun updateLocalFreeText(checkItemStateId: String, text: String) {
_uiState.update { state ->
val documentation = state.documentation ?: return@update state
val updatedBlocks = documentation.blocks.map { block ->
if (block.blockStateId == blockStateId) block.copy(freeText = text) else block
val updatedItems = block.items.map { item ->
if (item.checkItemStateId == checkItemStateId) item.copy(freeTextValue = text) else item
}
block.copy(items = updatedItems)
}
state.copy(documentation = documentation.copy(blocks = updatedBlocks))
}
@@ -142,7 +151,7 @@ class DocumentationEditViewModel(
val toFlush = pendingFreeText.toMap()
pendingFreeText.clear()
viewModelScope.launch {
toFlush.forEach { (blockStateId, text) -> repository.updateFreeText(blockStateId, text) }
toFlush.forEach { (checkItemStateId, text) -> repository.updateFreeText(checkItemStateId, text) }
}
}
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -15,6 +16,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@@ -31,15 +33,18 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.medapp.wunddoku.R
import de.medapp.wunddoku.ui.components.format.DATE_GROUP_SIZES
import de.medapp.wunddoku.ui.components.format.DigitGroupVisualTransformation
import de.medapp.wunddoku.ui.components.format.PHONE_GROUP_SIZES
/**
* Formular für Schritt 1 einer neuen Wunddokumentation (Aufgabenstellung Abschnitt 7) UND für
* das reine Bearbeiten von Patienten-Stammdaten (Abschnitt 6) – unterschieden über
* [PatientFormViewModel.startsNewDocumentation], s. Kommentar dort und bei [de.medapp.wunddoku.ui.navigation.Screen.PatientForm].
* Formular für Schritt 1 einer neuen Wunddokumentation UND für das reine Bearbeiten von
* Patienten-Stammdaten – unterschieden über [PatientFormViewModel.startsNewDocumentation].
*
* Hinweis: Das Geburtsdatum wird in Phase 2 noch als einfaches Textfeld (Format JJJJ-MM-TT)
* erfasst. Ein Material-3-DatePicker-Dialog für bessere Eingabe-UX ist eine sinnvolle
* Verbesserung für eine spätere Phase, aber kein Blocker für das Grundgerüst.
* Geburtsdatum und Telefonnummern nutzen dieselbe wiederverwendbare
* [DigitGroupVisualTransformation] (ui/components/format/FormattedInput.kt): Der zugrunde
* liegende Zustand im ViewModel bleibt jeweils eine reine Ziffernfolge, die Trennzeichen
* (Punkte bzw. Leerzeichen) werden ausschließlich für die Darstellung ergänzt.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -108,42 +113,62 @@ fun PatientFormScreen(
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = uiState.birthDateText,
onValueChange = viewModel::onBirthDateTextChanged,
value = uiState.birthDateDigits,
onValueChange = viewModel::onBirthDateDigitsChanged,
label = { Text(stringResource(R.string.patient_form_birth_date)) },
placeholder = { Text("JJJJ-MM-TT") },
placeholder = { Text("TT.MM.JJJJ") },
visualTransformation = DigitGroupVisualTransformation(DATE_GROUP_SIZES, '.'),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = uiState.street,
onValueChange = viewModel::onStreetChanged,
label = { Text(stringResource(R.string.patient_form_street)) },
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = uiState.address,
onValueChange = viewModel::onAddressChanged,
label = { Text(stringResource(R.string.patient_form_address)) },
value = uiState.postalCode,
onValueChange = viewModel::onPostalCodeChanged,
label = { Text(stringResource(R.string.patient_form_postal_code)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = uiState.city,
onValueChange = viewModel::onCityChanged,
label = { Text(stringResource(R.string.patient_form_city)) },
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = uiState.phone1,
onValueChange = viewModel::onPhone1Changed,
label = { Text(stringResource(R.string.patient_form_phone1)) },
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Phone),
visualTransformation = DigitGroupVisualTransformation(PHONE_GROUP_SIZES, ' '),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = uiState.phone2,
onValueChange = viewModel::onPhone2Changed,
label = { Text(stringResource(R.string.patient_form_phone2)) },
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Phone),
visualTransformation = DigitGroupVisualTransformation(PHONE_GROUP_SIZES, ' '),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = uiState.phone3,
onValueChange = viewModel::onPhone3Changed,
label = { Text(stringResource(R.string.patient_form_phone3)) },
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Phone),
visualTransformation = DigitGroupVisualTransformation(PHONE_GROUP_SIZES, ' '),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
modifier = Modifier.fillMaxWidth(),
)
uiState.validationError?.let { error ->
Text(error, color = androidx.compose.material3.MaterialTheme.colorScheme.error)
Text(error, color = MaterialTheme.colorScheme.error)
}
Button(onClick = { viewModel.save() }, modifier = Modifier.fillMaxWidth()) {
@@ -10,6 +10,12 @@ import de.medapp.wunddoku.data.repository.DocumentationRepository
import de.medapp.wunddoku.data.repository.PatientRepository
import de.medapp.wunddoku.di.wunddokuApplication
import de.medapp.wunddoku.domain.model.Patient
import de.medapp.wunddoku.ui.components.format.DATE_MAX_DIGITS
import de.medapp.wunddoku.ui.components.format.PHONE_MAX_DIGITS
import de.medapp.wunddoku.ui.components.format.POSTAL_CODE_MAX_DIGITS
import de.medapp.wunddoku.ui.components.format.filterDigits
import de.medapp.wunddoku.ui.components.format.germanDateDigitsToLocalDateOrNull
import de.medapp.wunddoku.ui.components.format.toGermanDateDigits
import de.medapp.wunddoku.ui.navigation.Screen
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -17,16 +23,22 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.LocalDate
data class PatientFormUiState(
val isEditing: Boolean = false,
val isLoading: Boolean = true,
val firstName: String = "",
val lastName: String = "",
/** Roh-Text des Eingabefelds; Format JJJJ-MM-TT. Parsen erfolgt beim Speichern. */
val birthDateText: String = "",
val address: String = "",
/** Rohe Ziffernfolge (max. 8 Zeichen, TTMMJJJJ) - die Formatierung TT.MM.JJJJ erfolgt
* ausschließlich über eine VisualTransformation in der Compose-UI, s.
* ui/components/format/FormattedInput.kt. */
val birthDateDigits: String = "",
val street: String = "",
/** Rohe Ziffernfolge, max. 5 Zeichen (deutsche PLZ). */
val postalCode: String = "",
val city: String = "",
/** Rohe Ziffernfolgen (führende Nullen bleiben erhalten, da als String geführt) - die
* Leerzeichen-Formatierung erfolgt ausschließlich über eine VisualTransformation in der UI. */
val phone1: String = "",
val phone2: String = "",
val phone3: String = "",
@@ -40,8 +52,7 @@ data class PatientFormUiState(
)
/**
* Deckt sowohl "neuer Patient" (patientId == null) als auch "Patient bearbeiten" ab
* (Aufgabenstellung Abschnitt 7 Schritt 1, Abschnitt 6).
* Deckt sowohl "neuer Patient" (patientId == null) als auch "Patient bearbeiten" ab.
*/
class PatientFormViewModel(
private val patientId: String?,
@@ -63,8 +74,10 @@ class PatientFormViewModel(
isLoading = false,
firstName = patient.firstName,
lastName = patient.lastName,
birthDateText = patient.birthDate.toString(),
address = patient.address,
birthDateDigits = patient.birthDate.toGermanDateDigits(),
street = patient.street,
postalCode = patient.postalCode,
city = patient.city,
phone1 = patient.phone1,
phone2 = patient.phone2.orEmpty(),
phone3 = patient.phone3.orEmpty(),
@@ -81,11 +94,25 @@ class PatientFormViewModel(
fun onFirstNameChanged(value: String) = _uiState.update { it.copy(firstName = value) }
fun onLastNameChanged(value: String) = _uiState.update { it.copy(lastName = value) }
fun onBirthDateTextChanged(value: String) = _uiState.update { it.copy(birthDateText = value) }
fun onAddressChanged(value: String) = _uiState.update { it.copy(address = value) }
fun onPhone1Changed(value: String) = _uiState.update { it.copy(phone1 = value) }
fun onPhone2Changed(value: String) = _uiState.update { it.copy(phone2 = value) }
fun onPhone3Changed(value: String) = _uiState.update { it.copy(phone3 = value) }
fun onBirthDateDigitsChanged(raw: String) =
_uiState.update { it.copy(birthDateDigits = filterDigits(raw, DATE_MAX_DIGITS)) }
fun onStreetChanged(value: String) = _uiState.update { it.copy(street = value) }
fun onPostalCodeChanged(raw: String) =
_uiState.update { it.copy(postalCode = filterDigits(raw, POSTAL_CODE_MAX_DIGITS)) }
fun onCityChanged(value: String) = _uiState.update { it.copy(city = value) }
fun onPhone1Changed(raw: String) =
_uiState.update { it.copy(phone1 = filterDigits(raw, PHONE_MAX_DIGITS)) }
fun onPhone2Changed(raw: String) =
_uiState.update { it.copy(phone2 = filterDigits(raw, PHONE_MAX_DIGITS)) }
fun onPhone3Changed(raw: String) =
_uiState.update { it.copy(phone3 = filterDigits(raw, PHONE_MAX_DIGITS)) }
fun dismissDuplicateWarning() = _uiState.update { it.copy(duplicateWarning = null) }
@@ -105,18 +132,21 @@ class PatientFormViewModel(
fun save(skipDuplicateCheck: Boolean = false) {
val state = _uiState.value
val birthDate = try {
LocalDate.parse(state.birthDateText.trim())
} catch (e: Exception) {
_uiState.update { it.copy(validationError = "Bitte ein gültiges Geburtsdatum im Format JJJJ-MM-TT eingeben.") }
val birthDate = germanDateDigitsToLocalDateOrNull(state.birthDateDigits)
if (birthDate == null) {
_uiState.update { it.copy(validationError = "Bitte ein gültiges Geburtsdatum eingeben (TT.MM.JJJJ).") }
return
}
if (state.firstName.isBlank() || state.lastName.isBlank()) {
_uiState.update { it.copy(validationError = "Vor- und Nachname dürfen nicht leer sein.") }
return
}
if (state.address.isBlank() || state.phone1.isBlank()) {
_uiState.update { it.copy(validationError = "Adresse und Telefonnummer 1 dürfen nicht leer sein.") }
if (state.street.isBlank() || state.postalCode.isBlank() || state.city.isBlank()) {
_uiState.update { it.copy(validationError = "Straße, PLZ und Stadt dürfen nicht leer sein.") }
return
}
if (state.phone1.isBlank()) {
_uiState.update { it.copy(validationError = "Telefonnummer 1 darf nicht leer sein.") }
return
}
@@ -134,10 +164,12 @@ class PatientFormViewModel(
firstName = state.firstName.trim(),
lastName = state.lastName.trim(),
birthDate = birthDate,
address = state.address.trim(),
phone1 = state.phone1.trim(),
phone2 = state.phone2.trim().ifBlank { null },
phone3 = state.phone3.trim().ifBlank { null },
street = state.street.trim(),
postalCode = state.postalCode.trim(),
city = state.city.trim(),
phone1 = state.phone1,
phone2 = state.phone2.ifBlank { null },
phone3 = state.phone3.ifBlank { null },
createdAt = Instant.EPOCH, // wird von PatientRepository.upsert() korrekt gesetzt
updatedAt = Instant.EPOCH,
)
@@ -150,8 +182,8 @@ class PatientFormViewModel(
/**
* Gemeinsamer Abschluss für beide Wege, an einen gültigen Patienten zu kommen (neu
* gespeichert oder als Duplikat übernommen): Je nach [startsNewDocumentation] entweder eine
* neue, leere Dokumentation anlegen und deren ID für die Navigation bereitstellen, oder
* nur signalisieren, dass gespeichert wurde (zurück zur Patientenliste).
* neue, leere Dokumentation anlegen und deren ID für die Navigation bereitstellen, oder nur
* signalisieren, dass gespeichert wurde (zurück zur Patientenliste).
*/
private suspend fun finish(resolvedPatientId: String) {
if (startsNewDocumentation) {
@@ -166,10 +198,6 @@ class PatientFormViewModel(
fun factory() = viewModelFactory {
initializer {
val container = wunddokuApplication().container
// createSavedStateHandle() ist die von Google empfohlene Art, innerhalb eines
// viewModelFactory-Initializers an das SavedStateHandle zu kommen; toRoute()
// dekodiert daraus direkt die typsichere Navigations-Route (Screen.PatientForm),
// statt Argumente manuell als lose Strings auszulesen.
val savedStateHandle = createSavedStateHandle()
val route = savedStateHandle.toRoute<Screen.PatientForm>()
PatientFormViewModel(
@@ -163,7 +163,7 @@ private fun PatientListItemCard(
"Geburtsdatum: ${item.birthDate.format(dateFormatter)}",
style = MaterialTheme.typography.bodySmall,
)
Text(item.address, style = MaterialTheme.typography.bodySmall)
Text(item.displayAddress, style = MaterialTheme.typography.bodySmall)
val lastDoc = item.lastDocumentationAt
if (lastDoc != null) {
Text(