refactor: Migrate from monolithic to modular architecture

1. **Docker-Compose für Entwicklung optimieren**
2. **Umgebungsvariablen für lokale Entwicklung**
3. **Service-Abhängigkeiten**
4. **Docker-Compose für Produktion**
5. **Dokumentation**
This commit is contained in:
stefan
2025-07-24 15:26:36 +02:00
parent e7b18da45d
commit dbbc303068
18 changed files with 56 additions and 5233 deletions
@@ -1,44 +0,0 @@
package at.mocode.client.common.components.events
import at.mocode.events.domain.model.Veranstaltung
/**
* Simple JS-specific utility functions for event management UI
*/
object EventUIUtils {
/**
* Formats an event for display in the browser
*/
fun formatEventForDisplay(event: Veranstaltung): String {
return buildString {
append("Event: ${event.name}")
append(" | Location: ${event.ort}")
append(" | From: ${event.startDatum} to: ${event.endDatum}")
if (event.beschreibung != null) {
append(" | Description: ${event.beschreibung}")
}
if (event.sparten.isNotEmpty()) {
append(" | Sports: ${event.sparten.joinToString(", ") { it.name }}")
}
}
}
/**
* Creates a simple HTML representation of an event
*/
fun createEventHtml(event: Veranstaltung): String {
return """
<div class="event-card">
<h3>${event.name}</h3>
<p><strong>Location:</strong> ${event.ort}</p>
<p><strong>Date:</strong> ${event.startDatum} - ${event.endDatum}</p>
${if (event.beschreibung != null) "<p><strong>Description:</strong> ${event.beschreibung}</p>" else ""}
${if (event.sparten.isNotEmpty())
"<p><strong>Sports:</strong> ${event.sparten.joinToString(", ") { it.name }}</p>"
else ""}
</div>
""".trimIndent()
}
}
@@ -1,176 +0,0 @@
package at.mocode.client.common.components.events
import at.mocode.events.domain.model.Veranstaltung
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import react.*
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.span
import emotion.react.css
/**
* Props for the VeranstaltungsListe component
*/
external interface VeranstaltungsListeProps : Props
// Create Ktor client for API calls
private val apiClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
/**
* React component that displays a list of events (Veranstaltungen).
*
* This component loads event data from the API and renders it as HTML.
* Uses useState for state management and useEffectOnce for data loading.
*/
val VeranstaltungsListe = FC<VeranstaltungsListeProps> { _ ->
// State management with useState
var events by useState<List<Veranstaltung>>(emptyList())
var loading by useState(true)
var error by useState<String?>(null)
// Data loading with useEffectOnce hook
useEffectOnce {
val scope = MainScope()
scope.launch {
try {
loading = true
error = null
// Load data with Ktor client
val response = apiClient.get("http://localhost:8080/api/events")
val loadedEvents: List<Veranstaltung> = response.body()
events = loadedEvents
} catch (e: Exception) {
error = "Fehler beim Laden der Veranstaltungen: ${e.message}"
console.error("Error loading events:", e)
} finally {
loading = false
}
}
}
// Render HTML with React DOM elements
div {
css {
// Basic styling for the main container
}
h1 {
+"Veranstaltungen"
}
when {
loading -> {
div {
+"Lade Veranstaltungen..."
}
}
error != null -> {
div {
+error!!
}
}
events.isEmpty() -> {
div {
+"Keine Veranstaltungen verfügbar"
}
}
else -> {
div {
events.forEach { event ->
div {
h3 {
+event.name
}
p {
span {
+"📍"
}
+" ${event.ort}"
}
p {
span {
+"📅"
}
if (event.isMultiDay()) {
+" ${event.startDatum} - ${event.endDatum} (${event.getDurationInDays()} Tage)"
} else {
+" ${event.startDatum} (Eintägige Veranstaltung)"
}
}
// Status indicators
val statusList = mutableListOf<String>()
if (event.istAktiv) statusList.add("Aktiv")
if (event.istOeffentlich) statusList.add("Öffentlich")
if (event.isRegistrationOpen()) statusList.add("Anmeldung offen")
if (statusList.isNotEmpty()) {
p {
span {
+"️"
}
+" Status: ${statusList.joinToString(", ")}"
}
}
// Description
if (!event.beschreibung.isNullOrBlank()) {
p {
span {
+"📝"
}
+" ${event.beschreibung}"
}
}
// Sports/Sparten
if (event.sparten.isNotEmpty()) {
p {
span {
+"🏆"
}
+" Sparten: ${event.sparten.joinToString(", ") { it.name }}"
}
}
// Additional info
event.maxTeilnehmer?.let { max ->
p {
span {
+"👥"
}
+" Max. Teilnehmer: $max"
}
}
event.anmeldeschluss?.let { deadline ->
p {
span {
+"⏰"
}
+" Anmeldeschluss: $deadline"
}
}
}
}
}
}
}
}
}
@@ -1,308 +0,0 @@
package at.mocode.client.common.components.horses
import at.mocode.horses.domain.model.DomPferd
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import react.*
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.span
import emotion.react.css
/**
* Props for the PferdeListe component
*/
external interface PferdeListeProps : Props
// Create Ktor client for API calls
private val apiClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
/**
* React component that displays a list of horses (Pferde).
*
* This component loads horse data from the API and renders it as HTML.
* Uses useState for state management and useEffectOnce for data loading.
*/
val PferdeListe = FC<PferdeListeProps> { _ ->
// State management with useState
var horses by useState<List<DomPferd>>(emptyList())
var loading by useState(true)
var error by useState<String?>(null)
// Data loading with useEffectOnce hook
useEffectOnce {
val scope = MainScope()
scope.launch {
try {
loading = true
error = null
// Load data with Ktor client
val response = apiClient.get("http://localhost:8080/api/horses")
val loadedHorses: List<DomPferd> = response.body()
horses = loadedHorses
} catch (e: Exception) {
error = "Fehler beim Laden der Pferde: ${e.message}"
console.error("Error loading horses:", e)
} finally {
loading = false
}
}
}
// Render HTML with React DOM elements
div {
css {
// Basic styling for the main container
"padding" to "20px"
"fontFamily" to "Arial, sans-serif"
"maxWidth" to "1200px"
"margin" to "0 auto"
}
h1 {
css {
"color" to "#2c3e50"
"borderBottom" to "2px solid #3498db"
"paddingBottom" to "10px"
"marginBottom" to "20px"
}
+"Pferde-Register"
}
when {
loading -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"fontSize" to "18px"
}
+"Lade Pferde..."
}
}
error != null -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#e74c3c"
"backgroundColor" to "#fdeaea"
"border" to "1px solid #e74c3c"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+error!!
}
}
horses.isEmpty() -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"backgroundColor" to "#f8f9fa"
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+"Keine Pferde verfügbar"
}
}
else -> {
div {
css {
"display" to "grid"
"gridTemplateColumns" to "repeat(auto-fill, minmax(300px, 1fr))"
"gap" to "20px"
}
horses.forEach { horse ->
div {
css {
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"padding" to "15px"
"backgroundColor" to "#f9f9f9"
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
"transition" to "transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out"
"hover" to {
"transform" to "translateY(-5px)"
"boxShadow" to "0 5px 15px rgba(0,0,0,0.1)"
}
}
h3 {
css {
"color" to "#3498db"
"marginTop" to "0"
"marginBottom" to "10px"
"borderBottom" to "1px solid #e0e0e0"
"paddingBottom" to "5px"
}
+horse.getDisplayName()
}
// Basic information
p {
span {
+"🐎"
}
+" Geschlecht: ${horse.geschlecht.name}"
}
horse.geburtsdatum?.let { birthDate ->
p {
span {
+"📅"
}
+" Geburtsdatum: $birthDate"
horse.getAge()?.let { age ->
+" (${age} Jahre alt)"
}
}
}
horse.rasse?.let { breed ->
p {
span {
+"🏇"
}
+" Rasse: $breed"
}
}
horse.farbe?.let { color ->
p {
span {
+"🎨"
}
+" Farbe: $color"
}
}
horse.stockmass?.let { height ->
p {
span {
+"📏"
}
+" Stockmaß: ${height} cm"
}
}
// Identification numbers
val identificationNumbers = mutableListOf<String>()
horse.lebensnummer?.let { identificationNumbers.add("Lebensnummer: $it") }
horse.chipNummer?.let { identificationNumbers.add("Chip: $it") }
horse.passNummer?.let { identificationNumbers.add("Pass: $it") }
horse.oepsNummer?.let { identificationNumbers.add("OEPS: $it") }
horse.feiNummer?.let { identificationNumbers.add("FEI: $it") }
if (identificationNumbers.isNotEmpty()) {
p {
span {
+"🆔"
}
+" Identifikation: ${identificationNumbers.joinToString(", ")}"
}
}
// Pedigree information
val pedigreeInfo = mutableListOf<String>()
horse.vaterName?.let { pedigreeInfo.add("Vater: $it") }
horse.mutterName?.let { pedigreeInfo.add("Mutter: $it") }
horse.mutterVaterName?.let { pedigreeInfo.add("Muttervater: $it") }
if (pedigreeInfo.isNotEmpty()) {
p {
span {
+"🧬"
}
+" Abstammung: ${pedigreeInfo.joinToString(", ")}"
}
}
// Breeding information
horse.zuechterName?.let { breeder ->
p {
span {
+"👨‍🌾"
}
+" Züchter: $breeder"
}
}
horse.zuchtbuchNummer?.let { studbook ->
p {
span {
+"📖"
}
+" Zuchtbuchnummer: $studbook"
}
}
// Status indicators
val statusList = mutableListOf<String>()
if (horse.istAktiv) statusList.add("Aktiv") else statusList.add("Inaktiv")
if (horse.hasCompleteIdentification()) statusList.add("Vollständig identifiziert")
if (horse.isOepsRegistered()) statusList.add("OEPS registriert")
if (horse.isFeiRegistered()) statusList.add("FEI registriert")
p {
span {
+"️"
}
+" Status: ${statusList.joinToString(", ")}"
}
// Data source
p {
span {
+"📊"
}
+" Datenquelle: ${horse.datenQuelle.name}"
}
// Notes
horse.bemerkungen?.let { notes ->
p {
span {
+"📝"
}
+" Bemerkungen: $notes"
}
}
// Creation and update dates
p {
span {
+"📅"
}
+" Erstellt am: ${horse.createdAt}"
}
p {
span {
+"🔄"
}
+" Zuletzt geändert: ${horse.updatedAt}"
}
}
}
}
}
}
}
}
@@ -1,257 +0,0 @@
package at.mocode.client.common.components.masterdata
import at.mocode.masterdata.domain.model.LandDefinition
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import react.*
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.h2
import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.span
import emotion.react.css
/**
* Props for the StammdatenListe component
*/
external interface StammdatenListeProps : Props
// Create Ktor client for API calls
private val apiClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
/**
* React component that displays master data (Stammdaten).
*
* This component loads master data from the API and renders it as HTML.
* Currently focuses on countries (LandDefinition) but can be extended for other master data types.
* Uses useState for state management and useEffectOnce for data loading.
*/
val StammdatenListe = FC<StammdatenListeProps> { _ ->
// State management with useState
var countries by useState<List<LandDefinition>>(emptyList())
var loading by useState(true)
var error by useState<String?>(null)
// Data loading with useEffectOnce hook
useEffectOnce {
val scope = MainScope()
scope.launch {
try {
loading = true
error = null
// Load data with Ktor client
val response = apiClient.get("http://localhost:8080/api/masterdata/countries")
val loadedCountries: List<LandDefinition> = response.body()
countries = loadedCountries
} catch (e: Exception) {
error = "Fehler beim Laden der Stammdaten: ${e.message}"
console.error("Error loading master data:", e)
} finally {
loading = false
}
}
}
// Render HTML with React DOM elements
div {
css {
// Basic styling for the main container
"padding" to "20px"
"fontFamily" to "Arial, sans-serif"
"maxWidth" to "1200px"
"margin" to "0 auto"
}
h1 {
css {
"color" to "#2c3e50"
"borderBottom" to "2px solid #3498db"
"paddingBottom" to "10px"
"marginBottom" to "20px"
}
+"Stammdaten"
}
h2 {
css {
"color" to "#34495e"
"marginTop" to "20px"
"marginBottom" to "15px"
"fontSize" to "1.5em"
}
+"Länder"
}
when {
loading -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"fontSize" to "18px"
}
+"Lade Stammdaten..."
}
}
error != null -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#e74c3c"
"backgroundColor" to "#fdeaea"
"border" to "1px solid #e74c3c"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+error!!
}
}
countries.isEmpty() -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"backgroundColor" to "#f8f9fa"
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+"Keine Länder verfügbar"
}
}
else -> {
div {
css {
"display" to "grid"
"gridTemplateColumns" to "repeat(auto-fill, minmax(300px, 1fr))"
"gap" to "20px"
}
countries.forEach { country ->
div {
css {
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"padding" to "15px"
"backgroundColor" to "#f9f9f9"
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
"transition" to "transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out"
"hover" to {
"transform" to "translateY(-5px)"
"boxShadow" to "0 5px 15px rgba(0,0,0,0.1)"
}
}
h3 {
css {
"color" to "#3498db"
"marginTop" to "0"
"marginBottom" to "10px"
"borderBottom" to "1px solid #e0e0e0"
"paddingBottom" to "5px"
}
+country.nameDeutsch
}
// ISO codes
p {
span {
+"🌍"
}
+" ISO-Codes: ${country.isoAlpha2Code} / ${country.isoAlpha3Code}"
country.isoNumerischerCode?.let { numCode ->
+" / $numCode"
}
}
// English name if available
country.nameEnglisch?.let { englishName ->
p {
span {
+"🇬🇧"
}
+" Englischer Name: $englishName"
}
}
// EU/EWR membership
val membershipInfo = mutableListOf<String>()
country.istEuMitglied?.let { isEuMember ->
if (isEuMember) membershipInfo.add("EU-Mitglied")
}
country.istEwrMitglied?.let { isEwrMember ->
if (isEwrMember) membershipInfo.add("EWR-Mitglied")
}
if (membershipInfo.isNotEmpty()) {
p {
span {
+"🇪🇺"
}
+" Mitgliedschaft: ${membershipInfo.joinToString(", ")}"
}
}
// Status
p {
span {
+"️"
}
+" Status: ${if (country.istAktiv) "Aktiv" else "Inaktiv"}"
}
// Sort order if available
country.sortierReihenfolge?.let { sortOrder ->
p {
span {
+"🔢"
}
+" Sortierreihenfolge: $sortOrder"
}
}
// Coat of arms/flag URL if available
country.wappenUrl?.let { flagUrl ->
p {
span {
+"🏴"
}
+" Wappen/Flagge: $flagUrl"
}
}
// Creation and update dates
p {
span {
+"📅"
}
+" Erstellt am: ${country.createdAt}"
}
p {
span {
+"🔄"
}
+" Zuletzt geändert: ${country.updatedAt}"
}
}
}
}
}
}
}
}
@@ -1,148 +0,0 @@
package at.mocode.client.common.di
import at.mocode.members.application.usecase.CreatePersonUseCase
import at.mocode.members.domain.repository.PersonRepository
import at.mocode.members.domain.repository.VereinRepository
import at.mocode.members.domain.service.MasterDataService
import at.mocode.client.web.viewmodel.CreatePersonViewModel
import at.mocode.client.web.viewmodel.PersonListViewModel
/**
* Simple dependency injection container for the application.
* In a real application, you might want to use a proper DI framework like Koin.
*/
object AppDependencies {
// Mock implementations for demonstration
// In a real application, these would be proper implementations
private val mockPersonRepository = object : PersonRepository {
override suspend fun save(person: at.mocode.members.domain.model.DomPerson): at.mocode.members.domain.model.DomPerson {
// Mock implementation - just return the person with an ID
return person.copy(personId = com.benasher44.uuid.uuid4())
}
override suspend fun findById(id: com.benasher44.uuid.Uuid): at.mocode.members.domain.model.DomPerson? {
return null // Mock implementation
}
override suspend fun findByOepsSatzNr(oepsSatzNr: String): at.mocode.members.domain.model.DomPerson? {
return null // Mock implementation
}
override suspend fun findByStammVereinId(vereinId: com.benasher44.uuid.Uuid): List<at.mocode.members.domain.model.DomPerson> {
return emptyList() // Mock implementation
}
override suspend fun findByName(searchTerm: String, limit: Int): List<at.mocode.members.domain.model.DomPerson> {
return emptyList() // Mock implementation
}
override suspend fun findAllActive(limit: Int, offset: Int): List<at.mocode.members.domain.model.DomPerson> {
return emptyList() // Mock implementation
}
override suspend fun countActive(): Long {
return 0L // Mock implementation
}
override suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean {
return false // Mock implementation - no duplicates for demo
}
override suspend fun delete(id: com.benasher44.uuid.Uuid): Boolean {
return true // Mock implementation
}
}
private val mockVereinRepository = object : VereinRepository {
override suspend fun findById(id: com.benasher44.uuid.Uuid): at.mocode.members.domain.model.DomVerein? {
return null // Mock implementation
}
override suspend fun findByOepsVereinsNr(oepsVereinsNr: String): at.mocode.members.domain.model.DomVerein? {
return null // Mock implementation
}
override suspend fun findByName(searchTerm: String, limit: Int): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun findByBundeslandId(bundeslandId: com.benasher44.uuid.Uuid): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun findByLandId(landId: com.benasher44.uuid.Uuid): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun findAllActive(limit: Int, offset: Int): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun findByLocation(searchTerm: String, limit: Int): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun save(verein: at.mocode.members.domain.model.DomVerein): at.mocode.members.domain.model.DomVerein {
return verein.copy(vereinId = com.benasher44.uuid.uuid4()) // Mock implementation
}
override suspend fun delete(id: com.benasher44.uuid.Uuid): Boolean {
return true // Mock implementation
}
override suspend fun existsByOepsVereinsNr(oepsVereinsNr: String): Boolean {
return false // Mock implementation
}
override suspend fun countActive(): Long {
return 0L // Mock implementation
}
override suspend fun countActiveByBundeslandId(bundeslandId: com.benasher44.uuid.Uuid): Long {
return 0L // Mock implementation
}
}
private val mockMasterDataService = object : MasterDataService {
override suspend fun countryExists(countryId: com.benasher44.uuid.Uuid): Boolean {
return true // Mock implementation - assume all countries exist
}
override suspend fun stateExists(stateId: com.benasher44.uuid.Uuid): Boolean {
return true // Mock implementation - assume all states exist
}
override suspend fun getCountryById(countryId: com.benasher44.uuid.Uuid): MasterDataService.CountryInfo? {
return null // Mock implementation
}
override suspend fun getStateById(stateId: com.benasher44.uuid.Uuid): MasterDataService.StateInfo? {
return null // Mock implementation
}
override suspend fun getAllCountries(): List<MasterDataService.CountryInfo> {
return emptyList() // Mock implementation
}
override suspend fun getStatesByCountry(countryId: com.benasher44.uuid.Uuid): List<MasterDataService.StateInfo> {
return emptyList() // Mock implementation
}
}
// Use case instances
private val createPersonUseCase = CreatePersonUseCase(
personRepository = mockPersonRepository,
vereinRepository = mockVereinRepository,
masterDataService = mockMasterDataService
)
// ViewModel factory methods
fun createPersonViewModel(): CreatePersonViewModel {
return CreatePersonViewModel(createPersonUseCase)
}
fun personListViewModel(): PersonListViewModel {
return PersonListViewModel(mockPersonRepository)
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
plugins {
kotlin("jvm")
kotlin("plugin.spring")
id("org.springframework.boot") version "3.2.3"
id("org.springframework.boot")
id("io.spring.dependency-management") version "1.1.4"
id("org.jetbrains.compose") version "1.7.3"
id("org.jetbrains.kotlin.plugin.compose") version "2.1.21"
+1 -31
View File
@@ -1,7 +1,7 @@
plugins {
kotlin("jvm")
kotlin("plugin.spring")
id("org.springframework.boot") version "3.2.3"
id("org.springframework.boot")
id("io.spring.dependency-management") version "1.1.4"
id("org.jetbrains.compose") version "1.7.3"
id("org.jetbrains.kotlin.plugin.compose") version "2.1.21"
@@ -12,38 +12,8 @@ repositories {
mavenCentral()
}
// Configure tests to exclude failing tests
tasks.withType<Test> {
useJUnitPlatform()
filter {
// Exclude all tests for now
excludeTestsMatching("at.mocode.client.web.*")
}
}
// Configure Kotlin source sets to exclude problematic files
kotlin {
sourceSets {
main {
kotlin {
// Exclude backup directories
exclude("at/mocode/client/web/screens/bak/**")
exclude("at/mocode/client/web/viewmodel/bak/**")
// We're now fixing these files, so don't exclude them
// exclude("at/mocode/client/web/di/AppDependencies.kt")
// exclude("**/screens/CreatePersonScreen.kt")
// exclude("**/screens/PersonListScreen.kt")
// exclude("**/viewmodel/CreatePersonViewModel.kt")
// exclude("**/viewmodel/PersonListViewModel.kt")
}
}
test {
kotlin {
// Exclude all test files for now
exclude("**/*Test.kt")
}
}
}
}
dependencies {
@@ -1,148 +0,0 @@
package at.mocode.client.web.di
import at.mocode.members.application.usecase.CreatePersonUseCase
import at.mocode.members.domain.repository.PersonRepository
import at.mocode.members.domain.repository.VereinRepository
import at.mocode.members.domain.service.MasterDataService
import at.mocode.client.web.viewmodel.CreatePersonViewModel
import at.mocode.client.web.viewmodel.PersonListViewModel
/**
* Simple dependency injection container for the application.
* In a real application, you might want to use a proper DI framework like Koin.
*/
object AppDependencies {
// Mock implementations for demonstration
// In a real application, these would be proper implementations
private val mockPersonRepository = object : PersonRepository {
override suspend fun save(person: at.mocode.members.domain.model.DomPerson): at.mocode.members.domain.model.DomPerson {
// Mock implementation - just return the person with an ID
return person.copy(personId = com.benasher44.uuid.uuid4())
}
override suspend fun findById(id: com.benasher44.uuid.Uuid): at.mocode.members.domain.model.DomPerson? {
return null // Mock implementation
}
override suspend fun findByOepsSatzNr(oepsSatzNr: String): at.mocode.members.domain.model.DomPerson? {
return null // Mock implementation
}
override suspend fun findByStammVereinId(vereinId: com.benasher44.uuid.Uuid): List<at.mocode.members.domain.model.DomPerson> {
return emptyList() // Mock implementation
}
override suspend fun findByName(searchTerm: String, limit: Int): List<at.mocode.members.domain.model.DomPerson> {
return emptyList() // Mock implementation
}
override suspend fun findAllActive(limit: Int, offset: Int): List<at.mocode.members.domain.model.DomPerson> {
return emptyList() // Mock implementation
}
override suspend fun countActive(): Long {
return 0L // Mock implementation
}
override suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean {
return false // Mock implementation - no duplicates for demo
}
override suspend fun delete(id: com.benasher44.uuid.Uuid): Boolean {
return true // Mock implementation
}
}
private val mockVereinRepository = object : VereinRepository {
override suspend fun findById(id: com.benasher44.uuid.Uuid): at.mocode.members.domain.model.DomVerein? {
return null // Mock implementation
}
override suspend fun findByOepsVereinsNr(oepsVereinsNr: String): at.mocode.members.domain.model.DomVerein? {
return null // Mock implementation
}
override suspend fun findByName(searchTerm: String, limit: Int): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun findByBundeslandId(bundeslandId: com.benasher44.uuid.Uuid): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun findByLandId(landId: com.benasher44.uuid.Uuid): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun findAllActive(limit: Int, offset: Int): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun findByLocation(searchTerm: String, limit: Int): List<at.mocode.members.domain.model.DomVerein> {
return emptyList() // Mock implementation
}
override suspend fun save(verein: at.mocode.members.domain.model.DomVerein): at.mocode.members.domain.model.DomVerein {
return verein.copy(vereinId = com.benasher44.uuid.uuid4()) // Mock implementation
}
override suspend fun delete(id: com.benasher44.uuid.Uuid): Boolean {
return true // Mock implementation
}
override suspend fun existsByOepsVereinsNr(oepsVereinsNr: String): Boolean {
return false // Mock implementation
}
override suspend fun countActive(): Long {
return 0L // Mock implementation
}
override suspend fun countActiveByBundeslandId(bundeslandId: com.benasher44.uuid.Uuid): Long {
return 0L // Mock implementation
}
}
private val mockMasterDataService = object : MasterDataService {
override suspend fun countryExists(countryId: com.benasher44.uuid.Uuid): Boolean {
return true // Mock implementation - assume all countries exist
}
override suspend fun stateExists(stateId: com.benasher44.uuid.Uuid): Boolean {
return true // Mock implementation - assume all states exist
}
override suspend fun getCountryById(countryId: com.benasher44.uuid.Uuid): MasterDataService.CountryInfo? {
return null // Mock implementation
}
override suspend fun getStateById(stateId: com.benasher44.uuid.Uuid): MasterDataService.StateInfo? {
return null // Mock implementation
}
override suspend fun getAllCountries(): List<MasterDataService.CountryInfo> {
return emptyList() // Mock implementation
}
override suspend fun getStatesByCountry(countryId: com.benasher44.uuid.Uuid): List<MasterDataService.StateInfo> {
return emptyList() // Mock implementation
}
}
// Use case instances
private val createPersonUseCase = CreatePersonUseCase(
personRepository = mockPersonRepository,
vereinRepository = mockVereinRepository,
masterDataService = mockMasterDataService
)
// ViewModel factory methods
fun createPersonViewModel(): CreatePersonViewModel {
return CreatePersonViewModel(createPersonUseCase)
}
fun personListViewModel(): PersonListViewModel {
return PersonListViewModel(mockPersonRepository)
}
}
@@ -1,319 +0,0 @@
package at.mocode.client.web.screens
import androidx.compose.foundation.layout.*
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
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import at.mocode.core.domain.model.GeschlechtE
import at.mocode.client.web.viewmodel.CreatePersonViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreatePersonScreen(
viewModel: CreatePersonViewModel,
onNavigateBack: () -> Unit
) {
var showGeschlechtDropdown by remember { mutableStateOf(false) }
// Handle success navigation
LaunchedEffect(viewModel.isSuccess) {
if (viewModel.isSuccess) {
onNavigateBack()
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Person erstellen") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Error message
viewModel.errorMessage?.let { error ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Text(
text = error,
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
// Basic Information Section
Text(
text = "Grunddaten",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
OutlinedTextField(
value = viewModel.nachname,
onValueChange = viewModel::updateNachname,
label = { Text("Nachname *") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = viewModel.vorname,
onValueChange = viewModel::updateVorname,
label = { Text("Vorname *") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = viewModel.titel,
onValueChange = viewModel::updateTitel,
label = { Text("Titel") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
placeholder = { Text("z.B. Dr., Ing.") }
)
OutlinedTextField(
value = viewModel.oepsSatzNr,
onValueChange = viewModel::updateOepsSatzNr,
label = { Text("OEPS Satznummer") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
placeholder = { Text("6-stellige Nummer") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
OutlinedTextField(
value = viewModel.geburtsdatum,
onValueChange = viewModel::updateGeburtsdatum,
label = { Text("Geburtsdatum") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
placeholder = { Text("YYYY-MM-DD") }
)
// Gender Dropdown
ExposedDropdownMenuBox(
expanded = showGeschlechtDropdown,
onExpandedChange = { showGeschlechtDropdown = !showGeschlechtDropdown }
) {
OutlinedTextField(
value = viewModel.geschlecht?.let {
when(it) {
GeschlechtE.M -> "Männlich"
GeschlechtE.W -> "Weiblich"
GeschlechtE.D -> "Divers"
GeschlechtE.UNBEKANNT -> "Unbekannt"
}
} ?: "",
onValueChange = { },
readOnly = true,
label = { Text("Geschlecht") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showGeschlechtDropdown) },
modifier = Modifier
.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = showGeschlechtDropdown,
onDismissRequest = { showGeschlechtDropdown = false }
) {
GeschlechtE.entries.forEach { option ->
DropdownMenuItem(
text = {
Text(when(option) {
GeschlechtE.M -> "Männlich"
GeschlechtE.W -> "Weiblich"
GeschlechtE.D -> "Divers"
GeschlechtE.UNBEKANNT -> "Unbekannt"
})
},
onClick = {
viewModel.updateGeschlecht(option)
showGeschlechtDropdown = false
}
)
}
}
}
// Contact Information Section
Text(
text = "Kontaktdaten",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
OutlinedTextField(
value = viewModel.telefon,
onValueChange = viewModel::updateTelefon,
label = { Text("Telefon") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
)
OutlinedTextField(
value = viewModel.email,
onValueChange = viewModel::updateEmail,
label = { Text("E-Mail") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
)
// Address Section
Text(
text = "Adresse",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
OutlinedTextField(
value = viewModel.strasse,
onValueChange = viewModel::updateStrasse,
label = { Text("Straße und Hausnummer") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = viewModel.plz,
onValueChange = viewModel::updatePlz,
label = { Text("PLZ") },
modifier = Modifier.weight(1f),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
OutlinedTextField(
value = viewModel.ort,
onValueChange = viewModel::updateOrt,
label = { Text("Ort") },
modifier = Modifier.weight(2f),
singleLine = true
)
}
OutlinedTextField(
value = viewModel.adresszusatz,
onValueChange = viewModel::updateAdresszusatz,
label = { Text("Adresszusatz") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
// Additional Information Section
Text(
text = "Weitere Informationen",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
OutlinedTextField(
value = viewModel.feiId,
onValueChange = viewModel::updateFeiId,
label = { Text("FEI ID") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = viewModel.mitgliedsNummer,
onValueChange = viewModel::updateMitgliedsNummer,
label = { Text("Mitgliedsnummer beim Stammverein") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = viewModel.istGesperrt,
onCheckedChange = viewModel::updateIstGesperrt
)
Spacer(modifier = Modifier.width(8.dp))
Text("Person ist gesperrt")
}
if (viewModel.istGesperrt) {
OutlinedTextField(
value = viewModel.sperrGrund,
onValueChange = viewModel::updateSperrGrund,
label = { Text("Sperrgrund") },
modifier = Modifier.fillMaxWidth(),
maxLines = 3
)
}
OutlinedTextField(
value = viewModel.notizen,
onValueChange = viewModel::updateNotizen,
label = { Text("Interne Notizen") },
modifier = Modifier.fillMaxWidth(),
maxLines = 4
)
// Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedButton(
onClick = onNavigateBack,
modifier = Modifier.weight(1f),
enabled = !viewModel.isLoading
) {
Text("Abbrechen")
}
Button(
onClick = {
viewModel.createPerson()
},
modifier = Modifier.weight(1f),
enabled = !viewModel.isLoading
) {
if (viewModel.isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp
)
} else {
Text("Erstellen")
}
}
}
}
}
}
@@ -1,212 +0,0 @@
package at.mocode.client.web.screens
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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.members.domain.model.DomPerson
import at.mocode.core.domain.model.GeschlechtE
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.client.web.viewmodel.PersonListViewModel
import kotlinx.datetime.LocalDate
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PersonListScreen(
viewModel: PersonListViewModel,
onNavigateToCreatePerson: () -> Unit
) {
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = onNavigateToCreatePerson
) {
Icon(Icons.Default.Add, contentDescription = "Person hinzufügen")
}
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
Text(
text = "Personen",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
// Error handling
viewModel.errorMessage?.let { error ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = error,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.weight(1f)
)
TextButton(
onClick = { viewModel.clearError() }
) {
Text("OK")
}
}
}
}
// Loading indicator
if (viewModel.isLoading) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
if (!viewModel.isLoading && viewModel.persons.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Keine Personen vorhanden",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(viewModel.persons) { person ->
PersonCard(person = person)
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PersonCard(person: DomPerson) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "${person.titel?.let { "$it " } ?: ""}${person.vorname} ${person.nachname}",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
person.oepsSatzNr?.let { oepsNr ->
Text(
text = "OEPS: $oepsNr",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
person.geburtsdatum?.let { birthDate ->
Text(
text = "Geboren: $birthDate",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Surface(
color = when (person.datenQuelle) {
DatenQuelleE.OEPS_ZNS -> MaterialTheme.colorScheme.primaryContainer
DatenQuelleE.MANUELL -> MaterialTheme.colorScheme.secondaryContainer
},
shape = MaterialTheme.shapes.small
) {
Text(
text = when (person.datenQuelle) {
DatenQuelleE.OEPS_ZNS -> "OEPS"
DatenQuelleE.MANUELL -> "Manuell"
},
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = when (person.datenQuelle) {
DatenQuelleE.OEPS_ZNS -> MaterialTheme.colorScheme.onPrimaryContainer
DatenQuelleE.MANUELL -> MaterialTheme.colorScheme.onSecondaryContainer
}
)
}
}
Spacer(modifier = Modifier.height(8.dp))
person.email?.let { email ->
Text(
text = "📧 $email",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
person.telefon?.let { phone ->
Text(
text = "📞 $phone",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (person.strasse != null && person.plz != null && person.ort != null) {
Text(
text = "📍 ${person.strasse}, ${person.plz} ${person.ort}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@@ -1,181 +0,0 @@
package at.mocode.client.web.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.GeschlechtE
import at.mocode.members.application.usecase.CreatePersonUseCase
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
class CreatePersonViewModel(
private val createPersonUseCase: CreatePersonUseCase
) : ViewModel() {
// Form state
var nachname by mutableStateOf("")
private set
var vorname by mutableStateOf("")
private set
var titel by mutableStateOf("")
private set
var oepsSatzNr by mutableStateOf("")
private set
var geburtsdatum by mutableStateOf("")
private set
var geschlecht by mutableStateOf<GeschlechtE?>(null)
private set
var telefon by mutableStateOf("")
private set
var email by mutableStateOf("")
private set
var strasse by mutableStateOf("")
private set
var plz by mutableStateOf("")
private set
var ort by mutableStateOf("")
private set
var adresszusatz by mutableStateOf("")
private set
var feiId by mutableStateOf("")
private set
var mitgliedsNummer by mutableStateOf("")
private set
var notizen by mutableStateOf("")
private set
var istGesperrt by mutableStateOf(false)
private set
var sperrGrund by mutableStateOf("")
private set
// UI state
var isLoading by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
var isSuccess by mutableStateOf(false)
private set
// Update methods
fun updateNachname(value: String) { nachname = value }
fun updateVorname(value: String) { vorname = value }
fun updateTitel(value: String) { titel = value }
fun updateOepsSatzNr(value: String) { oepsSatzNr = value }
fun updateGeburtsdatum(value: String) { geburtsdatum = value }
fun updateGeschlecht(value: GeschlechtE?) { geschlecht = value }
fun updateTelefon(value: String) { telefon = value }
fun updateEmail(value: String) { email = value }
fun updateStrasse(value: String) { strasse = value }
fun updatePlz(value: String) { plz = value }
fun updateOrt(value: String) { ort = value }
fun updateAdresszusatz(value: String) { adresszusatz = value }
fun updateFeiId(value: String) { feiId = value }
fun updateMitgliedsNummer(value: String) { mitgliedsNummer = value }
fun updateNotizen(value: String) { notizen = value }
fun updateIstGesperrt(value: Boolean) { istGesperrt = value }
fun updateSperrGrund(value: String) { sperrGrund = value }
fun clearError() {
errorMessage = null
}
fun createPerson() {
// Basic validation
when {
nachname.isBlank() -> {
errorMessage = "Nachname ist erforderlich"
return
}
vorname.isBlank() -> {
errorMessage = "Vorname ist erforderlich"
return
}
}
viewModelScope.launch {
isLoading = true
errorMessage = null
try {
// Parse birthdate if provided
val parsedGeburtsdatum = if (geburtsdatum.isNotBlank()) {
try {
val parts = geburtsdatum.split("-")
if (parts.size == 3) {
LocalDate(parts[0].toInt(), parts[1].toInt(), parts[2].toInt())
} else {
errorMessage = "Ungültiges Datumsformat. Verwenden Sie YYYY-MM-DD"
isLoading = false
isSuccess = false
return@launch
}
} catch (_: Exception) {
errorMessage = "Ungültiges Datumsformat. Verwenden Sie YYYY-MM-DD"
isLoading = false
isSuccess = false
return@launch
}
} else null
val request = CreatePersonUseCase.CreatePersonRequest(
oepsSatzNr = oepsSatzNr.takeIf { it.isNotBlank() },
nachname = nachname,
vorname = vorname,
titel = titel.takeIf { it.isNotBlank() },
geburtsdatum = parsedGeburtsdatum,
geschlechtE = geschlecht,
telefon = telefon.takeIf { it.isNotBlank() },
email = email.takeIf { it.isNotBlank() },
strasse = strasse.takeIf { it.isNotBlank() },
plz = plz.takeIf { it.isNotBlank() },
ort = ort.takeIf { it.isNotBlank() },
adresszusatzZusatzinfo = adresszusatz.takeIf { it.isNotBlank() },
feiId = feiId.takeIf { it.isNotBlank() },
mitgliedsNummerBeiStammVerein = mitgliedsNummer.takeIf { it.isNotBlank() },
istGesperrt = istGesperrt,
sperrGrund = sperrGrund.takeIf { it.isNotBlank() },
datenQuelle = DatenQuelleE.MANUELL,
notizenIntern = notizen.takeIf { it.isNotBlank() }
)
val response = createPersonUseCase.execute(request)
if (response.success) {
isSuccess = true
} else {
errorMessage = response.error?.message ?: "Unbekannter Fehler beim Erstellen der Person"
}
} catch (e: Exception) {
errorMessage = "Fehler beim Erstellen der Person: ${e.message}"
} finally {
isLoading = false
}
}
}
fun resetForm() {
nachname = ""
vorname = ""
titel = ""
oepsSatzNr = ""
geburtsdatum = ""
geschlecht = null
telefon = ""
email = ""
strasse = ""
plz = ""
ort = ""
adresszusatz = ""
feiId = ""
mitgliedsNummer = ""
notizen = ""
istGesperrt = false
sperrGrund = ""
isLoading = false
errorMessage = null
isSuccess = false
}
}
@@ -1,48 +0,0 @@
package at.mocode.client.web.viewmodel
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.members.domain.model.DomPerson
import at.mocode.members.domain.repository.PersonRepository
import kotlinx.coroutines.launch
class PersonListViewModel(
private val personRepository: PersonRepository
) : ViewModel() {
// UI state
var persons by mutableStateOf<List<DomPerson>>(emptyList())
private set
var isLoading by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
init {
loadPersons()
}
fun loadPersons() {
viewModelScope.launch {
isLoading = true
errorMessage = null
try {
persons = personRepository.findAllActive(limit = 100, offset = 0)
} catch (e: Exception) {
errorMessage = "Fehler beim Laden der Personen: ${e.message}"
} finally {
isLoading = false
}
}
}
fun clearError() {
errorMessage = null
}
fun refreshPersons() {
loadPersons()
}
}