feat(frontend): Struktur und Kommentare verfeinert, Mail-Service-Konfiguration erweitert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
# ===================================================================
|
||||||
|
# Multi-stage Dockerfile for Meldestelle Mail Service
|
||||||
|
# Features: Security hardening, monitoring support, optimal caching, BuildKit cache mounts
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||||
|
ARG GRADLE_VERSION=9.4.1
|
||||||
|
ARG JAVA_VERSION=25
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG VERSION=1.0.0-SNAPSHOT
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Build Stage
|
||||||
|
# ===================================================================
|
||||||
|
FROM eclipse-temurin:${JAVA_VERSION}-jdk-alpine AS builder
|
||||||
|
|
||||||
|
ARG VERSION
|
||||||
|
ARG BUILD_DATE
|
||||||
|
|
||||||
|
LABEL stage=builder \
|
||||||
|
service="mail-service" \
|
||||||
|
maintainer="Meldestelle Development Team"
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
# Gradle optimizations
|
||||||
|
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
||||||
|
-Dorg.gradle.daemon=false \
|
||||||
|
-Dorg.gradle.parallel=true \
|
||||||
|
-Dorg.gradle.workers.max=2 \
|
||||||
|
-Dorg.gradle.jvmargs=-Xmx2g \
|
||||||
|
-XX:+UseParallelGC \
|
||||||
|
-XX:MaxMetaspaceSize=512m"
|
||||||
|
ENV GRADLE_USER_HOME=/root/.gradle
|
||||||
|
|
||||||
|
# 1. Copy full project structure for a reliable monorepo build
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
|
# 2. Build the service
|
||||||
|
RUN --mount=type=cache,target=/root/.gradle/caches \
|
||||||
|
--mount=type=cache,target=/root/.gradle/wrapper \
|
||||||
|
./gradlew :backend:services:mail:mail-service:bootJar --no-daemon --info
|
||||||
|
|
||||||
|
# 3. Extract layers
|
||||||
|
WORKDIR /builder
|
||||||
|
RUN cp /workspace/backend/services/mail/mail-service/build/libs/*.jar app.jar && \
|
||||||
|
java -Djarmode=layertools -jar app.jar extract
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Runtime Stage
|
||||||
|
# ===================================================================
|
||||||
|
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime
|
||||||
|
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG VERSION
|
||||||
|
ARG JAVA_VERSION
|
||||||
|
|
||||||
|
LABEL service="mail-service" \
|
||||||
|
version="${VERSION}" \
|
||||||
|
description="Microservice for Mail and Online Entries" \
|
||||||
|
maintainer="Meldestelle Development Team" \
|
||||||
|
java.version="${JAVA_VERSION}" \
|
||||||
|
build.date="${BUILD_DATE}"
|
||||||
|
|
||||||
|
ARG APP_USER=appuser
|
||||||
|
ARG APP_GROUP=appgroup
|
||||||
|
ARG APP_UID=1001
|
||||||
|
ARG APP_GID=1001
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk upgrade && \
|
||||||
|
apk add --no-cache curl tzdata tini && \
|
||||||
|
rm -rf /var/cache/apk/* && \
|
||||||
|
addgroup -g ${APP_GID} -S ${APP_GROUP} && \
|
||||||
|
adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh && \
|
||||||
|
mkdir -p /app/logs /app/tmp /app/config && \
|
||||||
|
chown -R ${APP_USER}:${APP_GROUP} /app && \
|
||||||
|
chmod -R 750 /app
|
||||||
|
|
||||||
|
# Copy Spring Boot layers
|
||||||
|
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/dependencies/ ./
|
||||||
|
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/spring-boot-loader/ ./
|
||||||
|
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/snapshot-dependencies/ ./
|
||||||
|
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/application/ ./
|
||||||
|
|
||||||
|
USER ${APP_USER}
|
||||||
|
|
||||||
|
EXPOSE 8085 5005
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -fsS --max-time 2 http://localhost:8085/actuator/health/readiness || exit 1
|
||||||
|
|
||||||
|
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
||||||
|
-XX:+UseG1GC \
|
||||||
|
-XX:+UseStringDeduplication \
|
||||||
|
-XX:+UseContainerSupport \
|
||||||
|
-Djava.security.egd=file:/dev/./urandom \
|
||||||
|
-Djava.awt.headless=true \
|
||||||
|
-Dfile.encoding=UTF-8 \
|
||||||
|
-Duser.timezone=Europe/Vienna \
|
||||||
|
-Dmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus"
|
||||||
|
|
||||||
|
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
|
||||||
|
SERVER_PORT=8085 \
|
||||||
|
LOGGING_LEVEL_ROOT=INFO
|
||||||
|
|
||||||
|
ENTRYPOINT ["tini", "--", "sh", "-c", "\
|
||||||
|
echo 'Starting Mail Service with Java ${JAVA_VERSION}...'; \
|
||||||
|
if [ \"${DEBUG:-false}\" = \"true\" ]; then \
|
||||||
|
echo 'DEBUG mode enabled'; \
|
||||||
|
exec java ${JAVA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 org.springframework.boot.loader.launch.JarLauncher; \
|
||||||
|
else \
|
||||||
|
exec java ${JAVA_OPTS} org.springframework.boot.loader.launch.JarLauncher; \
|
||||||
|
fi"]
|
||||||
+14
-2
@@ -2,6 +2,9 @@ package at.mocode.mail.service.api
|
|||||||
|
|
||||||
import at.mocode.mail.service.persistence.NennungEntity
|
import at.mocode.mail.service.persistence.NennungEntity
|
||||||
import at.mocode.mail.service.persistence.NennungRepository
|
import at.mocode.mail.service.persistence.NennungRepository
|
||||||
|
import jakarta.validation.Valid
|
||||||
|
import jakarta.validation.constraints.Email
|
||||||
|
import jakarta.validation.constraints.NotBlank
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.mail.SimpleMailMessage
|
import org.springframework.mail.SimpleMailMessage
|
||||||
import org.springframework.mail.javamail.JavaMailSender
|
import org.springframework.mail.javamail.JavaMailSender
|
||||||
@@ -11,14 +14,23 @@ import kotlin.uuid.Uuid
|
|||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
data class NennungRequest(
|
data class NennungRequest(
|
||||||
|
@field:NotBlank(message = "Turniernummer ist erforderlich")
|
||||||
val turnierNr: String,
|
val turnierNr: String,
|
||||||
|
@field:NotBlank(message = "Vorname ist erforderlich")
|
||||||
val vorname: String,
|
val vorname: String,
|
||||||
|
@field:NotBlank(message = "Nachname ist erforderlich")
|
||||||
val nachname: String,
|
val nachname: String,
|
||||||
|
@field:NotBlank(message = "Lizenznummer ist erforderlich")
|
||||||
val lizenz: String,
|
val lizenz: String,
|
||||||
|
@field:NotBlank(message = "Pferdename ist erforderlich")
|
||||||
val pferdName: String,
|
val pferdName: String,
|
||||||
|
@field:NotBlank(message = "Pferdealter ist erforderlich")
|
||||||
val pferdAlter: String,
|
val pferdAlter: String,
|
||||||
|
@field:Email(message = "Ungültiges Email-Format")
|
||||||
|
@field:NotBlank(message = "Email ist erforderlich")
|
||||||
val email: String,
|
val email: String,
|
||||||
val telefon: String?,
|
val telefon: String?,
|
||||||
|
@field:NotBlank(message = "Bewerbe sind erforderlich")
|
||||||
val bewerbe: String,
|
val bewerbe: String,
|
||||||
val bemerkungen: String?
|
val bemerkungen: String?
|
||||||
)
|
)
|
||||||
@@ -26,7 +38,7 @@ data class NennungRequest(
|
|||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/mail")
|
@RequestMapping("/api/mail")
|
||||||
@CrossOrigin(origins = ["*"]) // Für Wasm-Web-App (Compose HTML/Wasm)
|
@CrossOrigin(origins = ["http://localhost:8080", "https://nennung.mo-code.at"]) // Für Wasm-Web-App (Compose HTML/Wasm)
|
||||||
class MailController(
|
class MailController(
|
||||||
private val nennungRepository: NennungRepository,
|
private val nennungRepository: NennungRepository,
|
||||||
private val mailSender: JavaMailSender
|
private val mailSender: JavaMailSender
|
||||||
@@ -34,7 +46,7 @@ class MailController(
|
|||||||
private val logger = LoggerFactory.getLogger(MailController::class.java)
|
private val logger = LoggerFactory.getLogger(MailController::class.java)
|
||||||
|
|
||||||
@PostMapping("/nennung")
|
@PostMapping("/nennung")
|
||||||
fun receiveNennung(@RequestBody request: NennungRequest) {
|
fun receiveNennung(@Valid @RequestBody request: NennungRequest) {
|
||||||
logger.info("Nennung via API erhalten: ${request.vorname} ${request.nachname} für Turnier ${request.turnierNr}")
|
logger.info("Nennung via API erhalten: ${request.vorname} ${request.nachname} für Turnier ${request.turnierNr}")
|
||||||
|
|
||||||
val entity = NennungEntity(
|
val entity = NennungEntity(
|
||||||
|
|||||||
@@ -2,34 +2,32 @@ spring:
|
|||||||
application:
|
application:
|
||||||
name: mail-service
|
name: mail-service
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:h2:mem:maildb;DB_CLOSE_DELAY=-1
|
url: ${SPRING_DATASOURCE_URL:jdbc:h2:mem:maildb;DB_CLOSE_DELAY=-1}
|
||||||
driver-class-name: org.h2.Driver
|
driver-class-name: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME:org.h2.Driver}
|
||||||
username: sa
|
username: ${SPRING_DATASOURCE_USERNAME:sa}
|
||||||
password: ""
|
password: ${SPRING_DATASOURCE_PASSWORD:""}
|
||||||
h2:
|
jpa:
|
||||||
console:
|
hibernate:
|
||||||
enabled: true
|
ddl-auto: update
|
||||||
path: /h2-console
|
show-sql: true
|
||||||
mail:
|
mail:
|
||||||
host: ${MAIL_HOST:imap.world4you.com}
|
host: ${SPRING_MAIL_HOST:imap.world4you.com}
|
||||||
port: ${MAIL_PORT:993}
|
port: ${SPRING_MAIL_PORT:993}
|
||||||
username: ${MAIL_USERNAME:online-nennen@mo-code.at}
|
username: ${SPRING_MAIL_USERNAME:online-nennen@mo-code.at}
|
||||||
password: ${MAIL_PASSWORD:}
|
password: ${SPRING_MAIL_PASSWORD:}
|
||||||
properties:
|
properties:
|
||||||
mail:
|
mail:
|
||||||
store:
|
store:
|
||||||
protocol: imaps
|
protocol: imaps
|
||||||
imaps:
|
imaps:
|
||||||
host: ${MAIL_HOST:imap.world4you.com}
|
host: ${SPRING_MAIL_HOST:imap.world4you.com}
|
||||||
port: ${MAIL_PORT:993}
|
port: ${SPRING_MAIL_PORT:993}
|
||||||
ssl:
|
ssl:
|
||||||
enable: true
|
enable: true
|
||||||
smtp:
|
smtp:
|
||||||
auth: true
|
auth: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH:true}
|
||||||
starttls:
|
starttls:
|
||||||
enable: true
|
enable: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE:true}
|
||||||
host-smtp: ${SMTP_HOST:smtp.world4you.com}
|
|
||||||
port-smtp: ${SMTP_PORT:587}
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8085
|
port: 8085
|
||||||
|
|||||||
+147
-72
@@ -61,6 +61,7 @@ services:
|
|||||||
PING_SERVICE_URL: "http://ping-service:8082"
|
PING_SERVICE_URL: "http://ping-service:8082"
|
||||||
MASTERDATA_SERVICE_URL: "http://masterdata-service:8086"
|
MASTERDATA_SERVICE_URL: "http://masterdata-service:8086"
|
||||||
EVENTS_SERVICE_URL: "http://events-service:8085"
|
EVENTS_SERVICE_URL: "http://events-service:8085"
|
||||||
|
MAIL_SERVICE_URL: "http://mail-service:8085"
|
||||||
ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095"
|
ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095"
|
||||||
RESULTS_SERVICE_URL: "http://results-service:8088"
|
RESULTS_SERVICE_URL: "http://results-service:8088"
|
||||||
BILLING_SERVICE_URL: "http://billing-service:8087"
|
BILLING_SERVICE_URL: "http://billing-service:8087"
|
||||||
@@ -76,6 +77,8 @@ services:
|
|||||||
condition: "service_healthy"
|
condition: "service_healthy"
|
||||||
zipkin:
|
zipkin:
|
||||||
condition: "service_healthy"
|
condition: "service_healthy"
|
||||||
|
mail-service:
|
||||||
|
condition: "service_healthy"
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8081/actuator/health/readiness" ]
|
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8081/actuator/health/readiness" ]
|
||||||
@@ -540,78 +543,150 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
- ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
||||||
|
|
||||||
# --- MICROSERVICE: Scheduling Service ---
|
# --- MICROSERVICE: Mail Service ---
|
||||||
# scheduling-service:
|
mail-service:
|
||||||
# image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/scheduling-service:${DOCKER_TAG:-latest}"
|
image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/mail-service:${DOCKER_TAG:-latest}"
|
||||||
# build:
|
build:
|
||||||
# context: .
|
context: .
|
||||||
# dockerfile: backend/services/scheduling/scheduling-service/Dockerfile
|
dockerfile: backend/services/mail/Dockerfile
|
||||||
# args:
|
args:
|
||||||
# GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||||
# JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||||
# VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||||
# BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||||
# labels:
|
labels:
|
||||||
# - "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
- "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
||||||
# container_name: "${PROJECT_NAME:-meldestelle}-scheduling-service"
|
container_name: "${PROJECT_NAME:-meldestelle}-mail-service"
|
||||||
# restart: unless-stopped
|
restart: unless-stopped
|
||||||
# ports:
|
ports:
|
||||||
# - "${SCHEDULING_PORT:-8084:8084}"
|
- "${MAIL_PORT:-8085:8085}"
|
||||||
# - "${SCHEDULING_DEBUG_PORT:-5013:5013}"
|
- "${MAIL_DEBUG_PORT:-5014:5014}"
|
||||||
# environment:
|
environment:
|
||||||
# SPRING_PROFILES_ACTIVE: "${SCHEDULING_SPRING_PROFILES_ACTIVE:-docker}"
|
SPRING_PROFILES_ACTIVE: "${MAIL_SPRING_PROFILES_ACTIVE:-docker}"
|
||||||
# DEBUG: "${SCHEDULING_DEBUG:-true}"
|
DEBUG: "${MAIL_DEBUG:-true}"
|
||||||
# SERVER_PORT: "${SCHEDULING_SERVER_PORT:-8084}"
|
SERVER_PORT: "${MAIL_SERVER_PORT:-8085}"
|
||||||
# SPRING_APPLICATION_NAME: "${SCHEDULING_SERVICE_NAME:-scheduling-service}"
|
|
||||||
#
|
# --- KEYCLOAK ---
|
||||||
# # --- KEYCLOAK ---
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://keycloak:8080/realms/meldestelle}"
|
||||||
# SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://keycloak:8080/realms/meldestelle}"
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI:-http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}"
|
||||||
# SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI:-http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}"
|
|
||||||
#
|
# --- CONSUL ---
|
||||||
# # --- CONSUL ---
|
SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}"
|
||||||
# SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}"
|
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
||||||
# SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${MAIL_SERVICE_NAME:-mail-service}"
|
||||||
# SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${SCHEDULING_SERVICE_NAME:-scheduling-service}"
|
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${MAIL_CONSUL_PREFER_IP:-true}"
|
||||||
# SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${SCHEDULING_CONSUL_PREFER_IP:-true}"
|
|
||||||
#
|
# - DATENBANK VERBINDUNG -
|
||||||
# # - DATENBANK VERBINDUNG -
|
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
||||||
# SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
SPRING_DATASOURCE_USERNAME: "${POSTGRES_USER:-pg-user}"
|
||||||
# SPRING_DATASOURCE_USERNAME: "${POSTGRES_USER:-pg-user}"
|
SPRING_DATASOURCE_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
||||||
# SPRING_DATASOURCE_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
|
||||||
#
|
# --- MAIL CONFIG (SMTP) ---
|
||||||
# # --- VALKEY ---
|
SPRING_MAIL_HOST: "${MAIL_SMTP_HOST:-smtp.mo-code.at}"
|
||||||
# SPRING_DATA_VALKEY_HOST: "${VALKEY_SERVER_HOSTNAME:-valkey}"
|
SPRING_MAIL_PORT: "${MAIL_SMTP_PORT:-587}"
|
||||||
# SPRING_DATA_VALKEY_PORT: "${VALKEY_SERVER_PORT:-6379}"
|
SPRING_MAIL_USERNAME: "${MAIL_SMTP_USER:-online-nennen@mo-code.at}"
|
||||||
#
|
SPRING_MAIL_PASSWORD: "${MAIL_SMTP_PASSWORD:-secret}"
|
||||||
# # --- ZIPKIN ---
|
SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "true"
|
||||||
# MANAGEMENT_ZIPKIN_TRACING_ENDPOINT: "${ZIPKIN_ENDPOINT:-http://zipkin:9411/api/v2/spans}"
|
SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "true"
|
||||||
#
|
|
||||||
# depends_on:
|
# --- ZIPKIN ---
|
||||||
# postgres:
|
MANAGEMENT_ZIPKIN_TRACING_ENDPOINT: "${ZIPKIN_ENDPOINT:-http://zipkin:9411/api/v2/spans}"
|
||||||
# condition: "service_healthy"
|
|
||||||
# keycloak:
|
depends_on:
|
||||||
# condition: "service_healthy"
|
postgres:
|
||||||
# consul:
|
condition: "service_healthy"
|
||||||
# condition: "service_healthy"
|
consul:
|
||||||
# valkey:
|
condition: "service_healthy"
|
||||||
# condition: "service_healthy"
|
zipkin:
|
||||||
# zipkin:
|
condition: "service_healthy"
|
||||||
# condition: "service_healthy"
|
|
||||||
#
|
healthcheck:
|
||||||
# healthcheck:
|
test: [ "CMD", "curl", "-f", "http://localhost:8085/actuator/health/readiness" ]
|
||||||
# test: [ "CMD", "curl", "-f", "http://localhost:8084/actuator/health" ]
|
interval: 15s
|
||||||
# interval: 15s
|
timeout: 5s
|
||||||
# timeout: 5s
|
retries: 5
|
||||||
# retries: 5
|
start_period: 40s
|
||||||
# start_period: 40s
|
|
||||||
#
|
networks:
|
||||||
# networks:
|
meldestelle-network:
|
||||||
# meldestelle-network:
|
aliases:
|
||||||
# aliases:
|
- "mail-service"
|
||||||
# - "scheduling-service"
|
profiles: [ "backend", "all" ]
|
||||||
# profiles: [ "backend", "all" ]
|
volumes:
|
||||||
# volumes:
|
- ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
||||||
# - ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
|
||||||
|
# --- MICROSERVICE: Scheduling Service ---
|
||||||
|
scheduling-service:
|
||||||
|
image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/scheduling-service:${DOCKER_TAG:-latest}"
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: backend/services/scheduling/scheduling-service/Dockerfile
|
||||||
|
args:
|
||||||
|
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||||
|
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||||
|
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||||
|
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||||
|
labels:
|
||||||
|
- "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
||||||
|
container_name: "${PROJECT_NAME:-meldestelle}-scheduling-service"
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${SCHEDULING_PORT:-8084:8084}"
|
||||||
|
- "${SCHEDULING_DEBUG_PORT:-5013:5013}"
|
||||||
|
environment:
|
||||||
|
SPRING_PROFILES_ACTIVE: "${SCHEDULING_SPRING_PROFILES_ACTIVE:-docker}"
|
||||||
|
DEBUG: "${SCHEDULING_DEBUG:-true}"
|
||||||
|
SERVER_PORT: "${SCHEDULING_SERVER_PORT:-8084}"
|
||||||
|
SPRING_APPLICATION_NAME: "${SCHEDULING_SERVICE_NAME:-scheduling-service}"
|
||||||
|
|
||||||
|
# --- KEYCLOAK ---
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://keycloak:8080/realms/meldestelle}"
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI:-http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}"
|
||||||
|
|
||||||
|
# --- CONSUL ---
|
||||||
|
SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}"
|
||||||
|
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${SCHEDULING_SERVICE_NAME:-scheduling-service}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${SCHEDULING_CONSUL_PREFER_IP:-true}"
|
||||||
|
|
||||||
|
# - DATENBANK VERBINDUNG -
|
||||||
|
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
||||||
|
SPRING_DATASOURCE_USERNAME: "${POSTGRES_USER:-pg-user}"
|
||||||
|
SPRING_DATASOURCE_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
||||||
|
|
||||||
|
# --- VALKEY ---
|
||||||
|
SPRING_DATA_VALKEY_HOST: "${VALKEY_SERVER_HOSTNAME:-valkey}"
|
||||||
|
SPRING_DATA_VALKEY_PORT: "${VALKEY_SERVER_PORT:-6379}"
|
||||||
|
|
||||||
|
# --- ZIPKIN ---
|
||||||
|
MANAGEMENT_ZIPKIN_TRACING_ENDPOINT: "${ZIPKIN_ENDPOINT:-http://zipkin:9411/api/v2/spans}"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: "service_healthy"
|
||||||
|
keycloak:
|
||||||
|
condition: "service_healthy"
|
||||||
|
consul:
|
||||||
|
condition: "service_healthy"
|
||||||
|
valkey:
|
||||||
|
condition: "service_healthy"
|
||||||
|
zipkin:
|
||||||
|
condition: "service_healthy"
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "curl", "-f", "http://localhost:8084/actuator/health" ]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
meldestelle-network:
|
||||||
|
aliases:
|
||||||
|
- "scheduling-service"
|
||||||
|
profiles: [ "backend", "all" ]
|
||||||
|
volumes:
|
||||||
|
- ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
||||||
|
|
||||||
# --- MICROSERVICE: Series Service ---
|
# --- MICROSERVICE: Series Service ---
|
||||||
series-service:
|
series-service:
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 🧹 Session Journal - 15. April 2026 (Live-Gang Vorbereitung)
|
||||||
|
|
||||||
|
## 🏗️ Status-Check (Lead Architect)
|
||||||
|
|
||||||
|
- **Phase 13 (Export & Mail-Service):** Infrastruktur und Deployment-Vorbereitungen für den Live-Gang des Online-Nennens
|
||||||
|
sind abgeschlossen.
|
||||||
|
- **Ziel erreicht:** Das System kann nun auf dem Produktions-Server deployt werden.
|
||||||
|
|
||||||
|
## 👷 Durchgeführte Arbeiten (DevOps & Frontend)
|
||||||
|
|
||||||
|
1. **Infrastruktur (Docker):**
|
||||||
|
- Dockerfile für `mail-service` erstellt.
|
||||||
|
- `dc-backend.yaml` um den `mail-service` erweitert (inkl. Postgres-Link, Consul-Discovery und SMTP-Konfiguration).
|
||||||
|
- `application.yaml` im `mail-service` für dynamische Konfiguration via Environment-Variablen fit gemacht.
|
||||||
|
2. **Frontend (Konfigurierbarkeit):**
|
||||||
|
- Common `PlatformConfig` erweitert um `resolveMailServiceUrl`.
|
||||||
|
- Implementierung für Wasm, JS und JVM hinzugefügt, um Backend-URLs zur Laufzeit steuern zu können (Wasm: via global
|
||||||
|
JS variables).
|
||||||
|
- `NennungRemoteRepository` nutzt nun die dynamisch aufgelöste Mail-Service-URL.
|
||||||
|
- Fehlende Projekt-Abhängigkeit (`frontend.core.network`) im `nennung-feature` ergänzt.
|
||||||
|
3. **Sicherheit:**
|
||||||
|
- CORS im `MailController` auf Ziel-Domains eingeschränkt (`nennung.mo-code.at`).
|
||||||
|
- Bean-Validierung für `NennungRequest` (Email-Format, Pflichtfelder) implementiert.
|
||||||
|
4. **Dokumentation:**
|
||||||
|
- `docs/05_Deployment/2026-04-15_Online-Nennung-Deployment.md` erstellt.
|
||||||
|
|
||||||
|
## 🧐 QA-Status & Bekannte Themen
|
||||||
|
|
||||||
|
- [x] **Infrastruktur-Check:** Docker-Stack ist bereit für `up -d mail-service`.
|
||||||
|
- [x] **Frontend-URL:** Die harte Verdrahtung auf `localhost:8085` wurde durch eine flexible Runtime-Konfiguration
|
||||||
|
ersetzt.
|
||||||
|
- [ ] **Mail-Versand:** Der tatsächliche Versand muss in der Ziel-Umgebung mit echten SMTP-Credentials validiert werden.
|
||||||
|
|
||||||
|
## 🧹 Curator's Note
|
||||||
|
|
||||||
|
- Die ROADMAP Phase 13 wurde in der Vormittags-Session bereits aktualisiert.
|
||||||
|
- Das "Biest" ist nun technologisch "Live-ready". 🚀
|
||||||
|
|
||||||
|
**Abschluss:** Online-Nennung bereit für das Neumarkt-Turnier (April 2026). 🐎
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# 🚀 Deployment Guide - Online-Nennung (Neumarkt 2026)
|
||||||
|
|
||||||
|
Dieser Guide beschreibt den Prozess zum Deployment des "Biest" Online-Nennung Stacks auf den Produktions-Server.
|
||||||
|
|
||||||
|
## 1. Voraussetzungen
|
||||||
|
|
||||||
|
- Docker & Docker Compose installiert.
|
||||||
|
- Zugriff auf den OEPS SMTP Server oder eine Alternative.
|
||||||
|
- Domain (z.B. `nennung.mo-code.at`) zeigt auf den Server.
|
||||||
|
|
||||||
|
## 2. Infrastruktur (Backend)
|
||||||
|
|
||||||
|
Der Stack wird über `dc-backend.yaml` gestartet.
|
||||||
|
|
||||||
|
### Umgebungsvariablen (`.env` Datei am Server)
|
||||||
|
|
||||||
|
Folgende Variablen müssen gesetzt sein:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Datenbank
|
||||||
|
POSTGRES_USER=pg-user
|
||||||
|
POSTGRES_PASSWORD=dein-geheimes-passwort
|
||||||
|
|
||||||
|
# SMTP (für Bestätigungs-Mails)
|
||||||
|
MAIL_SMTP_HOST=smtp.mo-code.at
|
||||||
|
MAIL_SMTP_PORT=587
|
||||||
|
MAIL_SMTP_USER=online-nennen@mo-code.at
|
||||||
|
MAIL_SMTP_PASSWORD=dein-smtp-passwort
|
||||||
|
```
|
||||||
|
|
||||||
|
### Starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f dc-backend.yaml up -d mail-service postgres consul
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Frontend (Wasm Web App)
|
||||||
|
|
||||||
|
Die Web-App kommuniziert direkt mit dem `mail-service`.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :frontend:shells:meldestelle-web:wasmJsBrowserDistribution
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Artefakte liegen in `frontend/shells/meldestelle-web/build/dist/wasmJs/productionExecutable/`.
|
||||||
|
|
||||||
|
### Konfiguration (Laufzeit)
|
||||||
|
|
||||||
|
In der `index.html` oder über ein vorgeschaltetes Nginx können globale Variablen gesetzt werden, um die Backend-URLs zu
|
||||||
|
steuern:
|
||||||
|
|
||||||
|
```html
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.MAIL_SERVICE_URL = "https://nennung.mo-code.at/api/mail";
|
||||||
|
window.API_BASE_URL = "https://nennung.mo-code.at/api";
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Sicherheit & Härtung
|
||||||
|
|
||||||
|
- **CORS:** Der `MailController` ist aktuell für `localhost:8080` und `nennung.mo-code.at` freigeschaltet.
|
||||||
|
- **Reverse Proxy:** Es wird empfohlen, einen Nginx oder Traefik mit SSL (Let's Encrypt) vor den Stack zu schalten.
|
||||||
|
- **Mail-Absender:** Die Absender-Adresse ist im `MailController` hartcodiert auf `online-nennen@mo-code.at`. Dies
|
||||||
|
sollte bei Bedarf angepasst werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Dokumentiert durch den Lead Architect am 15. April 2026.*
|
||||||
+2
@@ -1,6 +1,8 @@
|
|||||||
package at.mocode.frontend.core.network
|
package at.mocode.frontend.core.network
|
||||||
|
|
||||||
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||||
expect object PlatformConfig {
|
expect object PlatformConfig {
|
||||||
fun resolveApiBaseUrl(): String
|
fun resolveApiBaseUrl(): String
|
||||||
|
fun resolveMailServiceUrl(): String
|
||||||
fun resolveKeycloakUrl(): String
|
fun resolveKeycloakUrl(): String
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -36,6 +36,18 @@ actual object PlatformConfig {
|
|||||||
return fallbackUrl
|
return fallbackUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun resolveMailServiceUrl(): String {
|
||||||
|
val fromGlobal = try {
|
||||||
|
(globalScope.MAIL_SERVICE_URL as? String)?.trim().orEmpty()
|
||||||
|
} catch (_: dynamic) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
if (fromGlobal.isNotEmpty()) {
|
||||||
|
return fromGlobal.removeSuffix("/")
|
||||||
|
}
|
||||||
|
return "http://localhost:8085"
|
||||||
|
}
|
||||||
|
|
||||||
actual fun resolveKeycloakUrl(): String {
|
actual fun resolveKeycloakUrl(): String {
|
||||||
// 1) Prefer a global JS variable (injected by main.kt via AppConfig)
|
// 1) Prefer a global JS variable (injected by main.kt via AppConfig)
|
||||||
val fromGlobal = try {
|
val fromGlobal = try {
|
||||||
|
|||||||
+6
@@ -10,6 +10,12 @@ actual object PlatformConfig {
|
|||||||
return "http://localhost:8081"
|
return "http://localhost:8081"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun resolveMailServiceUrl(): String {
|
||||||
|
val env = System.getenv("MAIL_SERVICE_URL")?.trim().orEmpty()
|
||||||
|
if (env.isNotEmpty()) return env.removeSuffix("/")
|
||||||
|
return "http://localhost:8085"
|
||||||
|
}
|
||||||
|
|
||||||
actual fun resolveKeycloakUrl(): String {
|
actual fun resolveKeycloakUrl(): String {
|
||||||
val env = System.getenv("KEYCLOAK_URL")?.trim().orEmpty()
|
val env = System.getenv("KEYCLOAK_URL")?.trim().orEmpty()
|
||||||
if (env.isNotEmpty()) return env.removeSuffix("/")
|
if (env.isNotEmpty()) return env.removeSuffix("/")
|
||||||
|
|||||||
+16
@@ -6,6 +6,12 @@ package at.mocode.frontend.core.network
|
|||||||
|
|
||||||
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||||
actual object PlatformConfig {
|
actual object PlatformConfig {
|
||||||
|
actual fun resolveMailServiceUrl(): String {
|
||||||
|
val fromGlobal = getGlobalMailServiceUrl()
|
||||||
|
if (fromGlobal.isNotEmpty()) return fromGlobal.removeSuffix("/")
|
||||||
|
return "http://localhost:8085"
|
||||||
|
}
|
||||||
|
|
||||||
actual fun resolveKeycloakUrl(): String {
|
actual fun resolveKeycloakUrl(): String {
|
||||||
val fromGlobal = getGlobalKeycloakUrl()
|
val fromGlobal = getGlobalKeycloakUrl()
|
||||||
if (fromGlobal.isNotEmpty()) return fromGlobal.removeSuffix("/")
|
if (fromGlobal.isNotEmpty()) return fromGlobal.removeSuffix("/")
|
||||||
@@ -47,6 +53,16 @@ private fun getGlobalApiBaseUrl(): String = js(
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
|
private fun getGlobalMailServiceUrl(): String = js(
|
||||||
|
"""
|
||||||
|
(function() {
|
||||||
|
var global = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {}));
|
||||||
|
return (global.MAIL_SERVICE_URL && typeof global.MAIL_SERVICE_URL === 'string') ? global.MAIL_SERVICE_URL : "";
|
||||||
|
})()
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalWasmJsInterop::class)
|
@OptIn(ExperimentalWasmJsInterop::class)
|
||||||
private fun getGlobalKeycloakUrl(): String = js(
|
private fun getGlobalKeycloakUrl(): String = js(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ kotlin {
|
|||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
|
implementation(projects.frontend.core.network)
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
|
|||||||
+39
-39
@@ -1,52 +1,52 @@
|
|||||||
package at.mocode.frontend.features.nennung.domain
|
package at.mocode.frontend.features.nennung.domain
|
||||||
|
|
||||||
|
import at.mocode.frontend.core.network.PlatformConfig
|
||||||
import at.mocode.frontend.features.nennung.presentation.web.NennungPayload
|
import at.mocode.frontend.features.nennung.presentation.web.NennungPayload
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.http.*
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.http.contentType
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NennungApiRequest(
|
data class NennungApiRequest(
|
||||||
val turnierNr: String,
|
val turnierNr: String,
|
||||||
val vorname: String,
|
val vorname: String,
|
||||||
val nachname: String,
|
val nachname: String,
|
||||||
val lizenz: String,
|
val lizenz: String,
|
||||||
val pferdName: String,
|
val pferdName: String,
|
||||||
val pferdAlter: String,
|
val pferdAlter: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val telefon: String?,
|
val telefon: String?,
|
||||||
val bewerbe: String,
|
val bewerbe: String,
|
||||||
val bemerkungen: String?
|
val bemerkungen: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
class NennungRemoteRepository(private val client: HttpClient) {
|
class NennungRemoteRepository(private val client: HttpClient) {
|
||||||
suspend fun sendeNennung(turnierNr: String, payload: NennungPayload): Result<Unit> {
|
private val mailServiceUrl = PlatformConfig.resolveMailServiceUrl()
|
||||||
return try {
|
|
||||||
val request = NennungApiRequest(
|
|
||||||
turnierNr = turnierNr,
|
|
||||||
vorname = payload.vorname,
|
|
||||||
nachname = payload.nachname,
|
|
||||||
lizenz = payload.lizenz,
|
|
||||||
pferdName = payload.pferdName,
|
|
||||||
pferdAlter = payload.pferdAlter,
|
|
||||||
email = payload.email,
|
|
||||||
telefon = payload.telefon,
|
|
||||||
bewerbe = payload.bewerbe.joinToString(", ") { it.nr.toString() },
|
|
||||||
bemerkungen = payload.bemerkungen
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wir senden direkt an den mail-service (Port 8085)
|
suspend fun sendeNennung(turnierNr: String, payload: NennungPayload): Result<Unit> {
|
||||||
// In einer Prod-Umgebung würde dies über das Gateway laufen.
|
return try {
|
||||||
client.post("http://localhost:8085/api/mail/nennung") {
|
val request = NennungApiRequest(
|
||||||
contentType(ContentType.Application.Json)
|
turnierNr = turnierNr,
|
||||||
setBody(request)
|
vorname = payload.vorname,
|
||||||
}
|
nachname = payload.nachname,
|
||||||
Result.success(Unit)
|
lizenz = payload.lizenz,
|
||||||
} catch (e: Exception) {
|
pferdName = payload.pferdName,
|
||||||
Result.failure(e)
|
pferdAlter = payload.pferdAlter,
|
||||||
}
|
email = payload.email,
|
||||||
|
telefon = payload.telefon,
|
||||||
|
bewerbe = payload.bewerbe.joinToString(", ") { it.nr.toString() },
|
||||||
|
bemerkungen = payload.bemerkungen
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wir senden an den mail-service (URL dynamisch aufgelöst)
|
||||||
|
client.post("$mailServiceUrl/api/mail/nennung") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+202
-153
@@ -49,7 +49,8 @@ fun StammdatenTabContent(
|
|||||||
var znsDataLoaded by remember { mutableStateOf(false) }
|
var znsDataLoaded by remember { mutableStateOf(false) }
|
||||||
var znsPayloadVersion by remember { mutableStateOf<String?>(null) }
|
var znsPayloadVersion by remember { mutableStateOf<String?>(null) }
|
||||||
var znsImportedAt by remember { mutableStateOf<String?>(null) }
|
var znsImportedAt by remember { mutableStateOf<String?>(null) }
|
||||||
val znsImportHistory = remember { mutableStateListOf<Triple<String, String, Boolean>>() } // (source, payloadVersion, ok)
|
val znsImportHistory =
|
||||||
|
remember { mutableStateListOf<Triple<String, String, Boolean>>() } // (source, payloadVersion, ok)
|
||||||
var typ by remember { mutableStateOf("ÖTO (National)") }
|
var typ by remember { mutableStateOf("ÖTO (National)") }
|
||||||
|
|
||||||
val sparten = remember { mutableStateListOf<String>() }
|
val sparten = remember { mutableStateListOf<String>() }
|
||||||
@@ -63,10 +64,10 @@ fun StammdatenTabContent(
|
|||||||
var titel by remember { mutableStateOf("") }
|
var titel by remember { mutableStateOf("") }
|
||||||
var subTitel by remember { mutableStateOf("") }
|
var subTitel by remember { mutableStateOf("") }
|
||||||
|
|
||||||
// Initialisierung aus Mock-Store (StoreV2/TurnierStoreV2) falls vorhanden
|
// Initialisierung aus Mock-Store (`StoreV2/TurnierStoreV2`) falls vorhanden
|
||||||
LaunchedEffect(turnierId) {
|
LaunchedEffect(turnierId) {
|
||||||
// Da wir in einem anderen Modul sind, können wir nicht direkt auf StoreV2 zugreifen
|
// Da wir in einem anderen Modul sind, können wir nicht direkt auf StoreV2 zugreifen,
|
||||||
// ohne die Abhängigkeit zu haben. In einer echten Architektur käme dies über das Repository.
|
// ohne die Abhängigkeit zu haben. In einer echten Architektur kommt dies über das Repository.
|
||||||
// Aber für die Demo/Fakten-Präsentation im Desktop-Shell-Kontext:
|
// Aber für die Demo/Fakten-Präsentation im Desktop-Shell-Kontext:
|
||||||
try {
|
try {
|
||||||
val clazz = Class.forName("at.mocode.desktop.v2.TurnierStoreV2")
|
val clazz = Class.forName("at.mocode.desktop.v2.TurnierStoreV2")
|
||||||
@@ -76,37 +77,39 @@ fun StammdatenTabContent(
|
|||||||
val idField = t!!::class.java.getDeclaredField("turnierNr")
|
val idField = t!!::class.java.getDeclaredField("turnierNr")
|
||||||
idField.isAccessible = true
|
idField.isAccessible = true
|
||||||
idField.get(t).toString() == turnierId.toString() ||
|
idField.get(t).toString() == turnierId.toString() ||
|
||||||
t.hashCode().toLong() == turnierId // Fallback falls ID anders gemappt ist
|
t.hashCode().toLong() == turnierId // Fallback, falls die ID anders gemappt ist
|
||||||
}
|
}
|
||||||
|
|
||||||
if (turnier != null) {
|
when {
|
||||||
val tClass = turnier::class.java
|
turnier != null -> {
|
||||||
|
val tClass = turnier::class.java
|
||||||
|
|
||||||
val nrField = tClass.getDeclaredField("turnierNr")
|
val nrField = tClass.getDeclaredField("turnierNr")
|
||||||
nrField.isAccessible = true
|
nrField.isAccessible = true
|
||||||
turnierNr = nrField.get(turnier).toString()
|
turnierNr = nrField.get(turnier).toString()
|
||||||
nrConfirmed = true
|
nrConfirmed = true
|
||||||
|
|
||||||
val titelField = tClass.getDeclaredField("titel")
|
val titelField = tClass.getDeclaredField("titel")
|
||||||
titelField.isAccessible = true
|
titelField.isAccessible = true
|
||||||
titel = titelField.get(turnier) as String
|
titel = titelField.get(turnier) as String
|
||||||
|
|
||||||
val subField = tClass.getDeclaredField("subTitel")
|
val subField = tClass.getDeclaredField("subTitel")
|
||||||
subField.isAccessible = true
|
subField.isAccessible = true
|
||||||
subTitel = subField.get(turnier) as String
|
subTitel = subField.get(turnier) as String
|
||||||
|
|
||||||
val katField = tClass.getDeclaredField("kategorie")
|
val katField = tClass.getDeclaredField("kategorie")
|
||||||
katField.isAccessible = true
|
katField.isAccessible = true
|
||||||
val kats = katField.get(turnier) as? List<String>
|
val kats = katField.get(turnier) as? List<String>
|
||||||
kats?.let { kat.addAll(it) }
|
kats?.let { kat.addAll(it) }
|
||||||
|
|
||||||
val typField = tClass.getDeclaredField("typ")
|
val typField = tClass.getDeclaredField("typ")
|
||||||
typField.isAccessible = true
|
typField.isAccessible = true
|
||||||
typ = typField.get(turnier) as String
|
typ = typField.get(turnier) as String
|
||||||
|
|
||||||
val znsField = tClass.getDeclaredField("znsDataLoaded")
|
val znsField = tClass.getDeclaredField("znsDataLoaded")
|
||||||
znsField.isAccessible = true
|
znsField.isAccessible = true
|
||||||
znsDataLoaded = znsField.get(turnier) as Boolean
|
znsDataLoaded = znsField.get(turnier) as Boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// Reflection fehlgeschlagen oder Store nicht erreichbar -> Fallback auf leere Felder
|
// Reflection fehlgeschlagen oder Store nicht erreichbar -> Fallback auf leere Felder
|
||||||
@@ -118,7 +121,7 @@ fun StammdatenTabContent(
|
|||||||
var showZnsDialog by remember { mutableStateOf(false) }
|
var showZnsDialog by remember { mutableStateOf(false) }
|
||||||
var showZnsLog by remember { mutableStateOf(false) }
|
var showZnsLog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Hilfs-States für DatePicker
|
// Hilf's-States für DatePicker
|
||||||
var showDatePickerVon by remember { mutableStateOf(false) }
|
var showDatePickerVon by remember { mutableStateOf(false) }
|
||||||
var showDatePickerBis by remember { mutableStateOf(false) }
|
var showDatePickerBis by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -143,29 +146,35 @@ fun StammdatenTabContent(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
enabled = !nrConfirmed
|
enabled = !nrConfirmed
|
||||||
)
|
)
|
||||||
if (!nrConfirmed) {
|
when {
|
||||||
Button(
|
!nrConfirmed -> {
|
||||||
onClick = { showNrConfirm = true },
|
Button(
|
||||||
enabled = turnierNr.length == 5,
|
onClick = { showNrConfirm = true },
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)
|
enabled = turnierNr.length == 5,
|
||||||
) {
|
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)
|
||||||
Text("Bestätigen")
|
) {
|
||||||
|
Text("Bestätigen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
InputChip(
|
||||||
|
selected = true,
|
||||||
|
onClick = { },
|
||||||
|
label = { Text("Bestätigt") },
|
||||||
|
trailingIcon = { Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(16.dp)) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
InputChip(
|
|
||||||
selected = true,
|
|
||||||
onClick = { },
|
|
||||||
label = { Text("Bestätigt") },
|
|
||||||
trailingIcon = { Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(16.dp)) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (turnierNr.length == 5 && !nrConfirmed) {
|
when (turnierNr.length) {
|
||||||
Text(
|
5 if !nrConfirmed -> {
|
||||||
"Bitte Turnier-Nummer bestätigen um fortzufahren.",
|
Text(
|
||||||
color = MaterialTheme.colorScheme.error,
|
"Bitte Turnier-Nummer bestätigen um fortzufahren.",
|
||||||
fontSize = 11.sp
|
color = MaterialTheme.colorScheme.error,
|
||||||
)
|
fontSize = 11.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,8 +199,7 @@ fun StammdatenTabContent(
|
|||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { showZnsDialog = true },
|
onClick = { showZnsDialog = true },
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = AccentBlue)
|
colors = ButtonDefaults.buttonColors(containerColor = AccentBlue), enabled = nrConfirmed
|
||||||
, enabled = nrConfirmed
|
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(18.dp))
|
Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
@@ -254,7 +262,7 @@ fun StammdatenTabContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
FormRow("Klasse:") {
|
FormRow("Klasse:") {
|
||||||
val klassenListe = listOf("C-NEU", "C", "B", "A", "L", "LM", "M", "S")
|
val klassenListe = listOf("C-NEU", "C", "B", "A")
|
||||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
klassenListe.forEach { k ->
|
klassenListe.forEach { k ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
@@ -278,32 +286,37 @@ fun StammdatenTabContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggested.isEmpty()) {
|
when {
|
||||||
Text("Bitte Sparte und Klasse wählen", color = Color.Gray, fontSize = 13.sp)
|
suggested.isEmpty() -> {
|
||||||
} else {
|
Text("Bitte Sparte und Klasse wählen", color = Color.Gray, fontSize = 13.sp)
|
||||||
// Gruppiere nach Sparte (CDN/CSN)
|
}
|
||||||
val grouped = suggested.groupBy { if (it.startsWith("CDN")) "Dressur" else "Springen" }
|
|
||||||
grouped.forEach { (gruppe, eintraege) ->
|
else -> {
|
||||||
Text(gruppe, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
|
// Gruppiere nach Sparte (CDN/CSN)
|
||||||
Spacer(Modifier.height(4.dp))
|
val grouped = suggested.groupBy { if (it.startsWith("CDN")) "Dressur" else "Springen" }
|
||||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
grouped.forEach { (gruppe, eintraege) ->
|
||||||
eintraege.sorted().forEach { c ->
|
Text(gruppe, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
|
||||||
InputChip(
|
Spacer(Modifier.height(4.dp))
|
||||||
selected = kat.contains(c),
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
onClick = { if (kat.contains(c)) kat.remove(c) else kat.add(c) },
|
eintraege.sorted().forEach { c ->
|
||||||
enabled = nrConfirmed,
|
InputChip(
|
||||||
label = { Text(c) }
|
selected = kat.contains(c),
|
||||||
)
|
onClick = { if (kat.contains(c)) kat.remove(c) else kat.add(c) },
|
||||||
|
enabled = nrConfirmed,
|
||||||
|
label = { Text(c) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormRow("Zeitraum:") {
|
FormRow("Zeitraum:") {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
val vonMod = if (nrConfirmed) Modifier.width(160.dp).clickable { showDatePickerVon = true } else Modifier.width(160.dp)
|
val vonMod =
|
||||||
|
if (nrConfirmed) Modifier.width(160.dp).clickable { showDatePickerVon = true } else Modifier.width(160.dp)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = von,
|
value = von,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -314,7 +327,8 @@ fun StammdatenTabContent(
|
|||||||
trailingIcon = { Icon(Icons.Default.DateRange, null) }
|
trailingIcon = { Icon(Icons.Default.DateRange, null) }
|
||||||
)
|
)
|
||||||
Text("bis")
|
Text("bis")
|
||||||
val bisMod = if (nrConfirmed) Modifier.width(160.dp).clickable { showDatePickerBis = true } else Modifier.width(160.dp)
|
val bisMod =
|
||||||
|
if (nrConfirmed) Modifier.width(160.dp).clickable { showDatePickerBis = true } else Modifier.width(160.dp)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = bis,
|
value = bis,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -325,7 +339,8 @@ fun StammdatenTabContent(
|
|||||||
trailingIcon = { Icon(Icons.Default.DateRange, null) }
|
trailingIcon = { Icon(Icons.Default.DateRange, null) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val rangeText = if (eventVon != null && eventBis != null) "Muss zwischen $eventVon – $eventBis liegen." else "Muss innerhalb des Veranstaltungs-Zeitraums liegen."
|
val rangeText =
|
||||||
|
if (eventVon != null && eventBis != null) "Muss zwischen $eventVon – $eventBis liegen." else "Muss innerhalb des Veranstaltungs-Zeitraums liegen."
|
||||||
Text(rangeText, fontSize = 11.sp, color = Color.Gray)
|
Text(rangeText, fontSize = 11.sp, color = Color.Gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +350,8 @@ fun StammdatenTabContent(
|
|||||||
// Default-Titel-Vorschlag: [Kategorien] [Verein-Ort] [Bundesland]
|
// Default-Titel-Vorschlag: [Kategorien] [Verein-Ort] [Bundesland]
|
||||||
val defaultTitle = remember(kat.size, veranstalterOrt, veranstalterBundesland) {
|
val defaultTitle = remember(kat.size, veranstalterOrt, veranstalterBundesland) {
|
||||||
val cats = if (kat.isEmpty()) "" else kat.sorted().joinToString(" ")
|
val cats = if (kat.isEmpty()) "" else kat.sorted().joinToString(" ")
|
||||||
listOfNotNull(cats.ifBlank { null },
|
listOfNotNull(
|
||||||
|
cats.ifBlank { null },
|
||||||
listOfNotNull(veranstalterOrt, veranstalterBundesland).filter { it.isNotBlank() }.joinToString(" ")
|
listOfNotNull(veranstalterOrt, veranstalterBundesland).filter { it.isNotBlank() }.joinToString(" ")
|
||||||
.takeIf { it.isNotBlank() }
|
.takeIf { it.isNotBlank() }
|
||||||
).joinToString(" ")
|
).joinToString(" ")
|
||||||
@@ -367,7 +383,12 @@ fun StammdatenTabContent(
|
|||||||
supportingText = {
|
supportingText = {
|
||||||
if (eventOrt != null && ort.isNotBlank() && ort.trim() != eventOrt.trim()) {
|
if (eventOrt != null && ort.isNotBlank() && ort.trim() != eventOrt.trim()) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(Icons.Default.Warning, contentDescription = null, tint = Color(0xFFF59E0B), modifier = Modifier.size(14.dp))
|
Icon(
|
||||||
|
Icons.Default.Warning,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color(0xFFF59E0B),
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text("Abweichung zum Veranstaltungsort ($eventOrt) – bitte prüfen.", color = Color(0xFFF59E0B))
|
Text("Abweichung zum Veranstaltungsort ($eventOrt) – bitte prüfen.", color = Color(0xFFF59E0B))
|
||||||
}
|
}
|
||||||
@@ -409,14 +430,26 @@ fun StammdatenTabContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Footer ──────────────────────────────────────────────────────────
|
// ── Footer ──────────────────────────────────────────────────────────
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
// Save-Enable-Matrix (kleine Checkliste)
|
// Save-Enable-Matrix (kleine Checkliste)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
AssistChip(onClick = {}, label = { Text("Nr bestätigt") }, leadingIcon = {
|
AssistChip(onClick = {}, label = { Text("Nr bestätigt") }, leadingIcon = {
|
||||||
Icon(if (nrConfirmed) Icons.Default.Check else Icons.Default.Close, null, tint = if (nrConfirmed) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error)
|
Icon(
|
||||||
|
if (nrConfirmed) Icons.Default.Check else Icons.Default.Close,
|
||||||
|
null,
|
||||||
|
tint = if (nrConfirmed) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
})
|
})
|
||||||
AssistChip(onClick = {}, label = { Text("ZNS geladen") }, leadingIcon = {
|
AssistChip(onClick = {}, label = { Text("ZNS geladen") }, leadingIcon = {
|
||||||
Icon(if (znsDataLoaded) Icons.Default.Check else Icons.Default.Close, null, tint = if (znsDataLoaded) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error)
|
Icon(
|
||||||
|
if (znsDataLoaded) Icons.Default.Check else Icons.Default.Close,
|
||||||
|
null,
|
||||||
|
tint = if (znsDataLoaded) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
})
|
})
|
||||||
val dateOk = remember(von, bis, eventVon, eventBis) {
|
val dateOk = remember(von, bis, eventVon, eventBis) {
|
||||||
try {
|
try {
|
||||||
@@ -427,10 +460,16 @@ fun StammdatenTabContent(
|
|||||||
val tB = if (bis.isBlank()) tV else LocalDate.parse(bis)
|
val tB = if (bis.isBlank()) tV else LocalDate.parse(bis)
|
||||||
!tV.isBefore(evV) && !tB.isAfter(evB) && !tB.isBefore(tV)
|
!tV.isBefore(evV) && !tB.isAfter(evB) && !tB.isBefore(tV)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { false }
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AssistChip(onClick = {}, label = { Text("Datum gültig") }, leadingIcon = {
|
AssistChip(onClick = {}, label = { Text("Datum gültig") }, leadingIcon = {
|
||||||
Icon(if (dateOk) Icons.Default.Check else Icons.Default.Close, null, tint = if (dateOk) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error)
|
Icon(
|
||||||
|
if (dateOk) Icons.Default.Check else Icons.Default.Close,
|
||||||
|
null,
|
||||||
|
tint = if (dateOk) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +485,9 @@ fun StammdatenTabContent(
|
|||||||
val tB = if (bis.isBlank()) tV else LocalDate.parse(bis)
|
val tB = if (bis.isBlank()) tV else LocalDate.parse(bis)
|
||||||
!tV.isBefore(evV) && !tB.isAfter(evB) && !tB.isBefore(tV)
|
!tV.isBefore(evV) && !tB.isAfter(evB) && !tB.isBefore(tV)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { false }
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
base && dateValid
|
base && dateValid
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
|
||||||
@@ -460,88 +501,96 @@ fun StammdatenTabContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dialog-Simulationen
|
// Dialog-Simulationen
|
||||||
if (showZnsDialog) {
|
when {
|
||||||
AlertDialog(
|
showZnsDialog -> {
|
||||||
onDismissRequest = { showZnsDialog = false },
|
AlertDialog(
|
||||||
title = { Text("ZNS Import") },
|
onDismissRequest = { showZnsDialog = false },
|
||||||
text = { Text("Simuliere ZNS-Stammdaten Import für Turnier #$turnierNr...") },
|
title = { Text("ZNS Import") },
|
||||||
confirmButton = {
|
text = { Text("Simuliere ZNS-Stammdaten Import für Turnier #$turnierNr...") },
|
||||||
TextButton(onClick = {
|
confirmButton = {
|
||||||
znsDataLoaded = true
|
TextButton(onClick = {
|
||||||
znsPayloadVersion = "v2.4"
|
znsDataLoaded = true
|
||||||
znsImportedAt = java.time.Instant.now().toString()
|
znsPayloadVersion = "v2.4"
|
||||||
znsImportHistory.add(Triple("Internet/USB", znsPayloadVersion!!, true))
|
znsImportedAt = java.time.Instant.now().toString()
|
||||||
showZnsDialog = false
|
znsImportHistory.add(Triple("Internet/USB", znsPayloadVersion!!, true))
|
||||||
}) { Text("Importieren") }
|
showZnsDialog = false
|
||||||
},
|
}) { Text("Importieren") }
|
||||||
dismissButton = {
|
},
|
||||||
TextButton(onClick = { showZnsDialog = false }) { Text("Abbrechen") }
|
dismissButton = {
|
||||||
}
|
TextButton(onClick = { showZnsDialog = false }) { Text("Abbrechen") }
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showNrConfirm) {
|
when {
|
||||||
AlertDialog(
|
showNrConfirm -> {
|
||||||
onDismissRequest = { showNrConfirm = false },
|
AlertDialog(
|
||||||
title = { Text("Turnier-Nummer bestätigen?") },
|
onDismissRequest = { showNrConfirm = false },
|
||||||
text = { Text("Die Turnier-Nr. ist nach der Bestätigung nicht mehr änderbar.") },
|
title = { Text("Turnier-Nummer bestätigen?") },
|
||||||
confirmButton = {
|
text = { Text("Die Turnier-Nr. ist nach der Bestätigung nicht mehr änderbar.") },
|
||||||
TextButton(onClick = { nrConfirmed = true; showNrConfirm = false }) { Text("Ja, bestätigen") }
|
confirmButton = {
|
||||||
},
|
TextButton(onClick = { nrConfirmed = true; showNrConfirm = false }) { Text("Ja, bestätigen") }
|
||||||
dismissButton = {
|
},
|
||||||
TextButton(onClick = { showNrConfirm = false }) { Text("Abbrechen") }
|
dismissButton = {
|
||||||
}
|
TextButton(onClick = { showNrConfirm = false }) { Text("Abbrechen") }
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showZnsLog) {
|
when {
|
||||||
AlertDialog(
|
showZnsLog -> {
|
||||||
onDismissRequest = { showZnsLog = false },
|
AlertDialog(
|
||||||
title = { Text("ZNS Import-Log (letzte 5)") },
|
onDismissRequest = { showZnsLog = false },
|
||||||
text = {
|
title = { Text("ZNS Import-Log (letzte 5)") },
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
text = {
|
||||||
if (znsImportHistory.isEmpty()) {
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
Text("Keine Einträge vorhanden.", color = Color.Gray)
|
if (znsImportHistory.isEmpty()) {
|
||||||
} else {
|
Text("Keine Einträge vorhanden.", color = Color.Gray)
|
||||||
znsImportHistory.takeLast(5).asReversed().forEach { (src, ver, ok) ->
|
} else {
|
||||||
val c = if (ok) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
znsImportHistory.takeLast(5).asReversed().forEach { (src, ver, ok) ->
|
||||||
Text("• $src – Version $ver – ${if (ok) "OK" else "Fehler"}", color = c, fontSize = 13.sp)
|
val c = if (ok) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||||
|
Text("• $src – Version $ver – ${if (ok) "OK" else "Fehler"}", color = c, fontSize = 13.sp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
confirmButton = { TextButton(onClick = { showZnsLog = false }) { Text("Schließen") } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
showDatePickerVon -> {
|
||||||
|
val state = rememberDatePickerState()
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showDatePickerVon = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
state.selectedDateMillis?.let {
|
||||||
|
von = LocalDate.ofEpochDay(it / (24 * 60 * 60 * 1000)).toString()
|
||||||
|
}
|
||||||
|
showDatePickerVon = false
|
||||||
|
}) { Text("OK") }
|
||||||
}
|
}
|
||||||
},
|
) { DatePicker(state) }
|
||||||
confirmButton = { TextButton(onClick = { showZnsLog = false }) { Text("Schließen") } }
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDatePickerVon) {
|
showDatePickerBis -> {
|
||||||
val state = rememberDatePickerState()
|
val state = rememberDatePickerState()
|
||||||
DatePickerDialog(
|
DatePickerDialog(
|
||||||
onDismissRequest = { showDatePickerVon = false },
|
onDismissRequest = { showDatePickerBis = false },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
state.selectedDateMillis?.let {
|
state.selectedDateMillis?.let {
|
||||||
von = LocalDate.ofEpochDay(it / (24 * 60 * 60 * 1000)).toString()
|
bis = LocalDate.ofEpochDay(it / (24 * 60 * 60 * 1000)).toString()
|
||||||
}
|
}
|
||||||
showDatePickerVon = false
|
showDatePickerBis = false
|
||||||
}) { Text("OK") }
|
}) { Text("OK") }
|
||||||
}
|
}
|
||||||
) { DatePicker(state) }
|
) { DatePicker(state) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDatePickerBis) {
|
|
||||||
val state = rememberDatePickerState()
|
|
||||||
DatePickerDialog(
|
|
||||||
onDismissRequest = { showDatePickerBis = false },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
state.selectedDateMillis?.let {
|
|
||||||
bis = LocalDate.ofEpochDay(it / (24 * 60 * 60 * 1000)).toString()
|
|
||||||
}
|
|
||||||
showDatePickerBis = false
|
|
||||||
}) { Text("OK") }
|
|
||||||
}
|
|
||||||
) { DatePicker(state) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user