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:
-44
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
-176
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-308
@@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-257
@@ -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,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,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)
|
||||
}
|
||||
}
|
||||
-319
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-212
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-181
@@ -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
|
||||
}
|
||||
}
|
||||
-48
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user