diff --git a/.gitignore b/.gitignore index 52b1a00b3d46d27c2574fef1f927720c8eb81d2f..669357ca4ed9f1cee024bc72717aafda9e1d58b6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ # helm files generated by helm-chartsnap.sh k8s/helmcharts/eshg-ctr/values.deploy-test.central.yaml +k8s/helmcharts/eshg-ctr/charts k8s/helmcharts/eshg-gas/values.deploy-test.frankfurt.yaml +k8s/helmcharts/eshg-gas/charts # IntelliJ *.iws diff --git a/README.adoc b/README.adoc index d109fc6bfb0878efbb038c69b784e4bebb8cb12e..afb756c2b7b9f342d9166b7ff9aae7d98c7b0286 100644 --- a/README.adoc +++ b/README.adoc @@ -29,6 +29,7 @@ We appreciate your help in improving the project! - link:reverse-proxy/README.md[Reverse Proxy] - link:docs/content-security-policy-header.adoc[Content Security Policy (CSP) Header] - link:docs/flaky-tests.adoc[Flaky Tests] +- link:docs/migration.adoc[Migration Guide] == Licensing diff --git a/backend/auditlog/gradle.lockfile b/backend/auditlog/gradle.lockfile index e71a9aabbf739d78284e3debf0d260afc7f8acfa..d427b2fe07ed74f05d033ec219b50b4a46cb3e22 100644 --- a/backend/auditlog/gradle.lockfile +++ b/backend/auditlog/gradle.lockfile @@ -13,6 +13,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -20,6 +21,7 @@ com.github.docker-java:docker-java-transport:3.4.0=testCompileClasspath,testRunt com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.10.0=testRuntimeClasspath com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0=testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -28,12 +30,15 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=prod com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -42,7 +47,10 @@ com.tngtech.archunit:archunit-junit5:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit:1.3.0=testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.17.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.18.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-logging:commons-logging:1.3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:commons-lang:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath @@ -90,26 +98,43 @@ net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCom net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.24.0=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.13.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.12=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.12=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-bom:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-core:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.34=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.aspectj:aspectjweaver:1.9.22.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.assertj:assertj-core:3.26.3=testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcpkix-jdk18on:1.80=testRuntimeClasspath +org.bouncycastle:bcjmail-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.bouncycastle:bcprov-jdk18on:1.80=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.80=testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.checkerframework:checker-qual:3.43.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.eclipse.angus:angus-activation:2.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-core:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -128,6 +153,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/auditlog/src/main/java/de/eshg/auditlog/AuditLogController.java b/backend/auditlog/src/main/java/de/eshg/auditlog/AuditLogController.java index d9c9f3272184eb531e3015fcc7f7c7bf853b3cff..e8fcb36721e1d0d5d556bc6a219fd0df366a0e65 100644 --- a/backend/auditlog/src/main/java/de/eshg/auditlog/AuditLogController.java +++ b/backend/auditlog/src/main/java/de/eshg/auditlog/AuditLogController.java @@ -15,6 +15,8 @@ import de.eshg.auditlog.crypto.AsymmetricEncryption; import de.eshg.auditlog.crypto.AsymmetricEncryption.EncryptedKey; import de.eshg.auditlog.crypto.AuditLogDecryptionException; import de.eshg.auditlog.crypto.AuditLogEncryptionException; +import de.eshg.auditlog.crypto.PublicKeyService; +import de.eshg.auditlog.crypto.PublicKeyService.UserPublicKey; import de.eshg.auditlog.crypto.SymmetricEncryption; import de.eshg.auditlog.crypto.SymmetricEncryption.EncryptedPayload; import de.eshg.auditlog.domain.model.AuditLogAccessibleProjection; @@ -33,6 +35,7 @@ import de.eshg.rest.service.error.AlreadyExistsException; import de.eshg.rest.service.error.BadRequestException; import de.eshg.rest.service.error.ErrorCode; import de.eshg.rest.service.error.ErrorResponse; +import de.eshg.rest.service.error.InternalServerErrorException; import de.eshg.rest.service.error.NotFoundException; import de.eshg.rest.service.security.CurrentUserHelper; import jakarta.servlet.ServletRequest; @@ -108,6 +111,7 @@ public class AuditLogController implements AuditLogApi, AuditLogArchivingApi { private final Clock clock; private final UserApi userApi; private final AuditLogger auditLogger; + private final PublicKeyService publicKeyService; private final AsymmetricEncryption asymmetricEncryption; public AuditLogController( @@ -116,12 +120,14 @@ public class AuditLogController implements AuditLogApi, AuditLogArchivingApi { Clock clock, UserApi userApi, AuditLogger auditLogger, + PublicKeyService publicKeyService, AsymmetricEncryption asymmetricEncryption) { this.auditLogServiceConfig = auditLogServiceConfig; this.grantedAccessRepository = grantedAccessRepository; this.clock = clock; this.userApi = userApi; this.auditLogger = auditLogger; + this.publicKeyService = publicKeyService; this.asymmetricEncryption = asymmetricEncryption; } @@ -638,7 +644,9 @@ public class AuditLogController implements AuditLogApi, AuditLogArchivingApi { EncryptedPayload encryptedPayload = SymmetricEncryption.encrypt(file.getBytes()); log.info("Encrypting symmetric key asymmetrically for each user"); - List<EncryptedKey> encryptedKeys = asymmetricEncryption.encrypt(encryptedPayload.key()); + List<EncryptedKey> encryptedKeys = + asymmetricEncryption.encrypt( + encryptedPayload.key(), requireNotEmpty(publicKeyService.getPublicKeys())); for (EncryptedKey encryptedKey : encryptedKeys) { Path dateAndServiceSpecificDir = @@ -691,6 +699,14 @@ public class AuditLogController implements AuditLogApi, AuditLogArchivingApi { } } + private List<UserPublicKey> requireNotEmpty(List<UserPublicKey> publicKeys) { + if (publicKeys.isEmpty()) { + throw new InternalServerErrorException("No public key(s) found"); + } else { + return publicKeys; + } + } + private Path getUserSpecificDirOrThrow(UUID uuid, AuditLogSource source, LocalDate date) { Path userSpecificDir = getUserSpecificDir(uuid, source, date); if (!Files.isDirectory(userSpecificDir)) { diff --git a/backend/auditlog/src/main/java/de/eshg/auditlog/crypto/AsymmetricEncryption.java b/backend/auditlog/src/main/java/de/eshg/auditlog/crypto/AsymmetricEncryption.java index 78f45a7f547e4661eda2daa53bc87eae44597b81..58b0912c2055086fd480593e3f39830e4ce9fa1c 100644 --- a/backend/auditlog/src/main/java/de/eshg/auditlog/crypto/AsymmetricEncryption.java +++ b/backend/auditlog/src/main/java/de/eshg/auditlog/crypto/AsymmetricEncryption.java @@ -24,20 +24,16 @@ public class AsymmetricEncryption { private static final Logger log = LoggerFactory.getLogger(AsymmetricEncryption.class); - private final PublicKeyService publicKeyProvider; - - public AsymmetricEncryption(PublicKeyService publicKeyProvider) { - this.publicKeyProvider = publicKeyProvider; - + public AsymmetricEncryption() { Security.addProvider(new BouncyCastleProvider()); } - public List<EncryptedKey> encrypt(byte[] symmetricKey) { + public List<EncryptedKey> encrypt(byte[] symmetricKey, List<UserPublicKey> publicKeys) { HPKE hpke = new HPKE(HPKE.mode_base, HPKE.kem_P256_SHA256, HPKE.kdf_HKDF_SHA256, HPKE.aead_AES_GCM256); List<EncryptedKey> encryptedKeys = new ArrayList<>(); - for (UserPublicKey userPublicKey : publicKeyProvider.getPublicKeys()) { + for (UserPublicKey userPublicKey : publicKeys) { log.info("Encrypting symmetric key for user {}", userPublicKey.userId()); encryptedKeys.add(encryptAsymmetricallyForUser(userPublicKey, hpke, symmetricKey)); } diff --git a/backend/base/gradle.lockfile b/backend/base/gradle.lockfile index fef7a70fe113499710d79e4b5bed2563457649a4..8a05027d9ee2ba149f5eaa931160c7368702f543 100644 --- a/backend/base/gradle.lockfile +++ b/backend/base/gradle.lockfile @@ -18,6 +18,7 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClassp com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.woodstox:woodstox-core:7.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -27,6 +28,7 @@ com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0=testRuntime com.github.java-json-tools:json-patch:1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.mangstadt:vinnie:2.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -36,6 +38,8 @@ com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath,productionRuntimeCla com.googlecode.ez-vcard:ez-vcard:0.12.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.ibm.async:asyncutil:0.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -43,6 +47,7 @@ com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntim com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.squareup.okhttp3:mockwebserver:4.12.0=testCompileClasspath,testRuntimeClasspath com.squareup.okhttp3:okhttp:4.12.0=testCompileClasspath,testRuntimeClasspath com.squareup.okio:okio-jvm:3.6.0=testCompileClasspath,testRuntimeClasspath @@ -58,6 +63,7 @@ com.tngtech.archunit:archunit-junit5:1.3.0=testCompileClasspath,testRuntimeClass com.tngtech.archunit:archunit:1.3.0=testCompileClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath commons-beanutils:commons-beanutils:1.10.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-codec:commons-codec:1.17.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-collections:commons-collections:3.2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -120,9 +126,10 @@ net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCom net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.24.0=testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-csv:1.13.0=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.cxf:cxf-core:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.cxf:cxf-rt-frontend-jaxrs:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -143,12 +150,24 @@ org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,test org.apache.pdfbox:pdfbox-io:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-bom:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-core:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.34=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.ws.xmlschema:xmlschema-core:2.3.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-anim:1.17=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-awt-util:1.17=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-bridge:1.17=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -173,9 +192,10 @@ org.aspectj:aspectjweaver:1.9.22.1=compileClasspath,productionRuntimeClasspath,r org.assertj:assertj-core:3.26.3=testCompileClasspath,testRuntimeClasspath org.attoparser:attoparser:2.0.7.RELEASE=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcpkix-jdk18on:1.80=testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.80=testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.80=testRuntimeClasspath +org.bouncycastle:bcjmail-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.checkerframework:checker-qual:3.43.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.codehaus.woodstox:stax2-api:4.2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.eclipse.angus:angus-activation:2.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/base/openApi.json b/backend/base/openApi.json index 3e3626b3e2eb4be1154a7d5ec2b5b7973bfdb60f..273423f3fad5949a4f0c77f3123e6886ef93a74e 100644 --- a/backend/base/openApi.json +++ b/backend/base/openApi.json @@ -4265,7 +4265,7 @@ }, { "description" : "Limit of returned tasks", "in" : "query", - "name" : "limit", + "name" : "pageSize", "required" : false, "schema" : { "maximum" : 200, @@ -4277,10 +4277,10 @@ }, { "description" : "Offset used for pagination", "in" : "query", - "name" : "offset", + "name" : "pageNumber", "required" : false, "schema" : { - "maximum" : 2000, + "maximum" : 10, "minimum" : 0, "type" : "integer", "format" : "int32", @@ -7072,7 +7072,7 @@ }, "ErrorCode" : { "type" : "string", - "enum" : [ "UNEXPECTED_ERROR", "BAD_REQUEST", "UNAUTHORIZED", "INSUFFICIENT_USER_RIGHTS", "NOT_FOUND", "CONFLICT", "CONSTRAINT_VIOLATION", "TIMEOUT", "DATA_INTEGRITY_VIOLATION", "ALREADY_EXISTS", "AGGREGATION_EXCEPTION", "INVALID_FILE", "NONCONFORM_PDF", "CORRUPT", "LOCKED", "XLSX_TOO_MANY_ROWS" ] + "enum" : [ "UNEXPECTED_ERROR", "BAD_REQUEST", "UNAUTHORIZED", "INSUFFICIENT_USER_RIGHTS", "NOT_FOUND", "CONFLICT", "INTERNAL_SERVER_ERROR", "CONSTRAINT_VIOLATION", "TIMEOUT", "DATA_INTEGRITY_VIOLATION", "ALREADY_EXISTS", "AGGREGATION_EXCEPTION", "INVALID_FILE", "NONCONFORM_PDF", "CORRUPT", "LOCKED", "XLSX_TOO_MANY_ROWS" ] }, "ErrorResponseWithLocation" : { "required" : [ "errorCode", "errorLocation" ], diff --git a/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationController.java b/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationController.java index 381c85e44585b7c17534fa5163ac23cf05d0213c..0a3274dfc70a3f896eaee4dd970290133768aa7e 100644 --- a/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationController.java +++ b/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationController.java @@ -86,16 +86,16 @@ public class TaskAggregationController { Sorting order. Possible options "ASC" for ascending and "DESC" for descending """) GetTasksSortOrderDto sortOrder, - @RequestParam(name = "limit", required = false, defaultValue = "50") + @RequestParam(name = "pageSize", required = false, defaultValue = "50") @Min(1) @Max(200) @Parameter(description = "Limit of returned tasks") - Integer limit, - @RequestParam(name = "offset", required = false, defaultValue = "0") + Integer pageSize, + @RequestParam(name = "pageNumber", required = false, defaultValue = "0") @Min(0) - @Max(2000) + @Max(10) @Parameter(description = "Offset used for pagination") - Integer offset) { + Integer pageNumber) { if (!eitherAssigneeIdOrAssignedByIdAreGiven(assigneeId, assignedById)) { throw new BadRequestException("One of 'assigneeId' and 'assignedById' must be given."); } @@ -108,8 +108,8 @@ public class TaskAggregationController { .setTaskStatus(taskStatuses) .setSortBy(sortBy) .setSortOrder(sortOrder) - .setLimit(limit) - .setOffset(offset) + .setPageSize(pageSize) + .setPageNumber(pageNumber) .createTaskAggregationSpecification()); } diff --git a/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationService.java b/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationService.java index 2c1e58a447f8b5c525069a0bfa6167aacd316f0d..52a3a10a659e8f8254d3cf01f38e511b1c5b8f91 100644 --- a/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationService.java +++ b/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationService.java @@ -52,10 +52,11 @@ public class TaskAggregationService { long aggregatedCount = aggregateCount(taskResponses); - if (tas.offset() > aggregatedCount) { + long offset = (long) tas.pageNumber() * tas.pageSize(); + if (offset > aggregatedCount) { throw new BadRequestException( ErrorCode.AGGREGATION_EXCEPTION, - "Could not aggregate tasks, offset is larger than amount of tasks."); + "Could not aggregate tasks, requested page does not exist."); } List<TaskDto> aggregatedTasks = aggregateTasks(taskResponses, tas); @@ -99,23 +100,24 @@ public class TaskAggregationService { private static List<TaskDto> aggregateTasks( List<ClientResponse<TaskResponse>> responses, TaskAggregationSpecification tas) { - return aggregateTasks(responses, tas.sortBy(), tas.sortOrder(), tas.offset(), tas.limit()); + return aggregateTasks( + responses, tas.sortBy(), tas.sortOrder(), tas.pageNumber(), tas.pageSize()); } private static List<TaskDto> aggregateTasks( List<ClientResponse<TaskResponse>> businessModuleResponses, GetTasksSortByDto sortBy, GetTasksSortOrderDto sortOrder, - int offset, - int limit) { + int pageNumber, + int pageSize) { return businessModuleResponses.stream() .map(ClientResponse::response) .filter(Objects::nonNull) .map(TaskResponse::tasks) .flatMap(Collection::stream) .sorted(TaskSortHelper.getComparator(sortBy, sortOrder)) - .skip(offset) - .limit(limit) + .skip((long) pageNumber * pageSize) + .limit(pageSize) .toList(); } @@ -129,6 +131,10 @@ public class TaskAggregationService { private List<ClientResponse<TaskResponse>> requestTasksFromBusinessModules( TaskAggregationSpecification tas) { + // Due to sorting over aggregated tasks, the tasks on page `pageNumber` lie anywhere between the + // 1st and the (`pageSize * pageNumber + pageSize`)-th task in business modules + int limit = tas.pageSize() * (tas.pageNumber() + 1); + return requestTasksFromBusinessModules( tas.businessModules(), client -> @@ -136,7 +142,7 @@ public class TaskAggregationService { new GetTasksFilterOptions( tas.assigneeId(), tas.assignedById(), tas.taskTypes(), tas.taskStatuses()), new GetTasksSortOptions(tas.sortBy(), tas.sortOrder()), - tas.limit() + tas.offset())); + limit)); } private List<ClientResponse<TaskResponse>> requestTasksFromBusinessModulesForDashboard() { diff --git a/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationSpecification.java b/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationSpecification.java index 01f353e87ca9def4573fa6e831607b1b84bf000d..182b9ee24975290b1687812a02b27b4b1a1c236a 100644 --- a/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationSpecification.java +++ b/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationSpecification.java @@ -21,5 +21,5 @@ public record TaskAggregationSpecification( Set<TaskStatusDto> taskStatuses, GetTasksSortByDto sortBy, GetTasksSortOrderDto sortOrder, - Integer limit, - Integer offset) {} + Integer pageSize, + Integer pageNumber) {} diff --git a/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationSpecificationBuilder.java b/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationSpecificationBuilder.java index 42c9ecb0b4339ae94b595350be6095670e2b1345..6633ccada3863ca96460b70e5b5458d882d6ec34 100644 --- a/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationSpecificationBuilder.java +++ b/backend/base/src/main/java/de/eshg/base/aggregation/task/TaskAggregationSpecificationBuilder.java @@ -22,8 +22,8 @@ public class TaskAggregationSpecificationBuilder { private Set<TaskStatusDto> taskStatuses; private GetTasksSortByDto sortBy = GetTasksSortByDto.PRIORITY; private GetTasksSortOrderDto sortOrder = GetTasksSortOrderDto.ASC; - private Integer limit = 50; - private Integer offset = 0; + private Integer pageSize = 50; + private Integer pageNumber = 0; public TaskAggregationSpecificationBuilder setAssigneeId(UUID assigneeId) { this.assigneeId = assigneeId; @@ -61,13 +61,13 @@ public class TaskAggregationSpecificationBuilder { return this; } - public TaskAggregationSpecificationBuilder setLimit(Integer limit) { - this.limit = limit; + public TaskAggregationSpecificationBuilder setPageSize(Integer pageSize) { + this.pageSize = pageSize; return this; } - public TaskAggregationSpecificationBuilder setOffset(Integer offset) { - this.offset = offset; + public TaskAggregationSpecificationBuilder setPageNumber(Integer pageNumber) { + this.pageNumber = pageNumber; return this; } @@ -80,7 +80,7 @@ public class TaskAggregationSpecificationBuilder { taskStatuses, sortBy, sortOrder, - limit, - offset); + pageSize, + pageNumber); } } diff --git a/backend/base/src/main/java/de/eshg/base/config/DepartmentConfiguration.java b/backend/base/src/main/java/de/eshg/base/config/DepartmentConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..bf0794453f561d697374a05fc5ab50e1be36ac9c --- /dev/null +++ b/backend/base/src/main/java/de/eshg/base/config/DepartmentConfiguration.java @@ -0,0 +1,228 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.base.config; + +import de.eshg.domain.model.BaseEntity; +import de.eshg.lib.common.CountryCode; +import de.eshg.lib.common.DataSensitivity; +import de.eshg.lib.common.SensitivityLevel; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +@Entity +@Table(name = DepartmentConfiguration.TABLE_NAME) +public class DepartmentConfiguration extends BaseEntity { + + public static final String TABLE_NAME = "department_configuration"; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private String name; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private String abbreviation; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private String street; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private String houseNumber; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private String postalCode; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private String city; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @JdbcType(PostgreSQLEnumJdbcType.class) + @Column(nullable = false) + private CountryCode country; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private String phoneNumber; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private String homepage; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private String email; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private double latitude; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private double longitude; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private byte[] logo; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private byte[] securityTxt; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private byte[] securityTxtPublicKey; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private byte[] streetDirectory; + + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + private byte[] municipalityDirectory; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAbbreviation() { + return abbreviation; + } + + public void setAbbreviation(String abbreviation) { + this.abbreviation = abbreviation; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getHouseNumber() { + return houseNumber; + } + + public void setHouseNumber(String houseNumber) { + this.houseNumber = houseNumber; + } + + public String getPostalCode() { + return postalCode; + } + + public void setPostalCode(String postalCode) { + this.postalCode = postalCode; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public CountryCode getCountry() { + return country; + } + + public void setCountry(CountryCode country) { + this.country = country; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getHomepage() { + return homepage; + } + + public void setHomepage(String homepage) { + this.homepage = homepage; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Double getLatitude() { + return latitude; + } + + public void setLatitude(Double latitude) { + this.latitude = latitude; + } + + public Double getLongitude() { + return longitude; + } + + public void setLongitude(Double longitude) { + this.longitude = longitude; + } + + public byte[] getLogo() { + return logo; + } + + public void setLogo(byte[] logo) { + this.logo = logo; + } + + public byte[] getSecurityTxt() { + return securityTxt; + } + + public void setSecurityTxt(byte[] securityTxt) { + this.securityTxt = securityTxt; + } + + public byte[] getSecurityTxtPublicKey() { + return securityTxtPublicKey; + } + + public void setSecurityTxtPublicKey(byte[] securityTxtPublicKey) { + this.securityTxtPublicKey = securityTxtPublicKey; + } + + public byte[] getStreetDirectory() { + return streetDirectory; + } + + public void setStreetDirectory(byte[] streetDirectory) { + this.streetDirectory = streetDirectory; + } + + public byte[] getMunicipalityDirectory() { + return municipalityDirectory; + } + + public void setMunicipalityDirectory(byte[] municipalityDirectory) { + this.municipalityDirectory = municipalityDirectory; + } +} diff --git a/backend/base/src/main/java/de/eshg/base/config/DepartmentConfigurationRepository.java b/backend/base/src/main/java/de/eshg/base/config/DepartmentConfigurationRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..1c709d1b2c15c81ca636a9d3122a7c15b7b8dfd2 --- /dev/null +++ b/backend/base/src/main/java/de/eshg/base/config/DepartmentConfigurationRepository.java @@ -0,0 +1,11 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.base.config; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DepartmentConfigurationRepository + extends JpaRepository<DepartmentConfiguration, Long> {} diff --git a/backend/base/src/main/java/de/eshg/base/config/DepartmentConfigurationService.java b/backend/base/src/main/java/de/eshg/base/config/DepartmentConfigurationService.java new file mode 100644 index 0000000000000000000000000000000000000000..4cc14f0e4d3f16a276c10e16e093ac637cd62aa4 --- /dev/null +++ b/backend/base/src/main/java/de/eshg/base/config/DepartmentConfigurationService.java @@ -0,0 +1,76 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.base.config; + +import jakarta.annotation.PostConstruct; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +@Component +public class DepartmentConfigurationService { + + private static final Logger log = LoggerFactory.getLogger(DepartmentConfigurationService.class); + + private final DepartmentConfigurationRepository departmentConfigurationRepository; + private final InitialDepartmentConfigurationDefaults initialDepartmentConfiguration; + + public DepartmentConfigurationService( + DepartmentConfigurationRepository departmentConfigurationRepository, + InitialDepartmentConfigurationDefaults initialDepartmentConfiguration) { + this.departmentConfigurationRepository = departmentConfigurationRepository; + this.initialDepartmentConfiguration = initialDepartmentConfiguration; + } + + @PostConstruct + public void init() throws Exception { + long existingDepartmentConfigurations = departmentConfigurationRepository.count(); + if (existingDepartmentConfigurations == 0) { + log.info("Initializing department configurations in db."); + DepartmentConfiguration departmentConfiguration = new DepartmentConfiguration(); + departmentConfiguration.setName(initialDepartmentConfiguration.name()); + departmentConfiguration.setAbbreviation(initialDepartmentConfiguration.abbreviation()); + departmentConfiguration.setStreet(initialDepartmentConfiguration.street()); + departmentConfiguration.setHouseNumber(initialDepartmentConfiguration.houseNumber()); + departmentConfiguration.setPostalCode(initialDepartmentConfiguration.postalCode()); + departmentConfiguration.setCity(initialDepartmentConfiguration.city()); + departmentConfiguration.setCountry(initialDepartmentConfiguration.country()); + departmentConfiguration.setPhoneNumber(initialDepartmentConfiguration.phoneNumber()); + departmentConfiguration.setHomepage(initialDepartmentConfiguration.homepage()); + departmentConfiguration.setEmail(initialDepartmentConfiguration.email()); + departmentConfiguration.setLatitude(initialDepartmentConfiguration.latitude()); + departmentConfiguration.setLongitude(initialDepartmentConfiguration.longitude()); + departmentConfiguration.setLogo( + initialDepartmentConfiguration.logo().getContentAsByteArray()); + departmentConfiguration.setSecurityTxt( + initialDepartmentConfiguration.securityTxt().getContentAsByteArray()); + departmentConfiguration.setSecurityTxtPublicKey( + initialDepartmentConfiguration.securityTxtPublicKey().getContentAsByteArray()); + departmentConfiguration.setStreetDirectory( + initialDepartmentConfiguration.streetDirectory().getContentAsByteArray()); + departmentConfiguration.setMunicipalityDirectory( + initialDepartmentConfiguration.municipalityDirectory().getContentAsByteArray()); + + departmentConfigurationRepository.save(departmentConfiguration); + } else { + Assert.isTrue( + existingDepartmentConfigurations == 1, + "Found more than one department configuration entries in the database."); + } + } + + public DepartmentConfiguration getDepartmentConfiguration() { + List<DepartmentConfiguration> departmentConfigurations = + departmentConfigurationRepository.findAll(); + Assert.isTrue( + departmentConfigurations.size() == 1, + "Found more than one department configuration entries in the database."); + + return departmentConfigurations.getFirst(); + } +} diff --git a/backend/base/src/main/java/de/eshg/base/department/DepartmentConfiguration.java b/backend/base/src/main/java/de/eshg/base/config/InitialDepartmentConfigurationDefaults.java similarity index 95% rename from backend/base/src/main/java/de/eshg/base/department/DepartmentConfiguration.java rename to backend/base/src/main/java/de/eshg/base/config/InitialDepartmentConfigurationDefaults.java index 0489e88eb260e61bddc48b1e6b2365d6dcca0444..dafb304e63cd9aacef9178db0fb5103ac28008e4 100644 --- a/backend/base/src/main/java/de/eshg/base/department/DepartmentConfiguration.java +++ b/backend/base/src/main/java/de/eshg/base/config/InitialDepartmentConfigurationDefaults.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.base.department; +package de.eshg.base.config; import de.eshg.lib.common.CountryCode; import jakarta.validation.constraints.NotBlank; @@ -15,7 +15,7 @@ import org.springframework.validation.annotation.Validated; @Validated @ConfigurationProperties(prefix = "eshg.department") -public record DepartmentConfiguration( +record InitialDepartmentConfigurationDefaults( @NotBlank String name, @NotBlank String abbreviation, @NotBlank String street, @@ -34,7 +34,7 @@ public record DepartmentConfiguration( @NotNull Resource streetDirectory, @NotNull Resource municipalityDirectory) { - public DepartmentConfiguration( + InitialDepartmentConfigurationDefaults( @NotBlank String name, @NotBlank String abbreviation, @NotBlank String street, diff --git a/backend/base/src/main/java/de/eshg/base/department/DepartmentController.java b/backend/base/src/main/java/de/eshg/base/department/DepartmentController.java index 34ec53950341af7dbfb4a3071389b8e7eb012e54..dd66b9cd2805ddd58ec1bf74ae66d00288976beb 100644 --- a/backend/base/src/main/java/de/eshg/base/department/DepartmentController.java +++ b/backend/base/src/main/java/de/eshg/base/department/DepartmentController.java @@ -5,9 +5,11 @@ package de.eshg.base.department; +import de.eshg.base.config.DepartmentConfiguration; +import de.eshg.base.config.DepartmentConfigurationService; import de.eshg.file.common.CustomMediaTypes; import io.swagger.v3.oas.annotations.tags.Tag; -import java.io.IOException; +import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -16,15 +18,15 @@ import org.springframework.web.bind.annotation.RestController; @RestController @Tag(name = "Department") public class DepartmentController implements DepartmentApi { - private final DepartmentConfiguration departmentConfiguration; + private final DepartmentConfigurationService departmentConfigurationService; - public DepartmentController(DepartmentConfiguration departmentConfiguration) { - this.departmentConfiguration = departmentConfiguration; + public DepartmentController(DepartmentConfigurationService departmentConfiguration) { + this.departmentConfigurationService = departmentConfiguration; } @Override public GetDepartmentInfoResponse getDepartmentInfo() { - return mapToResponse(departmentConfiguration); + return mapToResponse(departmentConfigurationService.getDepartmentConfiguration()); } @Override @@ -32,45 +34,42 @@ public class DepartmentController implements DepartmentApi { // svg may contain JavaScript. Make sure the image comes from a trustworthy source. return ResponseEntity.ok() .contentType(CustomMediaTypes.IMAGE_SVG_XML) - .body(departmentConfiguration.logo()); + .body( + new ByteArrayResource( + departmentConfigurationService.getDepartmentConfiguration().getLogo())); } @Override public ResponseEntity<byte[]> getSecurityTxt() { - try { - byte[] securityTxt = departmentConfiguration.securityTxt().getContentAsByteArray(); - return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(securityTxt); - } catch (IOException e) { - throw new RuntimeException("Could not read security txt file.", e); - } + byte[] securityTxt = + departmentConfigurationService.getDepartmentConfiguration().getSecurityTxt(); + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(securityTxt); } @Override public ResponseEntity<byte[]> getSecurityTxtPublicKey() { - try { - byte[] securityTxt = departmentConfiguration.securityTxtPublicKey().getContentAsByteArray(); - return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(securityTxt); - } catch (IOException e) { - throw new RuntimeException("Could not read security txt public key file.", e); - } + byte[] securityTxt = + departmentConfigurationService.getDepartmentConfiguration().getSecurityTxtPublicKey(); + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(securityTxt); } - private GetDepartmentInfoResponse mapToResponse(DepartmentConfiguration departmentConfig) { + private GetDepartmentInfoResponse mapToResponse(DepartmentConfiguration departmentConfiguration) { return new GetDepartmentInfoResponse( - departmentConfig.name(), - departmentConfig.abbreviation(), - departmentConfig.street(), - departmentConfig.houseNumber(), - departmentConfig.postalCode(), - departmentConfig.city(), - departmentConfig.country(), - departmentConfig.phoneNumber(), - departmentConfig.homepage(), - departmentConfig.email(), - mapLocationToApi(departmentConfig)); + departmentConfiguration.getName(), + departmentConfiguration.getAbbreviation(), + departmentConfiguration.getStreet(), + departmentConfiguration.getHouseNumber(), + departmentConfiguration.getPostalCode(), + departmentConfiguration.getCity(), + departmentConfiguration.getCountry(), + departmentConfiguration.getPhoneNumber(), + departmentConfiguration.getHomepage(), + departmentConfiguration.getEmail(), + mapLocationToApi(departmentConfiguration)); } - private static LocationDto mapLocationToApi(DepartmentConfiguration departmentConfig) { - return new LocationDto(departmentConfig.latitude(), departmentConfig.longitude()); + private static LocationDto mapLocationToApi(DepartmentConfiguration departmentConfiguration) { + return new LocationDto( + departmentConfiguration.getLatitude(), departmentConfiguration.getLongitude()); } } diff --git a/backend/base/src/main/java/de/eshg/base/mail/MailController.java b/backend/base/src/main/java/de/eshg/base/mail/MailController.java index c93654db60d89dad368d7949950afb373751c24e..56b996c77a0f4b57ebe688cdafedb2e60eeffeac 100644 --- a/backend/base/src/main/java/de/eshg/base/mail/MailController.java +++ b/backend/base/src/main/java/de/eshg/base/mail/MailController.java @@ -5,9 +5,8 @@ package de.eshg.base.mail; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; -import de.eshg.base.department.DepartmentConfiguration; +import de.eshg.base.config.DepartmentConfiguration; +import de.eshg.base.config.DepartmentConfigurationService; import de.eshg.base.user.UserService; import de.eshg.lib.auditlog.AuditLogger; import de.eshg.rest.service.error.BadRequestException; @@ -15,6 +14,7 @@ import de.eshg.rest.service.security.CurrentUserHelper; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Base64; @@ -27,7 +27,6 @@ import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.image.PNGTranscoder; import org.keycloak.representations.idm.UserRepresentation; import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.web.bind.annotation.RestController; @@ -40,29 +39,27 @@ public class MailController implements MailApi { private final AuditLogger auditLogger; private final UserService userService; - private final DepartmentConfiguration departmentConfiguration; + private final DepartmentConfigurationService departmentConfigurationService; private final JavaMailSender mailSender; private final TemplateEngine templateEngine; private final String defaultFrom; private final String citizenPortalUrl; - private final Supplier<String> logoBase64PngSupplier; public MailController( AuditLogger auditLogger, UserService userService, - DepartmentConfiguration departmentConfiguration, + DepartmentConfigurationService departmentConfigurationService, JavaMailSender mailSender, TemplateEngine templateEngine, @Value("${eshg.mail.noreply}") String defaultFrom, @Value("${eshg.citizen-portal.reverse-proxy.url}") String citizenPortalUrl) { this.auditLogger = auditLogger; this.userService = userService; - this.departmentConfiguration = departmentConfiguration; + this.departmentConfigurationService = departmentConfigurationService; this.mailSender = mailSender; this.templateEngine = templateEngine; this.defaultFrom = defaultFrom; this.citizenPortalUrl = citizenPortalUrl; - logoBase64PngSupplier = Suppliers.memoize(() -> svgToBase64Png(departmentConfiguration.logo())); } @Override @@ -92,15 +89,18 @@ public class MailController implements MailApi { .getUserById(request.userId()) .orElseThrow(() -> new BadRequestException("User does not exist.")); + DepartmentConfiguration departmentConfiguration = + departmentConfigurationService.getDepartmentConfiguration(); + Context context = new Context(); context.setVariable("notificationMessage", request.notificationMessage()); context.setVariable("firstName", addressee.getFirstName()); context.setVariable("lastName", addressee.getLastName()); - context.setVariable("departmentName", departmentConfiguration.name()); - context.setVariable("departmentStreet", departmentConfiguration.street()); - context.setVariable("departmentHouseNumber", departmentConfiguration.houseNumber()); - context.setVariable("departmentCity", departmentConfiguration.city()); - context.setVariable("departmentPostalCode", departmentConfiguration.postalCode()); + context.setVariable("departmentName", departmentConfiguration.getName()); + context.setVariable("departmentStreet", departmentConfiguration.getStreet()); + context.setVariable("departmentHouseNumber", departmentConfiguration.getHouseNumber()); + context.setVariable("departmentCity", departmentConfiguration.getCity()); + context.setVariable("departmentPostalCode", departmentConfiguration.getPostalCode()); String process = templateEngine.process("user-notification-mail", context); @@ -110,7 +110,8 @@ public class MailController implements MailApi { helper.setFrom(defaultFrom); helper.setTo(addressee.getEmail()); helper.setSubject( - "(GA-Lotse %s) Neue Benachrichtigung".formatted(departmentConfiguration.abbreviation())); + "(GA-Lotse %s) Neue Benachrichtigung" + .formatted(departmentConfiguration.getAbbreviation())); helper.setText(process, true); mailSender.send(message); writeAuditLog( @@ -121,12 +122,14 @@ public class MailController implements MailApi { } String applyHtmlTemplate(String subject, String content) { + DepartmentConfiguration departmentConfiguration = + departmentConfigurationService.getDepartmentConfiguration(); Context context = new Context(); context.setVariable("title", subject); context.setVariable("content", content); - context.setVariable("departmentName", departmentConfiguration.name()); - context.setVariable("departmentCity", departmentConfiguration.city()); - context.setVariable("logoBase64Png", logoBase64PngSupplier.get()); + context.setVariable("departmentName", departmentConfiguration.getName()); + context.setVariable("departmentCity", departmentConfiguration.getCity()); + context.setVariable("logoBase64Png", svgToBase64Png(departmentConfiguration.getLogo())); context.setVariable("citizenPortalUrl", citizenPortalUrl); context.setVariable("year", Calendar.getInstance().get(Calendar.YEAR)); @@ -141,9 +144,10 @@ public class MailController implements MailApi { auditLogger.log("Mail", "Versand", attributes); } - public static String svgToBase64Png(Resource svg) { - try (ByteArrayOutputStream pngStream = new ByteArrayOutputStream()) { - TranscoderInput transcoderInput = new TranscoderInput(svg.getInputStream()); + private static String svgToBase64Png(byte[] svg) { + try (ByteArrayInputStream svgInputStream = new ByteArrayInputStream(svg); + ByteArrayOutputStream pngStream = new ByteArrayOutputStream()) { + TranscoderInput transcoderInput = new TranscoderInput(svgInputStream); TranscoderOutput transcoderOutput = new TranscoderOutput(pngStream); PNGTranscoder pngTranscoder = new PNGTranscoder(); pngTranscoder.transcode(transcoderInput, transcoderOutput); diff --git a/backend/base/src/main/java/de/eshg/base/pdf/gdpr/GdprRightToObjectLetterGenerator.java b/backend/base/src/main/java/de/eshg/base/pdf/gdpr/GdprRightToObjectLetterGenerator.java index b490dd7da3f96ec64de6901e4595291eb813bc5e..f4e7a28ef4a48ed3f9848fc1f5e12c06a6641ea6 100644 --- a/backend/base/src/main/java/de/eshg/base/pdf/gdpr/GdprRightToObjectLetterGenerator.java +++ b/backend/base/src/main/java/de/eshg/base/pdf/gdpr/GdprRightToObjectLetterGenerator.java @@ -12,7 +12,7 @@ import de.eshg.base.centralfile.persistence.entity.Facility; import de.eshg.base.centralfile.persistence.entity.Person; import de.eshg.base.centralfile.persistence.repository.FacilityRepository; import de.eshg.base.centralfile.persistence.repository.PersonRepository; -import de.eshg.base.department.DepartmentConfiguration; +import de.eshg.base.config.DepartmentConfigurationService; import de.eshg.base.department.DepartmentController; import de.eshg.base.gdpr.persistence.CentralFileIdWrapper; import de.eshg.base.gdpr.persistence.GdprFacility; @@ -26,8 +26,6 @@ import de.eshg.file.common.CustomMediaTypes; import de.eshg.lib.document.generator.DocumentGenerator; import de.eshg.lib.document.generator.department.DepartmentLogo; import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UncheckedIOException; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -46,7 +44,7 @@ public class GdprRightToObjectLetterGenerator { private final ClassPathResource templateFile; private final DocumentGenerator documentGenerator; private final DepartmentController departmentController; - private final DepartmentConfiguration departmentConfiguration; + private final DepartmentConfigurationService departmentConfigurationService; private final FacilityRepository facilityRepository; private final PersonRepository personRepository; @@ -54,13 +52,13 @@ public class GdprRightToObjectLetterGenerator { @Value(TEMPLATE_PATH) ClassPathResource templateFile, DocumentGenerator documentGenerator, DepartmentController departmentController, - DepartmentConfiguration departmentConfiguration, + DepartmentConfigurationService departmentConfigurationService, FacilityRepository facilityRepository, PersonRepository personRepository) { this.templateFile = templateFile; this.documentGenerator = documentGenerator; this.departmentController = departmentController; - this.departmentConfiguration = departmentConfiguration; + this.departmentConfigurationService = departmentConfigurationService; this.facilityRepository = facilityRepository; this.personRepository = personRepository; } @@ -130,14 +128,10 @@ public class GdprRightToObjectLetterGenerator { } private DepartmentLogo getDepartmentLogo() { - try { - return new DepartmentLogo( - CustomMediaTypes.IMAGE_SVG_XML, - Base64.getEncoder() - .encodeToString(departmentConfiguration.logo().getContentAsByteArray())); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + return new DepartmentLogo( + CustomMediaTypes.IMAGE_SVG_XML, + Base64.getEncoder() + .encodeToString(departmentConfigurationService.getDepartmentConfiguration().getLogo())); } public byte[] generatePdf(GdprProcedure procedure) { diff --git a/backend/base/src/main/java/de/eshg/base/street/MunicipalityDirectory.java b/backend/base/src/main/java/de/eshg/base/street/MunicipalityDirectory.java index 86800a899d5137a62fc8a6994ea51590a6a56083..78d59436928eebdef2cf442a2302a3c9a416ddce 100644 --- a/backend/base/src/main/java/de/eshg/base/street/MunicipalityDirectory.java +++ b/backend/base/src/main/java/de/eshg/base/street/MunicipalityDirectory.java @@ -5,7 +5,7 @@ package de.eshg.base.street; -import de.eshg.base.department.DepartmentConfiguration; +import de.eshg.base.config.DepartmentConfigurationService; import de.eshg.base.street.csv.CsvMapper; import de.eshg.base.street.csv.MunicipalityDirectoryCsvEntry; import java.util.List; @@ -28,10 +28,11 @@ public class MunicipalityDirectory { private final List<DirectoryEntry> entries; - public MunicipalityDirectory(DepartmentConfiguration departmentConfiguration) { + public MunicipalityDirectory(DepartmentConfigurationService departmentConfigurationService) { List<MunicipalityDirectoryCsvEntry> csvEntries = CsvMapper.csvToBeans( - departmentConfiguration.municipalityDirectory(), MunicipalityDirectoryCsvEntry.class); + departmentConfigurationService.getDepartmentConfiguration().getMunicipalityDirectory(), + MunicipalityDirectoryCsvEntry.class); this.entries = convertToDirectoryStructure(csvEntries); } diff --git a/backend/base/src/main/java/de/eshg/base/street/StreetDirectoryService.java b/backend/base/src/main/java/de/eshg/base/street/StreetDirectoryService.java index c62c454e205e88a540e1af8b825da7732878f139..5b398e54cdb9c841bbc67ad88e68023be70f9c71 100644 --- a/backend/base/src/main/java/de/eshg/base/street/StreetDirectoryService.java +++ b/backend/base/src/main/java/de/eshg/base/street/StreetDirectoryService.java @@ -6,7 +6,7 @@ package de.eshg.base.street; import de.cronn.commons.lang.StreamUtil; -import de.eshg.base.department.DepartmentConfiguration; +import de.eshg.base.config.DepartmentConfigurationService; import de.eshg.base.street.csv.CsvMapper; import de.eshg.base.street.csv.StreetDirectoryCsvEntry; import java.util.*; @@ -32,8 +32,11 @@ public class StreetDirectoryService implements StreetDirectory { private final PatriciaTrie<StreetDirectoryEntry> directory; @Autowired - public StreetDirectoryService(DepartmentConfiguration configuration) { - this(CsvMapper.csvToBeans(configuration.streetDirectory(), StreetDirectoryCsvEntry.class)); + public StreetDirectoryService(DepartmentConfigurationService departmentConfigurationService) { + this( + CsvMapper.csvToBeans( + departmentConfigurationService.getDepartmentConfiguration().getStreetDirectory(), + StreetDirectoryCsvEntry.class)); } public StreetDirectoryService(List<StreetDirectoryCsvEntry> csvEntries) { diff --git a/backend/base/src/main/java/de/eshg/base/street/csv/CsvMapper.java b/backend/base/src/main/java/de/eshg/base/street/csv/CsvMapper.java index 3db7fa107211fd5250b0db2959813a77c216390d..dac02c87cda82aada572676c2f835cdb2796b44f 100644 --- a/backend/base/src/main/java/de/eshg/base/street/csv/CsvMapper.java +++ b/backend/base/src/main/java/de/eshg/base/street/csv/CsvMapper.java @@ -8,28 +8,30 @@ package de.eshg.base.street.csv; import com.opencsv.bean.CsvToBean; import com.opencsv.bean.CsvToBeanBuilder; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; import java.io.Reader; import java.io.UncheckedIOException; import java.nio.file.Files; import java.util.List; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.Resource; -public class CsvMapper { +public final class CsvMapper { - public static <T> List<T> csvToBeans(Resource resource, Class<T> clazz) { - try (BufferedReader reader = Files.newBufferedReader(resource.getFile().toPath())) { + private CsvMapper() {} + + public static <T> List<T> csvToBeans(byte[] resource, Class<T> clazz) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(new ByteArrayInputStream(resource)))) { return csvToBeans(reader, clazz); } catch (IOException e) { - throw new UncheckedIOException( - "Could not parse CSV file '%s".formatted(resource.getFilename()), e); + throw new UncheckedIOException(e); } } - public static <T> List<T> csvToBeans(File file, Class<T> clazz) { - return csvToBeans(new FileSystemResource(file), clazz); + public static <T> List<T> csvToBeans(File file, Class<T> clazz) throws IOException { + return csvToBeans(Files.readAllBytes(file.toPath()), clazz); } public static <T> List<T> csvToBeans(Reader reader, Class<T> clazz) { diff --git a/backend/base/src/main/java/de/eshg/base/testhelper/BaseDatabaseResetAction.java b/backend/base/src/main/java/de/eshg/base/testhelper/BaseDatabaseResetAction.java index e5dd45f143e721cc6394112cbcc7a5d4afc6cefc..66184a0d968c0c3ac462dc0dcaf980c12e4b0bb3 100644 --- a/backend/base/src/main/java/de/eshg/base/testhelper/BaseDatabaseResetAction.java +++ b/backend/base/src/main/java/de/eshg/base/testhelper/BaseDatabaseResetAction.java @@ -5,6 +5,7 @@ package de.eshg.base.testhelper; +import de.eshg.base.config.DepartmentConfiguration; import de.eshg.base.icd10.persistence.entity.Icd10Code; import de.eshg.base.icd10.persistence.entity.Icd10Group; import de.eshg.testhelper.ConditionalOnTestHelperEnabled; @@ -24,6 +25,8 @@ public class BaseDatabaseResetAction extends DatabaseResetAction { @Override protected String[] getTablesToExclude() { - return new String[] {Icd10Code.TABLE_NAME, Icd10Group.TABLE_NAME}; + return new String[] { + DepartmentConfiguration.TABLE_NAME, Icd10Code.TABLE_NAME, Icd10Group.TABLE_NAME + }; } } diff --git a/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperResetAction.java b/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperResetAction.java index c2bf34fa98b4ecf546f04db56d4f4242f0ee5f4a..c6b95392a4adfddd11e8eb95c7b52859cf4c1a18 100644 --- a/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperResetAction.java +++ b/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperResetAction.java @@ -5,6 +5,7 @@ package de.eshg.base.testhelper; +import de.eshg.base.config.DepartmentConfigurationService; import de.eshg.base.user.UserControllerRateLimiter; import de.eshg.testhelper.ConditionalOnTestHelperEnabled; import de.eshg.testhelper.TestHelperServiceResetAction; @@ -17,17 +18,21 @@ import org.springframework.stereotype.Component; public class BaseTestHelperResetAction implements TestHelperServiceResetAction { private final UserControllerRateLimiter userControllerRateLimiter; private final Icd10CodeTestHelper icd10CodeTestHelper; + private final DepartmentConfigurationService departmentConfigurationService; public BaseTestHelperResetAction( UserControllerRateLimiter userControllerRateLimiter, - Icd10CodeTestHelper icd10CodeTestHelper) { + Icd10CodeTestHelper icd10CodeTestHelper, + DepartmentConfigurationService departmentConfigurationService) { this.userControllerRateLimiter = userControllerRateLimiter; this.icd10CodeTestHelper = icd10CodeTestHelper; + this.departmentConfigurationService = departmentConfigurationService; } @Override - public void reset() { - this.userControllerRateLimiter.reset(); - this.icd10CodeTestHelper.repopulateIcd10CodesIfNecessary(); + public void reset() throws Exception { + userControllerRateLimiter.reset(); + icd10CodeTestHelper.repopulateIcd10CodesIfNecessary(); + departmentConfigurationService.init(); } } diff --git a/backend/base/src/main/resources/migrations/0042_department_config.xml b/backend/base/src/main/resources/migrations/0042_department_config.xml new file mode 100644 index 0000000000000000000000000000000000000000..1b79e06f8e930a0a25483a15df7f4ba21a5ce6eb --- /dev/null +++ b/backend/base/src/main/resources/migrations/0042_department_config.xml @@ -0,0 +1,72 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2025 cronn GmbH + SPDX-License-Identifier: Apache-2.0 +--> + +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + <changeSet author="GA-Lotse" id="1739879705437-1"> + <createTable tableName="department_configuration"> + <column autoIncrement="true" name="id" type="BIGINT"> + <constraints nullable="false" primaryKey="true" + primaryKeyName="pk_department_configuration"/> + </column> + <column name="version" type="BIGINT"> + <constraints nullable="false"/> + </column> + <column name="abbreviation" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="city" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="country" type="COUNTRYCODE"> + <constraints nullable="false"/> + </column> + <column name="email" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="homepage" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="house_number" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="latitude" type="FLOAT8"> + <constraints nullable="false"/> + </column> + <column name="logo" type="BYTEA"> + <constraints nullable="false"/> + </column> + <column name="longitude" type="FLOAT8"> + <constraints nullable="false"/> + </column> + <column name="municipality_directory" type="BYTEA"> + <constraints nullable="false"/> + </column> + <column name="name" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="phone_number" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="postal_code" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="security_txt" type="BYTEA"> + <constraints nullable="false"/> + </column> + <column name="security_txt_public_key" type="BYTEA"> + <constraints nullable="false"/> + </column> + <column name="street" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="street_directory" type="BYTEA"> + <constraints nullable="false"/> + </column> + </createTable> + </changeSet> +</databaseChangeLog> \ No newline at end of file diff --git a/backend/base/src/main/resources/migrations/changelog.xml b/backend/base/src/main/resources/migrations/changelog.xml index 7a468ec09feefff1c3be36ee6a54425e7ec6010c..6103e3204263cc3f6789659bbd0dc659c337e4c2 100644 --- a/backend/base/src/main/resources/migrations/changelog.xml +++ b/backend/base/src/main/resources/migrations/changelog.xml @@ -49,5 +49,6 @@ <include file="migrations/0039-add-countrycodes.xml"/> <include file="migrations/0040_add_auditlog_entry.xml"/> <include file="migrations/0041_remove_gdpr_status_open.xml"/> + <include file="migrations/0042_department_config.xml"/> </databaseChangeLog> diff --git a/backend/buildSrc/src/main/groovy/eshg.service.gradle b/backend/buildSrc/src/main/groovy/eshg.service.gradle index 161bc76699512cfd41e8757806fd1e3748182244..01a7596eddc0bf1240c60aae5c95848c5307c2db 100644 --- a/backend/buildSrc/src/main/groovy/eshg.service.gradle +++ b/backend/buildSrc/src/main/groovy/eshg.service.gradle @@ -96,8 +96,8 @@ tasks.register('createDockerfile', Dockerfile) { def groupName = 'eshg' def userName = 'eshg' - runCommand("addgroup --system ${groupName}") - runCommand("adduser --shell /usr/sbin/nologin --system --home /app --ingroup ${groupName} ${userName}") + runCommand("addgroup --system --gid 1001 ${groupName}") + runCommand("adduser --shell /usr/sbin/nologin --system --home /app --ingroup ${groupName} --uid 1001 ${userName}") if (additionalAptPackages != null) { runCommand("DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ${additionalAptPackages} && rm -rf /var/lib/apt/lists/*") } diff --git a/backend/business-module-commons/src/main/java/de/eshg/rest/service/error/GlobalExceptionHandler.java b/backend/business-module-commons/src/main/java/de/eshg/rest/service/error/GlobalExceptionHandler.java index b7892dd7b3a6cad4899f43bff1aea8d77c77eeb9..8f0666f1fb7e8cda317d07d0c1f1d6cb263c4ee2 100644 --- a/backend/business-module-commons/src/main/java/de/eshg/rest/service/error/GlobalExceptionHandler.java +++ b/backend/business-module-commons/src/main/java/de/eshg/rest/service/error/GlobalExceptionHandler.java @@ -209,6 +209,12 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { return logAndMapToErrorResponse(ex); } + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleInternalServerErrorException(InternalServerErrorException ex) { + return logAndMapToErrorResponse(ex); + } + private static ErrorResponse logAndMapToErrorResponse(EshgBusinessException businessException) { ErrorResponse errorResponse = new ErrorResponse( diff --git a/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/NormalizeSequenceIdCustomizer.java b/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/NormalizeSequenceIdCustomizer.java new file mode 100644 index 0000000000000000000000000000000000000000..0f3660e1451f112566c2614570cc0fce282cab26 --- /dev/null +++ b/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/NormalizeSequenceIdCustomizer.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.domain.model.serialization; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.eshg.domain.model.GenericEntity; + +public class NormalizeSequenceIdCustomizer implements ObjectMapperCustomizer { + + @Override + public void customize(ObjectMapper objectMapper) { + objectMapper.addMixIn(GenericEntity.class, NormalizedSequenceIdGenericEntityMixin.class); + } + + @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "id") + private interface NormalizedSequenceIdGenericEntityMixin { + @JsonIgnore + Number getId(); + } +} diff --git a/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/ObjectMapperCustomizer.java b/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/ObjectMapperCustomizer.java new file mode 100644 index 0000000000000000000000000000000000000000..728bda7f6bda9357ea8fdf21fdf6004be5cf5fe6 --- /dev/null +++ b/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/ObjectMapperCustomizer.java @@ -0,0 +1,22 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.domain.model.serialization; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@FunctionalInterface +public interface ObjectMapperCustomizer { + + void customize(ObjectMapper objectMapper); + + static ObjectMapperCustomizer combine(ObjectMapperCustomizer... objectMapperCustomizers) { + return objectMapper -> { + for (ObjectMapperCustomizer customizer : objectMapperCustomizers) { + customizer.customize(objectMapper); + } + }; + } +} diff --git a/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/SerializationObjectMapperConfigurer.java b/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/SerializationObjectMapperConfigurer.java deleted file mode 100644 index 8f9667807ebfbf046ddabc5551cf23e564ed368d..0000000000000000000000000000000000000000 --- a/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/SerializationObjectMapperConfigurer.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2025 cronn GmbH - * SPDX-License-Identifier: Apache-2.0 - */ - -package de.eshg.domain.model.serialization; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.function.BiConsumer; -import java.util.function.UnaryOperator; - -public interface SerializationObjectMapperConfigurer { - void configure( - ObjectMapper objectMapper, - BiConsumer<String, byte[]> fileContentConsumer, - UnaryOperator<String> collisionFreeFileNameCreation); -} diff --git a/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/SerializationService.java b/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/SerializationService.java index 15eba83beb0530ab8b43592ab9bd605aa0630373..d874f59eff630ecd661d57d2bba7f6acb4c3c6db 100644 --- a/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/SerializationService.java +++ b/backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/SerializationService.java @@ -11,7 +11,6 @@ import com.fasterxml.jackson.annotation.ObjectIdGenerators; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.ext.SqlBlobSerializer; @@ -25,7 +24,6 @@ import de.eshg.domain.model.GenericEntity; import java.io.UncheckedIOException; import java.sql.Blob; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.commons.lang3.StringUtils; @@ -36,11 +34,8 @@ import org.springframework.stereotype.Component; public class SerializationService { private final ObjectMapper jsonObjectMapper; - private final Optional<SerializationObjectMapperConfigurer> serializationObjectMapperConfigurer; - public SerializationService( - ObjectMapper objectMapper, - Optional<SerializationObjectMapperConfigurer> serializationObjectMapperConfigurer) { + public SerializationService(ObjectMapper objectMapper) { jsonObjectMapper = objectMapper .copy() @@ -50,7 +45,6 @@ public class SerializationService { .setVisibility(PropertyAccessor.ALL, Visibility.NONE) .setVisibility(PropertyAccessor.FIELD, Visibility.ANY) .addMixIn(GenericEntity.class, GenericEntityMixin.class); - this.serializationObjectMapperConfigurer = serializationObjectMapperConfigurer; } public String toJson(GenericEntity<?> entity) { @@ -73,21 +67,17 @@ public class SerializationService { } public byte[] toZip(String dataFileBaseName, EntityWithExternalId entity) { - return toZip(dataFileBaseName, entity, (n, z) -> {}); + return toZip(dataFileBaseName, entity, (n, z) -> {}, o -> {}); } - public byte[] toZip(String dataFileBaseName, EntityWithExternalId entity, ZipEditor zipEditor) { + public byte[] toZip( + String dataFileBaseName, + EntityWithExternalId entity, + ZipEditor zipEditor, + ObjectMapperCustomizer objectMapperCustomizer) { ZipFileWrapper zipFileWrapper = new ZipFileWrapper(); - FileContentSerializer fileContentSerializer = - new FileContentSerializer( - zipFileWrapper::addEntry, zipFileWrapper::getCollisionFreeFileName); - - ObjectMapper objectMapper = createObjectMapperWithSerializer(fileContentSerializer); - serializationObjectMapperConfigurer.ifPresent( - p -> - p.configure( - objectMapper, zipFileWrapper::addEntry, zipFileWrapper::getCollisionFreeFileName)); + ObjectMapper objectMapper = createObjectMapper(zipFileWrapper, objectMapperCustomizer); JsonNode jsonNode = toJsonNode(entity, objectMapper); zipEditor.filter(jsonNode, zipFileWrapper); @@ -98,8 +88,21 @@ public class SerializationService { return zipFileWrapper.asByteArray(); } - private ObjectMapper createObjectMapperWithSerializer(JsonSerializer<?> serializer) { - return jsonObjectMapper.copy().registerModule(new SimpleModule().addSerializer(serializer)); + private ObjectMapper createObjectMapper( + ZipFileWrapper zipFileWrapper, ObjectMapperCustomizer objectMapperCustomizer) { + ObjectMapper objectMapper = + jsonObjectMapper + .copy() + .registerModule(createFileContentSerializationModule(zipFileWrapper)); + objectMapperCustomizer.customize(objectMapper); + return objectMapper; + } + + private static SimpleModule createFileContentSerializationModule(ZipFileWrapper zipFileWrapper) { + return new SimpleModule() + .addSerializer( + new FileContentSerializer( + zipFileWrapper::addEntry, zipFileWrapper::getCollisionFreeFileName)); } private String jsonNodeToCsv(String baseKey, JsonNode node) { diff --git a/backend/compliance-test/gradle.lockfile b/backend/compliance-test/gradle.lockfile index c77e4a7a514adcae323b627d1f17924c11ac7f19..8c20ab7a3ec5e49e216209a680c36b4b51b963dc 100644 --- a/backend/compliance-test/gradle.lockfile +++ b/backend/compliance-test/gradle.lockfile @@ -22,6 +22,7 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=testCompileCl com.fasterxml.jackson:jackson-bom:2.18.2=testCompileClasspath,testRuntimeClasspath com.fasterxml.woodstox:woodstox-core:7.1.0=testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testRuntimeClasspath @@ -43,6 +44,8 @@ com.google.zxing:core:3.5.3=testRuntimeClasspath com.googlecode.ez-vcard:ez-vcard:0.12.1=testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=testRuntimeClasspath com.ibm.async:asyncutil:0.1.0=testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=testCompileClasspath,testRuntimeClasspath @@ -50,6 +53,7 @@ com.nimbusds:lang-tag:1.7=testCompileClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=testCompileClasspath,testRuntimeClasspath com.opencsv:opencsv:5.10=testRuntimeClasspath +com.pff:java-libpst:0.9.3=testRuntimeClasspath com.slimjars.trove4j:trove4j-advancing-iterator:1.0.1=testRuntimeClasspath com.slimjars.trove4j:trove4j-constants:1.0.1=testRuntimeClasspath com.slimjars.trove4j:trove4j-hash-functions:1.0.1=testRuntimeClasspath @@ -187,6 +191,7 @@ net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-compress:1.27.1=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=testRuntimeClasspath org.apache.commons:commons-fileupload2-core:2.0.0-M2=testRuntimeClasspath org.apache.commons:commons-fileupload2-jakarta-servlet6:2.0.0-M2=testRuntimeClasspath org.apache.commons:commons-fileupload2:2.0.0-M2=testRuntimeClasspath @@ -216,11 +221,18 @@ org.apache.pdfbox:pdfbox:3.0.3=testRuntimeClasspath org.apache.pdfbox:xmpbox:3.0.3=testRuntimeClasspath org.apache.poi:poi-ooxml-lite:5.4.0=testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml:5.4.0=testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=testRuntimeClasspath org.apache.poi:poi:5.4.0=testCompileClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=testRuntimeClasspath org.apache.tika:tika-core:3.0.0=testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=testCompileClasspath,testRuntimeClasspath diff --git a/backend/dental/gradle.lockfile b/backend/dental/gradle.lockfile index 03d1be535a86413ba0f5043a301bc2b886f05dbd..91d7aa2f30e5a81d99b6b428b1e418a212483bf2 100644 --- a/backend/dental/gradle.lockfile +++ b/backend/dental/gradle.lockfile @@ -15,6 +15,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -31,12 +32,15 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=comp com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -99,12 +103,15 @@ net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-compress:1.27.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-math3:3.6.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -115,11 +122,18 @@ org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testR org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml-lite:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -154,6 +168,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/dental/openApi.json b/backend/dental/openApi.json index ec27e31ac85816498d9eaea8b400b06969767ceb..9aa5d3b0948f7f3103b90dcaae970b75eeeaf945 100644 --- a/backend/dental/openApi.json +++ b/backend/dental/openApi.json @@ -2536,7 +2536,7 @@ "name" : "limit", "required" : false, "schema" : { - "maximum" : 200, + "maximum" : 2200, "minimum" : 1, "type" : "integer", "format" : "int32", @@ -2685,6 +2685,34 @@ "tags" : [ "Task" ] } }, + "/test-helper/calculation/dmft" : { + "post" : { + "operationId" : "calculateDmftValues", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CalculateDmftValuesRequest" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/DmftValues" + } + } + }, + "description" : "OK" + } + }, + "tags" : [ "TestHelper" ] + } + }, "/test-helper/population" : { "post" : { "operationId" : "populateDefaults", @@ -3241,6 +3269,18 @@ } } }, + "CalculateDmftValuesRequest" : { + "required" : [ "toothDiagnoses" ], + "type" : "object", + "properties" : { + "toothDiagnoses" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/ToothDiagnosis" + } + } + } + }, "CheckFileStateUsageRequest" : { "required" : [ "fileStatesIds" ], "type" : "object", @@ -3812,6 +3852,20 @@ } } }, + "DmftValues" : { + "required" : [ "dmftPrimary", "dmftSecondary" ], + "type" : "object", + "properties" : { + "dmftPrimary" : { + "type" : "integer", + "format" : "int64" + }, + "dmftSecondary" : { + "type" : "integer", + "format" : "int64" + } + } + }, "DomesticAddress" : { "required" : [ "city", "country", "postalCode", "street" ], "type" : "object", @@ -3893,6 +3947,9 @@ "note" : { "type" : "string" }, + "prophylaxisDentitionType" : { + "$ref" : "#/components/schemas/DentitionType" + }, "prophylaxisType" : { "$ref" : "#/components/schemas/ProphylaxisType" }, @@ -5830,6 +5887,9 @@ } ] } }, + "prophylaxisDentitionType" : { + "$ref" : "#/components/schemas/DentitionType" + }, "result" : { "oneOf" : [ { "$ref" : "#/components/schemas/AbsenceExaminationResult" diff --git a/backend/dental/src/main/java/de/eshg/dental/api/ExaminationDto.java b/backend/dental/src/main/java/de/eshg/dental/api/ExaminationDto.java index 4d1052174978322b6c634bba823a1d596285c97e..2e34f9b7978e859b8df2e65f4476f4fde16e1d12 100644 --- a/backend/dental/src/main/java/de/eshg/dental/api/ExaminationDto.java +++ b/backend/dental/src/main/java/de/eshg/dental/api/ExaminationDto.java @@ -18,6 +18,7 @@ public record ExaminationDto( @NotNull Instant dateAndTime, @NotNull ProphylaxisTypeDto prophylaxisType, @NotNull boolean isScreening, + DentitionTypeDto prophylaxisDentitionType, @NotNull boolean isFluoridation, Boolean fluoridationConsentGiven, String note, diff --git a/backend/dental/src/main/java/de/eshg/dental/api/ProphylaxisSessionChildExaminationDto.java b/backend/dental/src/main/java/de/eshg/dental/api/ProphylaxisSessionChildExaminationDto.java index e20e9b1971f4097d41cae330ed8cfa9597b8a07e..8aa6da5203257dd303739a301a48b18c05c2c442 100644 --- a/backend/dental/src/main/java/de/eshg/dental/api/ProphylaxisSessionChildExaminationDto.java +++ b/backend/dental/src/main/java/de/eshg/dental/api/ProphylaxisSessionChildExaminationDto.java @@ -24,6 +24,7 @@ public record ProphylaxisSessionChildExaminationDto( @NotNull String groupName, GenderDto gender, String note, + DentitionTypeDto prophylaxisDentitionType, @Valid @NotNull List<FluoridationConsentDto> allFluoridationConsents, @Valid ExaminationResultDto result, @Valid @NotNull List<ExaminationResultDto> previousExaminationResults) {} diff --git a/backend/dental/src/main/java/de/eshg/dental/domain/model/OralHygieneStatus.java b/backend/dental/src/main/java/de/eshg/dental/domain/model/OralHygieneStatus.java index 159c431771cf39b355223658f1c16a54d67b5a74..ad5aac719dda8dff080c1c389f39ddb77ef26842 100644 --- a/backend/dental/src/main/java/de/eshg/dental/domain/model/OralHygieneStatus.java +++ b/backend/dental/src/main/java/de/eshg/dental/domain/model/OralHygieneStatus.java @@ -8,8 +8,8 @@ package de.eshg.dental.domain.model; public enum OralHygieneStatus { /** keine Zahnbeläge */ EXCELLENT, - /* vereinzelte Zahnbeläge */ + /** vereinzelte Zahnbeläge */ GOOD, - /* massive Zahnbeläge */ + /** massive Zahnbeläge */ POOR } diff --git a/backend/dental/src/main/java/de/eshg/dental/domain/model/Tooth.java b/backend/dental/src/main/java/de/eshg/dental/domain/model/Tooth.java index cd21b86b40f13f77470bffb19fe5d95593896c3a..f320baf6b16a3359f942dfd04564910acd7cfb6f 100644 --- a/backend/dental/src/main/java/de/eshg/dental/domain/model/Tooth.java +++ b/backend/dental/src/main/java/de/eshg/dental/domain/model/Tooth.java @@ -5,64 +5,82 @@ package de.eshg.dental.domain.model; +import static de.eshg.dental.domain.model.ToothType.PRIMARY; +import static de.eshg.dental.domain.model.ToothType.SECONDARY; +import static de.eshg.dental.domain.model.ToothType.WISDOM; + public enum Tooth { - T11, - T12, - T13, - T14, - T15, - T16, - T17, - T18, + T11(SECONDARY), + T12(SECONDARY), + T13(SECONDARY), + T14(SECONDARY), + T15(SECONDARY), + T16(SECONDARY), + T17(SECONDARY), + T18(WISDOM), + + T21(SECONDARY), + T22(SECONDARY), + T23(SECONDARY), + T24(SECONDARY), + T25(SECONDARY), + T26(SECONDARY), + T27(SECONDARY), + T28(WISDOM), + + T31(SECONDARY), + T32(SECONDARY), + T33(SECONDARY), + T34(SECONDARY), + T35(SECONDARY), + T36(SECONDARY), + T37(SECONDARY), + T38(WISDOM), + + T41(SECONDARY), + T42(SECONDARY), + T43(SECONDARY), + T44(SECONDARY), + T45(SECONDARY), + T46(SECONDARY), + T47(SECONDARY), + T48(WISDOM), + + T51(PRIMARY), + T52(PRIMARY), + T53(PRIMARY), + T54(PRIMARY), + T55(PRIMARY), - T21, - T22, - T23, - T24, - T25, - T26, - T27, - T28, + T61(PRIMARY), + T62(PRIMARY), + T63(PRIMARY), + T64(PRIMARY), + T65(PRIMARY), - T31, - T32, - T33, - T34, - T35, - T36, - T37, - T38, + T71(PRIMARY), + T72(PRIMARY), + T73(PRIMARY), + T74(PRIMARY), + T75(PRIMARY), - T41, - T42, - T43, - T44, - T45, - T46, - T47, - T48, + T81(PRIMARY), + T82(PRIMARY), + T83(PRIMARY), + T84(PRIMARY), + T85(PRIMARY); - T51, - T52, - T53, - T54, - T55, + private final ToothType type; - T61, - T62, - T63, - T64, - T65, + Tooth(ToothType type) { + this.type = type; + } - T71, - T72, - T73, - T74, - T75, + public boolean isPrimaryTooth() { + return this.type == PRIMARY; + } - T81, - T82, - T83, - T84, - T85, + public boolean isSecondaryTooth() { + return this.type == SECONDARY; + } } diff --git a/backend/dental/src/main/java/de/eshg/dental/domain/model/ToothType.java b/backend/dental/src/main/java/de/eshg/dental/domain/model/ToothType.java new file mode 100644 index 0000000000000000000000000000000000000000..30a9c6c586d0a8d9f5a996d67ae5104e8cc00c27 --- /dev/null +++ b/backend/dental/src/main/java/de/eshg/dental/domain/model/ToothType.java @@ -0,0 +1,12 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.dental.domain.model; + +public enum ToothType { + PRIMARY, + SECONDARY, + WISDOM +} diff --git a/backend/dental/src/main/java/de/eshg/dental/mapper/ExaminationMapper.java b/backend/dental/src/main/java/de/eshg/dental/mapper/ExaminationMapper.java index f167beb0053f16ce3733ebf099d241e6439d2067..abeed2cb3aedc27d3274e1b1535573458b63022e 100644 --- a/backend/dental/src/main/java/de/eshg/dental/mapper/ExaminationMapper.java +++ b/backend/dental/src/main/java/de/eshg/dental/mapper/ExaminationMapper.java @@ -36,6 +36,7 @@ public final class ExaminationMapper { prophylaxisSession.getDateAndTime(), ProphylaxisSessionMapper.mapToDto(prophylaxisSession.getType()), prophylaxisSession.isScreening(), + DentitionTypeMapper.mapToDto(prophylaxisSession.getDentitionType()), prophylaxisSession.hasFluoridationVarnish(), examination.getChild().isFluoridationConsentCurrentlyGivenOptionally(), examination.getNote(), @@ -86,6 +87,15 @@ public final class ExaminationMapper { ExaminationMapper::mapResultsToDomain)); } + public static Map<Tooth, ToothDiagnosis> mapToDomain( + Map<ToothDto, ToothDiagnosisDto> toothDiagnosesDto) { + return toothDiagnosesDto.values().stream() + .collect( + StreamUtil.toLinkedHashMap( + toothDiagnosis -> mapToDomain(toothDiagnosis.tooth()), + ExaminationMapper::mapResultsToDomain)); + } + public static List<ToothDiagnosisDto> mapToDto(Map<Tooth, ToothDiagnosis> toothDiagnoses) { return toothDiagnoses.entrySet().stream() .map(tooth -> mapToDto(tooth.getKey(), tooth.getValue())) diff --git a/backend/dental/src/main/java/de/eshg/dental/mapper/ProphylaxisSessionMapper.java b/backend/dental/src/main/java/de/eshg/dental/mapper/ProphylaxisSessionMapper.java index 5729ce1285d6c34f3b8f70b377935bd34b9426cd..95e6783af4cad49c5060bd5be81cbae0252b8e83 100644 --- a/backend/dental/src/main/java/de/eshg/dental/mapper/ProphylaxisSessionMapper.java +++ b/backend/dental/src/main/java/de/eshg/dental/mapper/ProphylaxisSessionMapper.java @@ -130,6 +130,7 @@ public final class ProphylaxisSessionMapper { examination.getChild().getGroupName().trim(), fileStateResponse.gender(), examination.getNote(), + DentitionTypeMapper.mapToDto(examination.getProphylaxisSession().getDentitionType()), ChildMapper.mapFluoridationToDto( examination.getChild().getFluoridationConsents().stream() .sorted(Comparator.comparing(FluoridationConsent::getModifiedAt).reversed()) diff --git a/backend/dental/src/main/java/de/eshg/dental/statistic/DentalChildAttributes.java b/backend/dental/src/main/java/de/eshg/dental/statistic/DentalChildAttributes.java index 778c9a124d3ca81510b0fa6ad25d0266bec3be57..4db1b219ebabc7c1437f6c19b3642126b5ebea2b 100644 --- a/backend/dental/src/main/java/de/eshg/dental/statistic/DentalChildAttributes.java +++ b/backend/dental/src/main/java/de/eshg/dental/statistic/DentalChildAttributes.java @@ -8,6 +8,7 @@ package de.eshg.dental.statistic; import static de.eshg.lib.statistics.util.ConvertToValueOptionHelper.convertToValueOptions; import de.eshg.dental.statistic.model.Group; +import de.eshg.dental.statistic.model.OralHygieneStatus; import de.eshg.lib.statistics.attributes.AttributeData; import de.eshg.lib.statistics.attributes.AttributeInfo; import de.eshg.lib.statistics.attributes.CentralFileIdPersonAttribute; @@ -43,6 +44,22 @@ public enum DentalChildAttributes implements AttributeInfo { "ANZAHL_PROPHYLAXEN", DentalChildAttributes.CATEGORY_PROPHYLAXIS, true)), + + MUNDHYGIENE_STATUS( + new ValueWithOptionsAttribute( + "Mundhygienestatus", + "MUNDHYGIENE_STATUS", + convertToValueOptions(OralHygieneStatus.values()), + DentalChildAttributes.CATEGORY_PROPHYLAXIS, + true)), + + DMFT_MILCH( + new IntegerAttribute( + "dmft-t", "DMFT_MILCH", DentalChildAttributes.CATEGORY_PROPHYLAXIS, true)), + + DMFT_BLEIBEND( + new IntegerAttribute( + "DMF-T", "DMFT_BLEIBEND", DentalChildAttributes.CATEGORY_PROPHYLAXIS, true)), ; static final String CATEGORY_CHILD = "Kind"; diff --git a/backend/dental/src/main/java/de/eshg/dental/statistic/DentalChildDataSource.java b/backend/dental/src/main/java/de/eshg/dental/statistic/DentalChildDataSource.java index a3f3007c1d52d2066300d61f2ef0941815a7e17a..0e6c56f1c19d4d720dd860e8572d2de15593dedf 100644 --- a/backend/dental/src/main/java/de/eshg/dental/statistic/DentalChildDataSource.java +++ b/backend/dental/src/main/java/de/eshg/dental/statistic/DentalChildDataSource.java @@ -5,13 +5,25 @@ package de.eshg.dental.statistic; +import static de.eshg.dental.statistic.DmftCalculationHelper.calculateDmftValue; + import de.eshg.dental.domain.model.Child; +import de.eshg.dental.domain.model.Examination; +import de.eshg.dental.domain.model.ScreeningExaminationResult; +import de.eshg.dental.domain.model.Tooth; import de.eshg.dental.domain.repository.ChildRepository; import de.eshg.dental.statistic.model.Group; +import de.eshg.dental.statistic.model.OralHygieneStatus; import de.eshg.lib.statistics.api.DataSourceSensitivity; import de.eshg.lib.statistics.datasource.ProcedureDataSource; import de.eshg.lib.statistics.util.TimeRange; +import java.time.LocalDate; +import java.time.Year; +import java.time.ZoneOffset; +import java.util.Comparator; +import java.util.List; import java.util.UUID; +import java.util.function.Predicate; import org.springframework.stereotype.Component; @Component @@ -40,9 +52,56 @@ public class DentalChildDataSource extends ProcedureDataSource<Child, DentalChil case EINRICHTUNG -> child.getInstitutionId(); case GRUPPE -> getGroup(child.getGroupName()); case ANZAHL_PROPHYLAXEN -> child.getExaminations().size(); + case MUNDHYGIENE_STATUS -> getOralHygieneStatus(child.getExaminations(), child.getYear()); + case DMFT_MILCH -> calculateDmftPrimaryTeethValue(child.getExaminations(), child.getYear()); + case DMFT_BLEIBEND -> + calculateDmftSecondaryTeethValue(child.getExaminations(), child.getYear()); }; } + private String getOralHygieneStatus(List<Examination> examinations, Year year) { + ScreeningExaminationResult latestScreeningExamination = + getLatestScreeningExaminationResultOrNull(examinations, year); + if (latestScreeningExamination == null) { + return null; + } + return OralHygieneStatus.convertOralHygieneStatusToValue( + latestScreeningExamination.getOralHygieneStatus()); + } + + private ScreeningExaminationResult getLatestScreeningExaminationResultOrNull( + List<Examination> examinations, Year year) { + return examinations.stream() + .filter( + examination -> + LocalDate.ofInstant(examination.getDateAndTime(), ZoneOffset.UTC).getYear() + == year.getValue()) + .filter(examination -> examination.getResult() instanceof ScreeningExaminationResult) + .max(Comparator.comparing(Examination::getDateAndTime)) + .map(Examination::getResult) + .map(ScreeningExaminationResult.class::cast) + .orElse(null); + } + + private Long calculateDmftPrimaryTeethValue(List<Examination> examinations, Year year) { + return calculateDmftTeethValue(examinations, year, Tooth::isPrimaryTooth); + } + + private Long calculateDmftSecondaryTeethValue(List<Examination> examinations, Year year) { + return calculateDmftTeethValue(examinations, year, Tooth::isSecondaryTooth); + } + + private Long calculateDmftTeethValue( + List<Examination> examinations, Year year, Predicate<Tooth> expectedToothType) { + ScreeningExaminationResult latestScreeningExamination = + getLatestScreeningExaminationResultOrNull(examinations, year); + if (latestScreeningExamination == null) { + return null; + } + + return calculateDmftValue(expectedToothType, latestScreeningExamination.getToothDiagnoses()); + } + private String getGroup(String groupName) { if (groupName == null) { return null; diff --git a/backend/dental/src/main/java/de/eshg/dental/statistic/DmftCalculationHelper.java b/backend/dental/src/main/java/de/eshg/dental/statistic/DmftCalculationHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..3454b99e3d06ad01cebe546c4574cf877f82bcaa --- /dev/null +++ b/backend/dental/src/main/java/de/eshg/dental/statistic/DmftCalculationHelper.java @@ -0,0 +1,29 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.dental.statistic; + +import de.eshg.dental.domain.model.MainResult; +import de.eshg.dental.domain.model.Tooth; +import de.eshg.dental.domain.model.ToothDiagnosis; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public class DmftCalculationHelper { + private DmftCalculationHelper() {} + + public static long calculateDmftValue( + Predicate<Tooth> expectedToothType, Map<Tooth, ToothDiagnosis> toothDiagnoses) { + return toothDiagnoses.entrySet().stream() + .filter(entry -> expectedToothType.test(entry.getKey())) + .filter(entry -> hasDmfDiagnosis(entry.getValue())) + .count(); + } + + private static boolean hasDmfDiagnosis(ToothDiagnosis diagnosis) { + return List.of(MainResult.D, MainResult.E, MainResult.F).contains(diagnosis.mainResult()); + } +} diff --git a/backend/dental/src/main/java/de/eshg/dental/statistic/model/OralHygieneStatus.java b/backend/dental/src/main/java/de/eshg/dental/statistic/model/OralHygieneStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..ce4ef3da6f250ed786e52541c6c381d0346928b3 --- /dev/null +++ b/backend/dental/src/main/java/de/eshg/dental/statistic/model/OralHygieneStatus.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.dental.statistic.model; + +import de.eshg.lib.statistics.util.ConvertibleToValueOptions; + +public enum OralHygieneStatus implements ConvertibleToValueOptions { + EXCELLENT("Sehr gut", "Sehr gut"), + GOOD("Gut", "Gut"), + POOR("Schlecht", "Schlecht"); + + private final String value; + private final String meaning; + + OralHygieneStatus(String value, String meaning) { + this.value = value; + this.meaning = meaning; + } + + @Override + public String getValue() { + return value; + } + + @Override + public String getMeaning() { + return meaning; + } + + public static String convertOralHygieneStatusToValue( + de.eshg.dental.domain.model.OralHygieneStatus status) { + return switch (status) { + case null -> null; + case EXCELLENT -> EXCELLENT.getValue(); + case GOOD -> GOOD.getValue(); + case POOR -> POOR.getValue(); + }; + } +} diff --git a/backend/dental/src/main/java/de/eshg/dental/testhelper/CalculateDmftValuesRequest.java b/backend/dental/src/main/java/de/eshg/dental/testhelper/CalculateDmftValuesRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..6e86c72b1f525c09b78d8f8c373a6757d39822f9 --- /dev/null +++ b/backend/dental/src/main/java/de/eshg/dental/testhelper/CalculateDmftValuesRequest.java @@ -0,0 +1,15 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.dental.testhelper; + +import de.eshg.dental.api.ToothDiagnosisDto; +import de.eshg.dental.api.ToothDto; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.Map; + +public record CalculateDmftValuesRequest( + @Valid @NotNull Map<ToothDto, ToothDiagnosisDto> toothDiagnoses) {} diff --git a/backend/dental/src/main/java/de/eshg/dental/testhelper/DentalTestHelperController.java b/backend/dental/src/main/java/de/eshg/dental/testhelper/DentalTestHelperController.java index a71a5daec5c9c6d6fa5d7a21f84ceea2cba8795d..ddffbf266b10e0b36abccd05fdb9b4ad4c76ab82 100644 --- a/backend/dental/src/main/java/de/eshg/dental/testhelper/DentalTestHelperController.java +++ b/backend/dental/src/main/java/de/eshg/dental/testhelper/DentalTestHelperController.java @@ -9,6 +9,10 @@ import de.eshg.dental.api.ChildrenPopulationResult; import de.eshg.dental.api.CreateChildResponse; import de.eshg.dental.api.CreateProphylaxisSessionResponse; import de.eshg.dental.api.ProphylaxisSessionPopulationResult; +import de.eshg.dental.domain.model.Tooth; +import de.eshg.dental.domain.model.ToothDiagnosis; +import de.eshg.dental.mapper.ExaminationMapper; +import de.eshg.dental.statistic.DmftCalculationHelper; import de.eshg.testhelper.ConditionalOnTestHelperEnabled; import de.eshg.testhelper.TestHelperController; import de.eshg.testhelper.TestHelperWithDatabaseService; @@ -16,6 +20,7 @@ import de.eshg.testhelper.api.PopulationRequest; import de.eshg.testhelper.environment.EnvironmentConfig; import de.eshg.testhelper.population.ListWithTotalNumber; import jakarta.validation.Valid; +import java.util.Map; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.service.annotation.PostExchange; @@ -52,4 +57,13 @@ public class DentalTestHelperController extends TestHelperController { return new ProphylaxisSessionPopulationResult( result.entities(), result.totalNumberOfElements()); } + + @PostExchange("/calculation/dmft") + public DmftValues calculateDmftValues(@Valid @RequestBody CalculateDmftValuesRequest request) { + Map<Tooth, ToothDiagnosis> toothDiagnoses = + ExaminationMapper.mapToDomain(request.toothDiagnoses()); + return new DmftValues( + DmftCalculationHelper.calculateDmftValue(Tooth::isPrimaryTooth, toothDiagnoses), + DmftCalculationHelper.calculateDmftValue(Tooth::isSecondaryTooth, toothDiagnoses)); + } } diff --git a/backend/dental/src/main/java/de/eshg/dental/testhelper/DmftValues.java b/backend/dental/src/main/java/de/eshg/dental/testhelper/DmftValues.java new file mode 100644 index 0000000000000000000000000000000000000000..269d8b2cb9271309d038098cdc86e913b73d65d4 --- /dev/null +++ b/backend/dental/src/main/java/de/eshg/dental/testhelper/DmftValues.java @@ -0,0 +1,10 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.dental.testhelper; + +import jakarta.validation.constraints.NotNull; + +public record DmftValues(@NotNull long dmftPrimary, @NotNull long dmftSecondary) {} diff --git a/backend/docker-compose.yaml b/backend/docker-compose.yaml index 296be336280d61df51bf7c386b6e942dfa3e1738..3e249cf853b62a44961adfd3e1763e05cd9d086b 100644 --- a/backend/docker-compose.yaml +++ b/backend/docker-compose.yaml @@ -317,6 +317,7 @@ services: - de.eshg.business-modules.clients[SCHOOL_ENTRY].url=http://school-entry:8080 - de.eshg.business-modules.clients[INSPECTION].url=http://inspection:8080 - de.eshg.business-modules.clients[DENTAL].url=http://dental:8080 + - de.eshg.business-modules.clients[OFFICIAL_MEDICAL_SERVICE].url=http://official-medical-service:8080 - de.eshg.auditlog.service-url=http://auditlog:8080 - de.eshg.centralrepository.service-url=http://central-repository:8080 - de.eshg.centralrepository.mock-cert-subject-cn=statistics.frankfurt.ga-lotse @@ -445,6 +446,9 @@ services: - spring.datasource.url=jdbc:postgresql://auditlog-db/auditlog - de.eshg.base.service-url=http://base:8080 - de.eshg.auditlog.log-storage-dir=/tmp/auditlog-storage + depends_on: + auditlog-db: + condition: service_healthy auditlog-db: extends: @@ -573,8 +577,19 @@ services: - de.eshg.base.service-url=http://base:8080 - eshg.keycloak.internal.url=http://keycloak:8080 - de.eshg.auditlog.service-url=http://auditlog:8080 + - de.eshg.official-medical-service.department-info.name=Amtsärztlicher Dienst + - de.eshg.official-medical-service.department-info.abbreviation=TMD + - de.eshg.official-medical-service.department-info.street=Wanderluststraße + - de.eshg.official-medical-service.department-info.houseNumber=202 + - de.eshg.official-medical-service.department-info.postalCode=12345 + - de.eshg.official-medical-service.department-info.city=Wandern Stadt + - de.eshg.official-medical-service.department-info.country=DE + - de.eshg.official-medical-service.department-info.phoneNumber=+49 123 12345678 + - de.eshg.official-medical-service.department-info.homepage=www.oms.de + - de.eshg.official-medical-service.department-info.email=gutachten@oms.de - de.eshg.official-medical-service.notification.fromAddress=tba@stadt-frankfurt.de - de.eshg.official-medical-service.notification.greeting=Ihr TBA-Team der Stadt Frankfurt + - de.eshg.official-medical-service.concerns.config=classpath:$${de.eshg.official-medical-service.concerns.templates.path}/concerns.test.yaml depends_on: official-medical-service-db: condition: service_healthy diff --git a/backend/file-commons/build.gradle b/backend/file-commons/build.gradle index c79551f363c7e64b66b225a21991364240a51979..2dae1119b2eabebe8a498c31501b9bd62db7bb88 100644 --- a/backend/file-commons/build.gradle +++ b/backend/file-commons/build.gradle @@ -5,7 +5,11 @@ plugins { dependencies { implementation project(':rest-service-errors') implementation 'org.springframework:spring-web' + + implementation platform('org.apache.tika:tika-bom:latest.release') implementation 'org.apache.tika:tika-core:latest.release' + implementation 'org.apache.tika:tika-parser-microsoft-module' + implementation 'org.verapdf:validation-model-jakarta:latest.release' implementation 'de.cronn:reflection-util:latest.release' implementation 'org.apache.commons:commons-text:latest.release' diff --git a/backend/file-commons/gradle.lockfile b/backend/file-commons/gradle.lockfile index 8d6033aab308517228df33b1067740d89a4602ad..5a76389aa14e78ef06215001019f4c11327edb3b 100644 --- a/backend/file-commons/gradle.lockfile +++ b/backend/file-commons/gradle.lockfile @@ -7,11 +7,13 @@ com.fasterxml.jackson.core:jackson-annotations:2.18.2=compileClasspath,productio com.fasterxml.jackson.core:jackson-core:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson.core:jackson-databind:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testRuntimeClasspath com.github.docker-java:docker-java-transport:3.4.0=testRuntimeClasspath com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.10.0=testRuntimeClasspath com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0=testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=testRuntimeClasspath com.google.guava:failureaccess:1.0.2=testRuntimeClasspath @@ -19,7 +21,10 @@ com.google.guava:guava:33.3.1-jre=testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -27,7 +32,10 @@ com.tngtech.archunit:archunit-junit5-engine:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit:1.3.0=testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.17.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.18.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-logging:commons-logging:1.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:commons-lang:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:postgres-snapshot-util:1.4=testRuntimeClasspath de.cronn:reflection-util:2.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -44,23 +52,41 @@ jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=compileClasspath,productionRuntimeCl junit:junit:4.13.2=testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.15.11=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.15.11=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testRuntimeClasspath +net.java.dev.jna:jna:5.16.0=testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=testCompileClasspath,testRuntimeClasspath net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath -org.apache.commons:commons-compress:1.24.0=testRuntimeClasspath +org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.logging.log4j:log4j-api:2.24.3=testCompileClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=testCompileClasspath,testRuntimeClasspath -org.apache.tika:tika-core:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-bom:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-core:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath org.assertj:assertj-core:3.26.3=testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcpkix-jdk18on:1.80=testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.80=testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.80=testRuntimeClasspath +org.bouncycastle:bcjmail-jdk18on:1.80=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.80=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.80=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.80=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.checkerframework:checker-qual:3.43.0=testRuntimeClasspath org.eclipse.angus:angus-activation:2.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-core:4.0.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -73,6 +99,7 @@ org.jacoco:org.jacoco.ant:0.8.12=jacocoAnt org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jetbrains:annotations:17.0.0=testRuntimeClasspath +org.jsoup:jsoup:1.18.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/file-commons/src/main/java/de/eshg/file/common/FileExtension.java b/backend/file-commons/src/main/java/de/eshg/file/common/FileExtension.java index 20d5bb726130dfbe1ca497b6cba62847a66243e4..f769632acc0e375ba690b716214013036fd79374 100644 --- a/backend/file-commons/src/main/java/de/eshg/file/common/FileExtension.java +++ b/backend/file-commons/src/main/java/de/eshg/file/common/FileExtension.java @@ -13,7 +13,8 @@ public enum FileExtension { PNG("png"), PDF("pdf"), EML("eml"), - CSV("csv"); + CSV("csv"), + XLSX("xlsx"); private final String value; diff --git a/backend/file-commons/src/main/java/de/eshg/file/common/FileType.java b/backend/file-commons/src/main/java/de/eshg/file/common/FileType.java index 9ce91083aa7821b8fa174acb5b0dd2760d40e4ec..aef950dfc3f424a8a176b2e1f59503bec117f8bf 100644 --- a/backend/file-commons/src/main/java/de/eshg/file/common/FileType.java +++ b/backend/file-commons/src/main/java/de/eshg/file/common/FileType.java @@ -20,7 +20,8 @@ public enum FileType { PNG(MediaType.IMAGE_PNG, FileExtension.PNG), PDF(MediaType.APPLICATION_PDF, FileExtension.PDF), EML(CustomMediaTypes.EML, FileExtension.EML), - CSV(CustomMediaTypes.CSV, FileExtension.CSV); + CSV(CustomMediaTypes.CSV, FileExtension.CSV), + XLSX(CustomMediaTypes.APPLICATION_XLSX, FileExtension.XLSX); private final MediaType mediaType; private final FileExtension defaultFileExtension; diff --git a/backend/inspection/gradle.lockfile b/backend/inspection/gradle.lockfile index 548afc417b729da751efecaedc84eb9df17a6214..2d045886a5c94a17914a2468a8cfc5bb53d65e5c 100644 --- a/backend/inspection/gradle.lockfile +++ b/backend/inspection/gradle.lockfile @@ -15,6 +15,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -32,12 +33,15 @@ com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeCla com.google.protobuf:protobuf-javalite:4.29.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.slimjars.trove4j:trove4j-advancing-iterator:1.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.slimjars.trove4j:trove4j-constants:1.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.slimjars.trove4j:trove4j-hash-functions:1.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -144,12 +148,15 @@ net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-compress:1.27.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-math3:3.6.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -160,11 +167,18 @@ org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testC org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml-lite:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -219,6 +233,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/inspection/openApi.json b/backend/inspection/openApi.json index 72e3232b3dab9fe463ae7ef1e8ed8c8503a1904c..9e878ed0fab792b89e07fda008a15f05d47ffedf 100644 --- a/backend/inspection/openApi.json +++ b/backend/inspection/openApi.json @@ -4476,7 +4476,7 @@ "name" : "limit", "required" : false, "schema" : { - "maximum" : 200, + "maximum" : 2200, "minimum" : 1, "type" : "integer", "format" : "int32", diff --git a/backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContent.java b/backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContent.java index 4c3bb50b6eed6fcecdd408a85dd692d712f87e87..3308daa76882edf0d9e0360670d5fbc707a54ccb 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContent.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContent.java @@ -7,6 +7,7 @@ package de.eshg.inspection.common.persistence; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import de.eshg.domain.model.BaseEntity; +import de.eshg.domain.model.HasFileContent; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; import jakarta.persistence.CascadeType; @@ -26,8 +27,8 @@ import org.hibernate.annotations.JdbcTypeCode; @Entity @DataSensitivity(SensitivityLevel.PROTECTED) -@JsonIgnoreProperties("mediaFiles") -public class MediaFileContent extends BaseEntity { +@JsonIgnoreProperties({MediaFileContent_.MEDIA_FILES, "filename", "content"}) +public class MediaFileContent extends BaseEntity implements HasFileContent { @Lob @JdbcTypeCode(Types.BINARY) @@ -61,4 +62,14 @@ public class MediaFileContent extends BaseEntity { throw new RuntimeException(e); } } + + @Override + public String getFileName() { + return getMediaFiles().stream().map(MediaFile::getFileName).findFirst().orElse("media"); + } + + @Override + public byte[] getContent() { + return getAllBytes(); + } } diff --git a/backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContentSerializer.java b/backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContentSerializer.java deleted file mode 100644 index 3b239422e4cdf8742ee37d8e3d349f81edcd3828..0000000000000000000000000000000000000000 --- a/backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContentSerializer.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2025 SCOOP Software GmbH, cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package de.eshg.inspection.common.persistence; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; -import java.io.Serial; -import java.util.function.BiConsumer; -import java.util.function.Function; - -public class MediaFileContentSerializer extends StdSerializer<MediaFileContent> { - - @Serial private static final long serialVersionUID = 1L; - - private final transient BiConsumer<String, byte[]> fileContentConsumer; - private final transient Function<String, String> collisionFreeFileNameCreation; - - public MediaFileContentSerializer( - BiConsumer<String, byte[]> fileContentConsumer, - Function<String, String> collisionFreeFileNameCreation) { - super(MediaFileContent.class); - this.fileContentConsumer = fileContentConsumer; - this.collisionFreeFileNameCreation = collisionFreeFileNameCreation; - } - - /** - * Replace the actual base64 encoded content by a collision free fileName that is referenced by - * the name of the actual file inside the zip file - * - * <p>fileContentConsumer is responsible for adding an entry (representing the file) to the zip - * file - * - * @param fileContent Value to serialize; can <b>not</b> be null. - * @param gen Generator used to output resulting Json content - * @param provider Provider that can be used to get serializers for serializing Objects value - * contains, if any. - * @throws IOException - */ - @Override - public void serialize( - MediaFileContent fileContent, JsonGenerator gen, SerializerProvider provider) - throws IOException { - String filename = - fileContent.getMediaFiles().stream() - .map(MediaFile::getFileName) - .findFirst() - .orElse("media"); - String collisionFreeFileName = collisionFreeFileNameCreation.apply(filename); - - fileContentConsumer.accept(collisionFreeFileName, fileContent.getAllBytes()); - gen.writeStartObject(); - gen.writeStringField("content", collisionFreeFileName); - gen.writeEndObject(); - } -} diff --git a/backend/inspection/src/main/java/de/eshg/inspection/config/InspectionProcedureConfiguration.java b/backend/inspection/src/main/java/de/eshg/inspection/config/InspectionProcedureConfiguration.java index d24adc907d26690ad258a0ceb15f83dfb9a494e6..ac534f25f62e5c53dd8cf4322a82e6bc074ecfc1 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/config/InspectionProcedureConfiguration.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/config/InspectionProcedureConfiguration.java @@ -5,14 +5,8 @@ package de.eshg.inspection.config; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import de.eshg.domain.model.serialization.SerializationObjectMapperConfigurer; -import de.eshg.inspection.common.persistence.MediaFileContentSerializer; import de.eshg.lib.keycloak.ModuleLeaderRole; import de.eshg.lib.keycloak.ModuleMemberGroup; -import java.util.function.BiConsumer; -import java.util.function.UnaryOperator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,19 +22,4 @@ public class InspectionProcedureConfiguration { ModuleLeaderRole moduleLeaderRole() { return ModuleLeaderRole.INSPECTION_LEADER; } - - @Bean - SerializationObjectMapperConfigurer serializationObjectMapperConfigurer() { - return new SerializationObjectMapperConfigurer() { - @Override - public void configure( - ObjectMapper objectMapper, - BiConsumer<String, byte[]> fileContentConsumer, - UnaryOperator<String> collisionFreeFileNameCreation) { - MediaFileContentSerializer serializer = - new MediaFileContentSerializer(fileContentConsumer, collisionFreeFileNameCreation); - objectMapper.registerModule(new SimpleModule().addSerializer(serializer)); - } - }; - } } diff --git a/backend/lib-auditlog/build.gradle b/backend/lib-auditlog/build.gradle index 5c5ea09a39092ecb2ead728dfa261a9f3c838215..87b2ae8f67848bfabdba48738959e47fe94f0bb5 100644 --- a/backend/lib-auditlog/build.gradle +++ b/backend/lib-auditlog/build.gradle @@ -25,6 +25,7 @@ dependencies { testImplementation testFixtures(project(':business-module-commons')) testImplementation testFixtures(project(':business-module-persistence-commons')) testImplementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + testImplementation 'org.bouncycastle:bcprov-jdk18on:latest.release' testRuntimeOnly 'org.springframework.boot:spring-boot-starter-web' testRuntimeOnly 'org.postgresql:postgresql' diff --git a/backend/lib-auditlog/gradle.lockfile b/backend/lib-auditlog/gradle.lockfile index ee147b940c4b775195b04e494f59b97f730a0d3e..af07985d9e1ee83fce1b7cf44601145e9466fb0c 100644 --- a/backend/lib-auditlog/gradle.lockfile +++ b/backend/lib-auditlog/gradle.lockfile @@ -105,7 +105,7 @@ org.aspectj:aspectjweaver:1.9.22.1=compileClasspath,productionRuntimeClasspath,r org.assertj:assertj-core:3.26.3=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.80=testFixturesRuntimeClasspath,testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.80=testFixturesRuntimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.80=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.bouncycastle:bcutil-jdk18on:1.80=testFixturesRuntimeClasspath,testRuntimeClasspath org.checkerframework:checker-qual:3.43.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.eclipse.angus:angus-activation:2.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath diff --git a/backend/lib-auditlog/src/main/java/de/eshg/lib/auditlog/AuditLogHousekeeping.java b/backend/lib-auditlog/src/main/java/de/eshg/lib/auditlog/AuditLogHousekeeping.java new file mode 100644 index 0000000000000000000000000000000000000000..dea5b7b7d477ab5b5c65ed936c8e50c74c474249 --- /dev/null +++ b/backend/lib-auditlog/src/main/java/de/eshg/lib/auditlog/AuditLogHousekeeping.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.auditlog; + +import de.eshg.lib.auditlog.domain.AuditLogEntryRepository; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; +import java.time.Period; +import net.javacrumbs.shedlock.core.LockAssert; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class AuditLogHousekeeping { + + private static final Logger log = LoggerFactory.getLogger(AuditLogHousekeeping.class); + + private static final Period RETENTION_PERIOD = Period.ofDays(5); + + private final AuditLogEntryRepository auditLogEntryRepository; + private final Clock clock; + + public AuditLogHousekeeping(AuditLogEntryRepository auditLogEntryRepository, Clock clock) { + this.auditLogEntryRepository = auditLogEntryRepository; + this.clock = clock; + } + + @Scheduled(cron = "0 0 4 * * *") + @SchedulerLock(name = "LibAuditLogAuditLogHousekeeping", lockAtMostFor = "23h") + @Transactional + public void runHousekeeping() { + LockAssert.assertLocked(); + Instant retentionThreshold = + LocalDate.now(clock).atStartOfDay(clock.getZone()).toInstant().minus(RETENTION_PERIOD); + log.info( + "Starting auditlog housekeeping - attempting to delete all entries created before {}", + retentionThreshold); + long numberOfEntriesDeleted = + auditLogEntryRepository.deleteAuditLogEntryByCreatedAtBefore(retentionThreshold); + log.info("{} entries deleted", numberOfEntriesDeleted); + } +} diff --git a/backend/lib-auditlog/src/main/java/de/eshg/lib/auditlog/domain/AuditLogEntryRepository.java b/backend/lib-auditlog/src/main/java/de/eshg/lib/auditlog/domain/AuditLogEntryRepository.java index bc5cd5d7a9f762b5414c54c22a432968f1434723..cfbc6b27156ac9a4079299d8665ea571d582c806 100644 --- a/backend/lib-auditlog/src/main/java/de/eshg/lib/auditlog/domain/AuditLogEntryRepository.java +++ b/backend/lib-auditlog/src/main/java/de/eshg/lib/auditlog/domain/AuditLogEntryRepository.java @@ -14,4 +14,6 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface AuditLogEntryRepository extends JpaRepository<AuditLogEntry, Long> { @EntityGraph(value = "AuditLogEntry.additionalData", type = EntityGraphType.LOAD) List<AuditLogEntry> findByCreatedAtBeforeOrderByCreatedAtAscIdAsc(Instant createdAt); + + long deleteAuditLogEntryByCreatedAtBefore(Instant threshold); } diff --git a/backend/lib-matrix-client/build.gradle b/backend/lib-matrix-client/build.gradle index 968be7e197bc878a1d0dff9fca6d54f072d0cfd2..5ad0c5652b4480d6686b641ea105f342d90523f7 100644 --- a/backend/lib-matrix-client/build.gradle +++ b/backend/lib-matrix-client/build.gradle @@ -56,7 +56,7 @@ tasks.register('unzipDownloadedMatrixSpec', Copy) { def registerGenerateMatrixClientTask(String type) { String taskName = "generateMatrixClient-${type}" - String inputSpecPath = "${zipDir}/matrix-spec-${matrixSpecVersion}/data/api/client-server/${type}.yaml" + String inputSpecPath = "${zipDir.toURI()}/matrix-spec-${matrixSpecVersion}/data/api/client-server/${type}.yaml" def outputDirPath = layout.buildDirectory.dir("generated/sources/matrix/${type}").get().asFile def generateMatrixClientTask = tasks.register(taskName, GenerateTask) { dependsOn unzipDownloadedMatrixSpec diff --git a/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/api/TaskListApi.java b/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/api/TaskListApi.java index 978389f6c6955f5635201e8b6396d7989d0945e2..7f7ced186484714f1e7ddeb9cdf60ecb25113a13 100644 --- a/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/api/TaskListApi.java +++ b/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/api/TaskListApi.java @@ -22,8 +22,10 @@ import org.springframework.web.service.annotation.GetExchange; public interface TaskListApi { - GetTasksSortByDto DASHBOARD_SORT_BY = GetTasksSortByDto.PRIORITY; + int MAXIMUM_AGGREGATION_PAGE_NUMBER = 10; + int MAXIMUM_AGGREGATION_PAGE_SIZE = 200; int DASHBOARD_LIMIT = 10; + GetTasksSortByDto DASHBOARD_SORT_BY = GetTasksSortByDto.PRIORITY; class QueryParameter { private QueryParameter() {} @@ -50,6 +52,6 @@ public interface TaskListApi { @InlineParameterObject @ParameterObject @Valid GetTasksSortOptions sortOptions, @RequestParam(name = QueryParameter.LIMIT, required = false, defaultValue = "50") @Min(1) - @Max(200) + @Max((MAXIMUM_AGGREGATION_PAGE_NUMBER + 1) * MAXIMUM_AGGREGATION_PAGE_SIZE) Integer limit); } diff --git a/backend/lib-procedures/gradle.lockfile b/backend/lib-procedures/gradle.lockfile index 3d9e1e60cc676e8ad61ff9b25c23e11e552f4a6d..a58afd49b43784054ad9321895e52b5d6a46f203 100644 --- a/backend/lib-procedures/gradle.lockfile +++ b/backend/lib-procedures/gradle.lockfile @@ -15,6 +15,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -22,6 +23,7 @@ com.github.docker-java:docker-java-transport:3.4.0=testCompileClasspath,testFixt com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.10.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -31,12 +33,15 @@ com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeCla com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.h2database:h2:2.3.232=testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.41.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,xjc com.sun.istack:istack-commons-tools:4.1.2=xjc com.sun.xml.bind.external:relaxng-datatype:4.0.5=xjc @@ -49,7 +54,8 @@ com.tngtech.archunit:archunit-junit5:1.3.0=testFixturesRuntimeClasspath,testRunt com.tngtech.archunit:archunit:1.3.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath -commons-codec:commons-codec:1.17.1=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.17.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath commons-io:commons-io:2.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.18.0=testFixturesRuntimeClasspath commons-logging:commons-logging:1.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -100,12 +106,16 @@ net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCom net.ttddyy:datasource-proxy:1.10=testFixturesRuntimeClasspath,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.27.1=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -114,14 +124,25 @@ org.apache.pdfbox:pdfbox-io:3.0.3=compileClasspath,productionRuntimeClasspath,ru org.apache.pdfbox:pdfbox-tools:3.0.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.pdfbox:pdfbox:3.0.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.pdfbox:xmpbox:3.0.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi:5.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.34=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.2.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.aspectj:aspectjweaver:1.9.22.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.assertj:assertj-core:3.26.3=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -155,6 +176,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-procedures/openApi.json b/backend/lib-procedures/openApi.json index 08f7c9f2c820ffaea7e548b52f44570d04a5468d..0047b36e52cbe6dbb3d050ea2b2a92150e5cd5d6 100644 --- a/backend/lib-procedures/openApi.json +++ b/backend/lib-procedures/openApi.json @@ -1921,7 +1921,7 @@ "name" : "limit", "required" : false, "schema" : { - "maximum" : 200, + "maximum" : 2200, "minimum" : 1, "type" : "integer", "format" : "int32", diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/AbstractGdprZipEditorProvider.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/AbstractGdprZipEditorProvider.java index 829f411474ed1c79b6a52b7fa7e8520120d36c39..2ab48bc2b2cceb3d577724cc19b14bf9a5a8ec72 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/AbstractGdprZipEditorProvider.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/AbstractGdprZipEditorProvider.java @@ -8,6 +8,8 @@ package de.eshg.lib.procedure.gdpr; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import de.eshg.domain.model.BaseEntity_; +import de.eshg.domain.model.SequencedBaseEntity_; import de.eshg.domain.model.serialization.ZipEditor; import de.eshg.domain.model.serialization.ZipFileWrapper; import de.eshg.lib.procedure.domain.model.FileContent_; @@ -29,6 +31,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.UUID; import java.util.stream.Stream; import java.util.stream.Stream.Builder; @@ -37,6 +40,17 @@ import org.springframework.core.io.Resource; public abstract class AbstractGdprZipEditorProvider { + protected static final Set<String> COMMON_SEQUENCE_ID_KEYS = + Set.copyOf( + List.of( + BaseEntity_.ID, + SequencedBaseEntity_.ID, + MetaData_.ID, + ProgressEntry_.PROCEDURE_ID, + RelatedPerson_.PROCEDURE, + RelatedFacility_.PROCEDURE, + Task_.PROCEDURE)); + private final Resource resource; public static final String FILE_META_DATA = "metaData"; diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/DefaultGdprZipEditorProvider.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/DefaultGdprZipEditorProvider.java index 8518d927a1823b25c404b3b01aa666712d4f18e8..a0292bd87c8bfefeaeccc59f04f36a273b6a2643 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/DefaultGdprZipEditorProvider.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/DefaultGdprZipEditorProvider.java @@ -20,6 +20,7 @@ public class DefaultGdprZipEditorProvider extends AbstractGdprZipEditorProvider super(resource); } + @Override protected ZipEditor createSpecificFilter() { return (jsonNode, zipFileWrapper) -> {}; } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/GdprValidationTaskController.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/GdprValidationTaskController.java index ac962a1c8d5e0f9b8ad59e4da18305ab82379b81..84eda37a4a157f0490842b75bfcb1ffcfec57f12 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/GdprValidationTaskController.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/GdprValidationTaskController.java @@ -128,7 +128,11 @@ public class GdprValidationTaskController< ZipEditor zipEditor = zipEditorProvider.create(fileStateIds); byte[] zip = - serializationService.toZip("DSGVO-Vorgang_" + businessProcedureId, procedure, zipEditor); + serializationService.toZip( + "DSGVO-Vorgang_" + businessProcedureId, + procedure, + zipEditor, + SerializationUtil.createNormalizedSequenceIdObjectMapperCustomizer()); UUID downloadId = service.createAndSaveDownloadPackage(businessProcedureId, zip).getExternalId(); service.sendDownloadId(gdprProcedureId, downloadId); diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/SerializationUtil.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/SerializationUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..4e877babfb2ccaf9d327b55a89325d67544f5b74 --- /dev/null +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/SerializationUtil.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.procedure.gdpr; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import de.eshg.domain.model.serialization.NormalizeSequenceIdCustomizer; +import de.eshg.domain.model.serialization.ObjectMapperCustomizer; +import de.eshg.lib.procedure.domain.model.ProgressEntry; + +final class SerializationUtil { + + private SerializationUtil() {} + + static ObjectMapperCustomizer createNormalizedSequenceIdObjectMapperCustomizer() { + return ObjectMapperCustomizer.combine( + objectMapper -> objectMapper.addMixIn(ProgressEntry.class, ProgressEntryMixin.class), + new NormalizeSequenceIdCustomizer()); + } + + private interface ProgressEntryMixin { + + @JsonIgnore + Long getProcedureId(); + } +} diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/OfficialMedicalServicePublicSecurityConfig.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/OfficialMedicalServicePublicSecurityConfig.java index 6a3280513ac8f0e76aae69953cf52b7c653b89d5..e82a7095a800db3e45ec5ef3513c1577184f9e58 100644 --- a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/OfficialMedicalServicePublicSecurityConfig.java +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/OfficialMedicalServicePublicSecurityConfig.java @@ -21,6 +21,7 @@ public class OfficialMedicalServicePublicSecurityConfig grantAccessToLibProceduresUrls( EmployeePermissionRole.OFFICIAL_MEDICAL_SERVICE_ADMIN, ModuleLeaderRole.OFFICIAL_MEDICAL_SERVICE_LEADER); + grantAccessToStatistics(EmployeePermissionRole.OFFICIAL_MEDICAL_SERVICE_ADMIN); requestMatchers(BaseUrls.OfficialMedicalService.CITIZEN_PUBLIC_API + "/**").permitAll(); diff --git a/backend/lib-statistics/gradle.lockfile b/backend/lib-statistics/gradle.lockfile index b0116d8995b4698efeca860a969dfd7ade6b356f..442d8a6c03f30972dda39148b6881fec23683adb 100644 --- a/backend/lib-statistics/gradle.lockfile +++ b/backend/lib-statistics/gradle.lockfile @@ -15,6 +15,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -22,6 +23,7 @@ com.github.docker-java:docker-java-transport:3.4.0=testCompileClasspath,testFixt com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.10.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -30,6 +32,8 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=prod com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -37,6 +41,7 @@ com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath, com.nimbusds:nimbus-jose-jwt:9.41.2=testFixturesRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testFixturesRuntimeClasspath,testRuntimeClasspath @@ -45,7 +50,8 @@ com.tngtech.archunit:archunit-junit5:1.3.0=testFixturesRuntimeClasspath,testRunt com.tngtech.archunit:archunit:1.3.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath -commons-codec:commons-codec:1.17.1=testFixturesRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.17.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath commons-io:commons-io:2.18.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.4=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath de.cronn:commons-lang:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -96,13 +102,16 @@ net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCom net.ttddyy:datasource-proxy:1.10=testFixturesRuntimeClasspath,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.24.0=testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.27.1=testFixturesRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -111,14 +120,25 @@ org.apache.pdfbox:pdfbox-io:3.0.3=productionRuntimeClasspath,runtimeClasspath,te org.apache.pdfbox:pdfbox-tools:3.0.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi:5.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.34=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.2.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.aspectj:aspectjweaver:1.9.22.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.assertj:assertj-core:3.26.3=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -147,6 +167,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-xlsx-import/gradle.lockfile b/backend/lib-xlsx-import/gradle.lockfile index 5898d672de6eeb50da161dc3cc51a3756a24ae16..4c5ea972364320e1428f6ef9bdf574a43fde8c8f 100644 --- a/backend/lib-xlsx-import/gradle.lockfile +++ b/backend/lib-xlsx-import/gradle.lockfile @@ -12,6 +12,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=productionRuntimeC com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testFixturesRuntimeClasspath,testRuntimeClasspath @@ -28,8 +29,11 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=test com.google.j2objc:j2objc-annotations:3.0.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.8=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testFixturesRuntimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testFixturesRuntimeClasspath,testRuntimeClasspath @@ -40,6 +44,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspat com.zaxxer:SparseBitSet:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath commons-codec:commons-codec:1.17.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath commons-io:commons-io:2.18.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +commons-logging:commons-logging:1.3.4=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath de.cronn:commons-lang:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath de.cronn:postgres-snapshot-util:1.4=testFixturesRuntimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -69,25 +74,36 @@ junit:junit:4.13.2=testFixturesRuntimeClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.15.11=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.15.11=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.2=testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testFixturesRuntimeClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.16.0=testFixturesRuntimeClasspath,testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=testCompileClasspath,testRuntimeClasspath net.ttddyy:datasource-proxy:1.10=testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-compress:1.27.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.13.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-math3:3.6.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.12=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.12=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml-lite:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.4.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.poi:poi:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath -org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-bom:3.1.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-core:3.1.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=testCompileClasspath,testRuntimeClasspath @@ -95,9 +111,10 @@ org.apache.xmlbeans:xmlbeans:5.3.0=compileClasspath,productionRuntimeClasspath,r org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testFixturesCompileClasspath,testRuntimeClasspath org.assertj:assertj-core:3.26.3=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcpkix-jdk18on:1.80=testFixturesRuntimeClasspath,testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.80=testFixturesRuntimeClasspath,testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.80=testFixturesRuntimeClasspath,testRuntimeClasspath +org.bouncycastle:bcjmail-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.checkerframework:checker-qual:3.43.0=testFixturesRuntimeClasspath,testRuntimeClasspath org.eclipse.angus:angus-activation:2.0.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.eclipse.angus:jakarta.mail:2.0.3=testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -114,6 +131,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testFixturesRuntimeClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportValidator.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportValidator.java index 00b159a8b52c90d99140bc5139b2089f79211143..6431a8ff56da06e4057313cd2460fd05ae54cfef 100644 --- a/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportValidator.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportValidator.java @@ -25,7 +25,8 @@ public class ImportValidator { private ImportValidator() {} - static <C extends XlsxColumn> List<C> validateHeaderFormat(C[] expectedColumns, XSSFSheet sheet) { + public static <C extends XlsxColumn> List<C> validateHeaderFormat( + C[] expectedColumns, XSSFSheet sheet) { Row headerRow = sheet.getRow(0); XSSFCellStyle headerCellStyle = XlsxUtil.createHeaderCellStyle(sheet); List<C> foundColumns = new ArrayList<>(); diff --git a/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxImport.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxImport.java index 7771f64726f45537c6c3d51b40356df4141873ad..d6284baee73e0d64eb20b63da2d83800b5536190 100644 --- a/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxImport.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxImport.java @@ -5,12 +5,13 @@ package de.eshg.lib.xlsximport; +import de.eshg.file.common.CustomMediaTypes; +import de.eshg.file.common.FileValidator; import de.eshg.rest.service.error.BadRequestException; import de.eshg.rest.service.error.ErrorCode; import java.io.IOException; import java.io.InputStream; import java.util.List; -import java.util.Objects; import org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; @@ -18,13 +19,16 @@ import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; import org.springframework.web.multipart.MultipartFile; public class XlsxImport { private static final Logger log = LoggerFactory.getLogger(XlsxImport.class); + private static final String NOT_A_VALID_XLSX_ERROR_MESSAGE = + "The provided file is not a valid XLSX document."; + private XlsxImport() {} @FunctionalInterface @@ -38,17 +42,8 @@ public class XlsxImport { C[] expectedColumns, SheetProcessor<T, C> sheetProcessor) throws IOException { - return processWorkbook(file.getResource(), maxNumberOfRows, expectedColumns, sheetProcessor); - } - - public static <T, C extends XlsxColumn> T processWorkbook( - Resource resource, - int maxNumberOfRows, - C[] expectedColumns, - SheetProcessor<T, C> sheetProcessor) - throws IOException { - validateFileExistsAndHasCorrectType(resource); - try (InputStream inputStream = resource.getInputStream(); + validateMediaType(FileValidator.validate(file)); + try (InputStream inputStream = file.getInputStream(); XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) { validateSheet(workbook); Sheet sheet = workbook.getSheetAt(0); @@ -64,18 +59,17 @@ public class XlsxImport { } } catch (NotOfficeXmlFileException e) { log.error("Failed to import from provided XLSX file", e); - throw new BadRequestException( - ErrorCode.INVALID_FILE, "The provided file is not a valid XLSX document."); + throw new BadRequestException(ErrorCode.INVALID_FILE, NOT_A_VALID_XLSX_ERROR_MESSAGE); } } - private static void validateFileExistsAndHasCorrectType(Resource resource) { - if (!resource.exists()) { + private static void validateMediaType(MediaType detectedMediaType) { + if (!CustomMediaTypes.APPLICATION_XLSX.equals(detectedMediaType)) { throw new BadRequestException( - ErrorCode.INVALID_FILE, "The file %s does not exist.".formatted(resource.getFilename())); - } - if (!Objects.requireNonNull(resource.getFilename()).endsWith(".xlsx")) { - throw new BadRequestException(ErrorCode.INVALID_FILE, "The file type is not xlsx."); + ErrorCode.INVALID_FILE, + NOT_A_VALID_XLSX_ERROR_MESSAGE, + "The detected media type %s is not %s" + .formatted(detectedMediaType, CustomMediaTypes.APPLICATION_XLSX_VALUE)); } } diff --git a/backend/measles-protection/gradle.lockfile b/backend/measles-protection/gradle.lockfile index e190fef8e7711c98c268a6ce69129551d5679d09..64444d70fb2aa4a18482650c8d759a1de2d12c37 100644 --- a/backend/measles-protection/gradle.lockfile +++ b/backend/measles-protection/gradle.lockfile @@ -15,6 +15,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -31,12 +32,15 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=prod com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -103,12 +107,15 @@ net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-compress:1.27.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-math3:3.6.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -119,11 +126,18 @@ org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testC org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml-lite:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -178,6 +192,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/measles-protection/openApi.json b/backend/measles-protection/openApi.json index e30bdb71b6cd3cfca8016eb63e3fa2656af82ba5..13e32046092a084ac31b7b6517c10496b74a2cf9 100644 --- a/backend/measles-protection/openApi.json +++ b/backend/measles-protection/openApi.json @@ -3197,7 +3197,7 @@ "name" : "limit", "required" : false, "schema" : { - "maximum" : 200, + "maximum" : 2200, "minimum" : 1, "type" : "integer", "format" : "int32", diff --git a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/MeaslesGdprZipEditorProvider.java b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/MeaslesGdprZipEditorProvider.java index 04f8d098d50dd20b335e9a6b98f257d4fdf26abf..d8473b05c189e776cf3d254444f7594fabce221c 100644 --- a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/MeaslesGdprZipEditorProvider.java +++ b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/MeaslesGdprZipEditorProvider.java @@ -7,6 +7,8 @@ package de.eshg.measlesprotection; import de.eshg.domain.model.serialization.ZipEditor; import de.eshg.lib.procedure.gdpr.AbstractGdprZipEditorProvider; +import de.eshg.measlesprotection.persistence.db.MeaslesProtectionProcedure_; +import de.eshg.measlesprotection.persistence.db.ProofSubmission_; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; @@ -21,6 +23,7 @@ public class MeaslesGdprZipEditorProvider extends AbstractGdprZipEditorProvider @Override protected ZipEditor createSpecificFilter() { - return (jsonNode, zipFileWrapper) -> {}; + return removeFieldFromArray( + ProofSubmission_.MANUAL_PROGRESS_ENTRY, MeaslesProtectionProcedure_.PROOF_SUBMISSIONS); } } diff --git a/backend/medical-registry/gradle.lockfile b/backend/medical-registry/gradle.lockfile index ea5441d9c587dee87e732d67e358eb19408ec8c0..03cb98a55238f47620b78f5c4fbabb654d1d8a70 100644 --- a/backend/medical-registry/gradle.lockfile +++ b/backend/medical-registry/gradle.lockfile @@ -16,6 +16,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -32,12 +33,15 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=prod com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -100,12 +104,15 @@ net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-compress:1.27.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-math3:3.6.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -116,11 +123,18 @@ org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testR org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml-lite:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -155,6 +169,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/medical-registry/openApi.json b/backend/medical-registry/openApi.json index 45f5f1117a56580844448e14fab502b8b6193e1c..edb9e395d5494a9615179b413906b94285011a1b 100644 --- a/backend/medical-registry/openApi.json +++ b/backend/medical-registry/openApi.json @@ -2372,7 +2372,7 @@ "name" : "limit", "required" : false, "schema" : { - "maximum" : 200, + "maximum" : 2200, "minimum" : 1, "type" : "integer", "format" : "int32", diff --git a/backend/official-medical-service/build.gradle b/backend/official-medical-service/build.gradle index a9f36344dc377478ac96b918b351e10ab333c443..b8b3e7b41d4855d664c8525f455852636e6e11f9 100644 --- a/backend/official-medical-service/build.gradle +++ b/backend/official-medical-service/build.gradle @@ -10,6 +10,7 @@ dependencies { implementation project(':business-module-persistence-commons') implementation project(':rest-oauth-client-commons') implementation project(':lib-appointmentblock') + implementation project(":lib-statistics") implementation 'org.springdoc:springdoc-openapi-starter-common:latest.release' @@ -19,6 +20,7 @@ dependencies { testImplementation testFixtures(project(':business-module-persistence-commons')) testImplementation testFixtures(project(':lib-procedures')) + testImplementation testFixtures(project(':lib-statistics')) testImplementation testFixtures(project(':base-api')) } diff --git a/backend/official-medical-service/gradle.lockfile b/backend/official-medical-service/gradle.lockfile index 6ddcff0cdd1409bfc437c7f9db64851a275a7b32..41de4df95ec2cbcb9974643022786f97ce4e587b 100644 --- a/backend/official-medical-service/gradle.lockfile +++ b/backend/official-medical-service/gradle.lockfile @@ -15,6 +15,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -22,6 +23,7 @@ com.github.docker-java:docker-java-transport:3.4.0=testCompileClasspath,testRunt com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.10.0=testRuntimeClasspath com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0=testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -30,12 +32,15 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=prod com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -44,6 +49,8 @@ com.tngtech.archunit:archunit-junit5:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit:1.3.0=testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.17.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.18.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:commons-lang:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -94,12 +101,16 @@ net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCom net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.24.0=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -108,14 +119,25 @@ org.apache.pdfbox:pdfbox-io:3.0.3=productionRuntimeClasspath,runtimeClasspath,te org.apache.pdfbox:pdfbox-tools:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.34=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.aspectj:aspectjweaver:1.9.22.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.assertj:assertj-core:3.26.3=testCompileClasspath,testRuntimeClasspath @@ -145,6 +167,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/official-medical-service/openApi.json b/backend/official-medical-service/openApi.json index 2d65e96be3da5cddbf047d68fb52a8df6e068a6e..1e0e47ed56947ea41fa0e33157c83ebb46f31298 100644 --- a/backend/official-medical-service/openApi.json +++ b/backend/official-medical-service/openApi.json @@ -549,6 +549,25 @@ "tags" : [ "Archiving" ] } }, + "/citizen-public/appointment-types" : { + "get" : { + "operationId" : "getAppointmentTypesForCitizen", + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/GetAppointmentTypesResponse" + } + } + }, + "description" : "OK" + } + }, + "summary" : "Gets all Appointment Types", + "tags" : [ "CitizenPublic" ] + } + }, "/citizen-public/concerns" : { "get" : { "operationId" : "getVisibleConcerns", @@ -3218,6 +3237,35 @@ "tags" : [ "ProgressEntry" ] } }, + "/statistics/procedure-ids" : { + "post" : { + "operationId" : "getProcedureIds", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/GetProcedureIdsRequest" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/GetProcedureIdsResponse" + } + } + }, + "description" : "OK" + } + }, + "summary" : "Get procedure ids for procedure references", + "tags" : [ "StatisticsProcedureReference" ] + } + }, "/task-metrics" : { "get" : { "operationId" : "getTaskMetrics", @@ -3325,7 +3373,7 @@ "name" : "limit", "required" : false, "schema" : { - "maximum" : 200, + "maximum" : 2200, "minimum" : 1, "type" : "integer", "format" : "int32", @@ -5812,6 +5860,34 @@ } } }, + "GetProcedureIdsRequest" : { + "required" : [ "procedureReferences" ], + "type" : "object", + "properties" : { + "procedureReferences" : { + "maxItems" : 200, + "minItems" : 1, + "type" : "array", + "items" : { + "type" : "string", + "format" : "uuid" + } + } + } + }, + "GetProcedureIdsResponse" : { + "required" : [ "referenceToId" ], + "type" : "object", + "properties" : { + "referenceToId" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "format" : "uuid" + } + } + } + }, "GetProcedureMetricsResponse" : { "required" : [ "procedureMetrics" ], "type" : "object", diff --git a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/citizenpublic/CitizenProcedureService.java b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/citizenpublic/CitizenProcedureService.java index 91b590eb5f950011ced1a6cdc5ce0a12058b99d4..c13e15620cc4e8dc0dee36eec380028942e44a6a 100644 --- a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/citizenpublic/CitizenProcedureService.java +++ b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/citizenpublic/CitizenProcedureService.java @@ -9,6 +9,7 @@ import de.eshg.base.centralfile.api.person.AddPersonFileStateResponse; import de.eshg.officialmedicalservice.appointment.OmsAppointmentService; import de.eshg.officialmedicalservice.concern.ConcernMapper; import de.eshg.officialmedicalservice.document.OmsDocumentService; +import de.eshg.officialmedicalservice.notification.NotificationService; import de.eshg.officialmedicalservice.person.PersonClient; import de.eshg.officialmedicalservice.person.PersonMapper; import de.eshg.officialmedicalservice.procedure.OmsProcedureOverviewMapper; @@ -28,18 +29,21 @@ public class CitizenProcedureService { private final OmsProcedureOverviewMapper omsProcedureOverviewMapper; private final OmsProcedureRepository omsProcedureRepository; private final OmsDocumentService omsDocumentService; + private final NotificationService notificationService; public CitizenProcedureService( OmsAppointmentService appointmentService, PersonClient personClient, OmsProcedureOverviewMapper omsProcedureOverviewMapper, OmsProcedureRepository omsProcedureRepository, - OmsDocumentService omsDocumentService) { + OmsDocumentService omsDocumentService, + NotificationService notificationService) { this.omsAppointmentService = appointmentService; this.personClient = personClient; this.omsProcedureOverviewMapper = omsProcedureOverviewMapper; this.omsProcedureRepository = omsProcedureRepository; this.omsDocumentService = omsDocumentService; + this.notificationService = notificationService; } @Transactional @@ -60,6 +64,8 @@ public class CitizenProcedureService { omsDocumentService.addLetterOfAssignmentCitizen(procedure, files); + notificationService.notifyNewCitizenProcedure(request.affectedPerson()); + return procedure.getExternalId(); } } diff --git a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/citizenpublic/CitizenPublicController.java b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/citizenpublic/CitizenPublicController.java index 3eb6ae60252e320db61445fc861b33fab523f1a9..9693caa61364a0b48fb73b6483a4ac320a5bf200 100644 --- a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/citizenpublic/CitizenPublicController.java +++ b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/citizenpublic/CitizenPublicController.java @@ -11,9 +11,11 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import de.eshg.base.department.GetDepartmentInfoResponse; import de.eshg.lib.appointmentblock.AppointmentBlockService; +import de.eshg.lib.appointmentblock.AppointmentTypeService; import de.eshg.lib.appointmentblock.MappingUtil; import de.eshg.lib.appointmentblock.api.AppointmentDto; import de.eshg.lib.appointmentblock.api.AppointmentTypeDto; +import de.eshg.lib.appointmentblock.api.GetAppointmentTypesResponse; import de.eshg.lib.appointmentblock.api.GetFreeAppointmentsResponse; import de.eshg.lib.appointmentblock.persistence.AppointmentType; import de.eshg.officialmedicalservice.citizenpublic.api.GetOpeningHoursResponse; @@ -33,6 +35,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -56,6 +59,7 @@ public class CitizenPublicController { public static final String PRIVACY_NOTICE_URL = "/privacy-notice"; public static final String PRIVACY_POLICY_URL = "/privacy-policy"; public static final String CONCERNS_URL = "/concerns"; + public static final String APPOINTMENT_TYPES_URL = "/appointment-types"; private final OpeningHoursProperties openingHoursProperties; private final DepartmentInfoService departmentInfoService; @@ -65,6 +69,7 @@ public class CitizenPublicController { private final Resource privacyNotice; private final Resource privacyPolicy; private final ConcernService concernService; + private final AppointmentTypeService appointmentTypeService; public CitizenPublicController( OpeningHoursProperties openingHoursProperties, @@ -74,7 +79,8 @@ public class CitizenPublicController { Clock clock, @Value("${de.eshg.official-medical-service.privacy-notice-location}") Resource privacyNotice, @Value("${de.eshg.official-medical-service.privacy-policy-location}") Resource privacyPolicy, - ConcernService concernService) { + ConcernService concernService, + AppointmentTypeService appointmentTypeService) { this.openingHoursProperties = openingHoursProperties; this.departmentInfoService = departmentInfoService; this.citizenProcedureService = citizenProcedureService; @@ -83,6 +89,7 @@ public class CitizenPublicController { this.privacyNotice = privacyNotice; this.privacyPolicy = privacyPolicy; this.concernService = concernService; + this.appointmentTypeService = appointmentTypeService; } @Operation(summary = "Get opening hours.") @@ -146,4 +153,11 @@ public class CitizenPublicController { public GetConcernsResponse getVisibleConcerns() { return concernService.getConcernsVisibleInOnlinePortal(); } + + @Operation(summary = "Gets all Appointment Types") + @GetMapping(path = APPOINTMENT_TYPES_URL) + @Transactional(readOnly = true) + public GetAppointmentTypesResponse getAppointmentTypesForCitizen() { + return appointmentTypeService.getAppointmentTypes(); + } } diff --git a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/document/OmsDocumentService.java b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/document/OmsDocumentService.java index 1416cb1efab1cfb090d24f698d5b1c1ea38434e5..0bb96db00eb0f65e48731e327c03436a172a1376 100644 --- a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/document/OmsDocumentService.java +++ b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/document/OmsDocumentService.java @@ -10,6 +10,7 @@ import static org.springframework.http.MediaType.APPLICATION_PDF_VALUE; import static org.springframework.http.MediaType.IMAGE_JPEG_VALUE; import static org.springframework.http.MediaType.IMAGE_PNG_VALUE; +import de.eshg.lib.procedure.domain.model.ProcedureStatus; import de.eshg.lib.procedure.model.FileTypeDto; import de.eshg.officialmedicalservice.document.api.PatchDocumentInformationRequest; import de.eshg.officialmedicalservice.document.api.PatchDocumentNoteRequest; @@ -21,10 +22,15 @@ import de.eshg.officialmedicalservice.document.persistence.entity.OmsDocumentRep import de.eshg.officialmedicalservice.document.persistence.entity.OmsDocumentStatus; import de.eshg.officialmedicalservice.file.persistence.entity.OmsFile; import de.eshg.officialmedicalservice.file.persistence.entity.OmsFileRepository; +import de.eshg.officialmedicalservice.notification.NotificationService; +import de.eshg.officialmedicalservice.person.PersonClient; +import de.eshg.officialmedicalservice.person.PersonMapper; import de.eshg.officialmedicalservice.procedure.OmsProgressEntryType; import de.eshg.officialmedicalservice.procedure.ProgressEntryService; +import de.eshg.officialmedicalservice.procedure.api.AffectedPersonDto; import de.eshg.officialmedicalservice.procedure.persistence.entity.OmsProcedure; import de.eshg.officialmedicalservice.procedure.persistence.entity.OmsProcedureRepository; +import de.eshg.officialmedicalservice.procedure.persistence.entity.Person; import de.eshg.rest.service.error.BadRequestException; import de.eshg.rest.service.error.NotFoundException; import java.io.IOException; @@ -46,20 +52,26 @@ public class OmsDocumentService { private final OmsFileRepository omsFileRepository; private final ProgressEntryService progressEntryService; private final Clock clock; + private final NotificationService notificationService; private static final Logger logger = LoggerFactory.getLogger(OmsDocumentService.class); + private final PersonClient personClient; public OmsDocumentService( OmsProcedureRepository omsProcedureRepository, OmsDocumentRepository omsDocumentRepository, OmsFileRepository omsFileRepository, ProgressEntryService progressEntryService, - Clock clock) { + Clock clock, + NotificationService notificationService, + PersonClient personClient) { this.omsProcedureRepository = omsProcedureRepository; this.omsDocumentRepository = omsDocumentRepository; this.omsFileRepository = omsFileRepository; this.progressEntryService = progressEntryService; this.clock = clock; + this.notificationService = notificationService; + this.personClient = personClient; } @Transactional @@ -107,6 +119,17 @@ public class OmsDocumentService { omsProcedure, OmsProgressEntryType.DOCUMENT_ACCEPTED, document); } + if (omsProcedure.getProcedureStatus() == ProcedureStatus.OPEN + && omsProcedure.isSendEmailNotifications() + && document.isUploadInCitizenPortal()) { + Person person = omsProcedure.findAffectedPerson(); + AffectedPersonDto affectedPersonDto = + PersonMapper.mapToAffectedPersonDto( + personClient.getPersonFileState(person.getCentralFileStateId()), person.getVersion()); + notificationService.notifyNewDocument( + affectedPersonDto, document.getDocumentTypeDe(), document.getHelpTextDe()); + } + return document.getExternalId(); } @@ -143,6 +166,7 @@ public class OmsDocumentService { String oldDocumentTypeDe = omsDocument.getDocumentTypeDe(); String oldHelpTextDe = omsDocument.getHelpTextDe(); + boolean oldIsUploadInCitizenPortal = omsDocument.isUploadInCitizenPortal(); omsDocument.setDocumentTypeDe(request.documentTypeDe()); omsDocument.setDocumentTypeEn(request.documentTypeEn()); omsDocument.setHelpTextDe(request.helpTextDe()); @@ -156,6 +180,22 @@ public class OmsDocumentService { progressEntryService.createProgressEntryUpdateDocumentInformation( omsProcedure, omsDocument, oldDocumentTypeDe, oldHelpTextDe); } + + OmsProcedure omsProcedure = omsDocument.getOmsProcedure(); + boolean newIsUploadInCitizenPortal = omsDocument.isUploadInCitizenPortal(); + Person person = omsProcedure.findAffectedPerson(); + AffectedPersonDto affectedPersonDto = + PersonMapper.mapToAffectedPersonDto( + personClient.getPersonFileState(person.getCentralFileStateId()), person.getVersion()); + if (omsProcedure.getProcedureStatus() == ProcedureStatus.OPEN + && omsProcedure.isSendEmailNotifications() + && !affectedPersonDto.emailAddresses().isEmpty() + && omsDocument.getDocumentStatus() == OmsDocumentStatus.MISSING + && !oldIsUploadInCitizenPortal + && newIsUploadInCitizenPortal) { + notificationService.notifyNewDocument( + affectedPersonDto, omsDocument.getDocumentTypeDe(), omsDocument.getHelpTextDe()); + } } @Transactional diff --git a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/MailClient.java b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/MailClient.java index de5450b8aa6f8d3f7d1b0281d0ea217663305c67..5f0cbeab7e48706d3e4d61889851b2070bc25965 100644 --- a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/MailClient.java +++ b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/MailClient.java @@ -22,11 +22,10 @@ public class MailClient { this.mailApi = mailApi; } - void sendMail(String to, String from, String subject, String text) { + void sendMail(String to, String from, String subject, String text, MailType mailType) { log.info("Sending E-Mail notification"); - SendEmailRequest sendEmailRequest = - new SendEmailRequest(to, from, subject, text, MailType.PLAIN_TEXT); + SendEmailRequest sendEmailRequest = new SendEmailRequest(to, from, subject, text, mailType); mailApi.sendEmail(sendEmailRequest); log.info("E-Mail notification sent"); diff --git a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/NotificationService.java b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/NotificationService.java index 65a59066fdc688091cd1fb8f1f00f23dee249c07..5ad852063d583aa1ac2f348e6042733295deca8d 100644 --- a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/NotificationService.java +++ b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/NotificationService.java @@ -5,6 +5,10 @@ package de.eshg.officialmedicalservice.notification; +import static de.eshg.base.mail.MailType.HTML; +import static de.eshg.base.mail.MailType.PLAIN_TEXT; + +import de.eshg.base.mail.MailType; import de.eshg.lib.rest.oauth.client.commons.ModuleClientAuthenticator; import de.eshg.officialmedicalservice.procedure.api.AffectedPersonDto; import java.util.List; @@ -86,10 +90,33 @@ public class NotificationService { newCitizenUserSubject, () -> sendMailWithModuleClientAuthentication( - newCitizenUserSubject, newCitizenUserBody, person)); + newCitizenUserSubject, newCitizenUserBody, person, PLAIN_TEXT)); + } + + public void notifyNewCitizenProcedure(AffectedPersonDto person) { + String newCitizenProcedureSubject = notificationText.getNewCitizenProcedureSubject(); + String newCitizenProcedureBody = + notificationText.assembleNewCitizenProcedureBody(person.firstName(), person.lastName()); + + sendMailWithModuleClientAuthentication( + newCitizenProcedureSubject, newCitizenProcedureBody, person, HTML); + } + + public void notifyNewDocument( + AffectedPersonDto person, String documentTypeDe, String helpTextDe) { + String newCitizenProcedureSubject = notificationText.getNewDocumentSubject(); + if (!helpTextDe.isBlank()) { + helpTextDe = "- " + helpTextDe; + } + String newCitizenProcedureBody = + notificationText.assembleNewDocumentBody( + person.firstName(), person.lastName(), documentTypeDe, helpTextDe); + + sendMailWithModuleClientAuthentication( + newCitizenProcedureSubject, newCitizenProcedureBody, person, HTML); } - private final NotificationSummary doNotification( + private NotificationSummary doNotification( MailEnabledProvider procedure, AffectedPersonDto person, String subject, @@ -107,23 +134,25 @@ public class NotificationService { return new NotificationSummary(subject, numSentMails, null); } - private final int sendMailWithModuleClientAuthentication( - String subject, String body, AffectedPersonDto personDto) { + private int sendMailWithModuleClientAuthentication( + String subject, String body, AffectedPersonDto personDto, MailType mailType) { SecurityContext previousContext = securityContextHolderStrategy.getContext(); try { securityContextHolderStrategy.clearContext(); return moduleClientAuthenticator.doWithModuleClientAuthentication( - () -> doSendMail(subject, body, personDto)); + () -> doSendMail(subject, body, personDto, mailType)); } finally { securityContextHolderStrategy.setContext(previousContext); } } - private final int doSendMail(String subject, String body, AffectedPersonDto personDto) { + private int doSendMail( + String subject, String body, AffectedPersonDto personDto, MailType mailType) { log.info("send mail(s): " + subject); for (String emailAddress : personDto.emailAddresses()) { - mailClient.sendMail(emailAddress, notificationProperties.fromAddress(), subject, body); + mailClient.sendMail( + emailAddress, notificationProperties.fromAddress(), subject, body, mailType); } return personDto.emailAddresses().size(); } diff --git a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/NotificationText.java b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/NotificationText.java index d4cfeee6386a71a3ce422b393c543c7e46a60813..cf8eb9451f7369f075f6373fd422f46354d57db1 100644 --- a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/NotificationText.java +++ b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/notification/NotificationText.java @@ -29,6 +29,18 @@ public class NotificationText { @Value("${de.eshg.official-medical-service.notification.template.new_citizen_user.body}") private Resource newCitizenUserBodyTemplate; + @Value("${de.eshg.official-medical-service.notification.template.new_citizen_procedure.subject}") + private String newCitizenProcedureSubject; + + @Value("${de.eshg.official-medical-service.notification.template.new_citizen_procedure.body}") + private Resource newCitizenProcedureBodyTemplate; + + @Value("${de.eshg.official-medical-service.notification.template.new_document.subject}") + private String newDocumentSubject; + + @Value("${de.eshg.official-medical-service.notification.template.new_document.body}") + private Resource newDocumentBodyTemplate; + public String getNewCitizenUserSubject() { return newCitizenUserSubject; } @@ -41,6 +53,25 @@ public class NotificationText { return String.format(templateBody, firstName, lastName, loginUrl, accessCode, greeting); } + public String getNewCitizenProcedureSubject() { + return newCitizenProcedureSubject; + } + + public String assembleNewCitizenProcedureBody(String firstName, String lastName) { + String templateBody = readTemplateBody(newCitizenProcedureBodyTemplate); + return String.format(templateBody, firstName, lastName); + } + + public String getNewDocumentSubject() { + return newDocumentSubject; + } + + public String assembleNewDocumentBody( + String firstName, String lastName, String documentTypeDe, String helpTextDe) { + String templateBody = readTemplateBody(newDocumentBodyTemplate); + return String.format(templateBody, firstName, lastName, documentTypeDe, helpTextDe); + } + private static String readTemplateBody(Resource bodyTemplateResource) { try (Reader reader = new InputStreamReader(bodyTemplateResource.getInputStream(), StandardCharsets.UTF_8)) { diff --git a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/AttributeUtil.java b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/AttributeUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..f175272660ba8288e165cb0d2e2d24722971151b --- /dev/null +++ b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/AttributeUtil.java @@ -0,0 +1,13 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.officialmedicalservice.statistics; + +class AttributeUtil { + + static final String ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE = "Amtsärztlicher Dienst"; + + private AttributeUtil() {} +} diff --git a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/OmsProcedureAttributes.java b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/OmsProcedureAttributes.java new file mode 100644 index 0000000000000000000000000000000000000000..60604b2d4b9882395325a387942bae987b385d35 --- /dev/null +++ b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/OmsProcedureAttributes.java @@ -0,0 +1,96 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.officialmedicalservice.statistics; + +import static de.eshg.officialmedicalservice.statistics.AttributeUtil.ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE; + +import de.eshg.lib.procedure.domain.model.ProcedureStatus; +import de.eshg.lib.statistics.api.ValueOptionInternal; +import de.eshg.lib.statistics.attributes.AttributeData; +import de.eshg.lib.statistics.attributes.AttributeInfo; +import de.eshg.lib.statistics.attributes.CentralFileIdPersonAttribute; +import de.eshg.lib.statistics.attributes.IntegerAttribute; +import de.eshg.lib.statistics.attributes.ProcedureAttribute; +import de.eshg.lib.statistics.attributes.TextAttribute; +import de.eshg.lib.statistics.attributes.ValueWithOptionsAttribute; +import java.util.Arrays; + +public enum OmsProcedureAttributes implements AttributeInfo { + PROCEDURE_ID( + new ProcedureAttribute( + "Vorgangsreferenz", ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, true)), + + STATUS( + new ValueWithOptionsAttribute( + "Vorgangsstatus", + "STATUS", + Arrays.stream(ProcedureStatus.values()) + .map(entry -> new ValueOptionInternal(entry.name(), entry.name(), false)) + .toList(), + ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, + false)), + + CONCERN( + new TextAttribute("Anliegen", "CONCERN", ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, false)), + + CONCERN_CATEGORY( + new TextAttribute( + "Kategorie (Anliegen)", + "CONCERN_CATEGORY", + ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, + false)), + + DURATION( + new IntegerAttribute( + "Dauer bis Vorgangsabschluss", + "DURATION", + ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, + false)), + + PERSON_CENTRAL_FILE_ID( + new CentralFileIdPersonAttribute( + "Person", "PERSON_CENTRAL_FILE_ID", ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, true)), + + NUMBER_OF_DOCUMENTS( + new IntegerAttribute( + "Anzahl der Dokumente", + "NUMBER_OF_DOCUMENTS", + ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, + true)), + + NUMBER_OF_APPOINTMENTS( + new IntegerAttribute( + "Anzahl der Termine", + "NUMBER_OF_APPOINTMENTS", + ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, + true)), + + NUMBER_OF_BOOKED_APPOINTMENTS( + new IntegerAttribute( + "Anzahl der gebuchten Termine", + "NUMBER_OF_BOOKED_APPOINTMENTS", + ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, + true)), + + NUMBER_OF_CANCELLED_APPOINTMENTS( + new IntegerAttribute( + "Anzahl der abgesagten Termine", + "NUMBER_OF_CANCELLED_APPOINTMENTS", + ATTRIBUTE_CATEGORY_OFFICIAL_MEDICAL_SERVICE, + true)), + ; + + private final AttributeData attribute; + + OmsProcedureAttributes(AttributeData attribute) { + this.attribute = attribute; + } + + @Override + public AttributeData getAttributeData() { + return attribute; + } +} diff --git a/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/OmsProcedureDataSource.java b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/OmsProcedureDataSource.java new file mode 100644 index 0000000000000000000000000000000000000000..c228c0e4d977ebd0f5e9a7f4cdc31f463fa8dd69 --- /dev/null +++ b/backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/OmsProcedureDataSource.java @@ -0,0 +1,73 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.officialmedicalservice.statistics; + +import de.eshg.lib.statistics.api.DataSourceSensitivity; +import de.eshg.lib.statistics.datasource.ProcedureDataSource; +import de.eshg.lib.statistics.util.TimeRange; +import de.eshg.officialmedicalservice.appointment.persistence.entity.BookingState; +import de.eshg.officialmedicalservice.procedure.persistence.entity.OmsProcedure; +import de.eshg.officialmedicalservice.procedure.persistence.entity.OmsProcedureRepository; +import de.eshg.officialmedicalservice.procedure.persistence.entity.OmsProcedure_; +import java.time.Duration; +import java.util.UUID; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +public class OmsProcedureDataSource + extends ProcedureDataSource<OmsProcedure, OmsProcedureAttributes> { + + public static final UUID DATA_SOURCE_ID = UUID.fromString("07d387be-ba7b-4925-a892-946f2da0a6da"); + public static final String DATA_SOURCE_NAME = "Amtsärztliches Gutachten"; + + public OmsProcedureDataSource(OmsProcedureRepository omsProcedureRepository) { + super( + DATA_SOURCE_ID, + DATA_SOURCE_NAME, + DataSourceSensitivity.INTERNAL_USAGE, + omsProcedureRepository, + OmsProcedureAttributes.values(), + false); + } + + @Override + protected Object mapSpecificValue( + OmsProcedure procedure, OmsProcedureAttributes attribute, TimeRange timeRange) { + return switch (attribute) { + case PROCEDURE_ID -> procedure.getExternalId(); + case STATUS -> procedure.getProcedureStatus().toString(); + case CONCERN -> procedure.getConcern() != null ? procedure.getConcern().getNameDe() : null; + case CONCERN_CATEGORY -> + procedure.getConcern() != null ? procedure.getConcern().getCategoryNameDe() : null; + case DURATION -> getDurationInMinutes(procedure); + case PERSON_CENTRAL_FILE_ID -> procedure.findAffectedPerson().getCentralFileStateId(); + case NUMBER_OF_DOCUMENTS -> procedure.getDocuments().size(); + case NUMBER_OF_APPOINTMENTS -> procedure.getAppointments().size(); + case NUMBER_OF_BOOKED_APPOINTMENTS -> + procedure.getAppointments().stream() + .filter(appointment -> BookingState.BOOKED.equals(appointment.getBookingState())) + .count(); + case NUMBER_OF_CANCELLED_APPOINTMENTS -> + procedure.getAppointments().stream() + .filter(appointment -> BookingState.CANCELLED.equals(appointment.getBookingState())) + .count(); + }; + } + + @Override + protected Specification<OmsProcedure> getProcedureSpecification(TimeRange timeRange) { + return (root, query, criteriaBuilder) -> + isInTimeRange(criteriaBuilder, root.get(OmsProcedure_.createdAt), timeRange); + } + + private Long getDurationInMinutes(OmsProcedure procedure) { + if (procedure.getStartedAt() == null || procedure.getClosedAt() == null) { + return null; + } + return Duration.between(procedure.getStartedAt(), procedure.getClosedAt()).toMinutes(); + } +} diff --git a/backend/official-medical-service/src/main/resources/application.properties b/backend/official-medical-service/src/main/resources/application.properties index 508fce33b0c9947ba95362dcc32b1a44eef93346..763b0720195a8677f3b01906e0136a4487e13270 100644 --- a/backend/official-medical-service/src/main/resources/application.properties +++ b/backend/official-medical-service/src/main/resources/application.properties @@ -51,6 +51,10 @@ de.eshg.official-medical-service.notification.greeting=Ihr TBA-Team der Stadt Fr de.eshg.official-medical-service.notification.templates.path=notifications/default/de de.eshg.official-medical-service.notification.template.new_citizen_user.subject=Bestätigung de.eshg.official-medical-service.notification.template.new_citizen_user.body=classpath:${de.eshg.official-medical-service.notification.templates.path}/new_citizen_user.txt +de.eshg.official-medical-service.notification.template.new_citizen_procedure.subject=Eingangsbestätigung +de.eshg.official-medical-service.notification.template.new_citizen_procedure.body=classpath:${de.eshg.official-medical-service.notification.templates.path}/new_citizen_procedure.html +de.eshg.official-medical-service.notification.template.new_document.subject=Neues Dokument +de.eshg.official-medical-service.notification.template.new_document.body=classpath:${de.eshg.official-medical-service.notification.templates.path}/new_document.html de.eshg.official-medical-service.privacy-notice-location=classpath:privacy_documents/privacy_notice.pdf de.eshg.official-medical-service.privacy-policy-location=classpath:privacy_documents/privacy_policy.pdf diff --git a/backend/official-medical-service/src/main/resources/concerns/concerns.test.yaml b/backend/official-medical-service/src/main/resources/concerns/concerns.test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cbd9dbb16630654781c7d3c0cdc4a0e9e9da8c39 --- /dev/null +++ b/backend/official-medical-service/src/main/resources/concerns/concerns.test.yaml @@ -0,0 +1,215 @@ +# Copyright 2025 cronn GmbH +# SPDX-License-Identifier: Apache-2.0 + +- # Kategorie: Beamtentum + category_de: Beamtentum + category_en: civil servant + concerns: + - # Alkohol/Drogenscreening + concern_de: Alkohol/Drogenscreening + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: false + - # Arbeitsversuch / Wiedereingliederung + concern_de: Arbeitsversuch / Wiedereingliederung + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_LONG + online_portal_visibility: false + - # Attest (AU ab 1. Krankheitstag) + concern_de: Attest (AU ab 1. Krankheitstag) + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: false + - # Beihilfe (nach Aktenlage) + concern_de: Beihilfe (nach Aktenlage) + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Dienstfähigkeit (gebührenfrei) + concern_de: Dienstfähigkeit (gebührenfrei) + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_LONG + online_portal_visibility: false + - # Dienstfähigkeit (gebührenpflichtig) + concern_de: Dienstfähigkeit (gebührenpflichtig) + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_LONG + online_portal_visibility: false + - # Dienstfähigkeit / Ergänzung + concern_de: Dienstfähigkeit / Ergänzung + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Dienstfähigkeit / Widerspruch + concern_de: Dienstfähigkeit / Widerspruch + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Einsatzfähigkeit + concern_de: Einsatzfähigkeit + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_LONG + online_portal_visibility: false + - # Einstellung (gebührenfrei) + concern_de: Einstellung (gebührenfrei) + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: false + - # Einstellung (gebührenpflichtig) + concern_de: Einstellung (gebührenpflichtig) + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: false + - # Einstellung BaP / Verbeamtung auf Probe + concern_de: Einstellung BaP / Verbeamtung auf Probe + concern_en: Employment / civil servants on probation + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: true + - # Einstellung BaL / Verbeamtung auf Lebenszeit + concern_de: Einstellung BaL / Verbeamtung auf Lebenszeit + concern_en: Employment / civil servant + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: true + - # Einstellung BaW / Verbeamtung auf Widerruf + concern_de: Einstellung BaW / Verbeamtung auf Widerruf + concern_en: Employment / probationary civil servant + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: true + - # Einstellung BaZ / Verbeamtung auf Zeit + concern_de: Einstellung BaZ / Verbeamtung auf Zeit + concern_en: Employment / temporary civil servant + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: true + - # Einstellung / Widerspruch + concern_de: Einstellung / Widerspruch + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Einstellung / Werkfeuerwehr + concern_de: Einstellung / Werkfeuerwehr + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: false + - # Stundenermäßigung (Lehrkräfte) + concern_de: Stundenermäßigung (Lehrkräfte) + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Unfallbegutachtung (gebührenfrei) + concern_de: Unfallbegutachtung (gebührenfrei) + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_LONG + online_portal_visibility: false + - # Unfallbegutachtung (gebührenpflichtig) + concern_de: Unfallbegutachtung (gebührenpflichtig) + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_LONG + online_portal_visibility: false +- # Kategorie: Sonstiges + category_de: Sonstiges + category_en: Miscellaneous + concerns: + - # § 27 Hess. Rettungsdienstgesetz + concern_de: § 27 Hess. Rettungsdienstgesetz + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Abstammungsgutachten + concern_de: Abstammungsgutachten + concern_en: Lineage report + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: true + - # Adoption + concern_de: Adoption + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: false + - # Arbeits-/ Erwerbsfähigkeit + concern_de: Arbeits-/ Erwerbsfähigkeit + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_LONG + online_portal_visibility: false + - # Gerichtl. Untersuchungsauftrag + concern_de: Gerichtl. Untersuchungsauftrag + concern_en: + high_priority: true + appointment_type: OFFICIAL_MEDICAL_SERVICE_LONG + online_portal_visibility: false + - # Aufnahme Pflegekind + concern_de: Aufnahme Pflegekind + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: false + - # Sozialmedizin + concern_de: Sozialmedizin + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # S-Behinderte / § 54 SGB XII + concern_de: S-Behinderte / § 54 SGB XII + concern_en: + high_priority: false + appointment_type: OFFICIAL_MEDICAL_SERVICE_LONG + online_portal_visibility: false + - # Vorauswahl für Feuerwehr + concern_de: Vorauswahl für Feuerwehr + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Vorauswahl für Feuerwehr Sehvermögen + concern_de: Vorauswahl für Feuerwehr Sehvermögen + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Widerspruch + concern_de: Widerspruch + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Zur Vorlage beim Finanzamt (Privatpersonen) + concern_de: Zur Vorlage beim Finanzamt (Privatpersonen) + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false + - # Zur Vorlage beim Prüfungsamt + concern_de: Zur Vorlage beim Prüfungsamt + concern_en: + high_priority: true + appointment_type: OFFICIAL_MEDICAL_SERVICE_SHORT + online_portal_visibility: false + - # Sonstiges + concern_de: Sonstiges + concern_en: + high_priority: false + appointment_type: + online_portal_visibility: false diff --git a/backend/official-medical-service/src/main/resources/notifications/default/de/new_citizen_procedure.html b/backend/official-medical-service/src/main/resources/notifications/default/de/new_citizen_procedure.html new file mode 100644 index 0000000000000000000000000000000000000000..5a75768dff11e75fe4c0c6aa91e1aadb2f8bc35b --- /dev/null +++ b/backend/official-medical-service/src/main/resources/notifications/default/de/new_citizen_procedure.html @@ -0,0 +1,18 @@ +<!-- + Copyright 2025 cronn GmbH + SPDX-License-Identifier: Apache-2.0 +--> + +<div>Hallo %s %s,<br> + <br> + vielen Dank für Ihre Anfrage. Wir bestätigen Ihnen hiermit, dass diese + erfolgreich bei uns eingegangen ist.<br> + <br> + Wir prüfen Ihre Anfrage. Nach erfolgreicher Prüfung erhalten Sie eine + Bestätigung per E-Mail mit Zugangskennung für das Online Portal. Dort sind + alle Informationen enthalten zu anstehenden Untersuchungsterminen und + benötigten Unterlagen.<br> + <br> + Mit freundlichen Grüßen<br> + Ihr Gesundheitsamt +</div> diff --git a/backend/official-medical-service/src/main/resources/notifications/default/de/new_document.html b/backend/official-medical-service/src/main/resources/notifications/default/de/new_document.html new file mode 100644 index 0000000000000000000000000000000000000000..3a7ee341216ea938e7cb76975013199647cbc6a4 --- /dev/null +++ b/backend/official-medical-service/src/main/resources/notifications/default/de/new_document.html @@ -0,0 +1,14 @@ +<!-- + Copyright 2025 cronn GmbH + SPDX-License-Identifier: Apache-2.0 +--> + +<div>Hallo %s %s,<br> + <br> + für die Bearbeitung Ihres Anliegens wird ein weiteres Dokument benötigt:<br> + %s %s<br> + Sie können dieses über das Online-Portal einreichen.<br> + <br> + Mit freundlichen Grüßen<br> + Ihr Gesundheitsamt +</div> diff --git a/backend/official-medical-service/src/main/resources/notifications/ga_frankfurt/de/new_citizen_procedure.html b/backend/official-medical-service/src/main/resources/notifications/ga_frankfurt/de/new_citizen_procedure.html new file mode 100644 index 0000000000000000000000000000000000000000..5a75768dff11e75fe4c0c6aa91e1aadb2f8bc35b --- /dev/null +++ b/backend/official-medical-service/src/main/resources/notifications/ga_frankfurt/de/new_citizen_procedure.html @@ -0,0 +1,18 @@ +<!-- + Copyright 2025 cronn GmbH + SPDX-License-Identifier: Apache-2.0 +--> + +<div>Hallo %s %s,<br> + <br> + vielen Dank für Ihre Anfrage. Wir bestätigen Ihnen hiermit, dass diese + erfolgreich bei uns eingegangen ist.<br> + <br> + Wir prüfen Ihre Anfrage. Nach erfolgreicher Prüfung erhalten Sie eine + Bestätigung per E-Mail mit Zugangskennung für das Online Portal. Dort sind + alle Informationen enthalten zu anstehenden Untersuchungsterminen und + benötigten Unterlagen.<br> + <br> + Mit freundlichen Grüßen<br> + Ihr Gesundheitsamt +</div> diff --git a/backend/official-medical-service/src/main/resources/notifications/ga_frankfurt/de/new_document.html b/backend/official-medical-service/src/main/resources/notifications/ga_frankfurt/de/new_document.html new file mode 100644 index 0000000000000000000000000000000000000000..3a7ee341216ea938e7cb76975013199647cbc6a4 --- /dev/null +++ b/backend/official-medical-service/src/main/resources/notifications/ga_frankfurt/de/new_document.html @@ -0,0 +1,14 @@ +<!-- + Copyright 2025 cronn GmbH + SPDX-License-Identifier: Apache-2.0 +--> + +<div>Hallo %s %s,<br> + <br> + für die Bearbeitung Ihres Anliegens wird ein weiteres Dokument benötigt:<br> + %s %s<br> + Sie können dieses über das Online-Portal einreichen.<br> + <br> + Mit freundlichen Grüßen<br> + Ihr Gesundheitsamt +</div> diff --git a/backend/opendata/gradle.lockfile b/backend/opendata/gradle.lockfile index b37e5539aa75d5e4d5543ae0a0b35bad692c6d3c..7190b0be74647953e5671d723902f9b9385fb125 100644 --- a/backend/opendata/gradle.lockfile +++ b/backend/opendata/gradle.lockfile @@ -13,6 +13,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -20,6 +21,7 @@ com.github.docker-java:docker-java-transport:3.4.0=testCompileClasspath,testRunt com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.10.0=testRuntimeClasspath com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0=testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -28,9 +30,12 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=prod com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -39,7 +44,10 @@ com.tngtech.archunit:archunit-junit5:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit:1.3.0=testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.17.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.18.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-logging:commons-logging:1.3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:commons-lang:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath @@ -84,26 +92,43 @@ net.minidev:json-smart:2.5.1=testCompileClasspath,testRuntimeClasspath net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.24.0=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.13.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.12=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.12=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-bom:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-core:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.34=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.aspectj:aspectjweaver:1.9.22.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.assertj:assertj-core:3.26.3=testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcpkix-jdk18on:1.80=testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.80=testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.80=testRuntimeClasspath +org.bouncycastle:bcjmail-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.checkerframework:checker-qual:3.43.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.eclipse.angus:angus-activation:2.0.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-core:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -123,6 +148,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/opendata/src/main/java/de/eshg/opendata/OpenDataService.java b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataService.java index ceef399d95b221fdcdac7221e45b3f5b402b323a..d27d37e17dcdcd494669b33a8012d233fdff2bc4 100644 --- a/backend/opendata/src/main/java/de/eshg/opendata/OpenDataService.java +++ b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataService.java @@ -168,7 +168,7 @@ public class OpenDataService { CsvValidator.validate(file.getBytes()); yield OpenDataFileType.CSV; } - case EML, JPEG, PNG -> throw new BadRequestException("File type not permitted"); + default -> throw new BadRequestException("File type not permitted"); }; } catch (IOException e) { log.error("File header was corrupt", e); diff --git a/backend/rest-service-errors/src/main/java/de/eshg/rest/service/error/ErrorCode.java b/backend/rest-service-errors/src/main/java/de/eshg/rest/service/error/ErrorCode.java index efc8c3f1a72d24b062f8d6ccf109a33593427c19..4cfe9b7eeb1cb8159a0e0584b9a898d0a9d6bdf6 100644 --- a/backend/rest-service-errors/src/main/java/de/eshg/rest/service/error/ErrorCode.java +++ b/backend/rest-service-errors/src/main/java/de/eshg/rest/service/error/ErrorCode.java @@ -22,6 +22,8 @@ public enum ErrorCode { NOT_FOUND, /** Equivalent to http status 409: Conflict */ CONFLICT, + /** Equivalent to http status 500: Internal server error */ + INTERNAL_SERVER_ERROR, /** Use when ConstraintViolationException is thrown */ CONSTRAINT_VIOLATION, /** Use when TimeoutException is thrown */ diff --git a/backend/rest-service-errors/src/main/java/de/eshg/rest/service/error/InternalServerErrorException.java b/backend/rest-service-errors/src/main/java/de/eshg/rest/service/error/InternalServerErrorException.java new file mode 100644 index 0000000000000000000000000000000000000000..b1aecfaea0f1f262a1784b51291a231ae7730f45 --- /dev/null +++ b/backend/rest-service-errors/src/main/java/de/eshg/rest/service/error/InternalServerErrorException.java @@ -0,0 +1,20 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.rest.service.error; + +import java.io.Serial; + +public class InternalServerErrorException extends EshgBusinessException { + @Serial private static final long serialVersionUID = 1L; + + public InternalServerErrorException(String clientVisibleMessage) { + super(ErrorCode.INTERNAL_SERVER_ERROR, clientVisibleMessage); + } + + public InternalServerErrorException(String clientVisibleMessage, String internalErrorMessage) { + super(ErrorCode.INTERNAL_SERVER_ERROR, clientVisibleMessage, internalErrorMessage); + } +} diff --git a/backend/school-entry/gradle.lockfile b/backend/school-entry/gradle.lockfile index 8a0a7f775b95e7a6537c508ad5452731706a5475..fe62b565cca501ddc5ac39664fbfb03ad7a48d5a 100644 --- a/backend/school-entry/gradle.lockfile +++ b/backend/school-entry/gradle.lockfile @@ -16,6 +16,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -35,12 +36,15 @@ com.google.zxing:core:3.5.3=compileClasspath,productionRuntimeClasspath,runtimeC com.google.zxing:javase:3.5.3=testCompileClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -108,12 +112,15 @@ net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-compress:1.27.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-math3:3.6.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -124,11 +131,18 @@ org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testC org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml-lite:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -184,6 +198,7 @@ org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:26.0.2=compileClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/school-entry/openApi.json b/backend/school-entry/openApi.json index 8fe44ab5aa66ef0b57cc69b24f9792d4f40e558f..88af09e79552ea38f3189d352c5c0c6b022be9cc 100644 --- a/backend/school-entry/openApi.json +++ b/backend/school-entry/openApi.json @@ -3905,7 +3905,7 @@ "name" : "limit", "required" : false, "schema" : { - "maximum" : 200, + "maximum" : 2200, "minimum" : 1, "type" : "integer", "format" : "int32", diff --git a/backend/statistics/gradle.lockfile b/backend/statistics/gradle.lockfile index 34fb44c6f5b0b0e93e527ef7f37652faaf26141f..2c13bedb7c6513644eecf3830fdb2af6f9b3bea9 100644 --- a/backend/statistics/gradle.lockfile +++ b/backend/statistics/gradle.lockfile @@ -15,6 +15,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -31,12 +32,15 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=prod com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -48,6 +52,7 @@ com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeCla com.zaxxer:SparseBitSet:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-codec:commons-codec:1.17.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.18.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-logging:commons-logging:1.3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath concurrent:concurrent:1.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:commons-lang:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath @@ -98,18 +103,29 @@ net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-compress:1.27.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.13.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-math3:3.6.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.12=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.12=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml-lite:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.poi:poi:5.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-bom:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-core:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -119,9 +135,10 @@ org.apiguardian:apiguardian-api:1.1.2=productionRuntimeClasspath,runtimeClasspat org.aspectj:aspectjweaver:1.9.22.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.assertj:assertj-core:3.26.3=testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcpkix-jdk18on:1.80=testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.80=testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.80=testRuntimeClasspath +org.bouncycastle:bcjmail-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.80=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.checkerframework:checker-qual:3.43.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.eclipse.angus:angus-activation:2.0.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-core:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -141,6 +158,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/statistics/openApi.json b/backend/statistics/openApi.json index b5621b430ff800b8204ac6c5d4bdf5a52a831534..90aff376e00ff5885c60215092aaa3e6b1853ba0 100644 --- a/backend/statistics/openApi.json +++ b/backend/statistics/openApi.json @@ -2938,7 +2938,7 @@ }, "ErrorCode" : { "type" : "string", - "enum" : [ "UNEXPECTED_ERROR", "BAD_REQUEST", "UNAUTHORIZED", "INSUFFICIENT_USER_RIGHTS", "NOT_FOUND", "CONFLICT", "CONSTRAINT_VIOLATION", "TIMEOUT", "DATA_INTEGRITY_VIOLATION", "ALREADY_EXISTS", "AGGREGATION_EXCEPTION", "INVALID_FILE", "NONCONFORM_PDF", "CORRUPT", "LOCKED", "XLSX_TOO_MANY_ROWS" ] + "enum" : [ "UNEXPECTED_ERROR", "BAD_REQUEST", "UNAUTHORIZED", "INSUFFICIENT_USER_RIGHTS", "NOT_FOUND", "CONFLICT", "INTERNAL_SERVER_ERROR", "CONSTRAINT_VIOLATION", "TIMEOUT", "DATA_INTEGRITY_VIOLATION", "ALREADY_EXISTS", "AGGREGATION_EXCEPTION", "INVALID_FILE", "NONCONFORM_PDF", "CORRUPT", "LOCKED", "XLSX_TOO_MANY_ROWS" ] }, "ErrorResponseWithLocation" : { "required" : [ "errorCode", "errorLocation" ], diff --git a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/AnalysisService.java b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/AnalysisService.java index 7ab44dc5e188a93d911095d6314fb3745484e712..a9626005fc11adac743830937425bc73b062cbd6 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/AnalysisService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/AnalysisService.java @@ -73,7 +73,7 @@ public class AnalysisService { private static final String PRIMARY_ATTRIBUTE = "primaryAttribute"; private static final String SECONDARY_ATTRIBUTE = "secondaryAttribute"; private static final String ERROR_MESSAGE_ATTRIBUTE_TYPE = - "'%s': %ss require an attribute of type BOOLEAN, TEXT or VALUE_WITH_OPTIONS as '%s'"; + "'%s': %ss require an attribute of type BOOLEAN, INTEGER, TEXT or VALUE_WITH_OPTIONS as '%s'"; private final EvaluationService evaluationService; private final GeoShapeService geoShapeService; @@ -234,7 +234,7 @@ public class AnalysisService { barChartConfiguration.primaryAttribute(), aggregationResult); String configName = "BarChartConfiguration"; - validateTableColumBooleanTextOrValueOption( + validateTableColumBooleanIntegerTextOrValueOption( tableColumnPrimary, ERROR_MESSAGE_ATTRIBUTE_TYPE.formatted(name, configName, PRIMARY_ATTRIBUTE)); @@ -248,7 +248,7 @@ public class AnalysisService { TableColumn tableColumnSecondary = AggregationResultUtil.getTableColumn( barChartConfiguration.secondaryAttribute(), aggregationResult); - validateTableColumBooleanTextOrValueOption( + validateTableColumBooleanIntegerTextOrValueOption( tableColumnSecondary, ERROR_MESSAGE_ATTRIBUTE_TYPE.formatted(name, configName, SECONDARY_ATTRIBUTE)); validateThatTableColumnsAreDifferent(tableColumnPrimary, tableColumnSecondary, name); @@ -313,7 +313,7 @@ public class AnalysisService { TableColumn tableColumnSecondary = AggregationResultUtil.getTableColumn( histogramChartConfiguration.secondaryAttribute(), aggregationResult); - validateTableColumBooleanTextOrValueOption( + validateTableColumBooleanIntegerTextOrValueOption( tableColumnSecondary, ERROR_MESSAGE_ATTRIBUTE_TYPE.formatted(name, configName, SECONDARY_ATTRIBUTE)); @@ -457,9 +457,9 @@ public class AnalysisService { AggregationResultUtil.getTableColumn( pieChartConfigurationDto.attribute(), aggregationResult); - validateTableColumBooleanTextOrValueOption( + validateTableColumBooleanIntegerTextOrValueOption( tableColumnPrimary, - "'%s': PieChartConfigurations require an attribute of type BOOLEAN, TEXT or VALUE_WITH_OPTIONS" + "'%s': PieChartConfigurations require an attribute of type BOOLEAN, INTEGER, TEXT or VALUE_WITH_OPTIONS" .formatted(name)); } @@ -498,7 +498,7 @@ public class AnalysisService { AggregationResultUtil.getTableColumn( chartConfiguration.secondaryAttribute(), aggregationResult); - validateTableColumBooleanTextOrValueOption( + validateTableColumBooleanIntegerTextOrValueOption( tableColumnSecondary, ERROR_MESSAGE_ATTRIBUTE_TYPE.formatted(name, configName, SECONDARY_ATTRIBUTE)); } @@ -512,11 +512,12 @@ public class AnalysisService { } } - private static void validateTableColumBooleanTextOrValueOption( + private static void validateTableColumBooleanIntegerTextOrValueOption( TableColumn tableColumn, String errorMessage) { - if (!tableColumn.getValueType().equals(TableColumnValueType.TEXT) - && !tableColumn.getValueType().equals(TableColumnValueType.VALUE_WITH_OPTIONS) - && !tableColumn.getValueType().equals(TableColumnValueType.BOOLEAN)) { + if (!tableColumn.getValueType().equals(TableColumnValueType.BOOLEAN) + && !tableColumn.getValueType().equals(TableColumnValueType.INTEGER) + && !tableColumn.getValueType().equals(TableColumnValueType.TEXT) + && !tableColumn.getValueType().equals(TableColumnValueType.VALUE_WITH_OPTIONS)) { throw new BadRequestException(errorMessage); } } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/AbstractChartDiagramCreationService.java b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/AbstractChartDiagramCreationService.java index 44484e31f9bf9bd7de4c0554366280614560ecaa..9c1ee39efa3544174657b4104d8e8110b873bf68 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/AbstractChartDiagramCreationService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/AbstractChartDiagramCreationService.java @@ -23,13 +23,13 @@ import de.eshg.statistics.persistence.entity.entry.IntegerEntry; import de.eshg.statistics.persistence.repository.AnalysisRepository; import de.eshg.statistics.persistence.repository.TableRowRepository; import java.math.BigDecimal; -import java.util.Collections; -import java.util.Comparator; +import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -40,6 +40,8 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.util.CollectionUtils; public abstract class AbstractChartDiagramCreationService<D, C> { + private static final List<String> BOOLEAN_KEYS = List.of("Ja", "Nein"); + protected final AnalysisService analysisService; protected final AnalysisRepository analysisRepository; @@ -57,7 +59,8 @@ public abstract class AbstractChartDiagramCreationService<D, C> { this.pageSizeForCollectionDiagramData = statisticsConfig.diagramData().pageSize(); } - abstract D initializeChartDataHolder(); + abstract D initializeChartDataHolder( + UUID analysisId, C chartConfigurationDto, List<TableColumnFilterParameter> filters); abstract int collectChartData( UUID analysisId, @@ -72,6 +75,33 @@ public abstract class AbstractChartDiagramCreationService<D, C> { AddDiagramRequest addDiagramRequest, D chartDataHolder); + protected static Map<Object, Integer> createCountingMap(TableColumn tableColumn) { + if (tableColumn == null) { + return new HashMap<>(); + } else if (tableColumn.getValueType().equals(TableColumnValueType.BOOLEAN) + || tableColumn.getValueType().equals(TableColumnValueType.VALUE_WITH_OPTIONS)) { + LinkedHashMap<Object, Integer> countingMap = new LinkedHashMap<>(); + initiallyFillKeyToCountingMapForStringKeys( + countingMap, getKeysForBooleanOrValueOptionsList(tableColumn)); + return countingMap; + } else { + return new TreeMap<>(); + } + } + + protected static List<String> getKeysForBooleanOrValueOptionsList(TableColumn tableColumn) { + if (tableColumn.getValueType().equals(TableColumnValueType.BOOLEAN)) { + return BOOLEAN_KEYS; + } else { + return tableColumn.getValueToMeanings().stream().map(ValueToMeaning::getValue).toList(); + } + } + + protected static void initiallyFillKeyToCountingMapForStringKeys( + Map<Object, Integer> destination, List<String> keys) { + keys.forEach(key -> destination.put(key, 0)); + } + protected static CellEntry getCellEntry(TableRow tableRow, TableColumn tableColumn) { return tableRow.getCellEntries().stream() .filter(cellEntry -> cellEntry.getTableColumn().getId().equals(tableColumn.getId())) @@ -122,38 +152,44 @@ public abstract class AbstractChartDiagramCreationService<D, C> { .map(filter -> TableRowSpecifications.createFilterSpecification(filter, aggregationResult)); } - protected static String getKeyForCellEntryBooleanTextOrValueOption(CellEntry cellEntry) { + protected static Object getKeyForCellEntryBooleanIntegerTextOrValueOption(CellEntry cellEntry) { if (cellEntry.getValue() == null) { return null; } if (cellEntry.getTableColumn().getValueType().equals(TableColumnValueType.BOOLEAN)) { return Boolean.TRUE.equals(cellEntry.getValue()) ? "Ja" : "Nein"; } + if (cellEntry.getTableColumn().getValueType().equals(TableColumnValueType.INTEGER)) { + return cellEntry.getValue(); + } if (cellEntry.getTableColumn().getValueType().equals(TableColumnValueType.TEXT)) { return cellEntry.getValue().toString(); } String stringValue = cellEntry.getValue().toString(); if (cellEntry.getTableColumn().getValueType().equals(TableColumnValueType.VALUE_WITH_OPTIONS) - && getValueToMeaningKeys(cellEntry.getTableColumn()).contains(stringValue)) { + && getValueToMeaningKeysSet(cellEntry.getTableColumn()).contains(stringValue)) { return stringValue; } return null; } - protected static Set<String> getValueToMeaningKeys(TableColumn tableColumn) { + protected static Set<String> getValueToMeaningKeysSet(TableColumn tableColumn) { return tableColumn.getValueToMeanings().stream() .map(ValueToMeaning::getValue) .collect(Collectors.toSet()); } - protected static <T> void addTableRowToCollectedChartData( - T primaryKey, String secondaryKey, Map<T, Map<String, Integer>> collectedChartData) { + protected static <T> void addTableRowToChartDataHolder( + Map<T, Map<Object, Integer>> chartDataHolder, + T primaryKey, + Object secondaryKey, + TableColumn secondaryTableColumn) { if (primaryKey == null || secondaryKey == null) { return; } - Map<String, Integer> secondaryToIntegerMap = - collectedChartData.computeIfAbsent(primaryKey, key -> new HashMap<>()); + Map<Object, Integer> secondaryToIntegerMap = + chartDataHolder.computeIfAbsent(primaryKey, key -> createCountingMap(secondaryTableColumn)); secondaryToIntegerMap.compute(secondaryKey, (key, count) -> (count == null) ? 1 : count + 1); } @@ -171,37 +207,38 @@ public abstract class AbstractChartDiagramCreationService<D, C> { }; } - protected static <T> Set<String> getKeysForTextValues(Map<T, Map<String, Integer>> valueMap) { - Set<String> keys = new HashSet<>(); - valueMap.values().forEach(map -> keys.addAll(map.keySet())); - return keys; - } - - protected static Set<String> getKeysForBooleanOrValueOption(TableColumn tableColumn) { - if (tableColumn == null) { - return Collections.emptySet(); - } - if (tableColumn.getValueType().equals(TableColumnValueType.BOOLEAN)) { - return Set.of("Ja", "Nein"); - } - if (tableColumn.getValueType().equals(TableColumnValueType.VALUE_WITH_OPTIONS)) { - return getValueToMeaningKeys(tableColumn); + protected static <T> void fillChartDataHolderWithMissingValues( + Map<T, Map<Object, Integer>> chartDataHolder, boolean onlyPrimaryAttribute) { + if (onlyPrimaryAttribute) { + chartDataHolder + .keySet() + .forEach(key -> chartDataHolder.get(key).computeIfAbsent(key, k -> 0)); + } else { + Set<Object> secondaryKeys = + chartDataHolder.values().stream() + .map(Map::keySet) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + chartDataHolder + .values() + .forEach( + secondaryToIntegerMap -> + secondaryKeys.forEach( + key -> secondaryToIntegerMap.computeIfAbsent(key, secondaryKey -> 0))); } - return Collections.emptySet(); } - protected static List<KeyToCount> mapToSortedKeyToCountList( - Map<String, Integer> keyToCountStringIntegerMap) { + protected static List<KeyToCount> mapToKeyToCounts( + Map<Object, Integer> keyToCountStringIntegerMap) { return keyToCountStringIntegerMap.entrySet().stream() - .map(AbstractChartDiagramCreationService::getKeyToCount) - .sorted(Comparator.comparing(KeyToCount::getKey)) + .map(entry -> getKeyToCount(String.valueOf(entry.getKey()), entry.getValue())) .toList(); } - private static KeyToCount getKeyToCount(Map.Entry<String, Integer> entry) { + private static KeyToCount getKeyToCount(String key, Integer count) { KeyToCount keyToCount = new KeyToCount(); - keyToCount.setKey(entry.getKey()); - keyToCount.setCount(entry.getValue()); + keyToCount.setKey(key); + keyToCount.setCount(count); return keyToCount; } } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/BarChartDiagramCreationService.java b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/BarChartDiagramCreationService.java index 573e97e1ee8d6cc6035eefe2118fd5423c2de1f5..653afb0d1cecd1a950da137cd8bff0b192f4036d 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/BarChartDiagramCreationService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/BarChartDiagramCreationService.java @@ -16,6 +16,7 @@ import de.eshg.statistics.mapper.AnalysisMapper; import de.eshg.statistics.persistence.entity.AbstractAggregationResult; import de.eshg.statistics.persistence.entity.Analysis; import de.eshg.statistics.persistence.entity.Diagram; +import de.eshg.statistics.persistence.entity.MinMaxNullUnknownValues; import de.eshg.statistics.persistence.entity.TableColumn; import de.eshg.statistics.persistence.entity.TableColumnValueType; import de.eshg.statistics.persistence.entity.TableRow; @@ -24,14 +25,14 @@ import de.eshg.statistics.persistence.entity.diagramdata.BarGroupData; import de.eshg.statistics.persistence.entity.diagramdata.KeyToCount; import de.eshg.statistics.persistence.repository.AnalysisRepository; import de.eshg.statistics.persistence.repository.TableRowRepository; +import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.TreeMap; import java.util.UUID; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; @@ -40,7 +41,7 @@ import org.springframework.transaction.annotation.Transactional; @Service public class BarChartDiagramCreationService extends AbstractChartDiagramCreationService< - Map<String, Map<String, Integer>>, BarChartConfigurationDto> { + Map<Object, Map<Object, Integer>>, BarChartConfigurationDto> { public BarChartDiagramCreationService( AnalysisService analysisService, AnalysisRepository analysisRepository, @@ -50,8 +51,68 @@ public class BarChartDiagramCreationService } @Override - Map<String, Map<String, Integer>> initializeChartDataHolder() { - return new HashMap<>(); + @Transactional(readOnly = true) + public Map<Object, Map<Object, Integer>> initializeChartDataHolder( + UUID analysisId, + BarChartConfigurationDto barChartConfigurationDto, + List<TableColumnFilterParameter> filters) { + Analysis analysis = analysisService.getAnalysisInternal(analysisId); + AbstractAggregationResult aggregationResult = analysis.getAggregationResult(); + + AggregationResultUtil.validateColumnFilters(filters, aggregationResult); + + TableColumn primaryTableColumn = + AggregationResultUtil.getTableColumn( + barChartConfigurationDto.primaryAttribute(), aggregationResult); + TableColumn secondaryTableColumn = + AggregationResultUtil.getTableColumn( + barChartConfigurationDto.secondaryAttribute(), aggregationResult); + + Map<Object, Map<Object, Integer>> chartDataHolder = + createChartDataHolderMap(primaryTableColumn.getValueType()); + initiallyFillBarChartMap( + chartDataHolder, + getKeysForIntegerBooleanOrValueOptions(primaryTableColumn), + secondaryTableColumn); + + return chartDataHolder; + } + + private static Map<Object, Map<Object, Integer>> createChartDataHolderMap( + TableColumnValueType valueType) { + return switch (valueType) { + case BOOLEAN, INTEGER, VALUE_WITH_OPTIONS -> new LinkedHashMap<>(); + default -> new TreeMap<>(); + }; + } + + private static List<?> getKeysForIntegerBooleanOrValueOptions(TableColumn primaryTableColumn) { + return switch (primaryTableColumn.getValueType()) { + case INTEGER -> getIntegerKeys(primaryTableColumn.getMinMaxNullUnknownValues()); + case BOOLEAN, VALUE_WITH_OPTIONS -> getKeysForBooleanOrValueOptionsList(primaryTableColumn); + default -> List.of(); + }; + } + + private static List<Integer> getIntegerKeys(MinMaxNullUnknownValues minMaxNullUnknownValues) { + List<Integer> integerKeys = new ArrayList<>(); + if (minMaxNullUnknownValues.getMinInteger() != null + && minMaxNullUnknownValues.getMaxInteger() != null) { + IntStream.rangeClosed( + minMaxNullUnknownValues.getMinInteger(), minMaxNullUnknownValues.getMaxInteger()) + .forEach(integerKeys::add); + } + if (minMaxNullUnknownValues.getUnknownValue() != null) { + integerKeys.add(Integer.parseInt(minMaxNullUnknownValues.getUnknownValue())); + } + return integerKeys; + } + + private static void initiallyFillBarChartMap( + Map<Object, Map<Object, Integer>> chartDataHolder, + List<?> keys, + TableColumn secondaryTableColumn) { + keys.forEach(key -> chartDataHolder.put(key, createCountingMap(secondaryTableColumn))); } @Override @@ -61,7 +122,7 @@ public class BarChartDiagramCreationService BarChartConfigurationDto barChartConfigurationDto, List<TableColumnFilterParameter> filters, int page, - Map<String, Map<String, Integer>> chartDataHolder) { + Map<Object, Map<Object, Integer>> chartDataHolder) { Analysis analysis = analysisService.getAnalysisInternal(analysisId); AbstractAggregationResult aggregationResult = analysis.getAggregationResult(); @@ -71,9 +132,6 @@ public class BarChartDiagramCreationService TableColumn secondaryTableColumn = AggregationResultUtil.getTableColumn( barChartConfigurationDto.secondaryAttribute(), aggregationResult); - if (page == 0) { - AggregationResultUtil.validateColumnFilters(filters, aggregationResult); - } Stream<Specification<TableRow>> notNullSpecifications; if (secondaryTableColumn == null) { @@ -98,21 +156,23 @@ public class BarChartDiagramCreationService private static void addTableRowToCollectedBarChartData( TableRow tableRow, - Map<String, Map<String, Integer>> chartDataHolder, + Map<Object, Map<Object, Integer>> chartDataHolder, TableColumn primaryTableColumn, TableColumn secondaryTableColumn) { - String primaryKey = - getKeyForCellEntryBooleanTextOrValueOption(getCellEntry(tableRow, primaryTableColumn)); + Object primaryKey = + getKeyForCellEntryBooleanIntegerTextOrValueOption( + getCellEntry(tableRow, primaryTableColumn)); - String secondaryKey; + Object secondaryKey; if (secondaryTableColumn == null) { secondaryKey = primaryKey; } else { secondaryKey = - getKeyForCellEntryBooleanTextOrValueOption(getCellEntry(tableRow, secondaryTableColumn)); + getKeyForCellEntryBooleanIntegerTextOrValueOption( + getCellEntry(tableRow, secondaryTableColumn)); } - addTableRowToCollectedChartData(primaryKey, secondaryKey, chartDataHolder); + addTableRowToChartDataHolder(chartDataHolder, primaryKey, secondaryKey, secondaryTableColumn); } @Override @@ -121,10 +181,10 @@ public class BarChartDiagramCreationService UUID analysisId, BarChartConfigurationDto barChartConfigurationDto, AddDiagramRequest addDiagramRequest, - Map<String, Map<String, Integer>> chartDataHolder) { + Map<Object, Map<Object, Integer>> chartDataHolder) { Analysis analysis = analysisService.getAnalysisInternal(analysisId); - fillBarChartDataWithMissingValues( - chartDataHolder, analysis.getAggregationResult(), barChartConfigurationDto); + fillChartDataHolderWithMissingValues( + chartDataHolder, barChartConfigurationDto.secondaryAttribute() == null); List<BarGroupData> groupDataList = getBarGroupDataList(chartDataHolder); @@ -145,65 +205,19 @@ public class BarChartDiagramCreationService return diagram.getExternalId(); } - private static void fillBarChartDataWithMissingValues( - Map<String, Map<String, Integer>> chartDataHolder, - AbstractAggregationResult aggregationResult, - BarChartConfigurationDto barChartConfigurationDto) { - TableColumn primaryTableColumn = - AggregationResultUtil.getTableColumn( - barChartConfigurationDto.primaryAttribute(), aggregationResult); - TableColumn secondaryTableColumn = - AggregationResultUtil.getTableColumn( - barChartConfigurationDto.secondaryAttribute(), aggregationResult); - - Set<String> primaryKeysBooleanValueOption = getKeysForBooleanOrValueOption(primaryTableColumn); - if (secondaryTableColumn == null) { - primaryKeysBooleanValueOption.forEach( - key -> - chartDataHolder.computeIfAbsent( - key, - secondaryKey -> { - Map<String, Integer> secondaryMap = new HashMap<>(); - secondaryMap.put(secondaryKey, 0); - return secondaryMap; - })); - } else { - Set<String> secondaryKeys; - if (secondaryTableColumn.getValueType().equals(TableColumnValueType.TEXT)) { - secondaryKeys = getKeysForTextValues(chartDataHolder); - } else { - secondaryKeys = getKeysForBooleanOrValueOption(secondaryTableColumn); - } - primaryKeysBooleanValueOption.forEach( - key -> chartDataHolder.computeIfAbsent(key, k -> new HashMap<>())); - - chartDataHolder - .keySet() - .forEach( - primaryKey -> { - Map<String, Integer> secondaryToIntegerMap = chartDataHolder.get(primaryKey); - secondaryKeys.forEach( - key -> secondaryToIntegerMap.computeIfAbsent(key, secondaryKey -> 0)); - }); - } - } - private static List<BarGroupData> getBarGroupDataList( - Map<String, Map<String, Integer>> chartDataHolder) { - Map<String, BarGroupData> groupDataMap = - chartDataHolder.entrySet().stream() - .map(entry -> mapToBarGroupData(entry.getKey(), entry.getValue())) - .collect(Collectors.toMap(BarGroupData::getKey, Function.identity())); - - return groupDataMap.keySet().stream().sorted().map(groupDataMap::get).toList(); + Map<Object, Map<Object, Integer>> chartDataHolder) { + return chartDataHolder.entrySet().stream() + .map(entry -> mapToBarGroupData(entry.getKey(), entry.getValue())) + .toList(); } private static BarGroupData mapToBarGroupData( - String key, Map<String, Integer> keyToCountStringIntegerMap) { - List<KeyToCount> keyToCounts = mapToSortedKeyToCountList(keyToCountStringIntegerMap); + Object primaryKey, Map<Object, Integer> keyToCountStringIntegerMap) { + List<KeyToCount> keyToCounts = mapToKeyToCounts(keyToCountStringIntegerMap); BarGroupData barGroupData = new BarGroupData(); - barGroupData.setKey(key); + barGroupData.setKey(String.valueOf(primaryKey)); barGroupData.addKeyToCounts(keyToCounts); return barGroupData; } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/ChoroplethMapDiagramCreationService.java b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/ChoroplethMapDiagramCreationService.java index 013d516fe1d0ad135791e51de8f2ebaa3ed1ad5b..76e725488e400644010a280897ffaf8b6051285c 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/ChoroplethMapDiagramCreationService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/ChoroplethMapDiagramCreationService.java @@ -52,8 +52,26 @@ public class ChoroplethMapDiagramCreationService } @Override - Map<String, List<BigDecimal>> initializeChartDataHolder() { - return new TreeMap<>(); + @Transactional(readOnly = true) + public Map<String, List<BigDecimal>> initializeChartDataHolder( + UUID analysisId, + ChoroplethMapConfigurationDto choroplethMapConfigurationDto, + List<TableColumnFilterParameter> filters) { + Analysis analysis = analysisService.getAnalysisInternal(analysisId); + AbstractAggregationResult aggregationResult = analysis.getAggregationResult(); + + AggregationResultUtil.validateColumnFilters(filters, aggregationResult); + + Map<String, List<BigDecimal>> chartDataHolder = new TreeMap<>(); + List<String> geoKeys = GeoJsonHandler.getGeoKeys(choroplethMapConfigurationDto.geoJson()); + initializeChoroplethMapData(chartDataHolder, geoKeys); + + return chartDataHolder; + } + + private static void initializeChoroplethMapData( + Map<String, List<BigDecimal>> chartDataHolder, List<String> geoKeys) { + geoKeys.forEach(geoKey -> chartDataHolder.computeIfAbsent(geoKey, key -> new ArrayList<>())); } @Override @@ -75,11 +93,6 @@ public class ChoroplethMapDiagramCreationService choroplethMapConfigurationDto.secondaryAttribute(), aggregationResult); List<String> geoKeys = GeoJsonHandler.getGeoKeys(choroplethMapConfigurationDto.geoJson()); - if (page == 0) { - AggregationResultUtil.validateColumnFilters(filters, aggregationResult); - initializeChoroplethMapData(chartDataHolder, geoKeys); - } - List<Specification<TableRow>> specifications = getNotNullSpecificationsForChoroplethMap(primaryTableColumn, secondaryTableColumn); @@ -97,11 +110,6 @@ public class ChoroplethMapDiagramCreationService tableRow, chartDataHolder, primaryTableColumn, secondaryTableColumn)); } - private static void initializeChoroplethMapData( - Map<String, List<BigDecimal>> chartDataHolder, List<String> geoKeys) { - geoKeys.forEach(geoKey -> chartDataHolder.computeIfAbsent(geoKey, key -> new ArrayList<>())); - } - private static List<Specification<TableRow>> getNotNullSpecificationsForChoroplethMap( TableColumn primaryTableColumn, TableColumn secondaryTableColumn) { List<Specification<TableRow>> notNullSpecifications = new ArrayList<>(); @@ -153,7 +161,7 @@ public class ChoroplethMapDiagramCreationService return switch (cellEntry.getTableColumn().getValueType()) { case TableColumnValueType.TEXT -> stringValue; case TableColumnValueType.VALUE_WITH_OPTIONS -> { - if (getValueToMeaningKeys(cellEntry.getTableColumn()).contains(stringValue)) { + if (getValueToMeaningKeysSet(cellEntry.getTableColumn()).contains(stringValue)) { yield stringValue; } else { yield null; diff --git a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/DataPointHolder.java b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/DataPointHolder.java index 544d327bb10ac30af62981c18336944e2886319e..1701a0755ea5d3dacec4c58bcbf8458b5f3acb2c 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/DataPointHolder.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/DataPointHolder.java @@ -7,5 +7,4 @@ package de.eshg.statistics.diagramcreation; import java.math.BigDecimal; -public record DataPointHolder( - Long rowId, BigDecimal xCoordinate, BigDecimal yCoordinate, String secondaryKey) {} +public record DataPointHolder(Long rowId, BigDecimal xCoordinate, BigDecimal yCoordinate) {} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/DiagramCreationService.java b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/DiagramCreationService.java index 04d277a47e10f81720989af4163bc0ccd81bf9ac..d7f6f31201780d994347b2a36522bdfcd4a92ba0 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/DiagramCreationService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/DiagramCreationService.java @@ -95,7 +95,9 @@ public class DiagramCreationService { UUID analysisId, C chartConfigurationDto, AddDiagramRequest addDiagramRequest) { - D chartDataHolder = service.initializeChartDataHolder(); + D chartDataHolder = + service.initializeChartDataHolder( + analysisId, chartConfigurationDto, addDiagramRequest.filters()); collectData(service, analysisId, chartConfigurationDto, addDiagramRequest, chartDataHolder); return service.addDiagram( analysisId, chartConfigurationDto, addDiagramRequest, chartDataHolder); diff --git a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/HistogramChartDiagramCreationService.java b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/HistogramChartDiagramCreationService.java index 3cde9cb7db05a21a2b763b133e35cfb4dd772467..ad97131c0e8791bdf83ee5dc272525ce1c0ffbbf 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/HistogramChartDiagramCreationService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/HistogramChartDiagramCreationService.java @@ -19,7 +19,6 @@ import de.eshg.statistics.persistence.entity.Analysis; import de.eshg.statistics.persistence.entity.ChartConfiguration; import de.eshg.statistics.persistence.entity.Diagram; import de.eshg.statistics.persistence.entity.TableColumn; -import de.eshg.statistics.persistence.entity.TableColumnValueType; import de.eshg.statistics.persistence.entity.TableRow; import de.eshg.statistics.persistence.entity.chart.HistogramBin; import de.eshg.statistics.persistence.entity.chart.HistogramChartConfiguration; @@ -32,7 +31,6 @@ import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; import java.util.stream.Stream; import org.hibernate.Hibernate; @@ -43,7 +41,7 @@ import org.springframework.transaction.annotation.Transactional; @Service public class HistogramChartDiagramCreationService extends AbstractChartDiagramCreationService< - Map<Long, Map<String, Integer>>, HistogramChartConfigurationDto> { + Map<Long, Map<Object, Integer>>, HistogramChartConfigurationDto> { public HistogramChartDiagramCreationService( AnalysisService analysisService, AnalysisRepository analysisRepository, @@ -53,8 +51,30 @@ public class HistogramChartDiagramCreationService } @Override - Map<Long, Map<String, Integer>> initializeChartDataHolder() { - return new HashMap<>(); + @Transactional(readOnly = true) + public Map<Long, Map<Object, Integer>> initializeChartDataHolder( + UUID analysisId, + HistogramChartConfigurationDto histogramChartConfigurationDto, + List<TableColumnFilterParameter> filters) { + Analysis analysis = analysisService.getAnalysisInternal(analysisId); + AbstractAggregationResult aggregationResult = analysis.getAggregationResult(); + + AggregationResultUtil.validateColumnFilters(filters, aggregationResult); + + HistogramChartConfiguration chartConfiguration = + (HistogramChartConfiguration) + Hibernate.unproxy(analysis.getChartConfiguration(), ChartConfiguration.class); + TableColumn secondaryTableColumn = + AggregationResultUtil.getTableColumn( + histogramChartConfigurationDto.secondaryAttribute(), aggregationResult); + + Map<Long, Map<Object, Integer>> chartDataHolder = new HashMap<>(); + + chartConfiguration + .getBins() + .forEach(bin -> chartDataHolder.put(bin.getId(), createCountingMap(secondaryTableColumn))); + + return chartDataHolder; } @Override @@ -64,7 +84,7 @@ public class HistogramChartDiagramCreationService HistogramChartConfigurationDto histogramChartConfigurationDto, List<TableColumnFilterParameter> filters, int page, - Map<Long, Map<String, Integer>> chartDataHolder) { + Map<Long, Map<Object, Integer>> chartDataHolder) { Analysis analysis = analysisService.getAnalysisInternal(analysisId); AbstractAggregationResult aggregationResult = analysis.getAggregationResult(); HistogramChartConfiguration chartConfiguration = @@ -81,9 +101,6 @@ public class HistogramChartDiagramCreationService TableColumn secondaryTableColumn = AggregationResultUtil.getTableColumn( histogramChartConfigurationDto.secondaryAttribute(), aggregationResult); - if (page == 0) { - AggregationResultUtil.validateColumnFilters(filters, aggregationResult); - } Specification<TableRow> notNullNotUnknownSpecification = TableRowSpecifications.getNotNullAndNotUnknownSpecificationDecimalAndInteger( @@ -115,7 +132,7 @@ public class HistogramChartDiagramCreationService private static void addTableRowToCollectedHistogramChartData( TableRow tableRow, - Map<Long, Map<String, Integer>> chartDataHolder, + Map<Long, Map<Object, Integer>> chartDataHolder, List<HistogramBin> bins, TableColumn primaryTableColumn, TableColumn secondaryTableColumn) { @@ -133,15 +150,16 @@ public class HistogramChartDiagramCreationService .map(BaseEntity::getId) .orElse(null); - String secondaryKey; + Object secondaryKey; if (secondaryTableColumn == null) { - secondaryKey = String.valueOf(primaryKey); + secondaryKey = primaryKey; } else { secondaryKey = - getKeyForCellEntryBooleanTextOrValueOption(getCellEntry(tableRow, secondaryTableColumn)); + getKeyForCellEntryBooleanIntegerTextOrValueOption( + getCellEntry(tableRow, secondaryTableColumn)); } - addTableRowToCollectedChartData(primaryKey, secondaryKey, chartDataHolder); + addTableRowToChartDataHolder(chartDataHolder, primaryKey, secondaryKey, secondaryTableColumn); } @Override @@ -150,16 +168,13 @@ public class HistogramChartDiagramCreationService UUID analysisId, HistogramChartConfigurationDto histogramChartConfigurationDto, AddDiagramRequest addDiagramRequest, - Map<Long, Map<String, Integer>> chartDataHolder) { + Map<Long, Map<Object, Integer>> chartDataHolder) { Analysis analysis = analysisService.getAnalysisInternal(analysisId); HistogramChartConfiguration chartConfiguration = (HistogramChartConfiguration) Hibernate.unproxy(analysis.getChartConfiguration(), ChartConfiguration.class); - fillHistogramChartDataWithMissingValues( - chartDataHolder, - chartConfiguration.getBins(), - analysis.getAggregationResult(), - histogramChartConfigurationDto); + fillChartDataHolderWithMissingValues( + chartDataHolder, histogramChartConfigurationDto.secondaryAttribute() == null); List<HistogramGroupData> histogramGroupDatas = chartConfiguration.getBins().stream() @@ -195,48 +210,18 @@ public class HistogramChartDiagramCreationService return diagram.getExternalId(); } - private static void fillHistogramChartDataWithMissingValues( - Map<Long, Map<String, Integer>> chartDataHolder, - List<HistogramBin> bins, - AbstractAggregationResult aggregationResult, - HistogramChartConfigurationDto histogramChartConfigurationDto) { - TableColumn secondaryTableColumn = - AggregationResultUtil.getTableColumn( - histogramChartConfigurationDto.secondaryAttribute(), aggregationResult); - bins.forEach(bin -> chartDataHolder.computeIfAbsent(bin.getId(), k -> new HashMap<>())); - if (secondaryTableColumn == null) { - chartDataHolder.forEach( - (key, secondaryMap) -> { - String stringKey = String.valueOf(key); - secondaryMap.computeIfAbsent(stringKey, k -> 0); - }); - } else { - Set<String> secondaryKeys; - if (secondaryTableColumn.getValueType().equals(TableColumnValueType.TEXT)) { - secondaryKeys = getKeysForTextValues(chartDataHolder); - } else { - secondaryKeys = getKeysForBooleanOrValueOption(secondaryTableColumn); - } - chartDataHolder - .values() - .forEach( - secondaryMap -> - secondaryKeys.forEach(key -> secondaryMap.computeIfAbsent(key, k -> 0))); - } - } - private static HistogramGroupData mapToHistogramGroupData( HistogramBin bin, - Map<Long, Map<String, Integer>> chartDataHolder, + Map<Long, Map<Object, Integer>> chartDataHolder, boolean withSecondaryAttribute) { HistogramGroupData histogramGroupData = new HistogramGroupData(); bin.addHistogramGroupData(histogramGroupData); - Map<String, Integer> dataForBin = chartDataHolder.get(bin.getId()); + Map<Object, Integer> dataForBin = chartDataHolder.get(bin.getId()); if (withSecondaryAttribute) { - histogramGroupData.addKeyToCounts(mapToSortedKeyToCountList(dataForBin)); + histogramGroupData.addKeyToCounts(mapToKeyToCounts(dataForBin)); } else { - histogramGroupData.setCount(dataForBin.values().stream().mapToInt(count -> count).sum()); + histogramGroupData.setCount(dataForBin.get(bin.getId())); } return histogramGroupData; } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/PieChartDiagramCreationService.java b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/PieChartDiagramCreationService.java index 57fe29525620690ab3cb8c5cce1b2714a2e5f29a..ecb0370c3a3c5683b023095190e18c397cdce7da 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/PieChartDiagramCreationService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/PieChartDiagramCreationService.java @@ -22,10 +22,8 @@ import de.eshg.statistics.persistence.entity.diagramdata.KeyToCount; import de.eshg.statistics.persistence.entity.diagramdata.PieChartData; import de.eshg.statistics.persistence.repository.AnalysisRepository; import de.eshg.statistics.persistence.repository.TableRowRepository; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; import java.util.stream.Stream; import org.springframework.data.jpa.domain.Specification; @@ -34,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional; @Service public class PieChartDiagramCreationService - extends AbstractChartDiagramCreationService<Map<String, Integer>, PieChartConfigurationDto> { + extends AbstractChartDiagramCreationService<Map<Object, Integer>, PieChartConfigurationDto> { public PieChartDiagramCreationService( AnalysisService analysisService, AnalysisRepository analysisRepository, @@ -44,8 +42,21 @@ public class PieChartDiagramCreationService } @Override - Map<String, Integer> initializeChartDataHolder() { - return new HashMap<>(); + @Transactional(readOnly = true) + public Map<Object, Integer> initializeChartDataHolder( + UUID analysisId, + PieChartConfigurationDto pieChartConfigurationDto, + List<TableColumnFilterParameter> filters) { + Analysis analysis = analysisService.getAnalysisInternal(analysisId); + AbstractAggregationResult aggregationResult = analysis.getAggregationResult(); + + AggregationResultUtil.validateColumnFilters(filters, aggregationResult); + + TableColumn tableColumn = + AggregationResultUtil.getTableColumn( + pieChartConfigurationDto.attribute(), aggregationResult); + + return createCountingMap(tableColumn); } @Override @@ -55,18 +66,13 @@ public class PieChartDiagramCreationService PieChartConfigurationDto pieChartConfigurationDto, List<TableColumnFilterParameter> filters, int page, - Map<String, Integer> chartDataHolder) { + Map<Object, Integer> chartDataHolder) { Analysis analysis = analysisService.getAnalysisInternal(analysisId); AbstractAggregationResult aggregationResult = analysis.getAggregationResult(); TableColumn tableColumn = AggregationResultUtil.getTableColumn( pieChartConfigurationDto.attribute(), aggregationResult); - if (page == 0) { - AggregationResultUtil.validateColumnFilters(filters, aggregationResult); - initiallyFillPieChartMap(chartDataHolder, tableColumn); - } - Stream<Specification<TableRow>> notNullSpecifications = Stream.of(TableRowSpecifications.getNotNullSpecification(tableColumn)); @@ -78,16 +84,10 @@ public class PieChartDiagramCreationService tableRow -> addTableRowToCollectedPieChartData(tableRow, chartDataHolder, tableColumn)); } - private static void initiallyFillPieChartMap( - Map<String, Integer> chartDataHolder, TableColumn tableColumn) { - Set<String> keys = getKeysForBooleanOrValueOption(tableColumn); - keys.forEach(key -> chartDataHolder.put(key, 0)); - } - private static void addTableRowToCollectedPieChartData( - TableRow tableRow, Map<String, Integer> collectedChartData, TableColumn tableColumn) { - String primaryKey = - getKeyForCellEntryBooleanTextOrValueOption(getCellEntry(tableRow, tableColumn)); + TableRow tableRow, Map<Object, Integer> collectedChartData, TableColumn tableColumn) { + Object primaryKey = + getKeyForCellEntryBooleanIntegerTextOrValueOption(getCellEntry(tableRow, tableColumn)); if (primaryKey != null) { collectedChartData.compute(primaryKey, (key, count) -> (count == null) ? 1 : count + 1); } @@ -99,10 +99,10 @@ public class PieChartDiagramCreationService UUID analysisId, PieChartConfigurationDto ignored, AddDiagramRequest addDiagramRequest, - Map<String, Integer> chartDataHolder) { + Map<Object, Integer> chartDataHolder) { Analysis analysis = analysisService.getAnalysisInternal(analysisId); - List<KeyToCount> keyToCounts = mapToSortedKeyToCountList(chartDataHolder); + List<KeyToCount> keyToCounts = mapToKeyToCounts(chartDataHolder); int evaluatedEntries = keyToCounts.stream().mapToInt(KeyToCount::getCount).sum(); diff --git a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/PointBasedChartDiagramCreationService.java b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/PointBasedChartDiagramCreationService.java index 27e068255d14e7dfd5086f413b2084efe5049dd6..5267eb0388aee66f20b06847954dbf6ad6152f2d 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/PointBasedChartDiagramCreationService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/diagramcreation/PointBasedChartDiagramCreationService.java @@ -30,10 +30,10 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Comparator; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; @@ -44,7 +44,10 @@ import org.springframework.transaction.annotation.Transactional; @Service public class PointBasedChartDiagramCreationService extends AbstractChartDiagramCreationService< - Map<String, List<DataPointHolder>>, PointBasedChartConfigurationDto> { + Map<Object, List<DataPointHolder>>, PointBasedChartConfigurationDto> { + + private static final String EMPTY_KEY = ""; + public PointBasedChartDiagramCreationService( AnalysisService analysisService, AnalysisRepository analysisRepository, @@ -54,8 +57,46 @@ public class PointBasedChartDiagramCreationService } @Override - Map<String, List<DataPointHolder>> initializeChartDataHolder() { - return new HashMap<>(); + @Transactional(readOnly = true) + public Map<Object, List<DataPointHolder>> initializeChartDataHolder( + UUID analysisId, + PointBasedChartConfigurationDto pointBasedChartConfiguration, + List<TableColumnFilterParameter> filters) { + Analysis analysis = analysisService.getAnalysisInternal(analysisId); + AbstractAggregationResult aggregationResult = analysis.getAggregationResult(); + + AggregationResultUtil.validateColumnFilters(filters, aggregationResult); + + TableColumn secondaryTableColumn = + AggregationResultUtil.getTableColumn( + pointBasedChartConfiguration.secondaryAttribute(), aggregationResult); + + Map<Object, List<DataPointHolder>> chartDataHolder = + createChartDataHolderMap(secondaryTableColumn); + getKeysForSecondaryTableColumn(secondaryTableColumn) + .forEach(key -> chartDataHolder.put(key, new ArrayList<>())); + + return chartDataHolder; + } + + private static Map<Object, List<DataPointHolder>> createChartDataHolderMap( + TableColumn secondaryTableColumn) { + if (secondaryTableColumn == null) { + return new LinkedHashMap<>(); + } else { + return switch (secondaryTableColumn.getValueType()) { + case BOOLEAN, VALUE_WITH_OPTIONS -> new LinkedHashMap<>(); + default -> new TreeMap<>(); + }; + } + } + + private static List<String> getKeysForSecondaryTableColumn(TableColumn secondaryTableColumn) { + if (secondaryTableColumn == null) { + return List.of(EMPTY_KEY); + } else { + return getKeysForBooleanOrValueOptionsList(secondaryTableColumn); + } } @Override @@ -65,17 +106,13 @@ public class PointBasedChartDiagramCreationService PointBasedChartConfigurationDto pointBasedChartConfiguration, List<TableColumnFilterParameter> filters, int page, - Map<String, List<DataPointHolder>> chartDataHolder) { + Map<Object, List<DataPointHolder>> chartDataHolder) { Analysis analysis = analysisService.getAnalysisInternal(analysisId); AbstractAggregationResult aggregationResult = analysis.getAggregationResult(); TableColumn secondaryTableColumn = AggregationResultUtil.getTableColumn( pointBasedChartConfiguration.secondaryAttribute(), aggregationResult); - if (page == 0) { - AggregationResultUtil.validateColumnFilters(filters, aggregationResult); - initiallyFillPointBasedChartMap(chartDataHolder, secondaryTableColumn); - } TableColumn xTableColumn = AggregationResultUtil.getTableColumn( @@ -98,12 +135,6 @@ public class PointBasedChartDiagramCreationService tableRow, chartDataHolder, xTableColumn, yTableColumn, secondaryTableColumn)); } - private static void initiallyFillPointBasedChartMap( - Map<String, List<DataPointHolder>> chartDataHolder, TableColumn secondaryTableColumn) { - Set<String> secondaryKeys = getKeysForBooleanOrValueOption(secondaryTableColumn); - secondaryKeys.forEach(key -> chartDataHolder.put(key, new ArrayList<>())); - } - private static List<Specification<TableRow>> getNotNullSpecificationsForDataPointCharts( TableColumn xTableColumn, TableColumn yTableColumn, TableColumn secondaryTableColumn) { List<Specification<TableRow>> notNullSpecifications = new ArrayList<>(); @@ -122,7 +153,7 @@ public class PointBasedChartDiagramCreationService private static void addTableRowToCollectedPointBasedChartData( TableRow tableRow, - Map<String, List<DataPointHolder>> chartDataHolder, + Map<Object, List<DataPointHolder>> chartDataHolder, TableColumn xTableColumn, TableColumn yTableColumn, TableColumn secondaryTableColumn) { @@ -133,16 +164,14 @@ public class PointBasedChartDiagramCreationService getValueAsBigDecimal(yTableColumn.getValueType(), getCellEntry(tableRow, yTableColumn)); if (secondaryTableColumn == null) { - chartDataHolder - .computeIfAbsent("", key -> new ArrayList<>()) - .add(new DataPointHolder(tableRow.getId(), xValue, yValue, null)); + chartDataHolder.get(EMPTY_KEY).add(new DataPointHolder(tableRow.getId(), xValue, yValue)); } else { CellEntry secondaryCellEntry = getCellEntry(tableRow, secondaryTableColumn); - String secondaryKey = getKeyForCellEntryBooleanTextOrValueOption(secondaryCellEntry); + Object secondaryKey = getKeyForCellEntryBooleanIntegerTextOrValueOption(secondaryCellEntry); if (secondaryKey != null) { chartDataHolder .computeIfAbsent(secondaryKey, key -> new ArrayList<>()) - .add(new DataPointHolder(tableRow.getId(), xValue, yValue, secondaryKey)); + .add(new DataPointHolder(tableRow.getId(), xValue, yValue)); } } } @@ -153,7 +182,7 @@ public class PointBasedChartDiagramCreationService UUID analysisId, PointBasedChartConfigurationDto pointBasedChartConfiguration, AddDiagramRequest addDiagramRequest, - Map<String, List<DataPointHolder>> chartDataHolder) { + Map<Object, List<DataPointHolder>> chartDataHolder) { Analysis analysis = analysisService.getAnalysisInternal(analysisId); Comparator<DataPointHolder> comparator = @@ -168,32 +197,27 @@ public class PointBasedChartDiagramCreationService List<DataPointGroup> dataPointGroups = new ArrayList<>(); if (pointBasedChartConfiguration.secondaryAttribute() == null) { List<DataPoint> dataPoints = - chartDataHolder.computeIfAbsent("", key -> new ArrayList<>()).stream() - .sorted(comparator) - .map(mapFunction) - .toList(); + chartDataHolder.get(EMPTY_KEY).stream().sorted(comparator).map(mapFunction).toList(); DataPointGroup dataPointGroup = new DataPointGroup(); dataPointGroup.addDataPoints(dataPoints); dataPointGroups.add(dataPointGroup); evaluatedDataAmount.addAndGet(dataPoints.size()); } else { - chartDataHolder.keySet().stream() - .sorted() + chartDataHolder + .keySet() .forEach( key -> { List<DataPoint> dataPoints = chartDataHolder.get(key).stream().sorted(comparator).map(mapFunction).toList(); DataPointGroup dataPointGroup = new DataPointGroup(); - dataPointGroup.setKey(key); + dataPointGroup.setKey(String.valueOf(key)); dataPointGroup.addDataPoints(dataPoints); dataPointGroups.add(dataPointGroup); evaluatedDataAmount.addAndGet(dataPoints.size()); }); } - if (pointBasedChartConfiguration - instanceof ScatterChartConfigurationDto scatterChartConfigurationDto - && scatterChartConfigurationDto.trendLine()) { + if (pointBasedChartConfiguration instanceof ScatterChartConfigurationDto) { dataPointGroups.forEach( dataPointGroup -> dataPointGroup.setTrendLine(determineTrendLine(dataPointGroup))); } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/mapper/AnalysisMapper.java b/backend/statistics/src/main/java/de/eshg/statistics/mapper/AnalysisMapper.java index 376de85c524919507aa41dc8c64264bad52cfe63..b1a2bb93b72980e8f83d25ecae55f198a2555439 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/mapper/AnalysisMapper.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/mapper/AnalysisMapper.java @@ -521,45 +521,70 @@ public class AnalysisMapper { } private static DiagramDataDto mapToApi(LineOrScatterChartData lineOrScatterChartData) { - ChartConfiguration chartConfiguration = - Hibernate.unproxy( - lineOrScatterChartData.getDiagram().getAnalysis().getChartConfiguration(), - ChartConfiguration.class); - - boolean isSimple; - boolean isLineChart; - if (chartConfiguration instanceof LineChartConfiguration lineChartConfiguration) { - isSimple = lineChartConfiguration.getSecondaryAttributeSelection() == null; - isLineChart = true; + return switch (Hibernate.unproxy( + lineOrScatterChartData.getDiagram().getAnalysis().getChartConfiguration(), + ChartConfiguration.class)) { + case LineChartConfiguration lineChartConfiguration -> + mapToApiLineChartData(lineOrScatterChartData, lineChartConfiguration); + case ScatterChartConfiguration scatterChartConfiguration -> + mapToApiScatterChartData(lineOrScatterChartData, scatterChartConfiguration); + default -> throw new IllegalStateException(""); + }; + } + + private static DiagramDataDto mapToApiScatterChartData( + LineOrScatterChartData lineOrScatterChartData, + ScatterChartConfiguration scatterChartConfiguration) { + boolean isSimple = scatterChartConfiguration.getSecondaryAttributeSelection() == null; + boolean mapTrendLine = scatterChartConfiguration.showTrendLine(); + if (isSimple) { + return mapToApiScatterSimple(lineOrScatterChartData, mapTrendLine); } else { - isSimple = - ((ScatterChartConfiguration) chartConfiguration).getSecondaryAttributeSelection() == null; - isLineChart = false; + return mapToApiScatterCategorized(lineOrScatterChartData, mapTrendLine); } - if (isLineChart) { - if (isSimple) { - return new LineChartDataSimpleDto( - mapToDataPoints(lineOrScatterChartData.getDataPointGroups().getFirst())); - } else { - return new LineChartDataCategorizedDto( - lineOrScatterChartData.getDataPointGroups().stream() - .map(AnalysisMapper::mapToApi) - .toList()); - } + } + + private static ScatterChartDataSimpleDto mapToApiScatterSimple( + LineOrScatterChartData lineOrScatterChartData, boolean mapTrendLine) { + DataPointGroup dataPointGroup = lineOrScatterChartData.getDataPointGroups().getFirst(); + return new ScatterChartDataSimpleDto( + mapToDataPoints(dataPointGroup), + mapTrendLine ? mapToApi(dataPointGroup.getTrendLine()) : null); + } + + private static ScatterChartDataCategorizedDto mapToApiScatterCategorized( + LineOrScatterChartData lineOrScatterChartData, boolean mapTrendLine) { + return new ScatterChartDataCategorizedDto( + lineOrScatterChartData.getDataPointGroups().stream() + .map(dataPointGroup -> AnalysisMapper.mapToApi(dataPointGroup, mapTrendLine)) + .toList()); + } + + private static DiagramDataDto mapToApiLineChartData( + LineOrScatterChartData lineOrScatterChartData, + LineChartConfiguration lineChartConfiguration) { + boolean isSimple = lineChartConfiguration.getSecondaryAttributeSelection() == null; + if (isSimple) { + return mapToApiLineSimple(lineOrScatterChartData); } else { - if (isSimple) { - DataPointGroup dataPointGroup = lineOrScatterChartData.getDataPointGroups().getFirst(); - return new ScatterChartDataSimpleDto( - mapToDataPoints(dataPointGroup), mapToApi(dataPointGroup.getTrendLine())); - } else { - return new ScatterChartDataCategorizedDto( - lineOrScatterChartData.getDataPointGroups().stream() - .map(AnalysisMapper::mapToApi) - .toList()); - } + return mapToApiLineCategorized(lineOrScatterChartData); } } + private static LineChartDataSimpleDto mapToApiLineSimple( + LineOrScatterChartData lineOrScatterChartData) { + return new LineChartDataSimpleDto( + mapToDataPoints(lineOrScatterChartData.getDataPointGroups().getFirst())); + } + + private static LineChartDataCategorizedDto mapToApiLineCategorized( + LineOrScatterChartData lineOrScatterChartData) { + return new LineChartDataCategorizedDto( + lineOrScatterChartData.getDataPointGroups().stream() + .map(AnalysisMapper::mapToApi) + .toList()); + } + private static List<DataPointDto> mapToDataPoints(DataPointGroup dataPointGroup) { return dataPointGroup.getDataPoints().stream().map(AnalysisMapper::mapToApi).toList(); } @@ -568,17 +593,21 @@ public class AnalysisMapper { return new DataPointDto(dataPoint.getXCoordinate(), dataPoint.getYCoordinate()); } - private static TrendLineDto mapToApi(TrendLine trendLine) { - return trendLine == null - ? null - : new TrendLineDto(trendLine.getLineSlope(), trendLine.getLineOffset()); + private static DataPointGroupDto mapToApi(DataPointGroup dataPointGroup) { + return mapToApi(dataPointGroup, false); } - private static DataPointGroupDto mapToApi(DataPointGroup dataPointGroup) { + private static DataPointGroupDto mapToApi(DataPointGroup dataPointGroup, boolean mapTrendLine) { return new DataPointGroupDto( dataPointGroup.getKey(), dataPointGroup.getDataPoints().stream().map(AnalysisMapper::mapToApi).toList(), - mapToApi(dataPointGroup.getTrendLine())); + mapTrendLine ? mapToApi(dataPointGroup.getTrendLine()) : null); + } + + private static TrendLineDto mapToApi(TrendLine trendLine) { + return trendLine == null + ? null + : new TrendLineDto(trendLine.getLineSlope(), trendLine.getLineOffset()); } private static DiagramDataDto mapToApi(PieChartData pieChartData) { diff --git a/backend/statistics/src/main/resources/application.properties b/backend/statistics/src/main/resources/application.properties index 4eb8e872d0a549411f284baf250063693a8f07a9..8a98f143dd81b1a58806cbabe53ac2254aef52a5 100644 --- a/backend/statistics/src/main/resources/application.properties +++ b/backend/statistics/src/main/resources/application.properties @@ -20,5 +20,6 @@ spring.security.oauth2.client.provider.eshg-keycloak.token-uri=${eshg.keycloak.i eshg.statistics.business-module.sensitive-data-permissions[SCHOOL_ENTRY]=SCHOOL_ENTRY_ADMIN eshg.statistics.business-module.sensitive-data-permissions[INSPECTION]=INSPECTION_PROCEDURE_EDIT eshg.statistics.business-module.sensitive-data-permissions[DENTAL]=DENTAL_ADMIN +eshg.statistics.business-module.sensitive-data-permissions[OFFICIAL_MEDICAL_SERVICE]=OFFICIAL_MEDICAL_SERVICE_ADMIN logging.level.de.eshg.statistics=DEBUG diff --git a/backend/sti-protection/gradle.lockfile b/backend/sti-protection/gradle.lockfile index 024a5d5b0cd25c98b0905c13b73aaaa864416d3b..d776476fca609d005004ef328dfd0e3a919ce4a3 100644 --- a/backend/sti-protection/gradle.lockfile +++ b/backend/sti-protection/gradle.lockfile @@ -15,6 +15,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -22,6 +23,7 @@ com.github.docker-java:docker-java-transport:3.4.0=testCompileClasspath,testRunt com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.10.0=testRuntimeClasspath com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0=testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -30,12 +32,15 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=comp com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -44,6 +49,8 @@ com.tngtech.archunit:archunit-junit5:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit:1.3.0=testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.17.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.18.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:commons-lang:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -99,12 +106,16 @@ net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCom net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.24.0=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -113,14 +124,25 @@ org.apache.pdfbox:pdfbox-io:3.0.3=productionRuntimeClasspath,runtimeClasspath,te org.apache.pdfbox:pdfbox-tools:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.34=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-anim:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-awt-util:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-bridge:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -170,6 +192,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/sti-protection/openApi.json b/backend/sti-protection/openApi.json index 8e9a37b4b15dbf69a509ead9229304ccd10a3a5e..0c0b986f6cf7548f651534ccf21bd276ec75afd9 100644 --- a/backend/sti-protection/openApi.json +++ b/backend/sti-protection/openApi.json @@ -2282,6 +2282,101 @@ "format" : "int32", "default" : 25 } + }, { + "in" : "query", + "name" : "creationDateStart", + "required" : false, + "schema" : { + "type" : "string", + "format" : "date" + } + }, { + "in" : "query", + "name" : "creationDateEnd", + "required" : false, + "schema" : { + "type" : "string", + "format" : "date" + } + }, { + "in" : "query", + "name" : "yearOfBirth", + "required" : false, + "schema" : { + "type" : "integer", + "format" : "int32" + } + }, { + "in" : "query", + "name" : "appointmentDateStart", + "required" : false, + "schema" : { + "type" : "string", + "format" : "date" + } + }, { + "in" : "query", + "name" : "appointmentDateEnd", + "required" : false, + "schema" : { + "type" : "string", + "format" : "date" + } + }, { + "in" : "query", + "name" : "gender", + "required" : false, + "schema" : { + "uniqueItems" : true, + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Gender" + } + } + }, { + "in" : "query", + "name" : "concern", + "required" : false, + "schema" : { + "uniqueItems" : true, + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Concern" + } + } + }, { + "in" : "query", + "name" : "procedureStatus", + "required" : false, + "schema" : { + "uniqueItems" : true, + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/ProcedureStatus" + } + } + }, { + "in" : "query", + "name" : "labStatus", + "required" : false, + "schema" : { + "uniqueItems" : true, + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/LabStatus" + } + } + }, { + "in" : "query", + "name" : "createdBy", + "required" : false, + "schema" : { + "uniqueItems" : true, + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/CreatedByUserType" + } + } } ], "responses" : { "200" : { @@ -3321,7 +3416,7 @@ "name" : "limit", "required" : false, "schema" : { - "maximum" : 200, + "maximum" : 2200, "minimum" : 1, "type" : "integer", "format" : "int32", @@ -4738,6 +4833,10 @@ } } }, + "CreatedByUserType" : { + "type" : "string", + "enum" : [ "EMPLOYEE", "CITIZEN_PORTAL" ] + }, "DataOrigin" : { "type" : "string", "description" : "A list of possible origins of Persons and Facility in the Central Files. EDIT will only be set automatically on changes. EXTERNAL is for entries that come, e.g., from the citizen portal. IMPORT is reserved for automatic imports. MANUAL shall be set for every creation or connection done by an employee.", diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/CitizenAppointmentService.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/CitizenAppointmentService.java index 76b661b52ca3ff405d557f0d1f8ba5e1c3444f96..4693ffa3a49d3b9c526d32d8f0db520d5394fa01 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/CitizenAppointmentService.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/CitizenAppointmentService.java @@ -11,6 +11,7 @@ import de.eshg.base.citizenuser.api.CitizenAccessCodeUserDto; import de.eshg.lib.rest.oauth.client.commons.ModuleClientAuthenticator; import de.eshg.stiprotection.persistence.data.PersonData; import de.eshg.stiprotection.persistence.db.Concern; +import de.eshg.stiprotection.persistence.db.CreatedByUserType; import de.eshg.stiprotection.persistence.db.ProcedureExpiration; import de.eshg.stiprotection.persistence.db.ProcedureExpirationRepository; import de.eshg.stiprotection.persistence.db.StiProtectionProcedure; @@ -38,7 +39,8 @@ public class CitizenAppointmentService { } public StiProtectionProcedure createProcedureWithExpiryDate(Concern concern) { - StiProtectionProcedure procedure = stiProtectionService.saveProcedure(concern); + StiProtectionProcedure procedure = + stiProtectionService.saveProcedure(concern, CreatedByUserType.CITIZEN_PORTAL); ProcedureExpiration procedureExpiration = new ProcedureExpiration(procedure); procedureExpirationRepository.save(procedureExpiration); return procedure; diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/CitizenService.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/CitizenService.java index fe0d2cdacb2ce2416a4fa97f92ff60a4a5925282..9134692fe261dabf31dfcc76fbd5aaf80116969b 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/CitizenService.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/CitizenService.java @@ -23,15 +23,14 @@ public class CitizenService { } public StiProtectionProcedureData getProcedure(Jwt principal) { - return new StiProtectionProcedureData( - findByAnonymouseUserlId(getCitizenUserId(principal)), null); + return new StiProtectionProcedureData(findByAnonymousUserId(getCitizenUserId(principal)), null); } private UUID getCitizenUserId(Jwt principal) { return UUID.fromString(principal.getSubject()); } - private StiProtectionProcedure findByAnonymouseUserlId(UUID anonymousUserId) { + private StiProtectionProcedure findByAnonymousUserId(UUID anonymousUserId) { return repository .findByAnonymousUserId(anonymousUserId) .orElseThrow( diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/StiProtectionProcedureController.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/StiProtectionProcedureController.java index 5bb25ba24a069421d462fd172fb4b0e069ce5f73..7a869635d36c16a32f6f73fda0654ef11cba46be 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/StiProtectionProcedureController.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/StiProtectionProcedureController.java @@ -22,6 +22,7 @@ import de.eshg.stiprotection.api.CreateProcedureRequest; import de.eshg.stiprotection.api.CreateProcedureResponse; import de.eshg.stiprotection.api.GetProcedureResponse; import de.eshg.stiprotection.api.GetProceduresOverviewResponse; +import de.eshg.stiprotection.api.GetStiProtectionProceduresFilterOptions; import de.eshg.stiprotection.api.GetStiProtectionProceduresPaginationOptions; import de.eshg.stiprotection.api.GetStiProtectionProceduresSortOptions; import de.eshg.stiprotection.api.UpdateAppointmentRequest; @@ -35,6 +36,7 @@ import de.eshg.stiprotection.persistence.data.AppointmentData; import de.eshg.stiprotection.persistence.data.ResultPage; import de.eshg.stiprotection.persistence.data.StiProtectionProcedureData; import de.eshg.stiprotection.persistence.db.AppointmentHistoryEntry; +import de.eshg.stiprotection.persistence.db.CreatedByUserType; import de.eshg.stiprotection.persistence.db.StiProtectionProcedure; import de.eshg.stiprotection.persistence.db.StiProtectionSystemProgressEntryType; import de.eshg.stiprotection.util.ProgressEntryUtil; @@ -71,7 +73,6 @@ public class StiProtectionProcedureController { private final StiProtectionProcedureService stiProtectionService; private final AppointmentService appointmentService; private final AuditLogger auditLogger; - private final StiProtectionProcedureDeletionService procedureDeletionService; private final StiProtectionProcedureFinder procedureFinder; private final ProgressEntryUtil progressEntryUtil; private final FollowUpProcedureService followUpProcedureService; @@ -80,14 +81,12 @@ public class StiProtectionProcedureController { StiProtectionProcedureService stiProtectionService, AppointmentService appointmentService, AuditLogger auditLogger, - StiProtectionProcedureDeletionService procedureDeletionService, StiProtectionProcedureFinder procedureFinder, ProgressEntryUtil progressEntryUtil, FollowUpProcedureService followUpProcedureService) { this.stiProtectionService = stiProtectionService; this.appointmentService = appointmentService; this.auditLogger = auditLogger; - this.procedureDeletionService = procedureDeletionService; this.procedureFinder = procedureFinder; this.progressEntryUtil = progressEntryUtil; this.followUpProcedureService = followUpProcedureService; @@ -99,7 +98,8 @@ public class StiProtectionProcedureController { public CreateProcedureResponse createProcedure( @Valid @RequestBody CreateProcedureRequest request) { StiProtectionProcedure procedure = - stiProtectionService.createProcedure(ConcernMapper.toDatabaseType(request.concern())); + stiProtectionService.createProcedure( + ConcernMapper.toDatabaseType(request.concern()), CreatedByUserType.EMPLOYEE); stiProtectionService.addPerson(procedure, PersonMapper.toDataType(request)); appointmentService.createAppointment(procedure, AppointmentMapper.toDataType(request)); String pin = stiProtectionService.generatePin(); @@ -132,10 +132,12 @@ public class StiProtectionProcedureController { @Valid @ParameterObject @InlineParameterObject GetStiProtectionProceduresSortOptions sortOptions, @Valid @ParameterObject @InlineParameterObject - GetStiProtectionProceduresPaginationOptions paginationOptions) { + GetStiProtectionProceduresPaginationOptions paginationOptions, + @Valid @ParameterObject @InlineParameterObject + GetStiProtectionProceduresFilterOptions filterOptions) { ResultPage<StiProtectionProcedureData> procedures = - stiProtectionService.getProcedures(sortOptions, paginationOptions); + stiProtectionService.getProcedures(sortOptions, paginationOptions, filterOptions); return new GetProceduresOverviewResponse( procedures.totalPages(), @@ -213,7 +215,9 @@ public class StiProtectionProcedureController { @ProcedureStatusTransition public void closeProcedure(@PathVariable("id") UUID procedureId) { StiProtectionProcedure procedure = procedureFinder.findByExternalId(procedureId); - appointmentService.cancelAppointment(procedure); + if (procedure.getAppointment() != null || procedure.getUserDefinedAppointment() != null) { + appointmentService.cancelAppointment(procedure); + } stiProtectionService.closeProcedure(procedure); } @@ -268,12 +272,15 @@ public class StiProtectionProcedureController { @Valid @RequestBody CreateFollowUpProcedureRequest request) { StiProtectionProcedure procedure = procedureFinder.findByExternalId(procedureId); if (procedure.getProcedureStatus().isOpen()) { - appointmentService.cancelAppointment(procedure); stiProtectionService.closeProcedure(procedure); + if (procedure.getAppointment() != null || procedure.getUserDefinedAppointment() != null) { + appointmentService.cancelAppointment(procedure); + } } StiProtectionProcedure followUpProcedure = - stiProtectionService.createProcedure(ConcernMapper.toDatabaseType(request.concern())); + stiProtectionService.createProcedure( + ConcernMapper.toDatabaseType(request.concern()), CreatedByUserType.EMPLOYEE); followUpProcedure.setFollowUp(true); stiProtectionService.addPerson( followUpProcedure, PersonMapper.toDataType(procedure.getPerson())); diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/StiProtectionProcedureService.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/StiProtectionProcedureService.java index dd7001ce368f425be80f46633cb1d2d00d939c9b..a1c06dbeae457c2f3e5d94d24641f058f6c6e1e3 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/StiProtectionProcedureService.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/StiProtectionProcedureService.java @@ -11,13 +11,16 @@ import static de.eshg.stiprotection.pdf.identification.DocumentParameters.toAppo import static de.eshg.stiprotection.pdf.identification.DocumentParameters.toConsultationAppointment; import static de.eshg.stiprotection.pdf.identification.DocumentParameters.toDocumentDate; import static de.eshg.stiprotection.persistence.db.StiProtectionSystemProgressEntryType.PERSON_DETAILS_UPDATED; +import static org.springframework.data.jpa.domain.Specification.allOf; +import de.eshg.base.GenderDto; import de.eshg.base.calendar.api.TimeRange; import de.eshg.base.citizenuser.CitizenAccessCodeUserApi; import de.eshg.base.citizenuser.api.AddCitizenAccessCodeUserWithPinCredentialRequest; import de.eshg.base.citizenuser.api.CitizenAccessCodeUserDto; import de.eshg.base.citizenuser.api.CredentialTypeDto; import de.eshg.base.citizenuser.api.VerifyCitizenAccessCodeUserCredentialsRequest; +import de.eshg.lib.appointmentblock.MappingUtil; import de.eshg.lib.auditlog.AuditLogger; import de.eshg.lib.document.generator.department.DepartmentClient; import de.eshg.lib.document.generator.department.DepartmentLogo; @@ -28,12 +31,17 @@ import de.eshg.lib.procedure.domain.model.Procedure_; import de.eshg.lib.procedure.domain.model.RelatedPerson; import de.eshg.lib.procedure.domain.model.TaskStatus; import de.eshg.lib.procedure.domain.model.TaskType; +import de.eshg.lib.procedure.model.ProcedureStatusDto; import de.eshg.rest.service.error.BadRequestException; import de.eshg.rest.service.security.CurrentUserHelper; +import de.eshg.stiprotection.api.ConcernDto; +import de.eshg.stiprotection.api.CreatedByUserTypeDto; +import de.eshg.stiprotection.api.GetStiProtectionProceduresFilterOptions; import de.eshg.stiprotection.api.GetStiProtectionProceduresPaginationOptions; import de.eshg.stiprotection.api.GetStiProtectionProceduresSortByDto; import de.eshg.stiprotection.api.GetStiProtectionProceduresSortOptions; import de.eshg.stiprotection.api.GetStiProtectionProceduresSortOrderDto; +import de.eshg.stiprotection.api.LabStatusDto; import de.eshg.stiprotection.mapper.PersonMapper; import de.eshg.stiprotection.pdf.identification.AnonymousIdentificationDocument; import de.eshg.stiprotection.pdf.identification.AnonymousIdentificationDocumentService; @@ -44,7 +52,11 @@ import de.eshg.stiprotection.persistence.data.PersonData; import de.eshg.stiprotection.persistence.data.ResultPage; import de.eshg.stiprotection.persistence.data.StiProtectionProcedureData; import de.eshg.stiprotection.persistence.db.Concern; +import de.eshg.stiprotection.persistence.db.CreatedByUserType; +import de.eshg.stiprotection.persistence.db.Gender; +import de.eshg.stiprotection.persistence.db.LabStatus; import de.eshg.stiprotection.persistence.db.Person; +import de.eshg.stiprotection.persistence.db.Person_; import de.eshg.stiprotection.persistence.db.StiProtectionProcedure; import de.eshg.stiprotection.persistence.db.StiProtectionProcedureRepository; import de.eshg.stiprotection.persistence.db.StiProtectionProcedure_; @@ -52,10 +64,17 @@ import de.eshg.stiprotection.persistence.db.StiProtectionTask; import de.eshg.stiprotection.util.ProgressEntryUtil; import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; +import jakarta.persistence.metamodel.SingularAttribute; import java.time.Clock; +import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; +import java.time.Year; +import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.data.domain.Page; @@ -96,15 +115,16 @@ public class StiProtectionProcedureService { this.progressEntryUtil = progressEntryUtil; } - public StiProtectionProcedure createProcedure(Concern concern) { + public StiProtectionProcedure createProcedure(Concern concern, CreatedByUserType createdBy) { StiProtectionProcedure procedure = - StiProtectionProcedure.newProcedure(concern, clock, auditLogger); + StiProtectionProcedure.newProcedure(concern, createdBy, clock, auditLogger); procedure.addTask(createTask()); return repository.save(procedure); } - public StiProtectionProcedure saveProcedure(Concern concern) { - return repository.save(StiProtectionProcedure.newProcedure(concern, clock, auditLogger)); + public StiProtectionProcedure saveProcedure(Concern concern, CreatedByUserType createdBy) { + return repository.save( + StiProtectionProcedure.newProcedure(concern, createdBy, clock, auditLogger)); } public void addPerson(StiProtectionProcedure procedure, PersonData personData) { @@ -142,16 +162,25 @@ public class StiProtectionProcedureService { public ResultPage<StiProtectionProcedureData> getProcedures( GetStiProtectionProceduresSortOptions sortOptions, - GetStiProtectionProceduresPaginationOptions paginationOptions) { + GetStiProtectionProceduresPaginationOptions paginationOptions, + GetStiProtectionProceduresFilterOptions filterOptions) { PageRequest pageRequest = PageRequest.of(paginationOptions.pageNumber(), paginationOptions.pageSize()); - Page<StiProtectionProcedure> procedures = - repository.findAll( - Specification.where(joinPersonAndSort(sortOptions.sortOrder(), sortOptions.sortBy())), - pageRequest); - + Specification<StiProtectionProcedure> spec = + allOf( + filterByCreatedAt(filterOptions), + filterByYearOfBirth(filterOptions), + filterByAppointmentDate(filterOptions), + filterByGender(filterOptions), + filterByConcern(filterOptions), + filterByProcedureStatus(filterOptions), + filterByLabStatus(filterOptions), + filterByCreatedBy(filterOptions), + orderBy(sortOptions.sortOrder(), sortOptions.sortBy())); + + Page<StiProtectionProcedure> procedures = repository.findAll(spec, pageRequest); if (procedures.isEmpty()) { return new ResultPage<>(0, 0, List.of()); } @@ -162,7 +191,137 @@ public class StiProtectionProcedureService { procedures.stream().map(this::toProcedureData).toList()); } - private Specification<StiProtectionProcedure> joinPersonAndSort( + private Specification<StiProtectionProcedure> filterByCreatedBy( + GetStiProtectionProceduresFilterOptions filterOptions) { + Set<CreatedByUserTypeDto> dto = filterOptions.createdBy(); + if (dto != null) { + return (root, query, criteriaBuilder) -> + root.get(StiProtectionProcedure_.CREATED_BY) + .in(mapFilterBy(CreatedByUserType.class, dto)); + } else { + return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + } + } + + private static <T extends Enum<T>, S extends Enum<S>> List<S> mapFilterBy( + Class<S> targetEnum, Collection<T> sourceValues) { + return sourceValues.stream() + .filter(Objects::nonNull) + .map(val -> MappingUtil.mapEnum(targetEnum, val)) + .toList(); + } + + private Specification<StiProtectionProcedure> filterByLabStatus( + GetStiProtectionProceduresFilterOptions filterOptions) { + Set<LabStatusDto> dto = filterOptions.labStatus(); + if (dto != null) { + return (root, query, criteriaBuilder) -> + root.get(StiProtectionProcedure_.LAB_STATUS).in(mapFilterBy(LabStatus.class, dto)); + } else { + return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + } + } + + private Specification<StiProtectionProcedure> filterByProcedureStatus( + GetStiProtectionProceduresFilterOptions filterOptions) { + Set<ProcedureStatusDto> dto = filterOptions.procedureStatus(); + if (dto != null) { + return (root, query, criteriaBuilder) -> + root.get(Procedure_.PROCEDURE_STATUS).in(mapFilterBy(ProcedureStatus.class, dto)); + } else { + return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + } + } + + private Specification<StiProtectionProcedure> filterByConcern( + GetStiProtectionProceduresFilterOptions filterOptions) { + Set<ConcernDto> dto = filterOptions.concern(); + if (dto != null) { + return (root, query, criteriaBuilder) -> + root.get(StiProtectionProcedure_.CONCERN).in(mapFilterBy(Concern.class, dto)); + } else { + return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + } + } + + private Specification<StiProtectionProcedure> filterByGender( + GetStiProtectionProceduresFilterOptions filterOptions) { + Set<GenderDto> dto = filterOptions.gender(); + if (dto != null) { + return (root, query, criteriaBuilder) -> + root.join(Procedure_.relatedPersons) + .get(Person_.GENDER) + .in(mapFilterBy(Gender.class, dto)); + } else { + return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + } + } + + private Specification<StiProtectionProcedure> filterByAppointmentDate( + GetStiProtectionProceduresFilterOptions filterOptions) { + Instant start = atStartOfDay(filterOptions.appointmentDateStart()); + Instant end = atEndOfDay(filterOptions.appointmentDateEnd()); + SingularAttribute<StiProtectionProcedure, Instant> appointmentStart = + StiProtectionProcedure_.appointmentStart; + if (start != null && end != null) { + return (root, query, criteriaBuilder) -> + criteriaBuilder.between(root.get(appointmentStart), start, end); + } else if (start != null) { + return (root, query, criteriaBuilder) -> + criteriaBuilder.greaterThanOrEqualTo(root.get(appointmentStart), start); + } else if (end != null) { + return (root, query, criteriaBuilder) -> + criteriaBuilder.lessThanOrEqualTo(root.get(appointmentStart), end); + } else { + return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + } + } + + private Specification<StiProtectionProcedure> filterByYearOfBirth( + GetStiProtectionProceduresFilterOptions filterOptions) { + Year yearOfBirth = filterOptions.yearOfBirth(); + if (yearOfBirth != null) { + return (root, query, criteriaBuilder) -> + criteriaBuilder.equal( + root.join(Procedure_.relatedPersons).get(Person_.YEAR_OF_BIRTH), yearOfBirth); + } else { + return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + } + } + + private Specification<StiProtectionProcedure> filterByCreatedAt( + GetStiProtectionProceduresFilterOptions filterOptions) { + Instant start = atStartOfDay(filterOptions.creationDateStart()); + Instant end = atEndOfDay(filterOptions.creationDateEnd()); + if (start != null && end != null) { + return (root, query, criteriaBuilder) -> + criteriaBuilder.between(root.get(Procedure_.createdAt), start, end); + } else if (start != null) { + return (root, query, criteriaBuilder) -> + criteriaBuilder.greaterThanOrEqualTo(root.get(Procedure_.createdAt), start); + } else if (end != null) { + return (root, query, criteriaBuilder) -> + criteriaBuilder.lessThanOrEqualTo(root.get(Procedure_.createdAt), end); + } else { + return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction(); + } + } + + private Instant atStartOfDay(LocalDate date) { + if (date == null) { + return null; + } + return date.atStartOfDay(clock.getZone()).toInstant(); + } + + private Instant atEndOfDay(LocalDate date) { + if (date == null) { + return null; + } + return atStartOfDay(date).plus(Duration.ofDays(1)).minusSeconds(1); + } + + private Specification<StiProtectionProcedure> orderBy( GetStiProtectionProceduresSortOrderDto sortOrder, GetStiProtectionProceduresSortByDto sortBy) { return (root, query, criteriaBuilder) -> { diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/CreatedByUserTypeDto.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/CreatedByUserTypeDto.java new file mode 100644 index 0000000000000000000000000000000000000000..4264dac3a7a466dca4285a9986162eb1a728aea4 --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/CreatedByUserTypeDto.java @@ -0,0 +1,14 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.api; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "CreatedByUserType") +public enum CreatedByUserTypeDto { + EMPLOYEE, + CITIZEN_PORTAL +} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/GetStiProtectionProceduresFilterOptions.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/GetStiProtectionProceduresFilterOptions.java new file mode 100644 index 0000000000000000000000000000000000000000..e4c910dccf5cd3035ac8a09a86bba57093d6ba4c --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/GetStiProtectionProceduresFilterOptions.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.api; + +import de.eshg.base.GenderDto; +import de.eshg.lib.procedure.model.ProcedureStatusDto; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Past; +import java.time.LocalDate; +import java.time.Year; +import java.util.Set; +import org.springframework.web.bind.annotation.BindParam; + +public record GetStiProtectionProceduresFilterOptions( + @BindParam("creationDateStart") @Parameter LocalDate creationDateStart, + @BindParam("creationDateEnd") @Parameter LocalDate creationDateEnd, + @BindParam("yearOfBirth") @Parameter @Schema(type = "integer") @Past Year yearOfBirth, + @BindParam("appointmentDateStart") @Parameter LocalDate appointmentDateStart, + @BindParam("appointmentDateEnd") @Parameter LocalDate appointmentDateEnd, + @BindParam("gender") @Parameter Set<GenderDto> gender, + @BindParam("concern") @Parameter Set<ConcernDto> concern, + @BindParam("procedureStatus") @Parameter Set<ProcedureStatusDto> procedureStatus, + @BindParam("labStatus") @Parameter Set<LabStatusDto> labStatus, + @BindParam("createdBy") @Parameter Set<CreatedByUserTypeDto> createdBy) {} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/CreatedByUserType.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/CreatedByUserType.java new file mode 100644 index 0000000000000000000000000000000000000000..a85e799bb6c5fe0df32c3a8945b50bc646f615d9 --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/CreatedByUserType.java @@ -0,0 +1,11 @@ +/* + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.persistence.db; + +public enum CreatedByUserType { + EMPLOYEE, + CITIZEN_PORTAL +} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/Person.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/Person.java index 443e1b8946b90e143a3cf31ef96c502c50bc5fe9..1f81a799d5a966db6e1f24e9ae4e89d19f90670b 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/Person.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/Person.java @@ -18,7 +18,12 @@ import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLEnumJdbcType; @Entity -@Table(indexes = @Index(columnList = "procedure_id", unique = true)) +@Table( + indexes = { + @Index(columnList = "procedure_id", unique = true), + @Index(columnList = "gender"), + @Index(columnList = "yearOfBirth"), + }) public class Person extends RelatedPerson<StiProtectionProcedure> { @JdbcType(PostgreSQLEnumJdbcType.class) diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/StiProtectionProcedure.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/StiProtectionProcedure.java index c30a0614db21a12f7813e636a60de459aa4520f6..0ca6aacee143f16cf1a38ab21507e88879b351ec 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/StiProtectionProcedure.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/StiProtectionProcedure.java @@ -40,6 +40,7 @@ import jakarta.persistence.PrePersist; import jakarta.persistence.PreUpdate; import jakarta.persistence.Table; import jakarta.persistence.Transient; +import jakarta.validation.constraints.NotNull; import java.time.Clock; import java.time.Instant; import java.util.ArrayList; @@ -57,6 +58,10 @@ import org.springframework.util.Assert; @Index( name = "idx_sti_protection_procedure_appointment_start", columnList = "appointment_start"), + @Index(columnList = "concern"), + @Index(columnList = "lab_status"), + @Index(columnList = "procedure_status"), + @Index(columnList = "created_by"), }) public class StiProtectionProcedure extends Procedure<StiProtectionProcedure, StiProtectionTask, Person, Facility> @@ -157,12 +162,19 @@ public class StiProtectionProcedure @DataSensitivity(SensitivityLevel.SENSITIVE) private Instant appointmentStart; + @DataSensitivity(SensitivityLevel.PUBLIC) + @Column(nullable = false) + @NotNull + @JdbcType(PostgreSQLEnumJdbcType.class) + private CreatedByUserType createdBy; + public static StiProtectionProcedure newProcedure( - Concern concern, Clock clock, AuditLogger auditLogger) { + Concern concern, CreatedByUserType createdBy, Clock clock, AuditLogger auditLogger) { StiProtectionProcedure procedure = new StiProtectionProcedure(); procedure.setProcedureType(ProcedureType.STI_PROTECTION); procedure.updateProcedureStatus(ProcedureStatus.OPEN, clock, auditLogger); procedure.setConcern(concern); + procedure.setCreatedBy(createdBy); return procedure; } @@ -370,4 +382,12 @@ public class StiProtectionProcedure public Instant getAppointmentStart() { return appointmentStart; } + + public CreatedByUserType getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(CreatedByUserType createdBy) { + this.createdBy = createdBy; + } } diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/waitingroom/WaitingRoomSpecification.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/waitingroom/WaitingRoomSpecification.java index 890632d38205bda503aa25cb6156c99bdcd91a49..724b5d6c69a63411a0eadda84c7404bf2aa61879 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/waitingroom/WaitingRoomSpecification.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/waitingroom/WaitingRoomSpecification.java @@ -15,9 +15,11 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Order; +import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import java.io.Serial; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import org.springframework.data.domain.Sort; @@ -40,10 +42,18 @@ public class WaitingRoomSpecification implements Specification<StiProtectionProc Root<StiProtectionProcedure> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) { List<Predicate> conjunctions = defaultProcedureFilters(root, criteriaBuilder); - query.orderBy(getSortOrder(root, criteriaBuilder)); + query.orderBy(getSortOrder(root, criteriaBuilder), createdAt(root, criteriaBuilder)); return criteriaBuilder.and(conjunctions.toArray(Predicate[]::new)); } + private Order createdAt(Root<StiProtectionProcedure> root, CriteriaBuilder criteriaBuilder) { + Path<Instant> createdAt = root.get(Procedure_.createdAt); + return switch (sortDirection) { + case ASC -> criteriaBuilder.asc(createdAt); + case DESC -> criteriaBuilder.desc(createdAt); + }; + } + private List<Predicate> defaultProcedureFilters( Root<StiProtectionProcedure> root, CriteriaBuilder criteriaBuilder) { List<Predicate> defaultFilter = new ArrayList<>(); diff --git a/backend/sti-protection/src/main/resources/migrations/0055_filter_procedures.xml b/backend/sti-protection/src/main/resources/migrations/0055_filter_procedures.xml new file mode 100644 index 0000000000000000000000000000000000000000..f12d01fed67e753f4b2afada72a55b702470b110 --- /dev/null +++ b/backend/sti-protection/src/main/resources/migrations/0055_filter_procedures.xml @@ -0,0 +1,56 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2025 cronn GmbH + SPDX-License-Identifier: AGPL-3.0-only +--> + +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + <changeSet author="GA-Lotse" id="1740055903378-1"> + <ext:createPostgresEnumType name="createdbyusertype" + values="CITIZEN_PORTAL, EMPLOYEE"/> + </changeSet> + <changeSet author="GA-Lotse" id="1740055903378-2"> + <addColumn tableName="sti_protection_procedure"> + <column name="created_by" type="CREATEDBYUSERTYPE"> + <constraints nullable="false"/> + </column> + </addColumn> + </changeSet> + <changeSet author="GA-Lotse" id="1740055903378-3"> + <createIndex indexName="idx_person_gender" tableName="person"> + <column name="gender"/> + </createIndex> + </changeSet> + <changeSet author="GA-Lotse" id="1740055903378-4"> + <createIndex indexName="idx_person_year_of_birth" tableName="person"> + <column name="year_of_birth"/> + </createIndex> + </changeSet> + <changeSet author="GA-Lotse" id="1740055903378-5"> + <createIndex indexName="idx_sti_protection_procedure_concern" + tableName="sti_protection_procedure"> + <column name="concern"/> + </createIndex> + </changeSet> + <changeSet author="GA-Lotse" id="1740055903378-6"> + <createIndex indexName="idx_sti_protection_procedure_created_by" + tableName="sti_protection_procedure"> + <column name="created_by"/> + </createIndex> + </changeSet> + <changeSet author="GA-Lotse" id="1740055903378-7"> + <createIndex indexName="idx_sti_protection_procedure_lab_status" + tableName="sti_protection_procedure"> + <column name="lab_status"/> + </createIndex> + </changeSet> + <changeSet author="GA-Lotse" id="1740055903378-8"> + <createIndex indexName="idx_sti_protection_procedure_procedure_status" + tableName="sti_protection_procedure"> + <column name="procedure_status"/> + </createIndex> + </changeSet> +</databaseChangeLog> diff --git a/backend/sti-protection/src/main/resources/migrations/changelog.xml b/backend/sti-protection/src/main/resources/migrations/changelog.xml index 4be33e14d19e5ac9bb7ee32cc4cbb0c5bfc5d8f8..4be4982f18437efd7b82a27cbab8a99fc3d5840b 100644 --- a/backend/sti-protection/src/main/resources/migrations/changelog.xml +++ b/backend/sti-protection/src/main/resources/migrations/changelog.xml @@ -62,5 +62,6 @@ <include file="migrations/0052_oms_appointment_type_extensions.xml"/> <include file="migrations/0053_idx_procedure_expiration_by_external_id.xml"/> <include file="migrations/0054_rename_rapid_test_syphilis_test_data_column.xml"/> + <include file="migrations/0055_filter_procedures.xml"/> </databaseChangeLog> diff --git a/backend/test-helper-commons/src/main/java/de/eshg/testhelper/DefaultTestHelperService.java b/backend/test-helper-commons/src/main/java/de/eshg/testhelper/DefaultTestHelperService.java index 957a3cab3a1a2708a152721ffc413be3a66ef8bf..a4b1dca3d731980890b34140ee9f3a163d2ce102 100644 --- a/backend/test-helper-commons/src/main/java/de/eshg/testhelper/DefaultTestHelperService.java +++ b/backend/test-helper-commons/src/main/java/de/eshg/testhelper/DefaultTestHelperService.java @@ -76,7 +76,9 @@ public class DefaultTestHelperService implements TestHelperWithDatabaseService { public Instant reset() throws Exception { environmentConfig.assertIsNotProduction(); resetResettableProperties(); - resetActions.forEach(TestHelperServiceResetAction::reset); + for (TestHelperServiceResetAction resetAction : resetActions) { + resetAction.reset(); + } return Instant.now(clock); } diff --git a/backend/test-helper-commons/src/main/java/de/eshg/testhelper/TestHelperServiceResetAction.java b/backend/test-helper-commons/src/main/java/de/eshg/testhelper/TestHelperServiceResetAction.java index 4bc4e91fd1eade5061dc446c1c7d2a99a1b4762f..08a7b39f18aaec6ce564907985ff7d7112a9dd1d 100644 --- a/backend/test-helper-commons/src/main/java/de/eshg/testhelper/TestHelperServiceResetAction.java +++ b/backend/test-helper-commons/src/main/java/de/eshg/testhelper/TestHelperServiceResetAction.java @@ -6,5 +6,5 @@ package de.eshg.testhelper; public interface TestHelperServiceResetAction { - void reset(); + void reset() throws Exception; } diff --git a/backend/travel-medicine/gradle.lockfile b/backend/travel-medicine/gradle.lockfile index 6cc1f3a24284dd0a6168fae01248dff65f19de40..63c67dce31b07456ece30eb39c777b0ba520aaea 100644 --- a/backend/travel-medicine/gradle.lockfile +++ b/backend/travel-medicine/gradle.lockfile @@ -15,6 +15,7 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2=compileClasspath,p com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.albfernandez:juniversalchardet:2.5.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.0=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.0=testCompileClasspath,testRuntimeClasspath @@ -22,6 +23,7 @@ com.github.docker-java:docker-java-transport:3.4.0=testCompileClasspath,testRunt com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.10.0=testRuntimeClasspath com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0=testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -30,12 +32,15 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=prod com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.50=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.healthmarketscience.jackcess:jackcess:4.0.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:content-type:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:lang-tag:1.7=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.pff:java-libpst:0.9.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath @@ -44,6 +49,8 @@ com.tngtech.archunit:archunit-junit5:1.3.0=testRuntimeClasspath com.tngtech.archunit:archunit:1.3.0=testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.17.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.18.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:commons-lang:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -99,12 +106,16 @@ net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCom net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath org.antlr:antlr4-runtime:4.13.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-collections4:4.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.24.0=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-csv:1.12.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.4.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5-h2:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.httpcomponents.core5:httpcore5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-core:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.james:apache-mime4j-dom:0.8.11=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -113,14 +124,25 @@ org.apache.pdfbox:pdfbox-io:3.0.3=productionRuntimeClasspath,runtimeClasspath,te org.apache.pdfbox:pdfbox-tools:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.pdfbox:pdfbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:xmpbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi-scratchpad:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.poi:poi:5.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-bom:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-html-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-mail-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-microsoft-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-text-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-xml-module:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-xmp-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.tika:tika-parser-zip-commons:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.34=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-anim:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-awt-util:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-bridge:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -170,6 +192,7 @@ org.jacoco:org.jacoco.core:0.8.12=jacocoAnt org.jacoco:org.jacoco.report:0.8.12=jacocoAnt org.jboss.logging:jboss-logging:3.6.1.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jsoup:jsoup:1.18.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testRuntimeClasspath diff --git a/backend/travel-medicine/openApi.json b/backend/travel-medicine/openApi.json index 6cd3f5da9747390313f2551c7748e908682187f4..93317af09560b1d765882f51761fc776f6df3b61 100644 --- a/backend/travel-medicine/openApi.json +++ b/backend/travel-medicine/openApi.json @@ -3285,7 +3285,7 @@ "name" : "limit", "required" : false, "schema" : { - "maximum" : 200, + "maximum" : 2200, "minimum" : 1, "type" : "integer", "format" : "int32", diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/TravelMedicineGdprZipEditorProvider.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/TravelMedicineGdprZipEditorProvider.java index 977ab60c3e9612bf668fa803c7459859136df343..1c42f3a119ad978edae67e057b5b962b41eb2bf7 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/TravelMedicineGdprZipEditorProvider.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/TravelMedicineGdprZipEditorProvider.java @@ -13,6 +13,7 @@ import de.eshg.lib.procedure.gdpr.AbstractGdprZipEditorProvider; import de.eshg.travelmedicine.document.medicalhistory.persistence.entity.MedicalHistory_; import de.eshg.travelmedicine.vaccinationconsultation.persistence.entity.ProcedureStep_; import de.eshg.travelmedicine.vaccinationconsultation.persistence.entity.VaccinationConsultation_; +import de.eshg.travelmedicine.vaccinationconsultation.persistence.entity.Vaccination_; import de.eshg.travelmedicine.vaccinationconsultation.persistence.entity.VcService_; import java.util.Iterator; import org.springframework.beans.factory.annotation.Value; @@ -40,7 +41,12 @@ public class TravelMedicineGdprZipEditorProvider extends AbstractGdprZipEditorPr ProcedureStep_.SERVICES)) .andThen( removeFieldFromNestedArray( - VcService_.MFA, VaccinationConsultation_.PROCEDURE_STEPS, ProcedureStep_.SERVICES)); + VcService_.MFA, VaccinationConsultation_.PROCEDURE_STEPS, ProcedureStep_.SERVICES)) + .andThen( + removeFieldFromNestedArray( + Vaccination_.BOOKING_ID, + VaccinationConsultation_.PROCEDURE_STEPS, + ProcedureStep_.SERVICES)); } protected ZipEditor removeFieldFromNestedArray( diff --git a/build.gradle b/build.gradle index 12e749843ee8399186a43aba9a5d4bf3ecc77972..2742ddf469b4b7b98bbb421965334bcce6d2235a 100644 --- a/build.gradle +++ b/build.gradle @@ -149,23 +149,6 @@ tasks.register('outdatedDependencies', PnpmTask) { args = ['outdated', '--recursive'] } -tasks.register('testCoverage', PnpmTask) { - environment = ['TZ': 'UTC'] - inputs.file "${rootDir}/config/tsconfig.base.json" - inputs.file "${rootDir}/config/vitest.base.ts" - inputs.file "${rootDir}/vitest.config.ts" - subprojects { - if (file("${projectDir}/vitest.config.ts").exists()) { - dependsOn project.tasks.named('prepareEnvironment') - inputs.file "${projectDir}/tsconfig.json" - inputs.file "${projectDir}/vitest.config.ts" - inputs.dir "${projectDir}/src" - } - } - outputs.dir rootProject.layout.buildDirectory.dir('vitest') - args = ['vitest', 'run', '--coverage', '--silent'] -} - tasks.register('clean') { dependsOn 'cleanDependencies' delete layout.buildDirectory diff --git a/buildSrc/src/main/groovy/next-app.gradle b/buildSrc/src/main/groovy/next-app.gradle index 0618a24474cbb867bf3d82245da2214908127fa6..712b3ee5e2a76724ac536ca34c312d18228b204b 100644 --- a/buildSrc/src/main/groovy/next-app.gradle +++ b/buildSrc/src/main/groovy/next-app.gradle @@ -90,26 +90,6 @@ tasks.register('build') { dependsOn 'assemble' } -tasks.register('testCoverage', PnpmTask) { - environment = ['TZ': 'UTC'] - inputs.file "${rootDir}/config/tsconfig.base.json" - inputs.file "${rootDir}/config/vitest.base.ts" - inputs.file "${rootDir}/vitest.config.ts" - if (file("${projectDir}/vitest.config.ts").exists()) { - dependsOn project.tasks.named('prepareEnvironment') - inputs.file "${projectDir}/tsconfig.json" - inputs.file "${projectDir}/vitest.config.ts" - inputs.dir "${projectDir}/src" - } - - outputs.dir layout.buildDirectory.dir('vitest') - args = ['vitest', 'run', '--coverage', '--passWithNoTests', '--silent'] -} - -tasks.named('findUnusedValidationFiles').configure { - mustRunAfter 'testCoverage' -} - tasks.register('analyzeBundle', PnpmTask) { dependsOn 'prepareEnvironment' inputs.files fileTree(srcDir) @@ -206,7 +186,6 @@ sonar { '**/*.test.tsx', '**/*.test.ts' ] - property 'sonar.javascript.lcov.reportPaths', 'build/vitest/coverage/lcov.info' property 'sonar.projectKey', nextExtension.sonarProjectKey.get() property 'sonar.qualitygate.wait', true property 'sonar.gradle.skipCompile', true diff --git a/buildSrc/src/main/groovy/vitest.gradle b/buildSrc/src/main/groovy/vitest.gradle index ef5fe126f15fac093e8fe5e0b4938c7a39e668a1..52df8737811341f560e1993e00dd32159dc65156 100644 --- a/buildSrc/src/main/groovy/vitest.gradle +++ b/buildSrc/src/main/groovy/vitest.gradle @@ -16,6 +16,8 @@ def testSrcDir = "${projectDir}/src" def validationFilesDir = project.layout.projectDirectory.dir('data/test') def test = tasks.register('test', PnpmTask) { + def coverageEnabled = project.hasProperty('coverage') + group = 'verification' dependsOn 'prepareEnvironment' environment = testEnvironment @@ -25,9 +27,13 @@ def test = tasks.register('test', PnpmTask) { inputs.dir validationFilesDir outputs.dir validationFilesDir } + if (coverageEnabled) { + outputs.dir project.layout.buildDirectory.dir('vitest') + } // Pass with no tests to avoid writing a dummy test within the new portal tests. // This parameter can be removed once we have tests in each portal project. - args = ['vitest', 'run', '--passWithNoTests', '--silent'] + args = ['vitest', 'run', '--passWithNoTests', '--silent'] + + (coverageEnabled ? ['--coverage'] : []) } tasks.register('testWatch', PnpmTask) { diff --git a/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sexarbeit/page.tsx b/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sexarbeit/page.tsx index 1b70ba695cfd14bc1653c02cfb9b59e9f248152c..368c27fa0c5d907705943a3c1a170658902ed698 100644 --- a/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sexarbeit/page.tsx +++ b/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sexarbeit/page.tsx @@ -3,29 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -"use client"; - import { ApiConcern } from "@eshg/sti-protection-api"; -import { LandingpageContent } from "@/lib/businessModules/stiProtection/pages/landingpage/LandingpageContent"; -import { LandingpageSidePanel } from "@/lib/businessModules/stiProtection/pages/landingpage/LandingpageSidePanel"; -import { useTranslation } from "@/lib/i18n/client"; -import { PageContent } from "@/lib/shared/components/layout/PageContent"; -import { TwoColumnGrid } from "@/lib/shared/components/layout/grid"; -import { PageLayout, PageTitle } from "@/lib/shared/components/layout/page"; +import { Landingpage } from "@/lib/businessModules/stiProtection/pages/landingpage/Landingpage"; export default function CitizenSexWorkPage() { - const { t } = useTranslation(["stiProtection/overview"]); - - return ( - <PageLayout banner="private"> - <PageContent> - <PageTitle>{t("page_title_sex_work")}</PageTitle> - <TwoColumnGrid - content={<LandingpageContent concern={ApiConcern.SexWork} />} - sidePanel={<LandingpageSidePanel />} - /> - </PageContent> - </PageLayout> - ); + return <Landingpage concern={ApiConcern.SexWork} />; } diff --git a/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sexarbeit/termin-buchen/page.tsx b/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sexarbeit/termin-buchen/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..06af9d78020d64401a65a6c80b91c2938e85af4d --- /dev/null +++ b/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sexarbeit/termin-buchen/page.tsx @@ -0,0 +1,12 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiConcern } from "@eshg/sti-protection-api"; + +import { BookAppointmentPage } from "@/lib/businessModules/stiProtection/components/appointment/BookAppointmentPage"; + +export default function CitizenSexWorkBookAppointmentPage() { + return <BookAppointmentPage concern={ApiConcern.SexWork} />; +} diff --git a/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sti-beratung/page.tsx b/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sti-beratung/page.tsx index 7ba70484ec3d5f0ce8528011a3715db69f5b34e3..f5afaef69c0bad40bf022e0ce32fad84aed95a4b 100644 --- a/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sti-beratung/page.tsx +++ b/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sti-beratung/page.tsx @@ -3,31 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -"use client"; - import { ApiConcern } from "@eshg/sti-protection-api"; -import { LandingpageContent } from "@/lib/businessModules/stiProtection/pages/landingpage/LandingpageContent"; -import { LandingpageSidePanel } from "@/lib/businessModules/stiProtection/pages/landingpage/LandingpageSidePanel"; -import { useTranslation } from "@/lib/i18n/client"; -import { PageContent } from "@/lib/shared/components/layout/PageContent"; -import { TwoColumnGrid } from "@/lib/shared/components/layout/grid"; -import { PageLayout, PageTitle } from "@/lib/shared/components/layout/page"; +import { Landingpage } from "@/lib/businessModules/stiProtection/pages/landingpage/Landingpage"; export default function CitizenStiConsultationPage() { - const { t } = useTranslation(["stiProtection/overview"]); - - return ( - <PageLayout banner="private"> - <PageContent> - <PageTitle>{t("page_title_sti_consultation")}</PageTitle> - <TwoColumnGrid - content={ - <LandingpageContent concern={ApiConcern.HivStiConsultation} /> - } - sidePanel={<LandingpageSidePanel />} - /> - </PageContent> - </PageLayout> - ); + return <Landingpage concern={ApiConcern.HivStiConsultation} />; } diff --git a/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sti-beratung/termin-buchen/page.tsx b/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sti-beratung/termin-buchen/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aa2a9b83c74721fa08a7ba549ec47c6c7c771cb0 --- /dev/null +++ b/citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sti-beratung/termin-buchen/page.tsx @@ -0,0 +1,12 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiConcern } from "@eshg/sti-protection-api"; + +import { BookAppointmentPage } from "@/lib/businessModules/stiProtection/components/appointment/BookAppointmentPage"; + +export default function CitizenStiConsultationBookAppointmentPage() { + return <BookAppointmentPage concern={ApiConcern.HivStiConsultation} />; +} diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/api/queries/citizenPublicApi.ts b/citizen-portal/src/lib/businessModules/officialMedicalService/api/queries/citizenPublicApi.ts index 9115a116ffc5828042f5722cc576730b17837642..9cdbe2e4dde3cdb483ba0f66722f66be45ce4b1f 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/api/queries/citizenPublicApi.ts +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/api/queries/citizenPublicApi.ts @@ -3,10 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiAppointmentType } from "@eshg/official-medical-service-api"; import { queryOptions } from "@tanstack/react-query"; import { useCitizenPublicApi } from "@/lib/businessModules/officialMedicalService/api/clients"; import { citizenPublicApiQueryKey } from "@/lib/businessModules/officialMedicalService/api/queries/apiQueryKeys"; +import { mapToConcernApiList } from "@/lib/businessModules/officialMedicalService/shared/helpers"; + +export function useGetAllAppointmentTypesQuery() { + const citizenPublicApi = useCitizenPublicApi(); + return queryOptions({ + queryKey: citizenPublicApiQueryKey(["getAppointmentTypesForCitizen"]), + queryFn: () => citizenPublicApi.getAppointmentTypesForCitizen(), + select: (response) => response.appointmentTypeConfigDtos ?? [], + refetchOnWindowFocus: false, + }); +} export function useGetDepartmentInfoQuery() { const departmentApi = useCitizenPublicApi(); @@ -24,14 +36,28 @@ export function useGetOpeningHoursQuery() { }); } -export function useGetFreeAppointmentsForCitizen() { +export function useGetFreeAppointmentsForCitizen( + appointmentType: ApiAppointmentType, +) { const citizenPublicApi = useCitizenPublicApi(); return queryOptions({ - queryKey: citizenPublicApiQueryKey(["getFreeAppointmentsForCitizen"]), + queryKey: citizenPublicApiQueryKey([ + "getFreeAppointmentsForCitizen", + appointmentType, + ]), queryFn: () => - citizenPublicApi.getFreeAppointmentsForCitizen( - "OFFICIAL_MEDICAL_SERVICE_SHORT", - ), + citizenPublicApi.getFreeAppointmentsForCitizen(appointmentType), + }); +} + +export function useGetConcerns() { + const citizenPublicApi = useCitizenPublicApi(); + + return queryOptions({ + queryKey: citizenPublicApiQueryKey(["getVisibleConcerns"]), + queryFn: () => citizenPublicApi.getVisibleConcerns(), + select: (data) => + data.categories.flatMap((category) => mapToConcernApiList(category)), }); } diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentForm.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentForm.tsx index 49bd1c1a7873285f3788037c867ab8842a6e0798..df1a53d43340d2f41703afbed54c2e9abddaec57 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentForm.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentForm.tsx @@ -11,6 +11,8 @@ import { import { OptionalFieldValue } from "@eshg/lib-portal/types/form"; import { ApiAppointment, + ApiAppointmentType, + ApiConcern, ApiSalutation, ApiTitle, PostCitizenProcedureRequest, @@ -49,7 +51,14 @@ export interface AppointmentFormValues { city: string; }; }; - concern: string; + concern: Omit< + ApiConcern, + "version" | "visibleInOnlinePortal" | "appointmentType" + > & { + index: string; + appointmentType: OptionalFieldValue<ApiAppointmentType>; + standardDurationInMinutes: string; + }; appointment?: ApiAppointment; confirmOnlineServices: boolean; confirmPrivacyNotice: boolean; @@ -64,7 +73,16 @@ const STEPS: StepFactory<AppointmentFormValues>[] = [ ]; const INITIAL_VALUES: AppointmentFormValues = { - concern: "", + concern: { + index: "", + standardDurationInMinutes: "", + appointmentType: "", + categoryNameDe: "", + categoryNameEn: "", + highPriority: false, + nameDe: "", + nameEn: "", + }, affectedPerson: { salutation: "", title: "", @@ -119,7 +137,7 @@ export function AppointmentForm() { <Formik initialValues={INITIAL_VALUES} onSubmit={handleSubmit}> {(formikProps) => ( <FormPlus> - {Outlet.name !== "AppointmentStepWrapper" ? ( + {currentStep !== STEPS.indexOf(AppointmentStepWrapper) + 1 ? ( <TwoColumnGrid content={<Outlet {...formikProps} />} sidePanel={<AppointmentFormSidePanel />} diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentFormSidePanel.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentFormSidePanel.tsx index 85482613497e3b55c60dda1bb17edaf2033cd297..dab901e72d59e43ffadf428c8fb2275e63dc4d4b 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentFormSidePanel.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentFormSidePanel.tsx @@ -19,7 +19,9 @@ export function AppointmentFormSidePanel() { const { currentStep, totalSteps } = useMultiStepForm(); return ( - <ContentSheet> + <ContentSheet + data-testid={currentStep === totalSteps ? "confirmation-form" : undefined} + > {currentStep !== totalSteps && ( <OverviewSection buttonBar={ diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentStepWrapper.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentStepWrapper.tsx index a5b299465d6779e344d93b0564833335f2204c53..43744b93d55ff51dbd0b7f5302f91d55dbae7c81 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentStepWrapper.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/AppointmentStepWrapper.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiAppointmentType } from "@eshg/official-medical-service-api"; import { useSuspenseQueries } from "@tanstack/react-query"; import { isAfter, isEqual } from "date-fns"; import { useFormikContext } from "formik"; @@ -23,9 +24,13 @@ function isDateCurrentDateOrGreater(date: Date) { } export function AppointmentStepWrapper() { - const { setFieldValue } = useFormikContext<AppointmentFormValues>(); + const { setFieldValue, values } = useFormikContext<AppointmentFormValues>(); const [{ data: freeAppointments }] = useSuspenseQueries({ - queries: [useGetFreeAppointmentsForCitizen()], + queries: [ + useGetFreeAppointmentsForCitizen( + values.concern.appointmentType as ApiAppointmentType, + ), + ], }); const [isInitialDate, setIsInitialDate] = useState(false); diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/NoAppointmentCard.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/NoAppointmentCard.tsx index d6a0a5cd7779946f36e3d926106b55cec897e872..2185daa6f35d14bb378605030396ad4c2a7ae285 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/NoAppointmentCard.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/NoAppointmentCard.tsx @@ -16,7 +16,7 @@ export function NoAppointmentCard() { const citizenRoutes = useCitizenRoutes(); return ( - <ContentSheet> + <ContentSheet data-testid={"no-appointment-form"}> <Typography level="h2">{t("appointment.title")}</Typography> <Stack direction="column" diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/AffectedPersonForm.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/AffectedPersonForm.tsx index cafb9a564049e00e77cc79c9e1531463ed197172..b0640fe1ff2d7f456f8ab836f8ee90a3308bdc2a 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/AffectedPersonForm.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/AffectedPersonForm.tsx @@ -32,7 +32,7 @@ export function AffectedPersonForm(props: { name: string }) { const fieldName = createFieldNameMapper<ApiAffectedPerson>(props.name); return ( - <ContentSheet> + <ContentSheet data-testid={"personal-data-form"}> <FormSheetTitle requiredTitle={t("common.requiredTitle")}> {t("affectedPerson.title")} </FormSheetTitle> diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/AppointmentStep.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/AppointmentStep.tsx index d5c2657fb0046ae6808cf4f05fbef1ac397e136a..1f9bb786dc416ee0a6bd47487a9a9efc33a1f577 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/AppointmentStep.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/AppointmentStep.tsx @@ -38,10 +38,12 @@ export function AppointmentStep({ appointments, }: Readonly<AppointmentStepProps>) { const { t } = useTranslation(["officialMedicalService/appointment"]); - const [month, setMonth] = useState<Date>(new Date()); + const [month, setMonth] = useState<Date>( + appointments[0]?.start ?? new Date(), + ); return ( - <ContentSheet> + <ContentSheet data-testid={"appointment-slot-form"}> <Typography level="h2">{t("appointment.title")}</Typography> <AppointmentPickerField name="appointment" diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/ConcerFilters.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/ConcerFilters.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fe8cbb5b9b56abd337ef6c16f6d16e04a637d9d5 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/ConcerFilters.tsx @@ -0,0 +1,97 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { SelectOptions } from "@eshg/lib-portal/components/formFields/SelectOptions"; +import { ApiConcern } from "@eshg/official-medical-service-api"; +import { Select, Stack, Typography } from "@mui/joy"; +import { isDefined } from "remeda"; + +import { + SEARCH_PARAMS, + useConcernFilterValues, +} from "@/lib/businessModules/officialMedicalService/components/appointment/steps/useConcernFilterValues"; +import { useTranslation } from "@/lib/i18n/client"; +import { byBreakpoint } from "@/lib/shared/breakpoints"; +import { + SearchParamReplacement, + useReplaceSearchParams, +} from "@/lib/shared/hooks/searchParams/useReplaceSearchParams"; + +interface ConcernFilterProps { + allConcerns: ApiConcern[]; +} +export function ConcernFilters({ allConcerns }: Readonly<ConcernFilterProps>) { + const { t } = useTranslation(["officialMedicalService/appointment"]); + + const filterValues = useConcernFilterValues(); + const replaceSearchParams = useReplaceSearchParams(); + + function updateFilterValue(replacement: SearchParamReplacement[]) { + replaceSearchParams([...replacement]); + } + + function onChangeCategory(newValue: string | null) { + updateFilterValue([{ name: SEARCH_PARAMS.category, value: newValue }]); + } + + return ( + <Stack gap={0.5}> + <Typography level="title-sm" component="label"> + {t("concern.filter.category")} + </Typography> + <Select + aria-label={t("concern.filter.category")} + value={filterValues.category ?? ""} + sx={{ + height: "40px", + width: byBreakpoint({ mobile: "100%", desktop: "220px" }), + }} + onChange={(event, value) => { + if (event !== null) { + onChangeCategory(value); + } + }} + > + <SelectOptions options={useConcernOptions(allConcerns)} /> + </Select> + </Stack> + ); +} + +function useConcernOptions(allConcerns: ApiConcern[]) { + const { t, i18n } = useTranslation(["officialMedicalService/appointment"]); + + const uniqueCategory = [ + ...new Set( + allConcerns + .map((item) => { + return { + categoryNameDe: item.categoryNameDe, + categoryNameEn: item.categoryNameEn ?? "", + }; + }) + .map((i) => + allConcerns.find( + (concern) => concern.categoryNameDe === i.categoryNameDe, + ), + ), + ), + ]; + + const options = [{ value: "", label: t("concern.filter.category_all") }]; + Object.values(uniqueCategory).forEach((concern) => { + if (isDefined(concern)) { + options.push({ + value: concern.categoryNameDe, + label: + isDefined(concern.categoryNameEn) && i18n.language === "en" + ? concern.categoryNameEn + : concern.categoryNameDe, + }); + } + }); + + return options; +} diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/ConcernStep.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/ConcernStep.tsx index 5423a82ac875b32c6ea4d774aba2f3c4c57f6fbe..11cb272c309f78ce0eb86ae4345375da6db8fc15 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/ConcernStep.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/ConcernStep.tsx @@ -4,24 +4,106 @@ */ import { Alert } from "@eshg/lib-portal/components/Alert"; -import { Typography } from "@mui/joy"; +import { isNonEmptyString } from "@eshg/lib-portal/helpers/guards"; +import { Stack, Typography } from "@mui/joy"; +import { useSuspenseQueries } from "@tanstack/react-query"; +import { useFormikContext } from "formik"; +import { useEffect } from "react"; +import { isDefined, isEmpty } from "remeda"; +import { + useGetAllAppointmentTypesQuery, + useGetConcerns, +} from "@/lib/businessModules/officialMedicalService/api/queries/citizenPublicApi"; +import { AppointmentFormValues } from "@/lib/businessModules/officialMedicalService/components/appointment/AppointmentForm"; +import { ConcernFilters } from "@/lib/businessModules/officialMedicalService/components/appointment/steps/ConcerFilters"; +import { useConcernFilterValues } from "@/lib/businessModules/officialMedicalService/components/appointment/steps/useConcernFilterValues"; +import { RadioSheet } from "@/lib/businessModules/travelMedicine/components/shared/components/RadioSheet"; +import { RadioGroupField } from "@/lib/businessModules/travelMedicine/components/shared/components/formField/RadioGroupField"; import { useTranslation } from "@/lib/i18n/client"; import { ContentSheet } from "@/lib/shared/components/layout/contentSheet"; export function ConcernStep() { - const { t } = useTranslation(["officialMedicalService/appointment"]); + const { t, i18n } = useTranslation(["officialMedicalService/appointment"]); + const { setFieldValue, values } = useFormikContext<AppointmentFormValues>(); + const filterValues = useConcernFilterValues(); + + const [{ data }, { data: appointmentTypes }] = useSuspenseQueries({ + queries: [useGetConcerns(), useGetAllAppointmentTypesQuery()], + }); + + const numberOfCategories = [ + ...new Set(data.map((concern) => concern.categoryNameDe)), + ].length; + + useEffect(() => { + if (!isEmpty(values.concern.index)) { + const tmp = data[Number(values.concern.index)]; + if (isDefined(tmp)) { + void (async () => { + await setFieldValue("concern", { + index: values.concern.index, + appointmentType: tmp.appointmentType, + standardDurationInMinutes: appointmentTypes + .filter((item) => item.appointmentTypeDto === tmp.appointmentType) + .map((i) => i.standardDurationInMinutes) + .toString(), + categoryNameDe: tmp.categoryNameDe, + categoryNameEn: tmp.categoryNameEn, + highPriority: tmp.highPriority, + nameDe: tmp.nameDe, + nameEn: tmp.nameEn, + }); + })(); + } + } + }, [data, appointmentTypes, setFieldValue, values.concern.index]); return ( - <ContentSheet> + <ContentSheet data-testid={"concern-form"}> <Typography level="h2">{t("concern.title")}</Typography> <Alert title={t("concern.infoText.title")} - color={"primary"} + color="primary" message={t("concern.infoText.description")} /> - <Typography level="body-md">{t("concern.description")}</Typography> - <Typography level="body-md">...to be done</Typography> + <Typography level="body-md" data-testid={"description"}> + {t("concern.description")} + </Typography> + {numberOfCategories > 1 && <ConcernFilters allConcerns={data} />} + <Stack gap={1}> + <RadioGroupField + name="concern.index" + required={t("concern.fields.concern_required")} + sx={{ gap: 2 }} + > + {data + .filter((item) => + isNonEmptyString(filterValues.category) + ? item.categoryNameDe === filterValues.category + : item, + ) + .map((concern, index) => { + return ( + <RadioSheet + key={`${concern.nameDe}.${index}`} + label={ + i18n.language === "en" && isDefined(concern.nameEn) + ? concern.nameEn + : concern.nameDe + } + value={index} + radioProps={{ + sx: (theme) => ({ + label: { ...theme.typography["title-md"] }, + alignItems: "center", + }), + }} + ></RadioSheet> + ); + })} + </RadioGroupField> + </Stack> </ContentSheet> ); } diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/DocumentForm.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/DocumentForm.tsx index 2c9c2c08d565ba81ea486923587b3b353685072d..e4bc896f7fc189ec77ccda9bb8288ae84cbe0ba6 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/DocumentForm.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/DocumentForm.tsx @@ -15,8 +15,14 @@ export function DocumentForm() { const { t } = useTranslation(["officialMedicalService/appointment"]); return ( - <ContentSheet sx={{ paddingX: byBreakpoint({ mobile: 0, desktop: 3 }) }}> - <FormSheetTitle requiredTitle={t("common.requiredTitle")}> + <ContentSheet + sx={{ paddingX: byBreakpoint({ mobile: 0, desktop: 3 }) }} + data-testid={"documents-form"} + > + <FormSheetTitle + requiredTitle={t("common.requiredTitle")} + sx={{ paddingX: byBreakpoint({ mobile: 2, desktop: 0 }) }} + > {t("documents.title")} </FormSheetTitle> <FileArrayField diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/InformationCard.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/InformationCard.tsx index 7783b7df05feaf196563152f61f7c73ef8ca9ef6..8ac57db9247cd49ff3ed06d5d4aba8bd4b503259 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/InformationCard.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/InformationCard.tsx @@ -15,7 +15,7 @@ export function InformationCard() { const { t, i18n } = useTranslation(["officialMedicalService/appointment"]); return ( - <ContentSheet> + <ContentSheet data-testid={"information-card"}> <FormSheetTitle>{t("appointmentInformation.title")}</FormSheetTitle> <Alert color="primary" @@ -54,8 +54,11 @@ export function InformationCard() { </ListItem> </List> <Typography> - {t("appointmentInformation.closingGreeting")} <br /> - {t("appointmentInformation.healthDepartment")} + <Trans + i18nKey="appointmentInformation.closingGreeting" + ns="officialMedicalService/appointment" + i18n={i18n} + /> </Typography> </ContentSheet> ); diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/OverviewSection.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/OverviewSection.tsx index 6bc70b632f3096a314f7f4fdc840a7ffc7cb82c9..5b249153f47a2887a4c68463a6a3441ccd730daa 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/OverviewSection.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/OverviewSection.tsx @@ -7,7 +7,6 @@ import { useMultiStepForm } from "@eshg/lib-portal/components/form/MultiStepForm import { formatDate, formatTime } from "@eshg/lib-portal/formatters/dateTime"; import { formatPersonName } from "@eshg/lib-portal/formatters/person"; import { formatDateToFullReadableString } from "@eshg/lib-portal/helpers/dateTime"; -import { ApiDomesticAddress } from "@eshg/official-medical-service-api"; import { AccessTimeOutlined, CakeOutlined, @@ -16,11 +15,13 @@ import { HomeOutlined, MailOutlined, MarkEmailReadOutlined, + MedicalServicesOutlined, PersonOutlined, } from "@mui/icons-material"; import { Stack, Typography } from "@mui/joy"; import { useFormikContext } from "formik"; import { ReactNode } from "react"; +import { Trans } from "react-i18next"; import { isDefined } from "remeda"; import { AppointmentFormValues } from "@/lib/businessModules/officialMedicalService/components/appointment/AppointmentForm"; @@ -28,15 +29,10 @@ import { useDepartmentContext } from "@/lib/businessModules/officialMedicalServi import { DetailsField } from "@/lib/businessModules/travelMedicine/components/shared/components/DetailsField"; import { formatDepartmentAddress } from "@/lib/businessModules/travelMedicine/helpers/appointmentFormHelper"; import { useTranslation } from "@/lib/i18n/client"; - -export function formatStreet(address: ApiDomesticAddress) { - const { houseNumber, street } = address; - return `${street}, ${houseNumber}`; -} -export function formatCity(address: ApiDomesticAddress) { - const { city, postalCode } = address; - return `${city}, ${postalCode}`; -} +import { + formatPostalCodeAndCity, + formatStreetAndHouseNumber, +} from "@/lib/shared/formatters/address"; export interface OverviewSectionProps { buttonBar?: ReactNode; @@ -46,96 +42,95 @@ export function OverviewSection({ buttonBar }: Readonly<OverviewSectionProps>) { const { t } = useTranslation(["officialMedicalService/appointment"]); const { department } = useDepartmentContext(); const { values } = useFormikContext<AppointmentFormValues>(); - const { currentStep, totalSteps } = useMultiStepForm(); + const { currentStep } = useMultiStepForm(); return ( - <> + <Stack gap={2} data-testid={"overview"}> <Typography level="h2">{t("overview.title")}</Typography> - <Stack gap={2}> - <Stack gap={1}> - {/*ToDo: add concern*/} - {/*{currentStep > 1 && (*/} - {/* <>*/} - {/* </>*/} - {/*)}*/} - {currentStep > 2 && ( - <> - {currentStep === totalSteps && isDefined(department) && ( - <DetailsField - value={formatDepartmentAddress(department)} - icon={<FmdGoodOutlined />} - /> - )} - {values.appointment && ( - <DetailsField - value={formatDateToFullReadableString( - values.appointment.start, - )} - icon={<DateRange />} - /> - )} - {values.appointment && ( + <Stack gap={1} data-testid={"appointment-overview-summary"}> + {currentStep > 1 && ( + <DetailsField + value={`${values.concern.nameDe} ${t("overview.values.appointmentDuration", { durationInMinutes: values.concern.standardDurationInMinutes })}`} + icon={<MedicalServicesOutlined />} + /> + )} + {isDefined(department) && ( + <DetailsField + value={formatDepartmentAddress(department)} + icon={<FmdGoodOutlined />} + /> + )} + {currentStep > 2 && ( + <> + {values.appointment && ( + <DetailsField + value={formatDateToFullReadableString(values.appointment.start)} + icon={<DateRange />} + /> + )} + {values.appointment && ( + <DetailsField + value={formatTime(values.appointment.start)} + icon={<AccessTimeOutlined />} + /> + )} + </> + )} + {currentStep > 3 && ( + <> + {values.affectedPerson.firstName && + values.affectedPerson.lastName && ( <DetailsField - value={formatTime(values.appointment.start)} - icon={<AccessTimeOutlined />} + value={formatPersonName(values.affectedPerson)} + icon={<PersonOutlined />} /> )} - </> - )} - {currentStep > 3 && ( - <> - {values.affectedPerson.firstName && - values.affectedPerson.lastName && ( + {values.affectedPerson.dateOfBirth && ( + <DetailsField + value={formatDate(new Date(values.affectedPerson.dateOfBirth))} + icon={<CakeOutlined />} + /> + )} + {values.affectedPerson.contactAddress.street && + values.affectedPerson.contactAddress.houseNumber && + values.affectedPerson.contactAddress.houseNumber && + values.affectedPerson.contactAddress.city && ( + <Stack gap={0}> <DetailsField - value={formatPersonName(values.affectedPerson)} - icon={<PersonOutlined />} + value={ + <Trans + i18nKey="overview.values.contactAddress" + ns="officialMedicalService/appointment" + values={{ + street: formatStreetAndHouseNumber( + values.affectedPerson.contactAddress, + ), + city: formatPostalCodeAndCity( + values.affectedPerson.contactAddress, + ), + }} + /> + } + icon={<HomeOutlined sx={{ alignSelf: "self-start" }} />} /> - )} - {values.affectedPerson.dateOfBirth && ( - <DetailsField - value={formatDate( - new Date(values.affectedPerson.dateOfBirth), - )} - icon={<CakeOutlined />} - /> - )} - {values.affectedPerson.contactAddress.street && - values.affectedPerson.contactAddress.houseNumber && - values.affectedPerson.contactAddress.houseNumber && - values.affectedPerson.contactAddress.city && ( - <Stack gap={0}> - <DetailsField - value={formatStreet( - values.affectedPerson - .contactAddress as ApiDomesticAddress, - )} - icon={<HomeOutlined />} - /> - <Typography sx={{ paddingInlineStart: "2.25rem" }}> - {formatCity( - values.affectedPerson - .contactAddress as ApiDomesticAddress, - )} - </Typography> - </Stack> - )} - {values.affectedPerson.emailAddresses && ( - <DetailsField - value={values.affectedPerson.emailAddresses} - icon={<MailOutlined />} - /> - )} - {values.confirmOnlineServices && ( - <DetailsField - value={t("overview.values.confirmOnlineServices")} - icon={<MarkEmailReadOutlined />} - /> + </Stack> )} - </> - )} - </Stack> - {isDefined(buttonBar) && buttonBar} + {values.affectedPerson.emailAddresses && ( + <DetailsField + value={values.affectedPerson.emailAddresses} + icon={<MailOutlined />} + /> + )} + {values.confirmOnlineServices && ( + <DetailsField + value={t("overview.values.confirmOnlineServices")} + icon={<MarkEmailReadOutlined />} + /> + )} + </> + )} </Stack> - </> + {isDefined(buttonBar) && buttonBar} + </Stack> ); } diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/useConcernFilterValues.ts b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/useConcernFilterValues.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3736c580f7c080fd14e3acf11f12c69be8b2564 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/useConcernFilterValues.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useSearchParams } from "next/navigation"; + +interface ConcernFilterValues { + category?: string; +} + +export function useConcernFilterValues(): ConcernFilterValues { + const searchParams = useSearchParams(); + + return { + [SEARCH_PARAMS.category]: + searchParams.get(SEARCH_PARAMS.category) ?? undefined, + }; +} + +export const SEARCH_PARAMS = { + category: "category", +} as const; diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/locales/de/appointment.json b/citizen-portal/src/lib/businessModules/officialMedicalService/locales/de/appointment.json index c5438c9d9659f5b5e19e199e4edd29d4c652ad68..54c5d7aa0c6ae37bd97956809b93bb2b19d81e3c 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/locales/de/appointment.json +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/locales/de/appointment.json @@ -13,6 +13,13 @@ "infoText": { "title": "Schulbezogene Anliegen", "description": "Kommen Sie bitte bei Prüfungen spätestens am Prüfungstag (auch ohne Termin) zu uns oder bei Abgabe einer wissenschaftlichen Arbeit nach vorheriger telefonischer Terminvereinbarung" + }, + "filter": { + "category": "Kategorie", + "category_all": "Alle" + }, + "fields": { + "concern_required": "Pflichtfeld ausfüllen." } }, "appointment": { @@ -93,7 +100,8 @@ "cancel": "Abbrechen", "values": { "confirmOnlineServices": "Bestätigungsmail senden", - "appointmentDuration": "(ca. {{ durationInMinutes }} Minuten)" + "appointmentDuration": "(ca. {{ durationInMinutes }} Minuten)", + "contactAddress": "{{ street }} <br> {{ city }} </br>" } }, "appointmentInformation": { @@ -104,8 +112,7 @@ "listItemIdCard": "Personalausweis / Reisepass ", "listItemMedicalDocuments": "ärztliche Unterlagen in Kopie (z.B. Krankenhausentlassungsberichte, Atteste, Bescheinigungen etc.)", "listItemCurrentMedication": "einen Nachweis der zurzeit verordneten Medikamente (falls vorhanden)", - "closingGreeting": "Mit freundlichen Grüßen", - "healthDepartment": "Ihr Gesundheitsamt" + "closingGreeting": "Mit freundlichen Grüßen <br> Ihr Gesundheitsamt </br> " }, "confirmation": { "title": "Anliegen amtsärztliches Gutachten", diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/locales/en/appointment.json b/citizen-portal/src/lib/businessModules/officialMedicalService/locales/en/appointment.json index 51e4aa5f9a5874be6b0ce487546df3ad7a99c212..9891aab1615886164ff26b3768b523d4dce058ed 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/locales/en/appointment.json +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/locales/en/appointment.json @@ -13,6 +13,13 @@ "infoText": { "title": "School-related concern", "description": "For exams, please come to us at the latest on the day of the exam (even without an appointment) or if you are submitting an academic paper, please make an appointment by telephone in advance" + }, + "filter": { + "category": "Category", + "category_all": "All" + }, + "fields": { + "concern_required": "Required" } }, "appointment": { @@ -71,16 +78,16 @@ "contactAddress": { "street": "Street", "street_required": "Required", - "houseNumber": "House number", + "houseNumber": "House Nr.", "houseNumber_required": "Required", - "addressAddition": "Apartment, unit, suite etc", + "addressAddition": "Apartment, unit, suite etc.", "postalCode": "Postal Code", "postalCode_required": "Required", "city": "City", "city_required": "Required" }, "phoneNumbers": "Phone", - "emailAddresses": "E-Mail Addresses", + "emailAddresses": "E-Mail Address", "emailAddresses_required": "Required", "confirmOnlineServices": "I confirm that I would like to use the online services and receive the necessary emails.", "confirmOnlineServices_required": "Please confirm." @@ -93,7 +100,8 @@ "cancel": "Cancel", "values": { "confirmOnlineServices": "Send confirmation email", - "appointmentDuration": "(ca. {{ durationInMinutes }} Minutes)" + "appointmentDuration": "(ca. {{ durationInMinutes }} Minutes)", + "contactAddress": "{{ street }} <br> {{ city }} </br>" } }, "appointmentInformation": { @@ -104,8 +112,7 @@ "listItemIdCard": "ID / Passport", "listItemMedicalDocuments": "Copies of medical documents (e.g. hospital discharge reports, certificates, etc.)", "listItemCurrentMedication": "proof of the currently prescribed medication (if any)", - "closingGreeting": "Sincerely", - "healthDepartment": "Your Health Department" + "closingGreeting": "Sincerely <br> Your Health Department </br>" }, "confirmation": { "title": "Request an official medical report", diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/shared/file/FileArrayField.tsx b/citizen-portal/src/lib/businessModules/officialMedicalService/shared/file/FileArrayField.tsx index da5ed3540fe5d9e43b96217f71fdf96be3a21fa6..b9065d541760ba820da4c4c4ed9f232abd4d3632 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/shared/file/FileArrayField.tsx +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/shared/file/FileArrayField.tsx @@ -170,7 +170,7 @@ export function FileArrayField({ <Typography>{labels.helperText}</Typography> )} {isNonEmptyArray(field.input.value) && ( - <Typography> + <Typography data-testid="uploadedFiles"> {labels.inputSummary(field.input.value.length)} </Typography> )} @@ -214,7 +214,12 @@ export function FileArrayField({ </Box> </ResponsiveGrid> {isNonEmptyArray(field.input.value) && ( - <Stack direction="column" gap={2} sx={{ width: "100%" }}> + <Stack + data-testid={`documents`} + direction="column" + gap={2} + sx={{ width: "100%" }} + > {field.input.value.map((file, index) => ( <FileSheet key={`${file.name}.${index}`} diff --git a/citizen-portal/src/lib/businessModules/officialMedicalService/shared/helpers.ts b/citizen-portal/src/lib/businessModules/officialMedicalService/shared/helpers.ts index 7434229eb1f22ff5d51b10c7b71c2e4035851e71..8728955379c402c9ae1eb9a327f61e563992aaa3 100644 --- a/citizen-portal/src/lib/businessModules/officialMedicalService/shared/helpers.ts +++ b/citizen-portal/src/lib/businessModules/officialMedicalService/shared/helpers.ts @@ -5,11 +5,35 @@ import { durationBetweenDatesInMinutes } from "@eshg/lib-portal/helpers/dateTime"; import { mapOptionalValue } from "@eshg/lib-portal/helpers/form"; -import { PostCitizenProcedureRequest } from "@eshg/official-medical-service-api"; +import { + ApiAppointmentType, + ApiConcern, + ApiConcernCategoryConfig, + PostCitizenProcedureRequest, +} from "@eshg/official-medical-service-api"; import { isDefined, isEmpty } from "remeda"; import { AppointmentFormValues } from "@/lib/businessModules/officialMedicalService/components/appointment/AppointmentForm"; +export function mapToConcernApiList( + val: ApiConcernCategoryConfig, +): ApiConcern[] { + const newArray: ApiConcern[] = []; + val.concerns.forEach((concern) => { + newArray.push({ + appointmentType: mapOptionalValue(concern.appointmentType), + categoryNameDe: val.nameDe, + categoryNameEn: val.nameEn, + highPriority: concern.highPriority, + nameDe: concern.nameDe, + nameEn: mapOptionalValue(concern.nameEn), + version: 0, + visibleInOnlinePortal: true, + }); + }); + return newArray; +} + export function mapToPostCitizenProcedureRequest( values: AppointmentFormValues, ): PostCitizenProcedureRequest { @@ -40,7 +64,9 @@ export function mapToPostCitizenProcedureRequest( version: 0, }, appointment: { - appointmentType: "OFFICIAL_MEDICAL_SERVICE_SHORT", // ToDo: change in upcoming ticket + appointmentType: + mapOptionalValue(values.concern.appointmentType) ?? + ApiAppointmentType.OfficialMedicalServiceShort, bookingInfo: { bookingType: "APPOINTMENT_BLOCK", duration: isDefined(values.appointment) @@ -52,13 +78,13 @@ export function mapToPostCitizenProcedureRequest( start: values.appointment!.start, }, }, - // ToDo: change in upcoming ticket concern: { - categoryNameDe: "categoryNameDe", - categoryNameEn: "categoryNameEn", - highPriority: true, - nameDe: "nameDe", - nameEn: "nameEn", + appointmentType: mapOptionalValue(values.concern.appointmentType), + categoryNameDe: values.concern.categoryNameDe, + categoryNameEn: values.concern.categoryNameEn, + highPriority: values.concern.highPriority, + nameDe: values.concern.nameDe, + nameEn: mapOptionalValue(values.concern.nameEn), version: 0, visibleInOnlinePortal: true, }, diff --git a/citizen-portal/src/lib/businessModules/stiProtection/api/queries/publicCitizenApi.ts b/citizen-portal/src/lib/businessModules/stiProtection/api/queries/publicCitizenApi.ts index 0bd08875ad5c3e6a76bde322354de327c2d4e12f..670243ae86ae7d6060559590c22154849322f304 100644 --- a/citizen-portal/src/lib/businessModules/stiProtection/api/queries/publicCitizenApi.ts +++ b/citizen-portal/src/lib/businessModules/stiProtection/api/queries/publicCitizenApi.ts @@ -35,3 +35,24 @@ export function useOpeningHoursQuery(concern: ApiConcern) { export function useOpeningHours(concern: ApiConcern) { return useSuspenseQuery(useOpeningHoursQuery(concern)); } + +export function useFreeAppointments({ + concern, + earliestDate, +}: { + concern: ApiConcern; + earliestDate: Date; +}) { + const publicCitizenApi = useCitizenPublicApi(); + return useSuspenseQuery({ + queryKey: stiProtectionPublicCitizenApiQueryKey([ + "freeAppointments", + { concern, earliestDate }, + ]), + queryFn: () => + publicCitizenApi.getFreeAppointmentsForCitizen(concern, earliestDate), + select(data) { + return data.appointments; + }, + }); +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentDataContext.tsx b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentDataContext.tsx new file mode 100644 index 0000000000000000000000000000000000000000..960b63ad10e5b2c0870453daac2575141f103c69 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentDataContext.tsx @@ -0,0 +1,45 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + PropsWithChildren, + createContext, + useContext, + useMemo, + useState, +} from "react"; + +type NotNull<T> = Exclude<T, null | undefined>; +type FormDataValue<T> = [NotNull<T>, (v: Partial<NotNull<T>>) => void]; + +const FormDataContext = createContext<FormDataValue<unknown> | null>(null); + +interface FormDataProps<T> { + initialData: T; +} + +export function FormDataProvider<T>({ + initialData, + children, +}: PropsWithChildren<FormDataProps<T>>) { + const [data, setData] = useState(initialData); + + const contextValue = useMemo( + () => [data, (newData: T) => setData((old) => ({ ...old, ...newData }))], + [data, setData], + ); + return ( + <FormDataContext.Provider value={contextValue as FormDataValue<unknown>}> + {children} + </FormDataContext.Provider> + ); +} +export function useFormData<T>() { + const context = useContext(FormDataContext); + if (!context) { + throw new Error("useFormData must be used with a FormDataProvider"); + } + return context as FormDataValue<T>; +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentPickerSection.tsx b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentPickerSection.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1d0a29b5030870ca54b9f22e0150c450bc030848 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentPickerSection.tsx @@ -0,0 +1,204 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { AppointmentListProps } from "@eshg/lib-portal/components/formFields/appointmentPicker/AppointmentListForDate"; +import { + Appointment, + AppointmentPickerField, + AppointmentPickerFieldProps, + AppointmentPickerLayoutProps, +} from "@eshg/lib-portal/components/formFields/appointmentPicker/AppointmentPickerField"; +import { isSameAppointment } from "@eshg/lib-portal/components/formFields/appointmentPicker/helpers"; +import { Box, Button, ListItem, Stack, Typography, styled } from "@mui/joy"; +import { useMemo, useState } from "react"; + +import { Row } from "@/lib/businessModules/measlesProtection/shared/components/Row"; +import { TranslateFn } from "@/lib/i18n/client"; + +interface AppointmentPickerSectionProps<T extends Appointment> { + name: string; + translationPrefix?: string; + t: TranslateFn; + onAppointmentSelected?: (d: T) => unknown; + onDateSelected?: (d: Date) => unknown; + appointments: T[]; +} + +export function AppointmentPickerSection<T extends Appointment>({ + appointments, + name, + translationPrefix = "appointment_calendar", + t, + onAppointmentSelected, + onDateSelected, +}: AppointmentPickerSectionProps<T>) { + const [month, setMonth] = useState<Date>(new Date()); + const labels = useMemo( + () => ({ + requiredAppointment: t(`${translationPrefix}.required_appointment`), + requiredDay: t(`${translationPrefix}.required_day`), + monthSelection: t(`${translationPrefix}.month_selection`), + nextMonth: t(`${translationPrefix}.next_month`), + prevMonth: t(`${translationPrefix}.prev_month`), + listLabel: t(`${translationPrefix}.list_label`), + calendarLabel: t(`${translationPrefix}.calendar_label`), + availableLegend: t(`${translationPrefix}.available`), + }), + [t, translationPrefix], + ); + + return ( + <AppointmentPickerField + name={name} + currentMonth={month} + setCurrentMonth={setMonth} + autoSelectFirst + monthAppointments={appointments} + required={true} + labels={labels} + onAppointmentSelected={onAppointmentSelected} + onDateSelected={onDateSelected} + showWeekdays={["monday", "tuesday", "wednesday", "thursday", "friday"]} + layout={AppointmentPickerCitizenLayout} + padDays={false} + appointmentList={TimeSlotList} + slots={AppointmentPickerCitizenSlots} + /> + ); +} + +const AppointmentPickerCitizenSlots: AppointmentPickerFieldProps<Appointment>["slots"] = + { + calendar: { + monthSelection: { + arrows: { + variant: "soft", + sx: { backgroundColor: "white" }, + }, + }, + }, + }; + +function AppointmentPickerCitizenLayout({ + calendar, + calendarError, + appointmentList, + labels: { calendarLabel, availableLegend }, +}: AppointmentPickerLayoutProps) { + if (!calendarLabel) { + throw Error("Calendar Label not defined"); + } + if (!availableLegend) { + throw Error("Available Legend not defined"); + } + return ( + <Row gap={3}> + <Stack gap={2}> + <Typography level="title-md">{calendarLabel}</Typography> + <Box + sx={(theme) => ({ + backgroundColor: theme.palette.background.level1, + padding: 2, + borderRadius: theme.radius.md, + alignSelf: "start", + })} + > + {calendar} + </Box> + <AvailableLegend label={availableLegend} /> + {calendarError} + </Stack> + {appointmentList} + </Row> + ); +} + +const ListGrid = styled("ol")(({ theme }) => ({ + display: "grid", + gridTemplateColumns: "1fr 1fr 1fr", + gap: theme.spacing(2), + margin: 0, + padding: 0, +})); + +function TimeSlotList<T extends Appointment>({ + date, + field, + appointments, + onAppointmentSelected, + label, +}: AppointmentListProps<T>) { + const hasAppointments = appointments.length > 0; + if (!hasAppointments || !date) { + return null; + } + + function createOnSelected(d: T) { + return () => { + onAppointmentSelected?.(d); + return field.helpers.setValue(d); + }; + } + + return ( + <Stack gap={2}> + <Typography level="title-md">{label}</Typography> + <ListGrid> + {appointments.map((apt: T) => { + const isSelected = isSameAppointment(field.input.value, apt); + return ( + <ListItem + sx={{ padding: 0, minHeight: 0 }} + key={apt.start.getTime()} + > + <Box + component="time" + sx={{ width: "100%" }} + dateTime={apt.start.toTimeString().slice(0, 5)} + > + <Button + onClick={createOnSelected(apt)} + aria-selected={isSelected} + variant={isSelected ? "solid" : "plain"} + sx={(theme) => ({ + display: "flex", + justifyContent: "center", + gap: 1, + minWidth: theme.spacing(12), + backgroundColor: isSelected + ? undefined + : theme.palette.background.level1, + width: "100%", + })} + > + {apt.start.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + })} + </Button> + </Box> + </ListItem> + ); + })} + </ListGrid> + </Stack> + ); +} + +function AvailableLegend({ label }: { label: string }) { + return ( + <Row alignItems="center" marginLeft={2}> + <Box + sx={(theme) => ({ + backgroundColor: theme.palette.primary.plainColor, + width: theme.spacing(1), + height: theme.spacing(0.5), + borderRadius: theme.radius.xs, + })} + /> + {label} + </Row> + ); +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentStepper.tsx b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentStepper.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b3895592559bc5f416ced73a253e17fd55f94903 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentStepper.tsx @@ -0,0 +1,37 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Appointment } from "@eshg/lib-portal/components/formFields/appointmentPicker/AppointmentPickerField"; +import { ApiConcern } from "@eshg/sti-protection-api"; + +import { Stepper } from "@/lib/businessModules/stiProtection/components/shared/StepContext"; + +import { FormDataProvider } from "./AppointmentDataContext"; +import { TimeSlotStep } from "./TimeSlotStep"; + +const steps = [ + TimeSlotStep, + TimeSlotStep, + // PersonalDataStep, + // PinStep, + // ShareAuthStep, + // AppointmentReviewStep, +] as const; + +export function AppointmentStepper({ concern }: { concern: ApiConcern }) { + return ( + <FormDataProvider initialData={{ concern } as AppointmentFormData}> + <Stepper steps={steps} /> + </FormDataProvider> + ); +} + +export interface AppointmentFormData { + concern: ApiConcern; + + // Timeslot Step + appointment?: Appointment; + date?: Date; +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/BookAppointmentPage.tsx b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/BookAppointmentPage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..99327160a8a0a4b2edd76be914c2cd4c48f3aa63 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/BookAppointmentPage.tsx @@ -0,0 +1,23 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +"use client"; + +import { ApiConcern } from "@eshg/sti-protection-api"; + +import { PageContent } from "@/lib/shared/components/layout/PageContent"; +import { PageLayout } from "@/lib/shared/components/layout/page"; + +import { AppointmentStepper } from "./AppointmentStepper"; + +export function BookAppointmentPage({ concern }: { concern: ApiConcern }) { + return ( + <PageLayout> + <PageContent> + <AppointmentStepper concern={concern} /> + </PageContent> + </PageLayout> + ); +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/StepButtons.tsx b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/StepButtons.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c0ce61a1aff8af9a4d2c287c3cda2be8bcc0c7cc --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/StepButtons.tsx @@ -0,0 +1,25 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Button, Stack } from "@mui/joy"; + +import { useStepContext } from "@/lib/businessModules/stiProtection/components/shared/StepContext"; +import { useTranslation } from "@/lib/i18n/client"; + +export function StepButtons() { + const { t } = useTranslation(); + const { goBack, isLastStep } = useStepContext(); + return ( + <Stack gap={2}> + <Button type="submit">{t("common.continue")}</Button> + {isLastStep ? ( + <Button variant="outlined" onClick={() => goBack()}> + {t("common.back")} + </Button> + ) : undefined} + <Button variant="soft">{t("common.cancel")}</Button> + </Stack> + ); +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/StepLayout.tsx b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/StepLayout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cad2a5af3f192b17b4e28e2f51b144a78c0ac891 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/StepLayout.tsx @@ -0,0 +1,130 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { FormPlus } from "@eshg/lib-portal/components/form/FormPlus"; +import { ApiConcern } from "@eshg/sti-protection-api"; +import { + AccessTimeOutlined, + DateRange, + MedicalServicesOutlined, +} from "@mui/icons-material"; +import { Box, Sheet, Stack, Typography } from "@mui/joy"; +import { formatDate } from "date-fns"; +import { Formik } from "formik"; +import { PropsWithChildren } from "react"; + +import { Row } from "@/lib/businessModules/measlesProtection/shared/components/Row"; +import { useStepContext } from "@/lib/businessModules/stiProtection/components/shared/StepContext"; +import { useTranslation } from "@/lib/i18n/client"; +import { TwoColumnGrid } from "@/lib/shared/components/layout/grid"; +import { PageTitle } from "@/lib/shared/components/layout/page"; + +import { useFormData } from "./AppointmentDataContext"; +import { AppointmentFormData } from "./AppointmentStepper"; +import { StepButtons } from "./StepButtons"; + +export function StepLayout({ children }: PropsWithChildren) { + const [formData, updateFormData] = useFormData<AppointmentFormData>(); + const { goForward } = useStepContext(); + + function handleSubmit(values: AppointmentFormData) { + updateFormData(values); + goForward(); + } + + return ( + <> + <BookAppointmentTitle /> + <Formik + enableReinitialize + initialValues={formData} + onSubmit={handleSubmit} + > + <FormPlus> + <TwoColumnGrid + content={ + <Sheet> + <Stack gap={3}> + {children} + <Box + sx={(theme) => ({ + [theme.breakpoints.up("md")]: { display: "none" }, + })} + > + <StepButtons /> + </Box> + </Stack> + </Sheet> + } + sidePanel={<AppointmentOverview />} + /> + </FormPlus> + </Formik> + </> + ); +} +export function BookAppointmentTitle() { + const { t } = useTranslation("stiProtection/forms"); + const { currentStepIndex, totalSteps } = useStepContext(); + return ( + <PageTitle> + <Row justifyContent="space-between"> + {t("common.appointment_booking_title")} + <Row sx={{ alignContent: "center" }}> + <Typography + level="h4" + sx={{ alignContent: "center" }} + textColor="text.tertiary" + > + {t("common.current_step", { + currentStep: currentStepIndex + 1, + totalSteps, + })} + </Typography> + </Row> + </Row> + </PageTitle> + ); +} + +function AppointmentOverview() { + const { t } = useTranslation("stiProtection/forms"); + const [{ concern, appointment, date }] = useFormData<AppointmentFormData>(); + const concernLabel = + concern === ApiConcern.SexWork + ? t("common.sex_work") + : t("common.hiv_sti_consultation"); + return ( + <Sheet + sx={(theme) => ({ + [theme.breakpoints.down("md")]: { display: "none" }, + })} + > + <Stack gap={3}> + <Typography level="h2">Termin Ãœbersicht</Typography> + <Stack gap={2}> + <Row> + <MedicalServicesOutlined /> {concernLabel} + </Row> + {date != null ? ( + <Row> + <DateRange /> {formatDate(date, "EEEE, d. MMMM y")} + </Row> + ) : null} + {appointment != null ? ( + <Row> + <AccessTimeOutlined />{" "} + {appointment.start.toLocaleTimeString(undefined, { + hour: "numeric", + minute: "2-digit", + })} + </Row> + ) : null} + </Stack> + <StepButtons /> + </Stack> + </Sheet> + ); +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/TimeSlotStep.tsx b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/TimeSlotStep.tsx new file mode 100644 index 0000000000000000000000000000000000000000..abc695879f8dee813874b50736da4924c76c5078 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/components/appointment/TimeSlotStep.tsx @@ -0,0 +1,106 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Alert } from "@eshg/lib-portal/components/Alert"; +import { InternalLinkButton } from "@eshg/lib-portal/components/navigation/InternalLinkButton"; +import { ApiConcern } from "@eshg/sti-protection-api"; +import { DateRangeOutlined } from "@mui/icons-material"; +import { Sheet, Stack, Typography } from "@mui/joy"; +import { startOfMonth } from "date-fns"; + +import { useFreeAppointments } from "@/lib/businessModules/stiProtection/api/queries/publicCitizenApi"; +import { useCitizenRoutes } from "@/lib/businessModules/stiProtection/shared/routes"; +import { useTranslation } from "@/lib/i18n/client"; + +import { useFormData } from "./AppointmentDataContext"; +import { AppointmentPickerSection } from "./AppointmentPickerSection"; +import { AppointmentFormData } from "./AppointmentStepper"; +import { BookAppointmentTitle, StepLayout } from "./StepLayout"; + +export function TimeSlotStep() { + const { t } = useTranslation("stiProtection/forms"); + const [formData, setFormData] = useFormData<AppointmentFormData>(); + const now = startOfMonth(new Date()); + const { data: appointments } = useFreeAppointments({ + concern: formData.concern, + earliestDate: now, + }); + + if ((appointments?.length ?? 0) === 0) { + return <NoAppointmentAvailable concern={formData.concern} />; + } + + return ( + <StepLayout> + <Typography level="h2">{t("time_slot.title")}</Typography> + <Alert + color="primary" + title={t("time_slot.consent_note_title")} + message={t("time_slot.consent_note_message")} + /> + <AppointmentPickerSection + appointments={appointments ?? []} + name="appointment" + t={t} + onDateSelected={(value) => + setFormData({ + date: value, + appointment: undefined, + }) + } + onAppointmentSelected={(value) => setFormData({ appointment: value })} + /> + </StepLayout> + ); +} + +function NoAppointmentAvailable({ concern }: { concern: ApiConcern }) { + const { t } = useTranslation("stiProtection/forms"); + const routes = useCitizenRoutes(); + return ( + <Stack gap={3}> + <BookAppointmentTitle /> + <Sheet> + <Stack gap={3} sx={{ padding: 3, alignItems: "center" }}> + <Typography level="h2" sx={{ alignSelf: "start" }}> + {t(`time_slot.title`)} + </Typography> + <DateRangeOutlined + sx={(theme) => ({ + height: theme.spacing(10), + width: theme.spacing(10), + color: theme.palette.primary.outlinedBorder, + })} + /> + <Typography level="title-md"> + {t("time_slot.no_appointments_available")} + </Typography> + <Typography + sx={(theme) => ({ + maxWidth: theme.spacing(80), + })} + > + {t("time_slot.try_later")} + </Typography> + <InternalLinkButton + href={ + concern === ApiConcern.SexWork + ? routes.sexWork + : routes.stiConsultation + } + size="lg" + sx={(theme) => ({ + maxWidth: theme.spacing(44), + width: "100%", + minWidth: "min-content", + })} + > + {t("base/translations:common.back")} + </InternalLinkButton> + </Stack> + </Sheet> + </Stack> + ); +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/components/shared/StepContext.tsx b/citizen-portal/src/lib/businessModules/stiProtection/components/shared/StepContext.tsx new file mode 100644 index 0000000000000000000000000000000000000000..47f9ef088d9c9d9406f1febd08f667921b12612f --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/components/shared/StepContext.tsx @@ -0,0 +1,95 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useReducer, + useState, +} from "react"; + +interface StepContextProps { + totalSteps: number; + currentStepIndex: number; + isFirstStep: boolean; + isLastStep: boolean; + onShowOverviewChange: (showOverview: boolean) => void; + isShowOverview: boolean; + goForward: (numOfSteps?: number) => void; + goBack: (numOfSteps?: number) => void; +} +export const StepContext = createContext<StepContextProps | null>(null); + +type ReactComponent = () => ReactNode; + +interface StepContextProviderProps { + steps: Readonly<[ReactComponent, ...ReactComponent[]]>; +} + +export function Stepper({ steps }: StepContextProviderProps) { + const [currentStepIndex, changeCurrentStepIndex] = useReducer( + (stepIndex: number, change: number) => + Math.min(Math.max(stepIndex + change, 0), steps.length - 1), + 0, + ); + + const [isShowOverview, setIsShowOverview] = useState(true); + const CurrentStep = steps[currentStepIndex]; + if (CurrentStep === undefined) { + throw new Error("Current step is undefined"); + } + + const goForward = useCallback( + (numOfSteps = 1) => changeCurrentStepIndex(numOfSteps), + [changeCurrentStepIndex], + ); + const goBack = useCallback( + (numOfSteps = 1) => changeCurrentStepIndex(-numOfSteps), + [changeCurrentStepIndex], + ); + + const isFirstStep = currentStepIndex === 0; + const isLastStep = currentStepIndex === steps.length - 1; + + const contextValue = useMemo( + () => ({ + goForward, + goBack, + currentStepIndex, + totalSteps: steps.length, + isFirstStep, + isLastStep, + onShowOverviewChange: setIsShowOverview, + isShowOverview, + }), + [ + goForward, + goBack, + currentStepIndex, + setIsShowOverview, + isShowOverview, + isFirstStep, + isLastStep, + steps.length, + ], + ); + + return ( + <StepContext.Provider value={contextValue}> + <CurrentStep /> + </StepContext.Provider> + ); +} + +export function useStepContext() { + const context = useContext(StepContext); + if (!context) { + throw new Error("useStepContext must be used with a Stepper"); + } + return context; +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/locales/de/forms.json b/citizen-portal/src/lib/businessModules/stiProtection/locales/de/forms.json new file mode 100644 index 0000000000000000000000000000000000000000..c541e4a58a20438c3067d8897e4b74fd9fafa8f5 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/locales/de/forms.json @@ -0,0 +1,67 @@ +{ + "common": { + "appointment_booking_title": "Termin buchen", + "required_title": "*Pflichtfeld", + "current_step": "Schritt {{currentStep}} von {{totalSteps}}", + "hiv_sti_consultation": "HIV-STI-Beratung", + "sex_work": "Sexarbeit" + }, + "time_slot": { + "title": "Verfügbare Termine", + "consent_note_title": "Einverständnis", + "consent_note_message": "Derpy merpus pee derpler berps! Perp sherper herp terp herpy derpler. Sherper merp herpler herp pee. Derpler terpus, mer re berp der perp se?", + "no_appointments_available": "Derzeit sind keine Termine verfügbar", + "try_later": "Wir schalten in kürze weitere Termine frei. Bitte versuchen Sie es zu einem späteren Zeitpunkt erneut." + }, + "personal_data": { + "title": "Persönliche Daten", + "fields": { + "first_name": "Vorname", + "first_name_required": "Bitte Vorname eingeben.", + "last_name": "Nachname", + "last_name_required": "Bitte Nachname eingeben.", + "date_of_birth": "Geburtsdatum", + "date_of_birth_required": "Bitte Geburtsdatum eingeben.", + "phone_numbers": "Telefon", + "email_addresses": "E-Mail-Adresse", + "email_addresses_required": "Bitte E-Mail-Adresse eingeben.", + "confirm_online_services": "Ich bestätige, dass ich die Online-Dienste für die Impfberatung nutzen und die hierzu notwendigen E-Mails erhalten möchte.", + "confirm_online_services_required": "Bitte Zustimmung erteilen um fortzufahren." + } + }, + "appointment_calendar": { + "required_appointment": "Bitte einen Termin auswählen", + "required_day": "Bitte einen Tag auswählen", + "month_selection": "Termin Kalendermonat", + "next_month": "zum nächsten Monat", + "prev_month": "zum vorherigen Monat", + "list_label": "Verfügbare Uhrzeiten *", + "calendar_label": "Datum *", + "available": "verfügbar" + }, + "appointment_info_section": { + "title": "Informationen zum Termin", + "alert_header": "Vorbereitungen zum Termin", + "alert_message": "Bitte bringen Sie Ihren Impfpass mit", + "info_text": "Sie erhalten in <t1>den nächsten Minuten</t1> eine <t1>Terminbestätigung</t1> per E-Mail. Dort sind alle Informationen zum Termin enthalten. Sie haben zudem die Möglichkeit den Termin zu ändern oder zu stornieren", + "required_documents_header": "Notwendige Dokumente, welche Sie bitte zum Termin mitbringen, sind:", + "list_item_id_card": "Personalausweis", + "list_item_vaccination_card": "Impfpass", + "closing_greeting": "Mit freundlichen Grüßen", + "health_department": "Ihr Gesundheitsamt" + }, + "confirmation_section": { + "title": "Terminbuchung", + "submit": "Termin verbindlich buchen", + "on_prev_step": "Zurück", + "on_cancel": "Abbrechen", + "fields": { + "confirm_privacy_notice": "Ich akzeptiere den Datenschutzhinweis.", + "privacy_notice": "Zum Datenschutzhinweis", + "confirm_privacy_notice_required": "Bitte Zustimmung erteilen um fortzufahren.", + "confirm_privacy_policy": "Ich akzeptiere die Datenschutzerklärung.", + "privacy_policy": "Zur Datenschutzerklärung", + "confirm_privacy_policy_required": "Bitte Zustimmung erteilen um fortzufahren." + } + } +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/locales/en/forms.json b/citizen-portal/src/lib/businessModules/stiProtection/locales/en/forms.json new file mode 100644 index 0000000000000000000000000000000000000000..997c093d74996665e5d6c24651b5dd50faa7841b --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/locales/en/forms.json @@ -0,0 +1,67 @@ +{ + "common": { + "appointment_booking_title": "Book appointment", + "required_title": "*Required field", + "current_step": "Step {{currentStep}} of {{totalSteps}}", + "hiv_sti_consultation": "HIV / STI Consultation", + "sex_work": "Sex work" + }, + "time_slot": { + "title": "Available Appointments", + "consent_note_title": "Consent", + "consent_note_message": "Derpy merpus pee derpler berps! Perp sherper herp terp herpy derpler. Sherper merp herpler herp pee. Derpler terpus, mer re berp der perp se?", + "no_appointments_available": "There are currently no appointments available", + "try_later": "We will add more appointments shortly. Please try again at a later date" + }, + "personal_data": { + "title": "Personal Data", + "fields": { + "first_name": "First Name", + "first_name_required": "Please enter your first name.", + "last_name": "Last Name", + "last_name_required": "Please enter your last name.", + "date_of_birth": "Date of Birth", + "date_of_birth_required": "Please enter your date of birth.", + "phone_numbers": "Phone", + "email_addresses": "Email Address", + "email_addresses_required": "Please enter your email address.", + "confirm_online_services": "I confirm that I wish to use the online services for vaccination consultation and receive the necessary emails.", + "confirm_online_services_required": "Please provide consent to continue." + } + }, + "appointment_calendar": { + "required_appointment": "Please select an appointment", + "required_day": "Please select a day", + "month_selection": "Date of calendar month", + "next_month": "next month", + "prev_month": "previous month", + "list_label": "Available times *", + "calendar_label": "Date *", + "available": "available" + }, + "appointment_info_section": { + "title": "Appointment Information", + "alert_header": "Preparations for the Appointment", + "alert_message": "Please bring your International Certificate of Vaccination or Prophylaxis (yellow card)", + "info_text": "You will receive an <t1>appointment confirmation</t1> via email in <t1>the next few minutes</t1>. All information about the appointment is included. You will also have the option to change or cancel the appointment.", + "required_documents_header": "Necessary documents that you should bring to the appointment are:", + "list_item_id_card": "Passport or identity card", + "list_item_vaccination_card": "International Certificate of Vaccination or Prophylaxis (yellow card)", + "closing_greeting": "Sincerely,", + "health_department": "Your Health Department" + }, + "confirmation_section": { + "title": "Appointment Booking", + "submit": "Book Appointment", + "on_prev_step": "Back", + "on_cancel": "Cancel", + "fields": { + "confirm_privacy_notice": "I accept the data protection notice.", + "privacy_notice": "Go to the data protection notice", + "confirm_privacy_notice_required": "Please give your consent to continue.", + "confirm_privacy_policy": "I accept the privacy policy.", + "privacy_policy": "Go to the privacy policy", + "confirm_privacy_policy_required": "Please give your consent to continue." + } + } +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/Landingpage.tsx b/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/Landingpage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f46d9ac44911ecb609612d915b42c2ddc1109e58 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/Landingpage.tsx @@ -0,0 +1,39 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +"use client"; + +import { ApiConcern } from "@eshg/sti-protection-api"; + +import { LandingpageContent } from "@/lib/businessModules/stiProtection/pages/landingpage/LandingpageContent"; +import { LandingpageSidePanel } from "@/lib/businessModules/stiProtection/pages/landingpage/LandingpageSidePanel"; +import { useTranslation } from "@/lib/i18n/client"; +import { PageContent } from "@/lib/shared/components/layout/PageContent"; +import { TwoColumnGrid } from "@/lib/shared/components/layout/grid"; +import { PageLayout, PageTitle } from "@/lib/shared/components/layout/page"; + +export function Landingpage({ concern }: { concern: ApiConcern }) { + const { t } = useTranslation(["stiProtection/overview"]); + const isSexWork = concern === ApiConcern.SexWork; + return ( + <PageLayout banner="private"> + <PageContent> + <PageTitle> + {t(isSexWork ? "page_title_sex_work" : "page_title_sti_consultation")} + </PageTitle> + <TwoColumnGrid + content={ + <LandingpageContent + concern={ + isSexWork ? ApiConcern.SexWork : ApiConcern.HivStiConsultation + } + /> + } + sidePanel={<LandingpageSidePanel />} + /> + </PageContent> + </PageLayout> + ); +} diff --git a/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/LandingpageContent.tsx b/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/LandingpageContent.tsx index 5a2094b1d8f552c736035fb5b0da2550e5ac103f..e9cbed6780db587e02a56ef9f09ab2cf0ca2818f 100644 --- a/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/LandingpageContent.tsx +++ b/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/LandingpageContent.tsx @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +"use client"; + import { ExternalLink } from "@eshg/lib-portal/components/navigation/ExternalLink"; import { ApiConcern } from "@eshg/sti-protection-api"; import { CallOutlined, MailOutlineOutlined } from "@mui/icons-material"; diff --git a/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/LandingpageSidePanel.tsx b/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/LandingpageSidePanel.tsx index 3eca5e518872e6fbb4eacfbd636d0271d58a832e..3ff099af7ae6ad7e9982d3fcbb1c6df674b16a28 100644 --- a/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/LandingpageSidePanel.tsx +++ b/citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/LandingpageSidePanel.tsx @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +"use client"; + import { InternalLinkButton } from "@eshg/lib-portal/components/navigation/InternalLinkButton"; import { Stack, Typography } from "@mui/joy"; diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/DetailsField.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/DetailsField.tsx index 4018fb45260a3cd0906653efa9f74dc52fc7e045..2bfe6d85e1f3101b1ed87fd99c02fb4e6153f370 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/DetailsField.tsx +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/DetailsField.tsx @@ -8,7 +8,7 @@ import { ReactNode } from "react"; export interface DetailsFieldProps { icon: ReactNode; - value: string; + value: string | ReactNode; } export function DetailsField(props: Readonly<DetailsFieldProps>) { diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/FormSheet.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/FormSheet.tsx index ac384f5f3687bf2f8dfa70b5653238d1d8a01c78..ebd516b8590439e8dc9ee6ea2cff5d11578701d8 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/FormSheet.tsx +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/FormSheet.tsx @@ -5,9 +5,10 @@ import { RequiresChildren } from "@eshg/lib-portal/types/react"; import { Sheet, Stack, Typography } from "@mui/joy"; +import { SxProps } from "@mui/joy/styles/types"; import { theme } from "@/lib/baseModule/theme/theme"; -import { useIsMobile } from "@/lib/shared/hooks/useIsMobile"; +import { byBreakpoint } from "@/lib/shared/breakpoints"; interface FormSheetProps extends RequiresChildren { "data-testid"?: string; @@ -31,13 +32,12 @@ export function FormSheet(props: FormSheetProps) { interface FormSheetTitleProps extends RequiresChildren { requiredTitle?: string; + sx?: SxProps; } export function FormSheetTitle(props: FormSheetTitleProps) { - const isMobile = useIsMobile(); - return ( - <Stack gap={isMobile ? 1 : 0}> + <Stack gap={byBreakpoint({ mobile: 1, desktop: 0 })} sx={props.sx}> <Typography level="h2">{props.children}</Typography> {props.requiredTitle && ( <Typography diff --git a/citizen-portal/src/lib/i18n/client.ts b/citizen-portal/src/lib/i18n/client.ts index cf132dbd0d44b9d58680960fc43ea38ec3fcd9a2..b8a88fdba87426946c12cf2604de8d4122a53194 100644 --- a/citizen-portal/src/lib/i18n/client.ts +++ b/citizen-portal/src/lib/i18n/client.ts @@ -95,7 +95,7 @@ export function useTWithCamelCase(t: TranslateFn): TranslateFn { return t(args, tOptions); } const newKeys: string[] = pipe( - keys.map((k) => [k, fromSnakeToCamel(k)]), + keys.map((k) => [fromSnakeToCamel(k), k]), flat(), unique(), ); diff --git a/citizen-portal/src/lib/shared/components/layout/grid.tsx b/citizen-portal/src/lib/shared/components/layout/grid.tsx index 502767c2ef2cbb23e11e989d70abd7e9edef939c..65b7904ecc3e1558c9fc6d369f12f521cd10a8eb 100644 --- a/citizen-portal/src/lib/shared/components/layout/grid.tsx +++ b/citizen-portal/src/lib/shared/components/layout/grid.tsx @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +"use client"; + import { RequiresChildren } from "@eshg/lib-portal/types/react"; import { Grid, Stack } from "@mui/joy"; import { ReactNode } from "react"; diff --git a/config/vitest.base.ts b/config/vitest.base.ts index 9b8e1f6e34cd54bfa270a2b6ea2c4d62dee94f15..09cd706e9512b944b408bcf3f411ce8da7c0738f 100644 --- a/config/vitest.base.ts +++ b/config/vitest.base.ts @@ -22,7 +22,7 @@ export const VITEST_BASE_CONFIG: ViteUserConfig = { provider: "istanbul", all: true, reportsDirectory: `${VITEST_OUT_DIR}/coverage`, - reporter: ["html", "lcov"], + reporter: ["text-summary", "html"], include: ["src/**/*"], exclude: VITEST_COVERAGE_EXCLUDES, }, diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c9491d6f1d04c693e58b42e2671cf2572a626aac --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,71 @@ +# Copyright 2025 cronn GmbH +# SPDX-License-Identifier: AGPL-3.0-only + +services: + reverse-proxy-base: + image: nginx:1.27.1 + read_only: true + tmpfs: + - /var/cache/nginx + - /var/run + extra_hosts: + - host.docker.internal:host-gateway + volumes: + - ./reverse-proxy/forward_headers.conf:/etc/nginx/forward_headers.conf:ro + - ./reverse-proxy/auth_request.conf:/etc/nginx/auth_request.conf:ro + - ./reverse-proxy/auth_api_request.conf:/etc/nginx/auth_api_request.conf:ro + - ./reverse-proxy/nginx.conf:/etc/nginx/nginx.conf:ro + + employee-portal-reverse-proxy: + extends: + service: reverse-proxy-base + ports: + - 4000:4000 + volumes: + - ./reverse-proxy/employee-portal.conf:/etc/nginx/conf.d/default.conf:ro + + citizen-portal-reverse-proxy: + extends: + service: reverse-proxy-base + ports: + - 4001:4001 + volumes: + - ./reverse-proxy/citizen-portal.conf:/etc/nginx/conf.d/default.conf:ro + + admin-portal-reverse-proxy: + extends: + service: reverse-proxy-base + ports: + - 4002:4002 + volumes: + - ./reverse-proxy/admin-portal.conf:/etc/nginx/conf.d/default.conf:ro + + employee-portal: + image: ga-lotse/employee-portal:${EMPLOYEE_PORTAL_TAG:-latest} + environment: + - HOSTNAME=0.0.0.0 + restart: unless-stopped + ports: + - 3000:3000 + depends_on: + - employee-portal-reverse-proxy + + citizen-portal: + image: ga-lotse/citizen-portal:${CITIZEN_PORTAL_TAG:-latest} + environment: + - HOSTNAME=0.0.0.0 + restart: unless-stopped + ports: + - 3001:3001 + depends_on: + - citizen-portal-reverse-proxy + + admin-portal: + image: ga-lotse/admin-portal:${ADMIN_PORTAL_TAG:-latest} + environment: + - HOSTNAME=0.0.0.0 + restart: unless-stopped + ports: + - 3002:3002 + depends_on: + - admin-portal-reverse-proxy diff --git a/docs/gradle.adoc b/docs/gradle.adoc index 5d082911f25411cbe46dfdf1bf2895195db584ca..8eb2bd12c3370f08dfc8009d978a25310f3ffa36 100644 --- a/docs/gradle.adoc +++ b/docs/gradle.adoc @@ -38,7 +38,7 @@ These are also available within the subprojects. | `./gradlew [project-name]:run` | Start the application in production mode | `./gradlew [project-name]:runDev [-Pwebpack]` | Start the application in development mode (hot-code reloading). Use `-Pwebpack` to use webpack instead of Turbopack (default). | `./gradlew [project-name]:build` | Run all checks and builds the {project-name} -| `./gradlew [project-name]:test` | Run all tests in a single run +| `./gradlew [project-name]:test [-Pcoverage]` | Run all tests in a single run. Add `-Pcoverage` to enable coverage reporting. | `./gradlew [project-name]:testWatch` | Run all tests in watch mode (reruns tests when they change) | `./gradlew [project-name]:analyzeBundle` | Generates a visual report of the size of each module and their dependencies |=== diff --git a/docs/migration.adoc b/docs/migration.adoc new file mode 100644 index 0000000000000000000000000000000000000000..1a18c7590953a1b504a80c9d25ffe227e4c11036 --- /dev/null +++ b/docs/migration.adoc @@ -0,0 +1,19 @@ += Migration Guide +GA-Lotse +:toc: + +This document provides migration notes for all version changes, starting with v1.6.4. + +== Version History + +=== v1.6.5 + +- No changes that made a migration necessary + +=== v1.6.4 + +- The Auditlog is now a database module which needs a new permission rule in the service directory: +[source] +---- +client XYZ/FM/* -> server XYZ/FM/auditlog +---- diff --git a/employee-portal/src/app/(businessModules)/dental/children/[childId]/examinations/[examinationId]/page.tsx b/employee-portal/src/app/(businessModules)/dental/children/[childId]/examinations/[examinationId]/page.tsx index 310927e80c4d390e037047a44fe2c4a010758aa7..28fb07b14f22efb491faf578738a33672fea1183 100644 --- a/employee-portal/src/app/(businessModules)/dental/children/[childId]/examinations/[examinationId]/page.tsx +++ b/employee-portal/src/app/(businessModules)/dental/children/[childId]/examinations/[examinationId]/page.tsx @@ -36,7 +36,10 @@ export default function ExaminationDetailsPage(props: DentalChildPageProps) { ); return ( - <DentalExaminationStoreProvider examinationResult={examination.result}> + <DentalExaminationStoreProvider + examinationResult={examination.result} + defaultDentitionType={examination.prophylaxisDentitionType} + > <ChildExaminationForm examination={examination}> <ExaminationFormLayout childInformation={ diff --git a/employee-portal/src/app/(businessModules)/dental/prophylaxis-sessions/[prophylaxisSessionId]/examinations/[participantIndex]/page.tsx b/employee-portal/src/app/(businessModules)/dental/prophylaxis-sessions/[prophylaxisSessionId]/examinations/[participantIndex]/page.tsx index a57c61e46f05f25c4f0f8103b6d96b6c515bb491..13671116ac80f21e0d69407835468d830376793d 100644 --- a/employee-portal/src/app/(businessModules)/dental/prophylaxis-sessions/[prophylaxisSessionId]/examinations/[participantIndex]/page.tsx +++ b/employee-portal/src/app/(businessModules)/dental/prophylaxis-sessions/[prophylaxisSessionId]/examinations/[participantIndex]/page.tsx @@ -39,7 +39,10 @@ export default function ProphylaxisSessionExaminationPage( } return ( - <DentalExaminationStoreProvider examinationResult={participant.result}> + <DentalExaminationStoreProvider + examinationResult={participant.result} + defaultDentitionType={participant.prophylaxisDentitionType} + > <ParticipantExaminationPage participant={participant} participantIndex={participantIndex} diff --git a/employee-portal/src/app/playground/charts/page.tsx b/employee-portal/src/app/playground/charts/page.tsx index b7e4cd618068e44e27c5d15b60a13c44122bb6b6..af18fdc2615d425c4588df3c46f87f5e07e04b51 100644 --- a/employee-portal/src/app/playground/charts/page.tsx +++ b/employee-portal/src/app/playground/charts/page.tsx @@ -7,6 +7,7 @@ import { MainContentLayout } from "@eshg/lib-employee-portal/components/layout/MainContentLayout"; import { Option, Select, Sheet, Stack, Switch, Typography } from "@mui/joy"; +import { MapSeriesOption } from "echarts"; import { ReactNode, useState } from "react"; import { @@ -18,6 +19,8 @@ import { DiagramScaling, DiagramType, } from "@/lib/businessModules/statistics/api/models/evaluationDetailsViewTypes"; +import { choroplethCountryCount } from "@/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ChartsSamplePreview"; +import { continentsGeoJSON } from "@/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/worldContinentsGeoJSON"; import { AnalysisDiagramBox } from "@/lib/businessModules/statistics/components/shared/AnalysisAccordion/AnalysisDiagramBox"; import { BarChart } from "@/lib/businessModules/statistics/components/shared/charts/BarChart"; import { ChoroplethMap } from "@/lib/businessModules/statistics/components/shared/charts/ChoroplethMap"; @@ -65,6 +68,7 @@ export default function PlaygroundChartsPage() { const [characteristicParameter, setCharacteristicParameter] = useState< DiagramCharacteristicParameter | undefined >(); + const [aspectScale, setAspectScale] = useState(false); const orientationSwitch = ( <Typography @@ -191,6 +195,22 @@ export default function PlaygroundChartsPage() { </Typography> ); + const aspectScaleSwitch = ( + <Typography + component="label" + endDecorator={ + <Switch + checked={aspectScale} + onChange={(event) => setAspectScale(event.target.checked)} + startDecorator={<p>0.75 (default)</p>} + endDecorator={<p>1</p>} + /> + } + > + Aspect Scale + </Typography> + ); + const barChartSimple = [ { label: "Hund", @@ -757,68 +777,6 @@ export default function PlaygroundChartsPage() { }, }; - const choroplethData = [ - { - name: "Altstadt", - value: 10, - }, - { - name: "Neustadt", - value: 23, - }, - ]; - - const geoJson = JSON.stringify({ - type: "FeatureCollection", - features: [ - { - type: "Feature", - geometry: { - type: "MultiPolygon", - coordinates: [ - [ - [ - [8, 50], - [9, 50], - [9, 52], - [8, 52], - [8, 50], - ], - ], - ], - }, - properties: { - name: "Altstadt", - cartodb_id: 1, - created_at: "2015-02-27T08:56:16Z", - updated_at: "2015-02-22T00:00:00Z", - }, - }, - { - type: "Feature", - geometry: { - type: "MultiPolygon", - coordinates: [ - [ - [ - [9, 50], - [9, 52], - [10, 51], - [9, 50], - ], - ], - ], - }, - properties: { - name: "Neustadt", - cartodb_id: 2, - created_at: "2015-02-27T08:56:16Z", - updated_at: "2015-02-22T00:00:00Z", - }, - }, - ], - }); - return ( <MainContentLayout> <Stack gap={3}> @@ -987,13 +945,25 @@ export default function PlaygroundChartsPage() { title="Choroplethenkarte" chart={ <ChoroplethMap - diagramData={choroplethData} + key={`${aspectScale}`} + diagramData={choroplethCountryCount} colorScheme={colorScheme} characteristicParameter={characteristicParameter} - geoJson={geoJson} + geoJson={continentsGeoJSON} + additionalEchartsSeriesOptions={ + aspectScale + ? ({ + aspectScale: 1, + } as MapSeriesOption) + : undefined + } /> } - switches={[colorSchemeSelect, characteristicParameterSelect]} + switches={[ + colorSchemeSelect, + characteristicParameterSelect, + aspectScaleSwitch, + ]} /> </Stack> </MainContentLayout> diff --git a/employee-portal/src/app/playground/sideNavigation/page.tsx b/employee-portal/src/app/playground/sideNavigation/page.tsx index f22d05213274501578de4d1a03b773f3569bee1a..f34eb7907b85968476443230aad8c04bf5e80c5c 100644 --- a/employee-portal/src/app/playground/sideNavigation/page.tsx +++ b/employee-portal/src/app/playground/sideNavigation/page.tsx @@ -9,6 +9,7 @@ import { MainContentLayout } from "@eshg/lib-employee-portal/components/layout/M import { StickyToolbarLayout } from "@eshg/lib-employee-portal/components/layout/StickyToolbarLayout"; import { Toolbar } from "@eshg/lib-employee-portal/components/toolbar/Toolbar"; import { noCheck } from "@eshg/lib-employee-portal/helpers/accessControl"; +import { SideNavigationItem } from "@eshg/lib-employee-portal/types/sideNavigation"; import { AcUnitOutlined, AppsOutlined, @@ -17,15 +18,27 @@ import { LightOutlined, WavingHandOutlined, } from "@mui/icons-material"; -import { Chip, Stack } from "@mui/joy"; +import { + Button, + Chip, + Divider, + Stack, + Switch, + ToggleButtonGroup, + Typography, +} from "@mui/joy"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { ReactNode, useState } from "react"; -import { NavigationListCollapsed } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsed"; -import { NavigationListExpanded } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationListExpanded"; +import { NavigationItem } from "@/lib/baseModule/components/layout/sideNavigation/items/NavigationItem"; +import { CollapsedNavigationList } from "@/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationList"; +import { ExpandedNavigationList } from "@/lib/baseModule/components/layout/sideNavigation/lists/ExpandedNavigationList"; import { SideNavItemGroups } from "@/lib/baseModule/components/layout/sideNavigation/types"; const itemGroups: SideNavItemGroups = { dashboardItem: [ { + type: "SideNavigationLinkItem", name: "Single Item", href: "#", decorator: <LightOutlined />, @@ -34,18 +47,21 @@ const itemGroups: SideNavItemGroups = { ], businessItems: [ { + type: "SideNavigationLinkItem", name: "Dashboard", href: "#", decorator: <AppsOutlined />, accessCheck: noCheck(), }, { + type: "SideNavigationLinkItem", name: "Selected", href: "/playground/sideNavigation", decorator: <WavingHandOutlined />, accessCheck: noCheck(), }, { + type: "SideNavigationLinkItem", name: "Chat", href: "#", decorator: <ChatOutlined />, @@ -53,6 +69,7 @@ const itemGroups: SideNavItemGroups = { chip: <Chip color="primary">15</Chip>, }, { + type: "SideNavigationLinkItem", name: "Rechtsschutzversicherungsgesellschaften", href: "#", decorator: <AcUnitOutlined />, @@ -61,6 +78,7 @@ const itemGroups: SideNavItemGroups = { ], baseItems: [ { + type: "SideNavigationParentItem", name: "Hauptmenü", decorator: <InsertEmoticonOutlined />, subItems: [ @@ -71,8 +89,8 @@ const itemGroups: SideNavItemGroups = { ], }, { + type: "SideNavigationParentItem", name: "Selected menu", - href: "/playground/sideNavigation", decorator: <WavingHandOutlined />, subItems: [ { @@ -84,41 +102,200 @@ const itemGroups: SideNavItemGroups = { ], }, { + type: "SideNavigationParentItem", name: "Kraftfahrzeug-Haftpflichtversicherung", decorator: <LightOutlined />, subItems: [{ name: "Item", href: "#", accessCheck: noCheck() }], }, { - name: "Noch ein Item", + type: "SideNavigationSuspenseItem", + name: "Loading endlessly", decorator: <LightOutlined />, - error: "error message", - subItems: [{ name: "Item", href: "#", accessCheck: noCheck() }], + accessCheck: noCheck(), + component: LoadingItem, + }, + { + type: "SideNavigationSuspenseItem", + name: "Load 5s", + decorator: <LightOutlined />, + accessCheck: noCheck(), + component: SuspendingItem, + }, + { + type: "SideNavigationSuspenseItem", + name: "Fail to load", + decorator: <LightOutlined />, + accessCheck: noCheck(), + component: FailingItem, }, ], }; +function LoadingItem(): ReactNode { + // Suspend forever + // eslint-disable-next-line @typescript-eslint/only-throw-error, @typescript-eslint/no-empty-function + throw new Promise(() => {}); +} + +function SuspendingItem() { + useSuspenseQuery({ + queryKey: ["playground", "suspense", "resolve"], + queryFn: () => + new Promise((resolve) => setTimeout(() => resolve("success"), 5000)), + }); + + return ( + <NavigationItem + item={{ + type: "SideNavigationLinkItem", + name: "Component", + decorator: <LightOutlined />, + href: "#", + accessCheck: noCheck(), + }} + /> + ); +} + +function FailingItem() { + useSuspenseQuery({ + queryKey: ["playground", "suspense", "reject"], + queryFn: () => + new Promise((_resolve, reject) => + setTimeout(() => reject(new Error("failed")), 5000), + ), + }); + + return undefined; +} + +function ItemStatePlayground() { + type ItemState = "link" | "parent" | "loading" | "error"; + const [collapsed, setCollapsed] = useState(false); + const [selected, setSelected] = useState(false); + const [itemState, setItemState] = useState<ItemState>("link"); + + const itemStateComponents: Record<ItemState, () => ReactNode> = { + link: () => ( + <NavigationItem + item={{ + type: "SideNavigationLinkItem", + name: "Demo Item", + decorator: <LightOutlined />, + accessCheck: noCheck(), + href: selected ? "/playground/sideNavigation" : "#", + }} + /> + ), + parent: () => ( + <NavigationItem + item={{ + type: "SideNavigationParentItem", + name: "Demo Item", + decorator: <LightOutlined />, + subItems: [ + { + name: "Sub Item", + href: selected ? "/playground/sideNavigation" : "#", + accessCheck: noCheck(), + }, + ], + }} + /> + ), + loading: LoadingItem, + error: () => { + throw new Error("Error"); + }, + }; + + const item: SideNavigationItem = { + type: "SideNavigationSuspenseItem", + name: "Demo Item", + decorator: <LightOutlined />, + accessCheck: noCheck(), + component: itemStateComponents[itemState], + }; + + const itemGroups: SideNavItemGroups = { + dashboardItem: [item], + businessItems: [], + baseItems: [], + }; + + return ( + <> + <Stack direction="row" spacing={2}> + <ToggleButtonGroup + value={itemState} + onChange={(_event, newValue) => { + setItemState(newValue!); + }} + > + <Button value="link">LinkItem</Button> + <Button value="parent">ParentItem</Button> + <Button value="loading">LoadingItem</Button> + <Button value="error">ErrorItem</Button> + </ToggleButtonGroup> + <Typography + component="label" + endDecorator={ + <Switch + checked={selected} + onChange={(event) => setSelected(event.target.checked)} + /> + } + > + Selected + </Typography> + </Stack> + {collapsed ? ( + <CollapsedNavigationList + key={itemState} + onExpand={() => { + setCollapsed(false); + }} + itemGroups={itemGroups} + /> + ) : ( + <ExpandedNavigationList + key={itemState} + showCollapseButton={true} + onCollapse={() => { + setCollapsed(true); + }} + itemGroups={itemGroups} + /> + )} + </> + ); +} + export default function SideNavigationPlaygroundPage() { return ( <StickyToolbarLayout toolbar={<Toolbar title="SideNavigation" backHref="/playground" />} > <MainContentLayout> - <Stack direction="row" spacing={2}> - <NavigationListExpanded - isLoading={false} - showCollapseButton={true} - onCollapse={() => { - alert("Collapse"); - }} - itemGroups={itemGroups} - /> - - <NavigationListCollapsed - onExpand={() => { - alert("Expand"); - }} - itemGroups={itemGroups} - /> + <Stack spacing={2}> + <Stack direction="row" spacing={2}> + <ExpandedNavigationList + showCollapseButton={true} + onCollapse={() => { + alert("Collapse"); + }} + itemGroups={itemGroups} + /> + + <CollapsedNavigationList + onExpand={() => { + alert("Expand"); + }} + itemGroups={itemGroups} + /> + </Stack> + <Divider /> + <ItemStatePlayground /> </Stack> </MainContentLayout> </StickyToolbarLayout> diff --git a/employee-portal/src/lib/baseModule/api/queries/tasks.ts b/employee-portal/src/lib/baseModule/api/queries/tasks.ts index 185feeb25866fdfa288ffd830ebd111559eec669..7efc4554b475ef5b02b67838f24eacb03ea84670 100644 --- a/employee-portal/src/lib/baseModule/api/queries/tasks.ts +++ b/employee-portal/src/lib/baseModule/api/queries/tasks.ts @@ -54,9 +54,6 @@ export function useFetchTasksForOverviewQueryOptions( ? ApiGetTasksSortOrderFromJSON(searchParams.sortDirection.toUpperCase()) : ApiGetTasksSortOrder.Desc; - const limit = searchParams.pageSize ?? 25; - const offset = searchParams.pageNumber ? limit * searchParams.pageNumber : 0; - const request: AggregateTasksRequest = { assigneeId: selfUser.userId, assignedById: filter.assignedById, @@ -65,8 +62,8 @@ export function useFetchTasksForOverviewQueryOptions( taskStatus: filter.taskStatus, sortBy: sortBy, sortOrder: sortOrder, - limit: limit, - offset: offset, + pageSize: searchParams.pageSize ?? 25, + pageNumber: searchParams.pageNumber ?? 0, }; return queryOptions({ diff --git a/employee-portal/src/lib/baseModule/components/layout/ChatSettingsSidebar.tsx b/employee-portal/src/lib/baseModule/components/layout/ChatSettingsSidebar.tsx index 28a66de0bb521e4676ce3a20bf8c17cc13777c51..9a34632b8bed66ef1b1930eb243b9deaf81b1b09 100644 --- a/employee-portal/src/lib/baseModule/components/layout/ChatSettingsSidebar.tsx +++ b/employee-portal/src/lib/baseModule/components/layout/ChatSettingsSidebar.tsx @@ -83,7 +83,7 @@ function ChatSettingsSidebar({ onClose }: DrawerProps) { togglePresenceStatus(sharePresence); if (matrixClient && isClientPrepared) { - if (!sharePresence) { + if (sharePresence) { await setPresenceOffline(matrixClient); } else { await setPresenceOnline(matrixClient); diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/SideNavigation.tsx b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/SideNavigation.tsx index d46c078e785ad290cd522b80648ba6a7d2dbefbe..769dde4ef115ee09f25d39404101d992f747de51 100644 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/SideNavigation.tsx +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/SideNavigation.tsx @@ -7,10 +7,10 @@ import { useHeaderHeights } from "@eshg/lib-employee-portal/hooks/useHeaderHeigh import { Box, Drawer } from "@mui/joy"; import { Dispatch, SetStateAction } from "react"; -import { NavigationListCollapsed } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsed"; -import { NavigationListExpanded } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationListExpanded"; -import { useNavigationItems } from "@/lib/baseModule/components/layout/sideNavigation/useNavigationItems"; +import { CollapsedNavigationList } from "@/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationList"; +import { ExpandedNavigationList } from "@/lib/baseModule/components/layout/sideNavigation/lists/ExpandedNavigationList"; import { sideNavigationWidth } from "@/lib/baseModule/components/layout/sizes"; +import { useResolveSideNavigationItems } from "@/lib/baseModule/moduleRegister/sideNavigationItemsResolver"; import { useSidenav } from "@/lib/shared/components/drawer/useSidenav"; export function SideNavigation({ @@ -21,7 +21,7 @@ export function SideNavigation({ setCollapsed: Dispatch<SetStateAction<boolean>>; }) { const sidenav = useSidenav(); - const { isLoading, itemGroups } = useNavigationItems(); + const itemGroups = useResolveSideNavigationItems(); const { headerHeightMobile, headerHeightDesktop } = useHeaderHeights(); return ( @@ -38,14 +38,13 @@ export function SideNavigation({ }} > {!collapsed ? ( - <NavigationListExpanded + <ExpandedNavigationList showCollapseButton onCollapse={() => setCollapsed(true)} itemGroups={itemGroups} - isLoading={isLoading} /> ) : ( - <NavigationListCollapsed + <CollapsedNavigationList onExpand={() => setCollapsed(false)} itemGroups={itemGroups} /> @@ -77,10 +76,9 @@ export function SideNavigation({ display: "flex", }} > - <NavigationListExpanded + <ExpandedNavigationList showCollapseButton={false} itemGroups={itemGroups} - isLoading={isLoading} /> </Box> </Drawer> diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/filterNavigationItemsWithAccess.ts b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/filterNavigationItemsWithAccess.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2e361f31cd07a4e5cf874de5448774dad3ac5ee --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/filterNavigationItemsWithAccess.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { AccessCheck } from "@eshg/lib-employee-portal/helpers/accessControl"; +import { SideNavigationItem } from "@eshg/lib-employee-portal/types/sideNavigation"; + +export function filterNavigationItemsWithAccess( + items: SideNavigationItem[], + checkAccess: (check: AccessCheck) => boolean, +): SideNavigationItem[] { + function removeRestrictedSubItems( + item: SideNavigationItem, + ): SideNavigationItem { + if (item.type === "SideNavigationParentItem") { + return { + ...item, + subItems: item.subItems.filter((subItem) => + checkAccess(subItem.accessCheck), + ), + }; + } + return item; + } + + function nonEmptyItem(item: SideNavigationItem): boolean { + if (item.type === "SideNavigationParentItem") { + return item.subItems.length > 0; + } + return true; + } + + function permittedItem(item: SideNavigationItem): boolean { + if ( + item.type === "SideNavigationLinkItem" || + item.type === "SideNavigationSuspenseItem" + ) { + return checkAccess(item.accessCheck); + } + return true; + } + + return items + .map(removeRestrictedSubItems) + .filter(nonEmptyItem) + .filter(permittedItem); +} diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/NavigationIconItem.tsx b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/CollapsedNavigationItem.tsx similarity index 80% rename from employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/NavigationIconItem.tsx rename to employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/CollapsedNavigationItem.tsx index bfd377313ef2a6073db283408040635298b76dc7..9e947df03d0250c28f600889f623b287b48c6438 100644 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/NavigationIconItem.tsx +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/CollapsedNavigationItem.tsx @@ -4,9 +4,9 @@ */ import { - SideNavigationItem, - SideNavigationItemWithSubItems, - SideNavigationItemWithoutSubItems, + SideNavigationLinkItem, + SideNavigationParentItem, + SideNavigationSuspenseItem, } from "@eshg/lib-employee-portal/types/sideNavigation"; import { NavigationLink } from "@eshg/lib-portal/components/navigation/NavigationLink"; import { @@ -17,12 +17,12 @@ import { Menu, MenuButton, MenuItem, + Skeleton, Tooltip, Typography, } from "@mui/joy"; import { usePathname } from "next/navigation"; -import { KeyboardEvent, useContext, useRef, useState } from "react"; -import { isDefined } from "remeda"; +import { KeyboardEvent, useRef, useState } from "react"; import { navItemSelectedBackgroundColor, @@ -31,19 +31,19 @@ import { import { ModuleErrorModal } from "@/lib/baseModule/components/layout/sideNavigation/items/ModuleErrorModal"; import { NavigationItemError } from "@/lib/baseModule/components/layout/sideNavigation/items/NavigationItemError"; import { isItemSelected } from "@/lib/baseModule/components/layout/sideNavigation/items/isItemSelected"; -import { NavigationListCollapsedContext } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsedContext"; +import { useCollapsedNavigationListContext } from "@/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationListContext"; import { tooltipEnterDelay } from "@/lib/baseModule/components/layout/sizes"; -function NavigationIconItemWithoutSubItems({ +export function CollapsedNavigationLinkItem({ item, }: { - item: SideNavigationItemWithoutSubItems; + item: SideNavigationLinkItem; }) { - const { setOpenMenuItemName } = useContext(NavigationListCollapsedContext); + const { setOpenMenuItemName } = useCollapsedNavigationListContext(); const pathname = usePathname(); const selected = isItemSelected(item, pathname); - function resetActiveIndex() { + function closeNavigationMenu() { setOpenMenuItemName(null); } @@ -67,9 +67,9 @@ function NavigationIconItemWithoutSubItems({ "--Icon-color": navItemSelectedIconColor, }, }} - onMouseEnter={resetActiveIndex} - onKeyDown={resetActiveIndex} - onClick={resetActiveIndex} + onMouseEnter={closeNavigationMenu} + onKeyDown={closeNavigationMenu} + onClick={closeNavigationMenu} > {item.decorator} </ListItemButton> @@ -78,15 +78,15 @@ function NavigationIconItemWithoutSubItems({ ); } -function ErrorNavigationIconItem({ +export function CollapsedNavigationErrorItem({ item, }: { - item: SideNavigationItemWithSubItems; + item: SideNavigationSuspenseItem; }) { - const { setOpenMenuItemName } = useContext(NavigationListCollapsedContext); + const { setOpenMenuItemName } = useCollapsedNavigationListContext(); const [errorModalOpen, setErrorModalOpen] = useState(false); - function resetActiveIndex() { + function closeNavigationMenu() { setOpenMenuItemName(null); } @@ -108,10 +108,10 @@ function ErrorNavigationIconItem({ sx={{ padding: 1, }} - onMouseEnter={resetActiveIndex} - onKeyDown={resetActiveIndex} + onMouseEnter={closeNavigationMenu} + onKeyDown={closeNavigationMenu} onClick={() => { - resetActiveIndex(); + closeNavigationMenu(); setErrorModalOpen(true); }} > @@ -124,8 +124,25 @@ function ErrorNavigationIconItem({ ); } -interface NavigationIconItemWithSubItemsProps { - item: SideNavigationItemWithSubItems; +export function CollapsedNavigationLoadingItem({ + item, +}: { + item: SideNavigationSuspenseItem; +}) { + return ( + <ListItem> + <ListItemButton + sx={{ + padding: 1, + }} + disabled + > + <Skeleton variant="circular" width={20} height={20}> + {item.decorator} + </Skeleton> + </ListItemButton> + </ListItem> + ); } const modifiers = [ @@ -142,12 +159,13 @@ const modifiers = [ }, ]; -function NavigationIconItemWithSubItems({ +export function CollapsedNavigationParentItem({ item, -}: NavigationIconItemWithSubItemsProps) { - const { openMenuItemName, setOpenMenuItemName } = useContext( - NavigationListCollapsedContext, - ); +}: { + item: SideNavigationParentItem; +}) { + const { openMenuItemName, setOpenMenuItemName } = + useCollapsedNavigationListContext(); const pathname = usePathname(); const isItemMenuOpen = openMenuItemName === item.name; @@ -276,13 +294,3 @@ function NavigationIconItemWithSubItems({ </Dropdown> ); } - -export function NavigationIconItem({ item }: { item: SideNavigationItem }) { - if ("subItems" in item) { - if (isDefined(item.error)) { - return <ErrorNavigationIconItem item={item} />; - } - return <NavigationIconItemWithSubItems item={item} />; - } - return <NavigationIconItemWithoutSubItems item={item} />; -} diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/ExpandedNavigationItem.tsx b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/ExpandedNavigationItem.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d35f5d789bb36db39d8478c23ace38f2aa57f10c --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/ExpandedNavigationItem.tsx @@ -0,0 +1,289 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + SideNavigationLinkItem, + SideNavigationParentItem, + SideNavigationSuspenseItem, +} from "@eshg/lib-employee-portal/types/sideNavigation"; +import { NavigationLink } from "@eshg/lib-portal/components/navigation/NavigationLink"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import { + Box, + List, + ListItem, + ListItemButton, + ListItemContent, + ListItemDecorator, + Skeleton, + Typography, +} from "@mui/joy"; +import { SxProps } from "@mui/joy/styles/types"; +import { usePathname } from "next/navigation"; +import { ReactNode, useEffect, useId, useState } from "react"; + +import { + navItemIconColor, + navItemSelectedBackgroundColor, + navItemSelectedIconColor, +} from "@/lib/baseModule/components/layout/sideNavigation/constants"; +import { ModuleErrorModal } from "@/lib/baseModule/components/layout/sideNavigation/items/ModuleErrorModal"; +import { NavigationItemError } from "@/lib/baseModule/components/layout/sideNavigation/items/NavigationItemError"; +import { isItemSelected } from "@/lib/baseModule/components/layout/sideNavigation/items/isItemSelected"; + +function textColor(selected: boolean) { + return selected ? "primary.softColor" : "text.primary"; +} + +function textStyle(selected: boolean) { + return selected ? "title-md" : "body-md"; +} + +function iconColor(selected: boolean) { + return selected ? navItemSelectedIconColor : navItemIconColor; +} + +const spacings = { + iconTopSpacing: "0.1875rem", // 3px + textTopSpacing: "0.125rem", // 2px + navItemPadding: "0.375rem", +}; + +function listItemButtonStyle(expanded: boolean): SxProps { + return { + alignItems: "flex-start", + padding: spacings.navItemPadding, + "&.Mui-selected": { + backgroundColor: navItemSelectedBackgroundColor, + }, + marginBottom: expanded ? "0.5rem" : 0, + }; +} + +function Decorator(props: { selected: boolean; children: ReactNode }) { + return ( + <ListItemDecorator + sx={{ + marginTop: spacings.iconTopSpacing, + "--ListItemDecorator-size": "2rem", + "--Icon-color": iconColor(props.selected), + }} + > + {props.children} + </ListItemDecorator> + ); +} + +function ItemLabel(props: { selected: boolean; children: ReactNode }) { + return ( + <ListItemContent> + <Typography + sx={{ + marginTop: spacings.textTopSpacing, + overflowWrap: "break-word", + hyphens: "auto", + }} + component="span" + level={textStyle(props.selected)} + textColor={textColor(props.selected)} + > + {props.children} + </Typography> + </ListItemContent> + ); +} + +export function ExpandedNavigationLinkItem({ + item, +}: { + item: SideNavigationLinkItem; +}) { + const pathname = usePathname(); + + const selected = isItemSelected(item, pathname); + + return ( + <ListItem> + <ListItemButton + component={NavigationLink} + href={item.href} + selected={selected} + aria-current={selected ? "page" : undefined} + sx={listItemButtonStyle(false)} + > + <Decorator selected={selected}>{item.decorator}</Decorator> + <ItemLabel selected={selected}>{item.name}</ItemLabel> + {item.chip} + </ListItemButton> + </ListItem> + ); +} + +export function ExpandedNavigationErrorItem({ + item, +}: { + item: SideNavigationSuspenseItem; +}) { + const [errorModalOpen, setErrorModalOpen] = useState(false); + + return ( + <> + <ModuleErrorModal + open={errorModalOpen} + onClose={() => setErrorModalOpen(false)} + moduleName={item.name} + /> + <ListItem> + <ListItemButton + sx={listItemButtonStyle(false)} + onClick={() => setErrorModalOpen(true)} + > + <NavigationItemError /> + <Decorator selected={false}>{item.decorator}</Decorator> + <ItemLabel selected={false}>{item.name}</ItemLabel> + </ListItemButton> + </ListItem> + </> + ); +} + +export function ExpandedNavigationLoadingItem({ + item, +}: { + item: SideNavigationSuspenseItem; +}) { + return ( + <ListItem> + <ListItemButton sx={listItemButtonStyle(false)} disabled> + <Decorator selected={false}> + <Skeleton variant="circular" width={20} height={20}> + {item.decorator} + </Skeleton> + </Decorator> + <ItemLabel selected={false}> + <Skeleton>{item.name}</Skeleton> + </ItemLabel> + </ListItemButton> + </ListItem> + ); +} + +export function ExpandedNavigationParentItem({ + item, +}: { + item: SideNavigationParentItem; +}) { + const buttonId = useId(); + const expandableContentId = useId(); + + const pathname = usePathname(); + + const selected = item.subItems.some((subItem) => { + return isItemSelected(subItem, pathname); + }); + const [expanded, setExpanded] = useState(selected); + + useEffect(() => { + if (selected) { + setExpanded(selected); + } + }, [selected]); + + return ( + <ListItem nested> + <ListItemButton + role="button" + onClick={() => setExpanded((prevState) => !prevState)} + selected={selected && !expanded} + sx={listItemButtonStyle(expanded)} + id={buttonId} + aria-expanded={expanded} + aria-controls={expandableContentId} + > + <Decorator selected={selected}>{item.decorator}</Decorator> + <ItemLabel selected={selected}>{item.name}</ItemLabel> + <KeyboardArrowDownIcon + sx={{ + marginTop: spacings.iconTopSpacing, + transform: expanded ? "rotate(180deg)" : "none", + marginLeft: -1.5, + }} + /> + </ListItemButton> + <Box + sx={{ + display: "grid", + visibility: expanded ? "visible" : "hidden", + gridTemplateRows: expanded ? "1fr" : "0fr", + transition: "0.2s ease", + "@media (prefers-reduced-motion)": { + transition: "none", + }, + "& > *": { + overflow: "hidden", + }, + }} + id={expandableContentId} + aria-labelledby={buttonId} + > + <List> + {item.subItems.map((subItem) => { + const selectedChild = isItemSelected(subItem, pathname); + + return ( + <Box + key={`${subItem.href}-${subItem.name}`} + component="li" + paddingY="0.25rem" + sx={{ + borderLeft: (theme) => `1px solid ${theme.palette.divider}`, + borderRadius: 0, + marginLeft: "1rem", + marginRight: "0.25rem", + "&:first-of-type": { + paddingTop: 0, + marginTop: "0.5rem", + }, + "&:last-of-type": { + paddingBottom: 0, + marginBottom: 0.5, + }, + }} + > + <ListItemButton + component={NavigationLink} + href={subItem.href} + selected={selectedChild} + aria-current={selectedChild ? "page" : undefined} + sx={{ + marginLeft: "0.8125rem", + padding: "0 0.5rem", + borderRadius: (theme) => theme.radius.md, + "&.Mui-selected": { + backgroundColor: navItemSelectedBackgroundColor, + }, + }} + > + <ListItemContent> + <Typography + component="span" + sx={{ + hyphens: "auto", + }} + level={textStyle(selectedChild)} + textColor={textColor(selectedChild)} + > + {subItem.name} + </Typography> + </ListItemContent> + </ListItemButton> + </Box> + ); + })} + </List> + </Box> + </ListItem> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/NavigationItem.tsx b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/NavigationItem.tsx index 57f148d2413df35f9913b96ef710da08cc2d019e..7d24bbc2c5abb7e5220d72994c7c8b9300929208 100644 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/NavigationItem.tsx +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/NavigationItem.tsx @@ -3,277 +3,73 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - SideNavigationItem, - SideNavigationItemWithSubItems, - SideNavigationItemWithoutSubItems, -} from "@eshg/lib-employee-portal/types/sideNavigation"; -import { NavigationLink } from "@eshg/lib-portal/components/navigation/NavigationLink"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import { - Box, - List, - ListItem, - ListItemButton, - ListItemContent, - ListItemDecorator, - Typography, -} from "@mui/joy"; -import { SxProps } from "@mui/joy/styles/types"; -import { usePathname } from "next/navigation"; -import { ReactNode, useEffect, useId, useState } from "react"; -import { isDefined } from "remeda"; +import { SideNavigationItem } from "@eshg/lib-employee-portal/types/sideNavigation"; +import { ReactNode, Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { filterNavigationItemsWithAccess } from "@/lib/baseModule/components/layout/sideNavigation/filterNavigationItemsWithAccess"; import { - navItemIconColor, - navItemSelectedBackgroundColor, - navItemSelectedIconColor, -} from "@/lib/baseModule/components/layout/sideNavigation/constants"; -import { ModuleErrorModal } from "@/lib/baseModule/components/layout/sideNavigation/items/ModuleErrorModal"; -import { NavigationItemError } from "@/lib/baseModule/components/layout/sideNavigation/items/NavigationItemError"; -import { isItemSelected } from "@/lib/baseModule/components/layout/sideNavigation/items/isItemSelected"; - -function textColor(selected: boolean) { - return selected ? "primary.softColor" : "text.primary"; -} - -function textStyle(selected: boolean) { - return selected ? "title-md" : "body-md"; -} - -function iconColor(selected: boolean) { - return selected ? navItemSelectedIconColor : navItemIconColor; -} - -const spacings = { - iconTopSpacing: "0.1875rem", // 3px - textTopSpacing: "0.125rem", // 2px - navItemPadding: "0.375rem", -}; - -function listItemButtonStyle(expanded: boolean): SxProps { + CollapsedNavigationErrorItem, + CollapsedNavigationLinkItem, + CollapsedNavigationLoadingItem, + CollapsedNavigationParentItem, +} from "@/lib/baseModule/components/layout/sideNavigation/items/CollapsedNavigationItem"; +import { + ExpandedNavigationErrorItem, + ExpandedNavigationLinkItem, + ExpandedNavigationLoadingItem, + ExpandedNavigationParentItem, +} from "@/lib/baseModule/components/layout/sideNavigation/items/ExpandedNavigationItem"; +import { useNavigationListContext } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationListContext"; +import { useSideNavigationItemProps } from "@/lib/baseModule/components/layout/sideNavigation/useSideNavigationItemProps"; +import { useAccessControl } from "@/lib/shared/hooks/useAccessControl"; + +function useNavigationItemComponents() { + const collapsed = useNavigationListContext(); + + if (collapsed) { + return { + LinkItem: CollapsedNavigationLinkItem, + ParentItem: CollapsedNavigationParentItem, + ErrorItem: CollapsedNavigationErrorItem, + LoadingItem: CollapsedNavigationLoadingItem, + }; + } return { - alignItems: "flex-start", - padding: spacings.navItemPadding, - "&.Mui-selected": { - backgroundColor: navItemSelectedBackgroundColor, - }, - marginBottom: expanded ? "0.5rem" : 0, + LinkItem: ExpandedNavigationLinkItem, + ParentItem: ExpandedNavigationParentItem, + ErrorItem: ExpandedNavigationErrorItem, + LoadingItem: ExpandedNavigationLoadingItem, }; } -function Decorator(props: { selected: boolean; children: ReactNode }) { - return ( - <ListItemDecorator - sx={{ - marginTop: spacings.iconTopSpacing, - "--ListItemDecorator-size": "2rem", - "--Icon-color": iconColor(props.selected), - }} - > - {props.children} - </ListItemDecorator> - ); -} - -function ItemLabel(props: { selected: boolean; children: ReactNode }) { - return ( - <ListItemContent> - <Typography - sx={{ - marginTop: spacings.textTopSpacing, - overflowWrap: "break-word", - hyphens: "auto", - }} - component="span" - level={textStyle(props.selected)} - textColor={textColor(props.selected)} - > - {props.children} - </Typography> - </ListItemContent> - ); -} - -function NavigationItemWithoutSubItems({ - item, -}: { - item: SideNavigationItemWithoutSubItems; -}) { - const pathname = usePathname(); +export function NavigationItem(props: { item: SideNavigationItem }): ReactNode { + const checkAccess = useAccessControl(); + const { LinkItem, ParentItem, ErrorItem, LoadingItem } = + useNavigationItemComponents(); + const sideNavigationItemProps = useSideNavigationItemProps(); - const selected = isItemSelected(item, pathname); - - return ( - <ListItem> - <ListItemButton - component={NavigationLink} - href={item.href} - selected={selected} - aria-current={selected ? "page" : undefined} - sx={listItemButtonStyle(false)} - > - <Decorator selected={selected}>{item.decorator}</Decorator> - <ItemLabel selected={selected}>{item.name}</ItemLabel> - {item.chip} - </ListItemButton> - </ListItem> - ); -} - -function ErrorNavigationItem({ - item, -}: { - item: SideNavigationItemWithSubItems; -}) { - const [errorModalOpen, setErrorModalOpen] = useState(false); - - return ( - <> - <ModuleErrorModal - open={errorModalOpen} - onClose={() => setErrorModalOpen(false)} - moduleName={item.name} - /> - <ListItem> - <ListItemButton - sx={listItemButtonStyle(false)} - onClick={() => setErrorModalOpen(true)} - > - <NavigationItemError /> - <Decorator selected={false}>{item.decorator}</Decorator> - <ItemLabel selected={false}>{item.name}</ItemLabel> - </ListItemButton> - </ListItem> - </> - ); -} - -function NavigationItemWithSubItems({ - item, -}: { - item: SideNavigationItemWithSubItems; -}) { - const buttonId = useId(); - const expandableContentId = useId(); - - const pathname = usePathname(); - - const selected = item.subItems.some((subItem) => { - return isItemSelected(subItem, pathname); - }); - const [expanded, setExpanded] = useState(selected); + const [item] = filterNavigationItemsWithAccess([props.item], checkAccess); + if (item === undefined) { + return undefined; + } - useEffect(() => { - if (selected) { - setExpanded(selected); + switch (item.type) { + case "SideNavigationLinkItem": { + return <LinkItem item={item} />; } - }, [selected]); - - return ( - <ListItem nested> - <ListItemButton - role="button" - onClick={() => setExpanded((prevState) => !prevState)} - selected={selected && !expanded} - sx={listItemButtonStyle(expanded)} - id={buttonId} - aria-expanded={expanded} - aria-controls={expandableContentId} - > - <Decorator selected={selected}>{item.decorator}</Decorator> - <ItemLabel selected={selected}>{item.name}</ItemLabel> - <KeyboardArrowDownIcon - sx={{ - marginTop: spacings.iconTopSpacing, - transform: expanded ? "rotate(180deg)" : "none", - marginLeft: -1.5, - }} - /> - </ListItemButton> - <Box - sx={{ - display: "grid", - visibility: expanded ? "visible" : "hidden", - gridTemplateRows: expanded ? "1fr" : "0fr", - transition: "0.2s ease", - "@media (prefers-reduced-motion)": { - transition: "none", - }, - "& > *": { - overflow: "hidden", - }, - }} - id={expandableContentId} - aria-labelledby={buttonId} - > - <List> - {item.subItems.map((subItem) => { - const selectedChild = isItemSelected(subItem, pathname); - - return ( - <Box - key={`${subItem.href}-${subItem.name}`} - component="li" - paddingY="0.25rem" - sx={{ - borderLeft: (theme) => `1px solid ${theme.palette.divider}`, - borderRadius: 0, - marginLeft: "1rem", - marginRight: "0.25rem", - "&:first-of-type": { - paddingTop: 0, - marginTop: "0.5rem", - }, - "&:last-of-type": { - paddingBottom: 0, - marginBottom: 0.5, - }, - }} - > - <ListItemButton - component={NavigationLink} - href={subItem.href} - selected={selectedChild} - aria-current={selectedChild ? "page" : undefined} - sx={{ - marginLeft: "0.8125rem", - padding: "0 0.5rem", - borderRadius: (theme) => theme.radius.md, - "&.Mui-selected": { - backgroundColor: navItemSelectedBackgroundColor, - }, - }} - > - <ListItemContent> - <Typography - component="span" - sx={{ - hyphens: "auto", - }} - level={textStyle(selectedChild)} - textColor={textColor(selectedChild)} - > - {subItem.name} - </Typography> - </ListItemContent> - </ListItemButton> - </Box> - ); - })} - </List> - </Box> - </ListItem> - ); -} - -export function NavigationItem({ item }: { item: SideNavigationItem }) { - if ("subItems" in item) { - if (isDefined(item.error)) { - return <ErrorNavigationItem item={item} />; + case "SideNavigationParentItem": { + return <ParentItem item={item} />; + } + case "SideNavigationSuspenseItem": { + const ItemComponent = item.component; + return ( + <ErrorBoundary fallback={<ErrorItem item={item} />}> + <Suspense fallback={<LoadingItem item={item} />}> + <ItemComponent {...sideNavigationItemProps} /> + </Suspense> + </ErrorBoundary> + ); } - return <NavigationItemWithSubItems item={item} />; } - - return <NavigationItemWithoutSubItems item={item} />; } diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/isItemSelected.ts b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/isItemSelected.ts index dbd3edcae47aecacfa204cbd69416a8b8994b04d..ed3a1f44214dc3faebecfa31360c1491ccce8427 100644 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/isItemSelected.ts +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/isItemSelected.ts @@ -4,12 +4,12 @@ */ import { - SideNavigationItemWithoutSubItems, + SideNavigationLinkItem, SideNavigationSubItem, } from "@eshg/lib-employee-portal/types/sideNavigation"; export function isItemSelected( - item: SideNavigationItemWithoutSubItems | SideNavigationSubItem, + item: SideNavigationLinkItem | SideNavigationSubItem, pathname: string, ) { return item.href !== "/" diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsed.tsx b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationList.tsx similarity index 57% rename from employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsed.tsx rename to employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationList.tsx index 0bf0a33aaf7825dbe8ae52eed09365bf7a7d5495..4b47be5f6ff223e07eabc20f088c17cd967beee8 100644 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsed.tsx +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationList.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { SideNavigationItem } from "@eshg/lib-employee-portal/types/sideNavigation"; import { ExpandNavigation } from "@eshg/lib-portal/components/icons/ExpandNavigation"; import { IconButton, Stack, Tooltip } from "@mui/joy"; import { useState } from "react"; @@ -12,27 +11,16 @@ import { navItemIconColor, sideNavAriaLabel, } from "@/lib/baseModule/components/layout/sideNavigation/constants"; -import { NavigationIconItem } from "@/lib/baseModule/components/layout/sideNavigation/items/NavigationIconItem"; -import { NavigationListCollapsedContext } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsedContext"; -import { StyledList } from "@/lib/baseModule/components/layout/sideNavigation/lists/StyledList"; +import { CollapsedNavigationListContext } from "@/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationListContext"; +import { NavigationItemGroup } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationItemGroup"; +import { NavigationListContext } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationListContext"; import { SideNavItemGroups } from "@/lib/baseModule/components/layout/sideNavigation/types"; import { sideNavigationCollapsedWidth, tooltipEnterDelay, } from "@/lib/baseModule/components/layout/sizes"; -function NavigationItemGroup(props: { itemGroup: SideNavigationItem[] }) { - if (props.itemGroup.length === 0) { - return undefined; - } - - const list = props.itemGroup.map((item) => { - return <NavigationIconItem key={item.name} item={item} />; - }); - return <StyledList>{list}</StyledList>; -} - -export function NavigationListCollapsed({ +export function CollapsedNavigationList({ onExpand, itemGroups, }: { @@ -70,13 +58,15 @@ export function NavigationListCollapsed({ alignItems="center" sx={{ overflowY: "auto", overflowX: "hidden", gap: 3 }} > - <NavigationListCollapsedContext.Provider - value={{ openMenuItemName, setOpenMenuItemName }} - > - <NavigationItemGroup itemGroup={itemGroups.dashboardItem} /> - <NavigationItemGroup itemGroup={itemGroups.businessItems} /> - <NavigationItemGroup itemGroup={itemGroups.baseItems} /> - </NavigationListCollapsedContext.Provider> + <NavigationListContext.Provider value={true}> + <CollapsedNavigationListContext.Provider + value={{ openMenuItemName, setOpenMenuItemName }} + > + <NavigationItemGroup itemGroup={itemGroups.dashboardItem} /> + <NavigationItemGroup itemGroup={itemGroups.businessItems} /> + <NavigationItemGroup itemGroup={itemGroups.baseItems} /> + </CollapsedNavigationListContext.Provider> + </NavigationListContext.Provider> </Stack> </Stack> ); diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationListContext.ts b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationListContext.ts new file mode 100644 index 0000000000000000000000000000000000000000..0d9b7da3c6f2f3bbb5a3eb2b1929a940363f2b8c --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationListContext.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Dispatch, SetStateAction, createContext, useContext } from "react"; + +export interface CollapsedNavigationListContextValue { + openMenuItemName: string | null; + setOpenMenuItemName: Dispatch<SetStateAction<string | null>>; +} + +export const CollapsedNavigationListContext = + createContext<CollapsedNavigationListContextValue | null>(null); + +export function useCollapsedNavigationListContext(): CollapsedNavigationListContextValue { + const value = useContext(CollapsedNavigationListContext); + + if (value === null) { + throw new Error("Missing CollapsedNavigationListContext"); + } + + return value; +} diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListExpanded.tsx b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/ExpandedNavigationList.tsx similarity index 61% rename from employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListExpanded.tsx rename to employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/ExpandedNavigationList.tsx index 9f794330d8d96a381f33087e417be326baa18fa6..c1c1c903594602c76e8721ba1e433b4990c4fc25 100644 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListExpanded.tsx +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/ExpandedNavigationList.tsx @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { SideNavigationItem } from "@eshg/lib-employee-portal/types/sideNavigation"; -import { LoadingOverlay } from "@eshg/lib-portal/components/LoadingOverlay"; import { ExpandNavigation } from "@eshg/lib-portal/components/icons/ExpandNavigation"; import { Button, Stack, Typography } from "@mui/joy"; @@ -12,31 +10,19 @@ import { navItemIconColor, sideNavAriaLabel, } from "@/lib/baseModule/components/layout/sideNavigation/constants"; -import { NavigationItem } from "@/lib/baseModule/components/layout/sideNavigation/items/NavigationItem"; -import { StyledList } from "@/lib/baseModule/components/layout/sideNavigation/lists/StyledList"; +import { NavigationItemGroup } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationItemGroup"; +import { NavigationListContext } from "@/lib/baseModule/components/layout/sideNavigation/lists/NavigationListContext"; import { SideNavItemGroups } from "@/lib/baseModule/components/layout/sideNavigation/types"; import { sideNavigationWidth } from "@/lib/baseModule/components/layout/sizes"; -function NavigationItemGroup(props: { itemGroup: SideNavigationItem[] }) { - if (props.itemGroup.length === 0) { - return undefined; - } - const list = props.itemGroup.map((item) => { - return <NavigationItem key={item.name} item={item} />; - }); - return <StyledList>{list}</StyledList>; -} - -export function NavigationListExpanded({ +export function ExpandedNavigationList({ onCollapse, showCollapseButton, itemGroups, - isLoading, }: { onCollapse?: () => void; showCollapseButton: boolean; itemGroups: SideNavItemGroups; - isLoading: boolean; }) { return ( <Stack @@ -69,10 +55,11 @@ export function NavigationListExpanded({ </Button> )} <Stack flex={1} sx={{ overflowY: "auto", paddingInline: 2, gap: 3 }}> - <NavigationItemGroup itemGroup={itemGroups.dashboardItem} /> - <NavigationItemGroup itemGroup={itemGroups.businessItems} /> - <NavigationItemGroup itemGroup={itemGroups.baseItems} /> - {isLoading && <LoadingOverlay />} + <NavigationListContext.Provider value={false}> + <NavigationItemGroup itemGroup={itemGroups.dashboardItem} /> + <NavigationItemGroup itemGroup={itemGroups.businessItems} /> + <NavigationItemGroup itemGroup={itemGroups.baseItems} /> + </NavigationListContext.Provider> </Stack> </Stack> ); diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationItemGroup.tsx b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationItemGroup.tsx new file mode 100644 index 0000000000000000000000000000000000000000..abc55b8ceabcb42f67109182ae012d61ea413bfa --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationItemGroup.tsx @@ -0,0 +1,35 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { SideNavigationItem } from "@eshg/lib-employee-portal/types/sideNavigation"; +import { List, styled } from "@mui/joy"; + +import { NavigationItem } from "@/lib/baseModule/components/layout/sideNavigation/items/NavigationItem"; + +export function NavigationItemGroup(props: { + itemGroup: SideNavigationItem[]; +}) { + if (props.itemGroup.length === 0) { + return undefined; + } + + const list = props.itemGroup.map((item) => { + return <NavigationItem key={item.name} item={item} />; + }); + return <StyledList>{list}</StyledList>; +} + +const StyledList = styled(List)(({ theme }) => ({ + padding: 0, + flex: 0, + gap: theme.spacing(1), + "--ListItem-radius": theme.radius.md, + position: "static", + // Small extra space that makes room for focus outline (keyboard navigation) + paddingBlock: "0.25rem", + "&:empty": { + display: "none", + }, +})); diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsedContext.ts b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsedContext.ts deleted file mode 100644 index 8646fd23392b1bae4b72db693a44d9b1fa22a953..0000000000000000000000000000000000000000 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsedContext.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright 2025 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Dispatch, SetStateAction, createContext } from "react"; - -interface NavigationListCollapsedContextValue { - openMenuItemName: string | null; - setOpenMenuItemName: Dispatch<SetStateAction<string | null>>; -} - -export const NavigationListCollapsedContext = - createContext<NavigationListCollapsedContextValue>(null!); diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListContext.ts b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListContext.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0aadaf4da66a282c4c01d5c3f4e441ecbf27167 --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListContext.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { createContext, useContext } from "react"; + +/** Indicates if the side navigation is collapsed (true) or expanded (false). */ +export type NavigationListContextValue = boolean; + +export const NavigationListContext = + createContext<NavigationListContextValue | null>(null); + +export function useNavigationListContext(): NavigationListContextValue { + const collapsed = useContext(NavigationListContext); + + if (collapsed === null) { + throw new Error("Missing NavigationListContext"); + } + + return collapsed; +} diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/StyledList.ts b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/StyledList.ts deleted file mode 100644 index 60e302433578bc9785c6ddbedd72421b03b7fcdc..0000000000000000000000000000000000000000 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/StyledList.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2025 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { List, styled } from "@mui/joy"; - -export const StyledList = styled(List)(({ theme }) => ({ - padding: 0, - flex: 0, - gap: theme.spacing(1), - "--ListItem-radius": theme.radius.md, - position: "static", - // Small extra space that makes room for focus outline (keyboard navigation) - paddingBlock: "0.25rem", -})); diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/types.ts b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/types.ts index c6d4d3b56ca9669e21da6c0267c172f489889ad4..9b8b57acb5093d9fa0c03444f92aff0fad59540b 100644 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/types.ts +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/types.ts @@ -10,8 +10,3 @@ export interface SideNavItemGroups { businessItems: SideNavigationItem[]; baseItems: SideNavigationItem[]; } - -export interface UseSideNavigationItemGroupsResult { - isLoading: boolean; - itemGroups: SideNavItemGroups; -} diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/useNavigationItems.ts b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/useNavigationItems.ts deleted file mode 100644 index 825b05fbf8e79d859f84398dde064c510d064802..0000000000000000000000000000000000000000 --- a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/useNavigationItems.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright 2025 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { AccessCheck } from "@eshg/lib-employee-portal/helpers/accessControl"; -import { SideNavigationItem } from "@eshg/lib-employee-portal/types/sideNavigation"; - -import { useResolveSideNavigationItems } from "@/lib/baseModule/moduleRegister/sideNavigationItemsResolver"; -import { useAccessControl } from "@/lib/shared/hooks/useAccessControl"; - -import { UseSideNavigationItemGroupsResult } from "./types"; - -export function filterNavigationItemsWithAccess( - items: SideNavigationItem[], - checkAccess: (check: AccessCheck) => boolean, -) { - return ( - items - // 1. Remove subItems that don't pass the check - .map((item) => { - if ("subItems" in item) { - return { - ...item, - subItems: item.subItems.filter((subItem) => - checkAccess(subItem.accessCheck), - ), - }; - } - return item; - }) - // 2. Remove items that do not have any subItems anymore - .filter((item) => { - if ("subItems" in item) { - return item.subItems.length > 0; - } - return true; - }) - // 3. Remove items that don't pass the check - .filter((item) => { - if ("accessCheck" in item) { - return checkAccess(item.accessCheck); - } - return true; - }) - ); -} - -export function useNavigationItems(): UseSideNavigationItemGroupsResult { - const checkAccess = useAccessControl(); - const { isLoading, itemGroups } = useResolveSideNavigationItems(); - - return { - isLoading, - itemGroups: { - dashboardItem: itemGroups.dashboardItem, - businessItems: filterNavigationItemsWithAccess( - itemGroups.businessItems, - checkAccess, - ), - baseItems: filterNavigationItemsWithAccess( - itemGroups.baseItems, - checkAccess, - ), - }, - }; -} diff --git a/employee-portal/src/lib/baseModule/components/layout/sideNavigation/useSideNavigationItemProps.ts b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/useSideNavigationItemProps.ts new file mode 100644 index 0000000000000000000000000000000000000000..04957ee44b1a40a8c24f9cddde94329e523fa85a --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/layout/sideNavigation/useSideNavigationItemProps.ts @@ -0,0 +1,15 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiBaseFeature } from "@eshg/base-api"; +import { SideNavigationItemsProps } from "@eshg/lib-employee-portal/types/sideNavigation"; + +import { useIsNewFeatureEnabled } from "@/lib/baseModule/api/queries/feature"; + +export function useSideNavigationItemProps(): SideNavigationItemsProps { + const isInboxEnabled = useIsNewFeatureEnabled(ApiBaseFeature.Inbox); + + return { isInboxEnabled }; +} diff --git a/employee-portal/src/lib/baseModule/moduleRegister/sideNavigationItemsResolver.tsx b/employee-portal/src/lib/baseModule/moduleRegister/sideNavigationItemsResolver.tsx index caa679f183f13733c782b636064ced4d9af4e2a8..acf74642d939c5ededf00935c954cca8ff69b383 100644 --- a/employee-portal/src/lib/baseModule/moduleRegister/sideNavigationItemsResolver.tsx +++ b/employee-portal/src/lib/baseModule/moduleRegister/sideNavigationItemsResolver.tsx @@ -4,90 +4,70 @@ */ import { ApiBusinessModule } from "@eshg/base-api"; -import { useSideNavigationItems as useDentalSideNavigationItems } from "@eshg/dental/shared/useSideNavigationItems"; +import { resolveSideNavigationItems as resolveDentalSideNavigationItems } from "@eshg/dental/shared/sideNavigationItem"; import { SideNavigationItem, + SideNavigationItemsProps, UseSideNavigationItemsResult, } from "@eshg/lib-employee-portal/types/sideNavigation"; -import { mapToObj } from "remeda"; +import { entries } from "remeda"; import { useServerConfig } from "@/lib/baseModule/api/queries/config"; import { SideNavItemGroups } from "@/lib/baseModule/components/layout/sideNavigation/types"; +import { useSideNavigationItemProps } from "@/lib/baseModule/components/layout/sideNavigation/useSideNavigationItemProps"; import { + dashboardItem, useSideNavigationItems as useBaseSideNavigationItems, - useDashboardItem, -} from "@/lib/baseModule/sideNavigationItems"; +} from "@/lib/baseModule/sideNavigationItem"; import { useSideNavigationItems as useChatSideNavigationItems } from "@/lib/businessModules/chat/shared/sideNavigationItem"; -import { useSideNavigationItems as useInspectionSideNavigationItems } from "@/lib/businessModules/inspection/shared/sideNavigationItem"; -import { useSideNavigationItems as useMeaslesProtectionSideNavigationItems } from "@/lib/businessModules/measlesProtection/shared/sideNavigationItem"; -import { useSideNavigationItems as useMedicalRegistrySideNavigationItems } from "@/lib/businessModules/medicalRegistry/shared/sideNavigationItem"; -import { useSideNavigationItems as useOfficialMedicalServiceSideNavigationItems } from "@/lib/businessModules/officialMedicalService/shared/sideNavigationItem"; -import { useSideNavigationItems as useSchoolEntrySideNavigationItems } from "@/lib/businessModules/schoolEntry/shared/sideNavigationItem"; +import { resolveSideNavigationItems as resolveInspectionSideNavigationItems } from "@/lib/businessModules/inspection/shared/sideNavigationItem"; +import { resolveSideNavigationItems as resolveMeaslesProtectionSideNavigationItems } from "@/lib/businessModules/measlesProtection/shared/sideNavigationItem"; +import { resolveSideNavigationItems as resolveMedicalRegistrySideNavigationItems } from "@/lib/businessModules/medicalRegistry/shared/sideNavigationItem"; +import { resolveSideNavigationItems as resolveOfficialMedicalServiceSideNavigationItems } from "@/lib/businessModules/officialMedicalService/shared/sideNavigationItem"; +import { resolveSideNavigationItems as resolveSchoolEntrySideNavigationItems } from "@/lib/businessModules/schoolEntry/shared/sideNavigationItem"; import { useSideNavigationItems as useStatisticsSideNavigationItems } from "@/lib/businessModules/statistics/shared/sideNavigationItem"; -import { useSideNavigationItems as useStiProtectionSideNavigationItems } from "@/lib/businessModules/stiProtection/shared/sideNavigationItem"; -import { useSideNavigationItems as useTravelMedicineSideNavigationItems } from "@/lib/businessModules/travelMedicine/shared/sideNavigationItem"; +import { resolveSideNavigationItems as resolveStiProtectionSideNavigationItems } from "@/lib/businessModules/stiProtection/shared/sideNavigationItem"; +import { resolveSideNavigationItems as resolveTravelMedicineSideNavigationItems } from "@/lib/businessModules/travelMedicine/shared/sideNavigationItem"; import { sideNavigationItems as archivingSideNavigationItems } from "@/lib/shared/components/archiving/shared/sideNavigationItem"; -interface UseSideNavigationItemGroupsResult { - isLoading: boolean; - itemGroups: SideNavItemGroups; -} +export type ResolveSideNavigationItems = ( + params: SideNavigationItemsProps, +) => SideNavigationItem[]; + +const businessItemResolvers: Record< + ApiBusinessModule, + ResolveSideNavigationItems +> = { + [ApiBusinessModule.SchoolEntry]: resolveSchoolEntrySideNavigationItems, + [ApiBusinessModule.Inspection]: resolveInspectionSideNavigationItems, + [ApiBusinessModule.TravelMedicine]: resolveTravelMedicineSideNavigationItems, + [ApiBusinessModule.MeaslesProtection]: + resolveMeaslesProtectionSideNavigationItems, + [ApiBusinessModule.StiProtection]: resolveStiProtectionSideNavigationItems, + [ApiBusinessModule.MedicalRegistry]: + resolveMedicalRegistrySideNavigationItems, + [ApiBusinessModule.Dental]: resolveDentalSideNavigationItems, + [ApiBusinessModule.OfficialMedicalService]: + resolveOfficialMedicalServiceSideNavigationItems, +}; -export function useResolveSideNavigationItems(): UseSideNavigationItemGroupsResult { +function useBusinessItems(): SideNavigationItem[] { const config = useServerConfig(); const activeModules = config.data.activeModules; - const activeModulesMap = mapToObj( - Object.values(ApiBusinessModule), - (module) => [module, activeModules.includes(module)], - ); + const resolveParams = useSideNavigationItemProps(); + + return entries(businessItemResolvers) + .filter(([module]) => activeModules.includes(module)) + .map(([_, resolveSideNavigationItems]) => { + return resolveSideNavigationItems(resolveParams); + }) + .flat(); +} - const inspectionSideNavigation = useInspectionSideNavigationItems( - activeModulesMap.INSPECTION, - ); - const schoolEntrySideNavigation = useSchoolEntrySideNavigationItems( - activeModulesMap.SCHOOL_ENTRY, - ); - const travelMedicineSideNavigation = useTravelMedicineSideNavigationItems( - activeModulesMap.TRAVEL_MEDICINE, - ); - const measlesProtectionSideNavigation = - useMeaslesProtectionSideNavigationItems( - activeModulesMap.MEASLES_PROTECTION, - ); - const stiProtectionSideNavigation = useStiProtectionSideNavigationItems( - activeModulesMap.STI_PROTECTION, - ); - const medicalRegistrySideNavigationItems = - useMedicalRegistrySideNavigationItems(activeModulesMap.MEDICAL_REGISTRY); +function useBaseItems(): SideNavigationItem[] { const statisticsSideNavigation = useStatisticsSideNavigationItems(); const chatSideNavigation = useChatSideNavigationItems(); - const dashboardItem = useDashboardItem(); const baseSideNavigation = useBaseSideNavigationItems(); - const dentalSideNavigationItems = useDentalSideNavigationItems( - activeModulesMap.DENTAL, - ); - const officialMedicalServiceSideNavigationItems = - useOfficialMedicalServiceSideNavigationItems( - activeModulesMap.OFFICIAL_MEDICAL_SERVICE, - ); - - const businessModules: [ApiBusinessModule, UseSideNavigationItemsResult][] = [ - [ApiBusinessModule.SchoolEntry, schoolEntrySideNavigation], - [ApiBusinessModule.Inspection, inspectionSideNavigation], - [ApiBusinessModule.TravelMedicine, travelMedicineSideNavigation], - [ApiBusinessModule.MeaslesProtection, measlesProtectionSideNavigation], - [ApiBusinessModule.StiProtection, stiProtectionSideNavigation], - [ApiBusinessModule.MedicalRegistry, medicalRegistrySideNavigationItems], - [ApiBusinessModule.Dental, dentalSideNavigationItems], - [ - ApiBusinessModule.OfficialMedicalService, - officialMedicalServiceSideNavigationItems, - ], - ]; - const orderedSideNavigationItems: UseSideNavigationItemsResult[] = - businessModules - .filter(([module]) => activeModulesMap[module]) - .map(([_, items]) => items); const orderedBaseItems: UseSideNavigationItemsResult[] = [ baseSideNavigation, @@ -96,18 +76,18 @@ export function useResolveSideNavigationItems(): UseSideNavigationItemGroupsResu chatSideNavigation, ]; - return { - isLoading: orderedSideNavigationItems.some(isLoading), - itemGroups: { - dashboardItem: dashboardItem.map(getItems).flat(), - businessItems: orderedSideNavigationItems.map(getItems).flat(), - baseItems: orderedBaseItems.map(getItems).flat(), - }, - }; + return orderedBaseItems.map(getItems).flat(); } -function isLoading(result: UseSideNavigationItemsResult): boolean { - return result.isLoading; +export function useResolveSideNavigationItems(): SideNavItemGroups { + const businessItems = useBusinessItems(); + const baseItems = useBaseItems(); + + return { + dashboardItem: [dashboardItem], + businessItems, + baseItems, + }; } function getItems(result: UseSideNavigationItemsResult): SideNavigationItem[] { diff --git a/employee-portal/src/lib/baseModule/sideNavigationItems.tsx b/employee-portal/src/lib/baseModule/sideNavigationItem.tsx similarity index 84% rename from employee-portal/src/lib/baseModule/sideNavigationItems.tsx rename to employee-portal/src/lib/baseModule/sideNavigationItem.tsx index 9ae45baa0b4caedbf122057a7b7dacc718e2faa9..0fbddffda433b0bdc4d28286e6d9463786ef160f 100644 --- a/employee-portal/src/lib/baseModule/sideNavigationItems.tsx +++ b/employee-portal/src/lib/baseModule/sideNavigationItem.tsx @@ -31,20 +31,13 @@ import { useIsNewFeatureEnabled } from "@/lib/baseModule/api/queries/feature"; import { routes } from "./shared/routes"; -const dashboardItem: SideNavigationItem[] = [ - { - name: "Dashboard", - href: routes.index, - decorator: <DashboardOutlined />, - accessCheck: noCheck(), - }, -]; - -export function useDashboardItem(): UseSideNavigationItemsResult[] { - const items = dashboardItem; - - return [{ isLoading: false, items }]; -} +export const dashboardItem: SideNavigationItem = { + type: "SideNavigationLinkItem", + name: "Dashboard", + href: routes.index, + decorator: <DashboardOutlined />, + accessCheck: noCheck(), +}; /** * These are the side navigation items of base module pages. @@ -52,66 +45,77 @@ export function useDashboardItem(): UseSideNavigationItemsResult[] { */ const sideNavigationItems: SideNavigationItem[] = [ { + type: "SideNavigationLinkItem", name: "DSGVO", href: routes.gdpr.index, decorator: <GppGoodOutlined />, accessCheck: hasUserRole(ApiUserRole.BaseGdprProcedureRead), }, { + type: "SideNavigationLinkItem", name: "Benutzer", href: routes.users.index, decorator: <PeopleAltOutlined />, accessCheck: noCheck(), }, { + type: "SideNavigationLinkItem", name: "Kalender", href: routes.calendar, decorator: <CalendarTodayOutlined />, accessCheck: noCheck(), }, { + type: "SideNavigationLinkItem", name: "Ressourcen", href: routes.resources.index, decorator: <WarehouseOutlined />, accessCheck: hasUserRole(ApiUserRole.BaseResourcesRead), }, { + type: "SideNavigationLinkItem", name: "Inventar", href: routes.inventory.index, decorator: <InventoryOutlined />, accessCheck: hasUserRole(ApiUserRole.BaseInventoryRead), }, { + type: "SideNavigationLinkItem", name: "Kontakte", href: routes.contacts.index, decorator: <ContactsOutlined />, accessCheck: hasUserRole(ApiUserRole.BaseContactsRead), }, { + type: "SideNavigationLinkItem", name: "Kennzahlen", href: routes.metrics.index, decorator: <TrackChangesOutlined />, accessCheck: hasUserRole(ApiUserRole.BaseProcedureMetricsRead), }, { + type: "SideNavigationLinkItem", name: "Auditlog", href: routes.auditlog.index, decorator: <ContentPasteSearchOutlined />, accessCheck: hasUserRole(ApiUserRole.AuditlogDecryptAndAccess), }, { + type: "SideNavigationLinkItem", name: "Auditlog Freigabe", href: routes.auditlog.authorize, decorator: <ContentPasteSearch />, accessCheck: hasUserRole(ApiUserRole.AuditlogAuthorizeAccess), }, { + type: "SideNavigationLinkItem", name: "Open Data", href: routes.opendata.index, decorator: <PermMediaOutlined />, accessCheck: hasUserRole(ApiUserRole.OpenDataAdmin), }, { + type: "SideNavigationLinkItem", name: "Posteingang", href: routes.inbox, decorator: <MailOutline />, diff --git a/employee-portal/src/lib/businessModules/chat/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/chat/shared/sideNavigationItem.tsx index aec5a429d39b81c18f779add24941f4170323755..c1e1b9451a32d2232b9461cc7177f08026c74034 100644 --- a/employee-portal/src/lib/businessModules/chat/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/chat/shared/sideNavigationItem.tsx @@ -17,6 +17,7 @@ import { useChat } from "@/lib/businessModules/chat/shared/ChatProvider"; import { routes } from "./routes"; export const sideNavigationItem: SideNavigationItem = { + type: "SideNavigationLinkItem", name: "Chat", href: routes.index, decorator: <ChatOutlined />, diff --git a/employee-portal/src/lib/businessModules/dental/features/children/details/ChildExaminationForm.tsx b/employee-portal/src/lib/businessModules/dental/features/children/details/ChildExaminationForm.tsx index 18ca9b0848070ffe8cf8887160a0ed3be418eaac..61705094b51f309004d705949d5ee54235e813e1 100644 --- a/employee-portal/src/lib/businessModules/dental/features/children/details/ChildExaminationForm.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/children/details/ChildExaminationForm.tsx @@ -6,7 +6,6 @@ "use client"; import { - ApiDentitionType, ApiExaminationResult, UpdateExaminationRequest, } from "@eshg/dental-api"; @@ -55,6 +54,7 @@ export function ChildExaminationForm(props: ChildExaminationFormProps) { initialValues={mapToExaminationFormValues( examination.result, examination.note, + examination.prophylaxisDentitionType, )} onSubmit={handleSubmit} enableReinitialize @@ -98,10 +98,10 @@ function mapExaminationResultRequest( if (examination.screening) { return { type: "ScreeningExaminationResult", + dentitionType: mapRequiredValue(formValues.dentitionType), oralHygieneStatus: mapOptionalValue(formValues.oralHygieneStatus), fluorideVarnishApplied: mapOptionalValue(formValues.fluorideVarnishApplied) ?? false, - dentitionType: ApiDentitionType.Mixed, toothDiagnoses: Object.values(toothDiagnoses), }; } diff --git a/employee-portal/src/lib/businessModules/dental/features/examinations/AdditionalInformationFormSection.tsx b/employee-portal/src/lib/businessModules/dental/features/examinations/AdditionalInformationFormSection.tsx index 20f7e9706ee8fcf4456044cf6448169899110e8a..30c11dd1f16d4d825af4e7bfca5eb9cefdddff42 100644 --- a/employee-portal/src/lib/businessModules/dental/features/examinations/AdditionalInformationFormSection.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/examinations/AdditionalInformationFormSection.tsx @@ -3,18 +3,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiOralHygieneStatus } from "@eshg/dental-api"; +import { ApiDentitionType, ApiOralHygieneStatus } from "@eshg/dental-api"; import { ExaminationStatus } from "@eshg/dental/api/models/ExaminationStatus"; import { Alert } from "@eshg/lib-portal/components/Alert"; import { SoftRequiredBooleanSelectField, SoftRequiredSelectField, } from "@eshg/lib-portal/components/form/fieldVariants"; +import { SelectField } from "@eshg/lib-portal/components/formFields/SelectField"; import { buildEnumOptions } from "@eshg/lib-portal/helpers/form"; import { OptionalFieldValue } from "@eshg/lib-portal/types/form"; +import { Divider, Stack, Typography } from "@mui/joy"; import { ExaminationStatusChip } from "@/lib/businessModules/dental/features/examinations/ExaminationStatusChip"; +import { useDentalExaminationStore } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider"; +import { DENTITION_TYPE_OPTIONS } from "@/lib/businessModules/dental/features/prophylaxisSessions/options"; import { DetailsSection } from "@/lib/shared/components/detailsSection/DetailsSection"; +import { DetailsItem } from "@/lib/shared/components/detailsSection/items/DetailsItem"; import { InformationSheet } from "@/lib/shared/components/infoTile/InformationSheet"; import { ORAL_HYGIENE_STATUS } from "./translations"; @@ -23,6 +28,7 @@ export const ORAL_HYGIENE_STATUS_OPTIONS = buildEnumOptions<ApiOralHygieneStatus>(ORAL_HYGIENE_STATUS, true); export interface AdditionalInformationFormValues { + dentitionType: OptionalFieldValue<ApiDentitionType>; oralHygieneStatus?: OptionalFieldValue<ApiOralHygieneStatus>; fluorideVarnishApplied: OptionalFieldValue<boolean>; } @@ -37,30 +43,52 @@ interface AdditionalInformationFormSectionProps { export function AdditionalInformationFormSection( props: AdditionalInformationFormSectionProps, ) { - const { screening, fluoridation, fluoridationConsentGiven } = props; + const { screening, fluoridation, fluoridationConsentGiven, status } = props; + const dmftValues = useDentalExaminationStore((store) => store.dmftValues); return ( <InformationSheet> <DetailsSection title="Zusatzinfos"> - <ExaminationStatusChip status={props.status} /> - {screening && ( - <SoftRequiredSelectField - name="oralHygieneStatus" - label="Mundhygienestatus" - options={ORAL_HYGIENE_STATUS_OPTIONS} - orientation="vertical" - /> - )} + <ExaminationStatusChip status={status} /> + {screening && <ScreeningFields />} {fluoridation && ( <FluoridationField fluoridationConsentGiven={fluoridationConsentGiven} /> )} + <Divider orientation="horizontal" /> + <Typography component="h3" fontWeight={600}> + Automatisierte Werte + </Typography> + <Stack direction="row" gap={3}> + <DetailsItem + label="dmf-t/DMF-T" + value={`${dmftValues.primaryTeeth}/${dmftValues.secondaryTeeth}`} + /> + </Stack> </DetailsSection> </InformationSheet> ); } +function ScreeningFields() { + return ( + <> + <SelectField + name="dentitionType" + label="Gebisstyp" + options={DENTITION_TYPE_OPTIONS} + /> + <SoftRequiredSelectField + name="oralHygieneStatus" + label="Mundhygienestatus" + options={ORAL_HYGIENE_STATUS_OPTIONS} + orientation="vertical" + /> + </> + ); +} + interface FluoridationFieldProps { fluoridationConsentGiven?: boolean; } diff --git a/employee-portal/src/lib/businessModules/dental/features/examinations/ExaminationFormLayout.tsx b/employee-portal/src/lib/businessModules/dental/features/examinations/ExaminationFormLayout.tsx index b33f68bafdc3a9273bfee004c2ad6f31cae35c39..b4e5f39b7ba3994506979abd48c5a553344842e8 100644 --- a/employee-portal/src/lib/businessModules/dental/features/examinations/ExaminationFormLayout.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/examinations/ExaminationFormLayout.tsx @@ -5,6 +5,7 @@ "use client"; +import { ApiDentitionType } from "@eshg/dental-api"; import { ExaminationResult } from "@eshg/dental/api/models/ExaminationResult"; import { parseOptionalValue } from "@eshg/lib-portal/helpers/form"; import { Grid } from "@mui/joy"; @@ -53,18 +54,21 @@ export function ExaminationFormLayout(props: ExaminationFormLayoutProps) { export function mapToExaminationFormValues( examinationResult: ExaminationResult | undefined, note: string | undefined, + defaultDentitionType: ApiDentitionType | undefined, ): ExaminationFormValues { return { note: parseOptionalValue(note), - ...mapExaminationResultFormValues(examinationResult), + ...mapExaminationResultFormValues(examinationResult, defaultDentitionType), }; } function mapExaminationResultFormValues( examinationResult: ExaminationResult | undefined, + defaultDentitionType: ApiDentitionType | undefined, ): AdditionalInformationFormValues { if (examinationResult?.type === "screening") { return { + dentitionType: parseOptionalValue(examinationResult.dentitionType), oralHygieneStatus: parseOptionalValue( examinationResult.oralHygieneStatus, ), @@ -76,6 +80,7 @@ function mapExaminationResultFormValues( if (examinationResult?.type === "fluoridation") { return { + dentitionType: "", oralHygieneStatus: "", fluorideVarnishApplied: parseOptionalValue( examinationResult.fluorideVarnishApplied, @@ -83,5 +88,9 @@ function mapExaminationResultFormValues( }; } - return { oralHygieneStatus: "", fluorideVarnishApplied: "" }; + return { + dentitionType: defaultDentitionType ?? "", + oralHygieneStatus: "", + fluorideVarnishApplied: "", + }; } diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/AddToothButton.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/AddToothButton.tsx index fe7b01ad98cb4909f1eaf7f0ec034275bc8a8847..24cbd482996538cb5ff0cc46d2990aaa0ec2bbd7 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/AddToothButton.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/AddToothButton.tsx @@ -37,6 +37,7 @@ export function AddToothButton(props: AddToothButtonProps) { toothIndex: props.index, }); }} + aria-label={"Zahn hinzufügen"} > <SizedAddCircleIcon color="primary" /> </ToothIconButton> diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/FullDentitionOverview.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/FullDentitionOverview.tsx index 5a4955d443df6a261cfc609a298cbadd2bb6fe56..a938ed35fb6c78ab20b1922487c5c928a2c39ebd 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/FullDentitionOverview.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/FullDentitionOverview.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Button, Grid, GridProps, Stack, Typography } from "@mui/joy"; +import { Grid, GridProps, Stack } from "@mui/joy"; import { SxProps } from "@mui/joy/styles/types"; import { useId } from "react"; @@ -12,14 +12,9 @@ import { QuadrantHeading, QuadrantHeadingRow, } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/QuadrantHeading"; -import { ToothIcon } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Teeth"; -import { ToothNumber } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ToothNumber"; -import { useDentalExaminationStore } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider"; -import { - QuadrantNumber, - Tooth, - isToothWithDiagnosis, -} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; +import { QuadrantNumber } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; + +import { ReadonlyToothButton } from "./ReadonlyToothButton"; export function FullDentitionOverview() { const upperJawRightId = useId(); @@ -99,62 +94,18 @@ function QuadrantSection(props: QuadrantSectionProps) { : "none", }; - const setFocus = useDentalExaminationStore((state) => state.setFocus); return ( <Grid {...props} xxs={6} sx={styles} component="section"> <Quadrant quadrantNumber={quadrantNumber} gap={0}> {(tooth, index) => ( - <Button + <ReadonlyToothButton key={tooth.toothNumber} - variant="plain" - sx={{ - padding: "4px", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "flex-start", - gap: 2, - backgroundColor: "none", - }} - onClick={() => - setFocus({ - toothContext: { - quadrantNumber, - toothIndex: index, - }, - field: "main", - }) - } - > - <ToothNumber tooth={tooth} /> - <ToothIcon - tooth={tooth} - toothContext={{ quadrantNumber, toothIndex: index }} - /> - <ExaminationResult tooth={tooth} /> - </Button> + quadrantNumber={quadrantNumber} + index={index} + tooth={tooth} + /> )} </Quadrant> </Grid> ); } - -function ExaminationResult({ tooth }: { tooth: Tooth }) { - if (!isToothWithDiagnosis(tooth)) { - return undefined; - } - const mainResult = tooth.mainResult; - const secondaryResult1 = tooth.secondaryResult1; - const secondaryResult2 = tooth.secondaryResult2; - return ( - <Stack sx={{ alignItems: "center" }}> - <Typography>{mainResult?.value ? mainResult.value : "-"}</Typography> - <Typography> - {secondaryResult1?.value ? secondaryResult1.value : undefined} - </Typography> - <Typography> - {secondaryResult2?.value ? secondaryResult2.value : undefined} - </Typography> - </Stack> - ); -} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Legend.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Legend.tsx index ad524cd84b5352915d2770ccbd63455163116e35..cde9c7f282b46b7676b75cb9475d8ed2f2a81795 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Legend.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Legend.tsx @@ -6,7 +6,7 @@ import CircleIcon from "@mui/icons-material/Circle"; import CircleOutlinedIcon from "@mui/icons-material/CircleOutlined"; import ErrorIcon from "@mui/icons-material/Error"; -import { Button, Stack, Typography } from "@mui/joy"; +import { Button, List, ListItem, Stack, Typography } from "@mui/joy"; import { ReactNode } from "react"; import { ButtonBar } from "@/lib/shared/components/buttons/ButtonBar"; @@ -21,20 +21,44 @@ export function Legend() { }); return ( <Stack direction="row" sx={{ justifyContent: "space-between" }}> - <Stack direction="row" gap={3}> - <LegendItem - icon={<ErrorIcon color="danger" />} - helpText="Vorbefund vorhanden" - /> - <LegendItem - icon={<CircleIcon color="neutral" />} - helpText="Bleibender Zahn" - /> - <LegendItem - icon={<CircleOutlinedIcon color="neutral" />} - helpText="Milchzahn" - /> - </Stack> + <List orientation="horizontal" size="sm" aria-label="Legende"> + <ListItem> + <LegendItem + icon={ + <ErrorIcon + color="danger" + aria-label="Ausrufezeichen" + aria-hidden={false} + /> + } + helpText="Vorbefund vorhanden" + /> + </ListItem> + <ListItem> + <LegendItem + icon={ + <CircleIcon + color="neutral" + aria-label="Ausgefüllt" + aria-hidden={false} + /> + } + helpText="Bleibender Zahn" + /> + </ListItem> + <ListItem> + <LegendItem + icon={ + <CircleOutlinedIcon + color="neutral" + aria-label="Nicht ausgefüllt" + aria-hidden={false} + /> + } + helpText="Milchzahn" + /> + </ListItem> + </List> <Button variant="plain" onClick={findingsOverviewSidebar.open}> <Typography component="u" color="primary"> Befundwerte? @@ -53,7 +77,7 @@ function LegendItem({ icon, helpText }: LegendItemProps) { return ( <Stack direction="row" gap={0.5} alignItems="center"> {icon} - <Typography>= {helpText}</Typography> + <Typography component="span">= {helpText}</Typography> </Stack> ); } @@ -62,15 +86,17 @@ function FindingsOverviewSidebar({ onClose }: DrawerProps) { return ( <> <SidebarContent title="Mögliche Befundwerte"> - <Stack> + <List size="sm" aria-label="Abkürzungsverzeichnis"> {Object.entries(POSSIBLE_DIAGNOSES).map(([abbr, expl]) => ( - <Diagnosis - key={abbr} - abbreviation={abbr as Abbreviation} - explanation={expl} - /> + <ListItem key={abbr}> + <Diagnosis + key={abbr} + abbreviation={abbr as Abbreviation} + explanation={expl} + /> + </ListItem> ))} - </Stack> + </List> </SidebarContent> <SidebarActions> <ButtonBar @@ -128,11 +154,17 @@ interface DiagnosisProp { function Diagnosis({ abbreviation, explanation }: DiagnosisProp) { return ( - <Stack direction="row" gap={2}> - <Typography sx={{ fontWeight: 600, width: 24 }}> + <Stack direction="row" gap={1}> + <Typography + component="span" + level="title-md" + sx={{ fontWeight: 600, width: 24 }} + > {abbreviation} </Typography> - <Typography>= {explanation}</Typography> + <Typography component="span" level="body-md"> + = {explanation} + </Typography> </Stack> ); } diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Quadrant.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Quadrant.tsx index af76e6cf40c78af4e62711cc7969421f23472bee..509d432f63229997112b1f1ce56ca92be017fc6f 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Quadrant.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Quadrant.tsx @@ -4,7 +4,6 @@ */ import { Stack } from "@mui/joy"; -import { Property } from "csstype"; import { ReactNode } from "react"; import { useDentalExaminationStore } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider"; @@ -18,7 +17,7 @@ import { ToothColumn } from "./ToothColumn"; interface QuadrantProps { quadrantNumber: QuadrantNumber; children?: (tooth: Tooth, index: number) => ReactNode; - gap?: Property.Gap; + gap?: number; "aria-labelledby"?: string; } diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/QuadrantHeading.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/QuadrantHeading.tsx index eb290e4e8758d4594ecc813f70965717ce5df4b3..c6a0281821effd4eaa62c2ccab31b523b5d775a2 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/QuadrantHeading.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/QuadrantHeading.tsx @@ -5,13 +5,12 @@ import { RequiresChildren } from "@eshg/lib-portal/types/react"; import { Stack, Typography } from "@mui/joy"; -import { Property } from "csstype"; import { theme } from "@/lib/baseModule/theme/theme"; interface QuadrantHeadingRowProps extends RequiresChildren { - marginTop?: Property.MarginTop; - marginBottom?: Property.MarginBottom; + marginTop?: string; + marginBottom?: string; } export function QuadrantHeadingRow(props: QuadrantHeadingRowProps) { diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ReadonlyExaminationResult.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ReadonlyExaminationResult.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8098fbbbcfb2a9a211a873665bebfcc66296a98e --- /dev/null +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ReadonlyExaminationResult.tsx @@ -0,0 +1,43 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { isNonEmptyString } from "@eshg/lib-portal/helpers/guards"; +import { Stack, Typography } from "@mui/joy"; + +import { + ToothResult, + ToothWithDiagnosis, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; + +interface ExaminationResultProps { + tooth: ToothWithDiagnosis; +} + +export function ReadonlyExaminationResult({ tooth }: ExaminationResultProps) { + const mainResult = tooth.mainResult; + const secondaryResult1 = tooth.secondaryResult1; + const secondaryResult2 = tooth.secondaryResult2; + return ( + <Stack sx={{ alignItems: "center" }}> + <Typography color={getColorForResult(mainResult)}> + {isNonEmptyString(mainResult.value) ? mainResult.value : "-"} + </Typography> + {isNonEmptyString(secondaryResult1.value) && ( + <Typography color={getColorForResult(secondaryResult1)}> + {secondaryResult1.value} + </Typography> + )} + {isNonEmptyString(secondaryResult2.value) && ( + <Typography color={getColorForResult(secondaryResult2)}> + {secondaryResult2.value} + </Typography> + )} + </Stack> + ); +} + +function getColorForResult(result: ToothResult) { + return result.isInvalid ? "danger" : undefined; +} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ReadonlyToothButton.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ReadonlyToothButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..acb19c954599a6f975de54d75b9a1e1c5272d982 --- /dev/null +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ReadonlyToothButton.tsx @@ -0,0 +1,83 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Button, styled } from "@mui/joy"; + +import { ToothIcon } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Teeth"; +import { ToothNumber } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ToothNumber"; +import { useDentalExaminationStore } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider"; +import { useElementFocus } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useElementFocus"; +import { useKeyboardNavigationHandler } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useKeyboardNavigationHandler"; +import { + ElementContext, + QuadrantNumber, + Tooth, + isToothWithDiagnosis, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; + +import { ReadonlyExaminationResult } from "./ReadonlyExaminationResult"; + +interface FocusableButtonProps { + focused: boolean; +} + +const FocusableButton = styled(Button, { + shouldForwardProp: (propName) => propName !== "focused", +})<FocusableButtonProps>(({ theme, focused }) => ({ + "--Button-focused": focused ? "1" : "0", + padding: theme.spacing(0.5), + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "flex-start", + gap: theme.spacing(2), + backgroundColor: "none", + "&:focus-visible": { + outlineOffset: "-2px", + }, +})); + +export interface ReadonlyToothButtonProps { + quadrantNumber: QuadrantNumber; + index: number; + tooth: Tooth; +} + +export function ReadonlyToothButton(props: ReadonlyToothButtonProps) { + const { quadrantNumber, index, tooth } = props; + const buttonContext: ElementContext = { + toothContext: { + quadrantNumber, + toothIndex: index, + }, + }; + + const { elementRef, isFocused, focusHandler } = useElementFocus( + buttonContext, + (button: HTMLButtonElement) => button.focus(), + ); + const navigateTo = useDentalExaminationStore((state) => state.navigateTo); + const keyboardNavigationHandler = useKeyboardNavigationHandler(); + + return ( + <FocusableButton + ref={elementRef} + variant="plain" + focused={isFocused} + onClick={() => navigateTo(buttonContext.toothContext)} + onFocus={focusHandler} + onKeyDown={keyboardNavigationHandler} + > + <ToothNumber tooth={tooth} /> + <ToothIcon + tooth={tooth} + toothContext={{ quadrantNumber, toothIndex: index }} + /> + {isToothWithDiagnosis(tooth) && ( + <ReadonlyExaminationResult tooth={tooth} /> + )} + </FocusableButton> + ); +} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/RemoveToothButton.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/RemoveToothButton.tsx index 950f635ab6057a96d8077e2899d3b452e21b8e25..931eee16c4eae22b6fabcced4b09582733d8cbf4 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/RemoveToothButton.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/RemoveToothButton.tsx @@ -8,18 +8,18 @@ import { styled } from "@mui/joy"; import { ToothIconButton } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/AddToothButton"; import { useDentalExaminationStore } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider"; -import { ToothContext } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; +import { + Tooth, + ToothContext, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; + +import { ToothIcon } from "./Teeth"; interface RemoveToothButtonProps { + tooth: Tooth; toothContext: ToothContext; } -const DeleteIconButton = styled(ToothIconButton)({ - position: "absolute", - top: 0, - right: 0, -}); - const RoundedDeleteIcon = styled(DeleteOutlined)(({ theme }) => ({ padding: 4, borderRadius: "50%", @@ -31,16 +31,31 @@ export function RemoveToothButton(props: RemoveToothButtonProps) { const removeTooth = useDentalExaminationStore((state) => state.removeTooth); return ( - <DeleteIconButton + <ToothIconButton color="danger" variant="plain" - className="remove-tooth-button" - onClick={() => { - removeTooth(props.toothContext); + aria-label="Zahn entfernen" + onClick={() => removeTooth(props.toothContext)} + sx={{ + ".remove-icon": { + display: "none", + }, + "&:hover": { + ".tooth-icon": { + display: "none", + }, + ".remove-icon": { + display: "inline-flex", + }, + }, }} - aria-label={"Zahn entfernen"} > - <RoundedDeleteIcon /> - </DeleteIconButton> + <ToothIcon + tooth={props.tooth} + toothContext={props.toothContext} + className="tooth-icon" + /> + <RoundedDeleteIcon className="remove-icon" /> + </ToothIconButton> ); } diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ResultInputField.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ResultInputField.tsx index 9583aefb40260f6f95308ce18bcce2aced4a7c9d..07a76724bb9f20d458be8998490da279a7084fea 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ResultInputField.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ResultInputField.tsx @@ -4,13 +4,10 @@ */ import { Input, InputProps, VariantProp } from "@mui/joy"; -import { useEffect, useRef } from "react"; -import { isDefined } from "remeda"; -import { useShallow } from "zustand/react/shallow"; -import { useDentalExaminationStore } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider"; -import { NAVIGATE_DIRECTIONS } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/constants"; import { SetToothResultAction } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/dentalExaminationStore"; +import { useElementFocus } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useElementFocus"; +import { useKeyboardNavigationHandler } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useKeyboardNavigationHandler"; import { ElementContext, ResultField, @@ -27,67 +24,41 @@ interface ResultInputFieldProps extends InputProps { } export function ResultInputField(props: ResultInputFieldProps) { - const elementContext: ElementContext = { + const fieldContext: ElementContext = { field: props.field, toothContext: props.toothContext, }; - const isFocused = useIsFocused(elementContext); - const setFocus = useDentalExaminationStore((state) => state.setFocus); - const navigate = useDentalExaminationStore((state) => state.navigate); - const input = useRef<HTMLInputElement>(null); - - useEffect(() => { - if (isFocused) { - input?.current?.focus(); - } - }, [input, isFocused]); - - function handleOnFocus() { - setFocus(elementContext); - } + const { elementRef, focusHandler } = useElementFocus( + fieldContext, + (input: HTMLInputElement) => { + input.focus(); + requestAnimationFrame(() => input.select()); // delay value selection to ensure focus is active + }, + ); + const keyboardNavigationHandler = useKeyboardNavigationHandler(); return ( <Input {...props} - slotProps={{ input: { ref: input } }} + slotProps={{ + input: { + ref: elementRef, + "aria-invalid": props.result.isInvalid, + }, + }} value={props.result.value} sx={{ width: 60 }} color={props.result.isInvalid ? "danger" : "primary"} type="text" variant={props.variant} - onFocus={handleOnFocus} onChange={(event) => { props.setResultAction( props.toothContext, event.target.value.toUpperCase(), ); }} - onKeyDown={(event) => { - const direction = NAVIGATE_DIRECTIONS[event.code]; - - if (isDefined(direction)) { - navigate(direction); - } - }} + onFocus={focusHandler} + onKeyDown={keyboardNavigationHandler} /> ); } - -function useIsFocused(element: ElementContext) { - return useDentalExaminationStore( - useShallow((state) => equalsElement(element, state.currentFocus)), - ); -} - -function equalsElement( - elementContext: ElementContext, - currentFocus: ElementContext, -): boolean { - return ( - currentFocus.toothContext.quadrantNumber === - elementContext.toothContext.quadrantNumber && - currentFocus.toothContext.toothIndex === - elementContext.toothContext.toothIndex && - currentFocus.field === elementContext.field - ); -} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Teeth.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Teeth.tsx index ec796d5d0ac6a1d4afd74fa2d5bf0578f35e0a5f..221ff83ace21d1454e7bc019b8196cc189f6e82e 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Teeth.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Teeth.tsx @@ -5,12 +5,12 @@ "use client"; +import { RequiresChildren } from "@eshg/lib-portal/types/react"; import ClearIcon from "@mui/icons-material/Clear"; -import { Box, styled } from "@mui/joy"; +import { Box } from "@mui/joy"; import SvgIcon from "@mui/joy/SvgIcon"; import { theme } from "@/lib/baseModule/theme/theme"; -import { RemoveToothButton } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/RemoveToothButton"; import { TOOTH_SIZE } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/styles"; import { Tooth, @@ -38,9 +38,10 @@ const TOOTH_COMPONENTS = { interface ToothProps { tooth: Tooth; toothContext: ToothContext; + className?: string; } -export function ToothIcon({ tooth, toothContext }: ToothProps) { +export function ToothIcon({ tooth, toothContext, className }: ToothProps) { const inUpperJaw = isInUpperJaw(tooth); if (!isToothWithDiagnosis(tooth)) { @@ -57,6 +58,7 @@ export function ToothIcon({ tooth, toothContext }: ToothProps) { isPrimaryTooth={tooth.toothType === "PRIMARY_TOOTH"} hasPreviousExaminationResult={hasPreviousExaminationResult(tooth)} toothContext={toothContext} + className={className} /> ); } @@ -66,16 +68,12 @@ interface ToothIconProps { isPrimaryTooth?: boolean; variant: "upperJaw" | "lowerJaw"; toothContext: ToothContext; + className?: string; } export function Incisor(props: ToothIconProps) { return ( - <SvgIcon - sx={TOOTH_SIZE} - viewBox="0 0 60 66" - fill="none" - data-testid="tooth-icon" - > + <ToothSvgIcon className={props.className}> <g transform={props.variant === "upperJaw" ? "" : "rotate(180, 30, 33)"}> <path d="M30.8944 12.0249L34.6584 19.5528C34.9908 20.2177 34.5073 21 33.7639 21H26.2361C25.4927 21 25.0092 20.2177 25.3416 19.5528L29.1056 12.0249C29.4741 11.2879 30.5259 11.2879 30.8944 12.0249Z" @@ -106,18 +104,13 @@ export function Incisor(props: ToothIconProps) { </g> )} </g> - </SvgIcon> + </ToothSvgIcon> ); } export function Premolar(props: ToothIconProps) { return ( - <SvgIcon - sx={TOOTH_SIZE} - viewBox="0 0 60 66" - fill="none" - data-testid="tooth-icon" - > + <ToothSvgIcon className={props.className}> <g transform={props.variant === "upperJaw" ? "" : "rotate(180, 30, 33)"}> <path d="M22.8944 4.02492L26.6584 11.5528C26.9908 12.2177 26.5073 13 25.7639 13H18.2361C17.4927 13 17.0092 12.2177 17.3416 11.5528L21.1056 4.02492C21.4741 3.28787 22.5259 3.28787 22.8944 4.02492Z" @@ -154,18 +147,13 @@ export function Premolar(props: ToothIconProps) { </g> )} </g> - </SvgIcon> + </ToothSvgIcon> ); } export function Cuspid(props: ToothIconProps) { return ( - <SvgIcon - sx={TOOTH_SIZE} - viewBox="0 0 60 66" - fill="none" - data-testid="tooth-icon" - > + <ToothSvgIcon className={props.className}> <g transform={props.variant === "upperJaw" ? "" : "rotate(180, 30, 33)"}> <path d="M30.8944 4.02492L34.6584 11.5528C34.9908 12.2177 34.5073 13 33.7639 13H26.2361C25.4927 13 25.0092 12.2177 25.3416 11.5528L29.1056 4.02492C29.4741 3.28787 30.5259 3.28787 30.8944 4.02492Z" @@ -196,29 +184,13 @@ export function Cuspid(props: ToothIconProps) { </g> )} </g> - </SvgIcon> + </ToothSvgIcon> ); } -const ToothSizedContainer = styled("div")({ - ...TOOTH_SIZE, - position: "relative", - ".remove-tooth-button": { - display: "none", - }, - "&:hover .remove-tooth-button": { - display: "inline-flex", - }, -}); - export function Molar(props: ToothIconProps) { return ( - <SvgIcon - sx={TOOTH_SIZE} - viewBox="0 0 60 66" - fill="none" - data-testid="tooth-icon" - > + <ToothSvgIcon className={props.className}> <g transform={props.variant === "upperJaw" ? "" : "rotate(180, 30, 33)"}> <path d="M14.8944 4.02492L18.6584 11.5528C18.9908 12.2177 18.5073 13 17.7639 13H10.2361C9.49269 13 9.00919 12.2177 9.34164 11.5528L13.1056 4.02492C13.4741 3.28787 14.5259 3.28787 14.8944 4.02492Z" @@ -261,16 +233,21 @@ export function Molar(props: ToothIconProps) { </g> )} </g> - </SvgIcon> + </ToothSvgIcon> ); } -export function RemovableToothIcon(props: ToothProps) { +function ToothSvgIcon(props: RequiresChildren & { className?: string }) { return ( - <ToothSizedContainer data-testid="tooth-icon-button"> - <ToothIcon {...props} /> - <RemoveToothButton toothContext={props.toothContext} /> - </ToothSizedContainer> + <SvgIcon + sx={TOOTH_SIZE} + viewBox="0 0 60 66" + fill="none" + data-testid="tooth-icon" + className={props.className} + > + {props.children} + </SvgIcon> ); } diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ToothForm.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ToothForm.tsx index 106833dbdda3a3fb841cfcc0f4d2d591344e9367..a9d5a044f0ccb0a55225f4b47ed3d155d90d67c4 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ToothForm.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ToothForm.tsx @@ -6,11 +6,9 @@ import { isEmptyString } from "@eshg/lib-portal/helpers/guards"; import { Typography } from "@mui/joy"; +import { RemoveToothButton } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/RemoveToothButton"; import { ResultInputField } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ResultInputField"; -import { - RemovableToothIcon, - ToothIcon, -} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Teeth"; +import { ToothIcon } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/Teeth"; import { useDentalExaminationStore } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider"; import { QuadrantNumber, @@ -42,7 +40,7 @@ export function ToothForm(props: ToothFormProps) { return ( <> {tooth.isRemovable ? ( - <RemovableToothIcon tooth={tooth} toothContext={toothContext} /> + <RemoveToothButton tooth={tooth} toothContext={toothContext} /> ) : ( <ToothIcon tooth={tooth} toothContext={toothContext} /> )} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider.tsx b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider.tsx index c837c33cf5c3a1a761d4c14ab280bb064eeaca40..b7f2ad2db34d6b8fd9f84a67309470b1fb411b63 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider.tsx +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider.tsx @@ -5,6 +5,7 @@ "use client"; +import { ApiDentitionType } from "@eshg/dental-api"; import { ExaminationResult } from "@eshg/dental/api/models/ExaminationResult"; import { RequiresChildren } from "@eshg/lib-portal/types/react"; import { createContext, useContext, useState } from "react"; @@ -25,14 +26,18 @@ const DentalExaminationStoreContext = interface DentalExaminationStoreProviderProps extends RequiresChildren { examinationResult?: ExaminationResult; + defaultDentitionType?: ApiDentitionType; } export function DentalExaminationStoreProvider({ examinationResult, + defaultDentitionType, children, }: DentalExaminationStoreProviderProps) { const [store] = useState(() => - createDentalExaminationStore(initDentalExaminationStore(examinationResult)), + createDentalExaminationStore( + initDentalExaminationStore(examinationResult, defaultDentitionType), + ), ); // TODO: handle updated examinationResult diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions.ts deleted file mode 100644 index ac4b8328db94a8a8a28fae70c7668a656d1f58c9..0000000000000000000000000000000000000000 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * Copyright 2025 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ApiMainResult, ApiSecondaryResult } from "@eshg/dental-api"; -import { ToothDiagnoses } from "@eshg/dental/api/models/ExaminationResult"; -import { isEmptyString } from "@eshg/lib-portal/helpers/guards"; - -import { DentalExaminationState } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/dentalExaminationStore"; - -import { createToothResult, createToothWithDiagnosis } from "./factories"; -import { - AddableTooth, - Dentition, - ElementContext, - ToothContext, - ToothResult, - ToothWithDiagnosis, - isAddableTooth, -} from "./types"; - -export function setMainResult( - toothContext: ToothContext, - newValue: string, - dentition: Dentition, -) { - const tooth = getToothFromToothContext(dentition, toothContext); - - const isInvalid = isEmptyString(newValue) - ? !isEmptyString(tooth.secondaryResult1.value) || - !isEmptyString(tooth.secondaryResult2.value) - : !isValidMainResult(newValue); - - return updateToothWithDiagnosis(toothContext, dentition, { - mainResult: createToothResult(newValue, isInvalid), - }); -} - -export function setSecondaryResult1( - toothContext: ToothContext, - newValue: string, - dentition: Dentition, -) { - const tooth = getToothFromToothContext(dentition, toothContext); - - const isInvalid = - !isEmptyString(newValue) && !isValidSecondaryResult(newValue); - - const mainResult = setMainResultInvalidIfEmpty( - tooth.mainResult, - tooth.secondaryResult2, - newValue, - ); - - return updateToothWithDiagnosis(toothContext, dentition, { - mainResult, - secondaryResult1: createToothResult(newValue, isInvalid), - }); -} - -export function setSecondaryResult2( - toothContext: ToothContext, - newValue: string, - dentition: Dentition, -) { - const tooth = getToothFromToothContext(dentition, toothContext); - - const isInvalid = - !isEmptyString(newValue) && !isValidSecondaryResult(newValue); - - const mainResult = setMainResultInvalidIfEmpty( - tooth.mainResult, - tooth.secondaryResult1, - newValue, - ); - - return updateToothWithDiagnosis(toothContext, dentition, { - mainResult, - secondaryResult2: createToothResult(newValue, isInvalid), - }); -} - -function setMainResultInvalidIfEmpty( - mainResult: ToothResult, - secondaryResult: ToothResult, - newValue: string, -) { - if (isEmptyToothResult(mainResult)) { - if (isEmptyString(newValue) && isEmptyToothResult(secondaryResult)) { - return createToothResult(mainResult.value, false); - } else { - return createToothResult("", true); - } - } - return mainResult; -} - -function getToothFromToothContext( - dentition: Dentition, - toothContext: ToothContext, -) { - return dentition[toothContext.quadrantNumber].teeth[ - toothContext.toothIndex - ] as ToothWithDiagnosis; -} - -function isValidSecondaryResult( - newValue: string, -): newValue is ApiSecondaryResult { - return Object.values(ApiSecondaryResult).includes( - newValue as ApiSecondaryResult, - ); -} - -function isValidMainResult(newValue: string): newValue is ApiMainResult { - return Object.values(ApiMainResult).includes(newValue as ApiMainResult); -} - -export function addTooth( - toothContext: ToothContext, - dentition: Dentition, -): Dentition { - const { quadrantNumber, toothIndex } = toothContext; - const targetQuadrant = dentition[quadrantNumber]; - const tooth = targetQuadrant.teeth[toothIndex]; - - if (tooth === undefined) { - throw new Error( - `Tooth with index ${toothIndex} does not exist in quadrant ${quadrantNumber}`, - ); - } - - if (!isAddableTooth(tooth)) { - throw new Error("Tooth must be of type AddableTooth"); - } - - const newTooth = createToothWithDiagnosis(tooth.toothNumber); - - return { - ...dentition, - [quadrantNumber]: { - ...targetQuadrant, - teeth: targetQuadrant.teeth.with(toothContext.toothIndex, newTooth), - }, - }; -} - -export function removeTooth( - toothContext: ToothContext, - dentition: Dentition, -): Dentition { - const { quadrantNumber, toothIndex } = toothContext; - const targetQuadrant = dentition[quadrantNumber]; - const tooth = targetQuadrant.teeth[toothIndex]; - - if (tooth === undefined) { - throw new Error( - `Tooth with index ${toothIndex} does not exist in quadrant ${quadrantNumber}`, - ); - } - - if (tooth.type !== "ToothWithDiagnosis") { - throw new Error("Tooth must be of type ToothWithDiagnosis"); - } - - if (!tooth.isRemovable) { - throw new Error("Tooth is not removable"); - } - - const newTooth: AddableTooth = { - type: "AddableTooth", - toothNumber: tooth.toothNumber, - }; - - return { - ...dentition, - [quadrantNumber]: { - ...targetQuadrant, - teeth: targetQuadrant.teeth.with(toothContext.toothIndex, newTooth), - }, - }; -} - -function updateToothWithDiagnosis( - toothContext: ToothContext, - dentition: Dentition, - newTooth: Partial<ToothWithDiagnosis>, -): Dentition { - const { quadrantNumber, toothIndex } = toothContext; - const targetQuadrant = dentition[quadrantNumber]; - const tooth = targetQuadrant.teeth[toothIndex]; - - if (tooth === undefined) { - throw new Error( - `Tooth with index ${toothIndex} does not exist in quadrant ${quadrantNumber}`, - ); - } - - if (tooth.type !== "ToothWithDiagnosis") { - throw new Error("Tooth must be of type ToothWithDiagnosis"); - } - - return { - ...dentition, - [quadrantNumber]: { - ...targetQuadrant, - teeth: targetQuadrant.teeth.with(toothIndex, { ...tooth, ...newTooth }), - }, - }; -} - -export function getToothDiagnoses(dentition: Dentition): ToothDiagnoses { - const toothDiagnoses: ToothDiagnoses = {}; - - Object.values(dentition) - .flatMap((quadrant) => quadrant.teeth) - .forEach((tooth) => { - if (tooth.type !== "ToothWithDiagnosis") { - return; - } - - const { toothNumber, mainResult, secondaryResult1, secondaryResult2 } = - tooth; - - assertIsValid(mainResult); - - if ( - isEmptyToothResult(mainResult) || - !isValidMainResult(mainResult.value) - ) { - return; - } - - toothDiagnoses[toothNumber] = { - tooth: toothNumber, - mainResult: mainResult.value, - secondaryResult1: resolveSecondaryResult(secondaryResult1), - secondaryResult2: resolveSecondaryResult(secondaryResult2), - }; - }); - - return toothDiagnoses; -} - -function resolveSecondaryResult( - toothResult: ToothResult, -): ApiSecondaryResult | undefined { - assertIsValid(toothResult); - - if (!isValidSecondaryResult(toothResult.value)) { - return undefined; - } - - return toothResult.value; -} - -function assertIsValid(toothResult: ToothResult): void { - if (toothResult.isInvalid) { - throw new Error("Invalid tooth result"); - } -} - -function isEmptyToothResult(toothResult: ToothResult): boolean { - return toothResult.value === ""; -} - -type FocusState = Pick<DentalExaminationState, "currentView" | "currentFocus">; - -export function setFocus(newFocus: ElementContext): FocusState { - const quadrantNumber = newFocus.toothContext.quadrantNumber; - const nextView = - quadrantNumber === "Q1" || quadrantNumber === "Q2" - ? "UPPER_JAW" - : "LOWER_JAW"; - return { - currentFocus: newFocus, - currentView: nextView, - }; -} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigate.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigate.ts index c70bbc6c56f90f0db249e103080453aec1aeb6ea..f4a250cf87e9d9ffd7715bcf364d7be90034f0cb 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigate.ts +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigate.ts @@ -134,11 +134,17 @@ function navigateRight(state: NavigateState): NavigateState { } if (quadrantNumber === "Q2") { - return navigateToLastTooth("LOWER_JAW", "Q3"); + return navigateToLastTooth( + currentView === "FULL_DENTITION" ? "FULL_DENTITION" : "LOWER_JAW", + "Q3", + ); } if (quadrantNumber === "Q3") { - return navigateToLastTooth("UPPER_JAW", "Q2"); + return navigateToLastTooth( + currentView === "FULL_DENTITION" ? "FULL_DENTITION" : "UPPER_JAW", + "Q2", + ); } if (quadrantNumber === "Q4") { diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigateTo.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigateTo.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e37715bf91cb7a1036ceca09d12efcae04e08fa --- /dev/null +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigateTo.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { DentalExaminationState } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/dentalExaminationStore"; +import { + DentalExaminationView, + Dentition, + ElementContext, + QuadrantNumber, + ToothContext, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; + +import { resolveTooth } from "./utils"; + +type NavigateToInputState = Pick<DentalExaminationState, "dentition">; + +type NavigateToOutputState = Pick< + DentalExaminationState, + "currentView" | "currentFocus" +>; + +export function navigateTo( + toothContext: ToothContext, + state: NavigateToInputState, +): NavigateToOutputState { + const targetElement = resolveTargetElement(toothContext, state.dentition); + + return { + currentView: resolveViewByQuadrant(toothContext.quadrantNumber), + currentFocus: targetElement, + }; +} + +function resolveTargetElement( + toothContext: ToothContext, + dentition: Dentition, +): ElementContext { + const tooth = resolveTooth(toothContext, dentition); + + if (tooth.type === "AddableTooth") { + return { toothContext }; + } + + return { field: "main", toothContext }; +} + +function resolveViewByQuadrant( + quadrantNumber: QuadrantNumber, +): DentalExaminationView { + return quadrantNumber === "Q1" || quadrantNumber === "Q2" + ? "UPPER_JAW" + : "LOWER_JAW"; +} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/result.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/result.ts new file mode 100644 index 0000000000000000000000000000000000000000..57fb151abc04bb3adb88695f911052f3aba8003e --- /dev/null +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/result.ts @@ -0,0 +1,211 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiMainResult, ApiSecondaryResult } from "@eshg/dental-api"; +import { isEmptyString } from "@eshg/lib-portal/helpers/guards"; + +import { + NavigateState, + navigate, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigate"; +import { + QUADRANT_NUMBERS, + WISDOM_TEETH, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/constants"; +import { + DentalExaminationState, + DmftValuesState, + calculateDmftValues, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/dentalExaminationStore"; +import { createToothResult } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/factories"; +import { + AddableTooth, + Dentition, + ToothContext, + ToothResult, + ToothType, + ToothWithDiagnosis, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; + +type SetResultState = Pick<DentalExaminationState, "dentition">; +type SetMainResultState = SetResultState & NavigateState; + +export function setMainResult( + toothContext: ToothContext, + newValue: string, + state: SetMainResultState, +): SetMainResultState & DmftValuesState { + const { dentition, ...navigateState } = state; + const tooth = getToothFromToothContext(dentition, toothContext); + + const isInvalid = isEmptyString(newValue) + ? !isEmptyString(tooth.secondaryResult1.value) || + !isEmptyString(tooth.secondaryResult2.value) + : !isValidMainResult(newValue); + + const navigateDirection = + navigateState.currentView === "UPPER_JAW" ? "RIGHT" : "LEFT"; + const newDentition = updateToothWithDiagnosis(toothContext, dentition, { + mainResult: createToothResult(newValue, isInvalid), + }); + + return { + ...(isInvalid ? navigateState : navigate(navigateDirection, navigateState)), + dentition: newDentition, + dmftValues: calculateDmftValues(newDentition), + }; +} + +export function setSecondaryResult1( + toothContext: ToothContext, + newValue: string, + state: SetResultState, +): SetResultState { + const { dentition } = state; + const tooth = getToothFromToothContext(dentition, toothContext); + + const isInvalid = + !isEmptyString(newValue) && !isValidSecondaryResult(newValue); + + const mainResult = setMainResultInvalidIfEmpty( + tooth.mainResult, + tooth.secondaryResult2, + newValue, + ); + + return { + dentition: updateToothWithDiagnosis(toothContext, dentition, { + mainResult, + secondaryResult1: createToothResult(newValue, isInvalid), + }), + }; +} + +export function setSecondaryResult2( + toothContext: ToothContext, + newValue: string, + state: SetResultState, +): SetResultState { + const { dentition } = state; + const tooth = getToothFromToothContext(dentition, toothContext); + + const isInvalid = + !isEmptyString(newValue) && !isValidSecondaryResult(newValue); + + const mainResult = setMainResultInvalidIfEmpty( + tooth.mainResult, + tooth.secondaryResult1, + newValue, + ); + + return { + dentition: updateToothWithDiagnosis(toothContext, dentition, { + mainResult, + secondaryResult2: createToothResult(newValue, isInvalid), + }), + }; +} + +function setMainResultInvalidIfEmpty( + mainResult: ToothResult, + secondaryResult: ToothResult, + newValue: string, +) { + if (isEmptyToothResult(mainResult)) { + if (isEmptyString(newValue) && isEmptyToothResult(secondaryResult)) { + return createToothResult(mainResult.value, false); + } else { + return createToothResult("", true); + } + } + return mainResult; +} + +function getToothFromToothContext( + dentition: Dentition, + toothContext: ToothContext, +) { + return dentition[toothContext.quadrantNumber].teeth[ + toothContext.toothIndex + ] as ToothWithDiagnosis; +} + +export function isEmptyToothResult(toothResult: ToothResult): boolean { + return toothResult.value === ""; +} + +export function isValidSecondaryResult( + newValue: string, +): newValue is ApiSecondaryResult { + return Object.values(ApiSecondaryResult).includes( + newValue as ApiSecondaryResult, + ); +} + +export function isValidMainResult(newValue: string): newValue is ApiMainResult { + return Object.values(ApiMainResult).includes(newValue as ApiMainResult); +} + +function updateToothWithDiagnosis( + toothContext: ToothContext, + dentition: Dentition, + newTooth: Partial<ToothWithDiagnosis>, +): Dentition { + const { quadrantNumber, toothIndex } = toothContext; + const targetQuadrant = dentition[quadrantNumber]; + const tooth = targetQuadrant.teeth[toothIndex]; + + if (tooth === undefined) { + throw new Error( + `Tooth with index ${toothIndex} does not exist in quadrant ${quadrantNumber}`, + ); + } + + if (tooth.type !== "ToothWithDiagnosis") { + throw new Error("Tooth must be of type ToothWithDiagnosis"); + } + + return { + ...dentition, + [quadrantNumber]: { + ...targetQuadrant, + teeth: targetQuadrant.teeth.with(toothIndex, { ...tooth, ...newTooth }), + }, + }; +} + +export function calculateDmftValue( + dentition: Dentition, + type: ToothType, +): number { + let dmft = 0; + QUADRANT_NUMBERS.forEach((quadrant) => { + const teethWithDmftDiagnosis = dentition[quadrant].teeth.filter((tooth) => + isToothWithDmftDiagnosis(tooth, type), + ); + dmft += teethWithDmftDiagnosis.length; + }); + return dmft; +} + +function isToothWithDmftDiagnosis( + tooth: ToothWithDiagnosis | AddableTooth, + type: "PRIMARY_TOOTH" | "SECONDARY_TOOTH", +) { + return ( + tooth.type === "ToothWithDiagnosis" && + tooth.toothType === type && + !WISDOM_TEETH.has(tooth.toothNumber) && + isDmfDiagnosis(tooth.mainResult.value) + ); +} + +function isDmfDiagnosis(result: string) { + return ( + result === ApiMainResult.F || + result === ApiMainResult.D || + result === ApiMainResult.E + ); +} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/tooth.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/tooth.ts new file mode 100644 index 0000000000000000000000000000000000000000..affd0f90ec784f0f23b63264d7f49ea55d02844b --- /dev/null +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/tooth.ts @@ -0,0 +1,161 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiSecondaryResult } from "@eshg/dental-api"; +import { ToothDiagnoses } from "@eshg/dental/api/models/ExaminationResult"; + +import { + DentalExaminationState, + calculateDmftValues, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/dentalExaminationStore"; +import { createToothWithDiagnosis } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/factories"; +import { + AddableTooth, + Dentition, + ElementContext, + ToothContext, + ToothResult, + isAddableTooth, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; + +import { + isEmptyToothResult, + isValidMainResult, + isValidSecondaryResult, +} from "./result"; + +export function addTooth( + toothContext: ToothContext, + dentition: Dentition, +): Dentition { + const { quadrantNumber, toothIndex } = toothContext; + const targetQuadrant = dentition[quadrantNumber]; + const tooth = targetQuadrant.teeth[toothIndex]; + + if (tooth === undefined) { + throw new Error( + `Tooth with index ${toothIndex} does not exist in quadrant ${quadrantNumber}`, + ); + } + + if (!isAddableTooth(tooth)) { + throw new Error("Tooth must be of type AddableTooth"); + } + + const newTooth = createToothWithDiagnosis(tooth.toothNumber); + + return { + ...dentition, + [quadrantNumber]: { + ...targetQuadrant, + teeth: targetQuadrant.teeth.with(toothContext.toothIndex, newTooth), + }, + }; +} + +type RemoveToothState = Pick< + DentalExaminationState, + "dentition" | "dmftValues" +>; + +export function removeTooth( + toothContext: ToothContext, + dentition: Dentition, +): RemoveToothState { + const { quadrantNumber, toothIndex } = toothContext; + const targetQuadrant = dentition[quadrantNumber]; + const tooth = targetQuadrant.teeth[toothIndex]; + + if (tooth === undefined) { + throw new Error( + `Tooth with index ${toothIndex} does not exist in quadrant ${quadrantNumber}`, + ); + } + + if (tooth.type !== "ToothWithDiagnosis") { + throw new Error("Tooth must be of type ToothWithDiagnosis"); + } + + if (!tooth.isRemovable) { + throw new Error("Tooth is not removable"); + } + + const newTooth: AddableTooth = { + type: "AddableTooth", + toothNumber: tooth.toothNumber, + }; + + const newDentition = { + ...dentition, + [quadrantNumber]: { + ...targetQuadrant, + teeth: targetQuadrant.teeth.with(toothContext.toothIndex, newTooth), + }, + }; + + return { + dentition: newDentition, + dmftValues: calculateDmftValues(newDentition), + }; +} + +export function getToothDiagnoses(dentition: Dentition): ToothDiagnoses { + const toothDiagnoses: ToothDiagnoses = {}; + + Object.values(dentition) + .flatMap((quadrant) => quadrant.teeth) + .forEach((tooth) => { + if (tooth.type !== "ToothWithDiagnosis") { + return; + } + + const { toothNumber, mainResult, secondaryResult1, secondaryResult2 } = + tooth; + + assertIsValid(mainResult); + + if ( + isEmptyToothResult(mainResult) || + !isValidMainResult(mainResult.value) + ) { + return; + } + + toothDiagnoses[toothNumber] = { + tooth: toothNumber, + mainResult: mainResult.value, + secondaryResult1: resolveSecondaryResult(secondaryResult1), + secondaryResult2: resolveSecondaryResult(secondaryResult2), + }; + }); + + return toothDiagnoses; +} + +function resolveSecondaryResult( + toothResult: ToothResult, +): ApiSecondaryResult | undefined { + assertIsValid(toothResult); + + if (!isValidSecondaryResult(toothResult.value)) { + return undefined; + } + + return toothResult.value; +} + +function assertIsValid(toothResult: ToothResult): void { + if (toothResult.isInvalid) { + throw new Error("Invalid tooth result"); + } +} + +type FocusOutputState = Pick<DentalExaminationState, "currentFocus">; + +export function setFocus(newFocus: ElementContext): FocusOutputState { + return { + currentFocus: newFocus, + }; +} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/utils.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..eaf4ae3e410ea225c02d9e4f3fbbd838612af3d8 --- /dev/null +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/utils.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + Dentition, + Tooth, + ToothContext, +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; + +export function resolveTooth( + toothContext: ToothContext, + dentition: Dentition, +): Tooth { + const { quadrantNumber, toothIndex } = toothContext; + const tooth = dentition[quadrantNumber].teeth[toothIndex]; + + if (tooth === undefined) { + throw new Error(`Tooth not found: ${quadrantNumber}:${toothIndex}`); + } + + return tooth; +} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/constants.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/constants.ts index 8badadf4396b2db4d4697fb0919d962f1676a59a..eac7ef0cb2a226124a1f84692c799ebdfa95e3c0 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/constants.ts +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/constants.ts @@ -7,64 +7,13 @@ import { ApiTooth } from "@eshg/dental-api"; import { NavigateDirection } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigate"; -import { ToothType } from "./types"; +import { QuadrantNumber, ToothType } from "./types"; + +export const QUADRANT_NUMBERS: QuadrantNumber[] = ["Q1", "Q2", "Q3", "Q4"]; export const MIN_TOOTH_INDEX = 0; export const MAX_TOOTH_INDEX = 7; -/** - * Defines a mapping from milk teeth to permanent teeth and vice versa - */ -export const RELATED_TEETH: Partial<Record<ApiTooth, ApiTooth>> = { - T11: "T51", - T12: "T52", - T13: "T53", - T14: "T54", - T15: "T55", - - T21: "T61", - T22: "T62", - T23: "T63", - T24: "T64", - T25: "T65", - - T31: "T71", - T32: "T72", - T33: "T73", - T34: "T74", - T35: "T75", - - T41: "T81", - T42: "T82", - T43: "T83", - T44: "T84", - T45: "T85", - - T51: "T11", - T52: "T12", - T53: "T13", - T54: "T14", - T55: "T15", - - T61: "T21", - T62: "T22", - T63: "T23", - T64: "T24", - T65: "T25", - - T71: "T31", - T72: "T32", - T73: "T33", - T74: "T34", - T75: "T35", - - T81: "T41", - T82: "T42", - T83: "T43", - T84: "T44", - T85: "T45", -}; - export const TOOTH_TYPES: Record<ApiTooth, ToothType> = { T11: "SECONDARY_TOOTH", T12: "SECONDARY_TOOTH", @@ -145,6 +94,8 @@ export const OPTIONAL_TEETH = new Set<ApiTooth>([ "T48", ]); +export const WISDOM_TEETH = new Set<ApiTooth>(["T18", "T28", "T38", "T48"]); + export const NAVIGATE_DIRECTIONS: Record<string, NavigateDirection> = { ArrowUp: "UP", ArrowDown: "DOWN", diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/dentalExaminationStore.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/dentalExaminationStore.ts index ea487e7f087a9d732cbee00ac4a630876192722f..5384d9bf2cbdccfd60ecf3ef98c783a14c527c54 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/dentalExaminationStore.ts +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/dentalExaminationStore.ts @@ -4,6 +4,7 @@ */ /* eslint-disable unused-imports/no-unused-vars */ +import { ApiDentitionType } from "@eshg/dental-api"; import { ExaminationResult, ToothDiagnoses, @@ -11,16 +12,21 @@ import { import { createStore } from "zustand"; import { - addTooth, - getToothDiagnoses, - removeTooth, - setFocus, + calculateDmftValue, setMainResult, setSecondaryResult1, setSecondaryResult2, -} from "./actions"; +} from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/result"; + import { NavigateDirection, navigate } from "./actions/navigate"; -import { createSecondaryDentition } from "./factories"; +import { navigateTo } from "./actions/navigateTo"; +import { + addTooth, + getToothDiagnoses, + removeTooth, + setFocus, +} from "./actions/tooth"; +import { createPrimaryDentition, createSecondaryDentition } from "./factories"; import { DentalExaminationView, Dentition, @@ -32,12 +38,14 @@ export interface DentalExaminationState { currentView: DentalExaminationView; currentFocus: ElementContext; dentition: Dentition; + dmftValues: { primaryTeeth: number; secondaryTeeth: number }; } export interface DentalExaminationActions { setView: (newView: DentalExaminationView) => void; setFocus: (focus: ElementContext) => void; navigate: (direction: NavigateDirection) => void; + navigateTo: (toothContext: ToothContext) => void; addTooth: ToothAction; removeTooth: ToothAction; @@ -58,22 +66,39 @@ export type SetToothResultAction = ( export type DentalExaminationStore = DentalExaminationState & DentalExaminationActions; +export type DmftValuesState = Pick<DentalExaminationState, "dmftValues">; + +export function calculateDmftValues(dentition: Dentition) { + return { + primaryTeeth: calculateDmftValue(dentition, "PRIMARY_TOOTH"), + secondaryTeeth: calculateDmftValue(dentition, "SECONDARY_TOOTH"), + }; +} + export function initDentalExaminationStore( examinationResult: ExaminationResult | undefined, + defaultDentitionType: ApiDentitionType | undefined, ): DentalExaminationState { - const toothDiagnoses = - examinationResult?.type === "screening" - ? examinationResult.toothDiagnoses - : {}; + const isScreening = examinationResult?.type === "screening"; + + const toothDiagnoses = isScreening ? examinationResult.toothDiagnoses : {}; + + const dentitionType = + (isScreening ? examinationResult.dentitionType : undefined) ?? + defaultDentitionType; + const dentition = + dentitionType === ApiDentitionType.Primary + ? createPrimaryDentition(toothDiagnoses) + : createSecondaryDentition(toothDiagnoses); return { currentView: "UPPER_JAW", - // TODO ISSUE-6584: distinguish between type of dentition - dentition: createSecondaryDentition(toothDiagnoses), + dentition: dentition, currentFocus: { toothContext: { quadrantNumber: "Q1", toothIndex: 0 }, field: "main", }, + dmftValues: calculateDmftValues(dentition), }; } @@ -89,9 +114,7 @@ export function createDentalExaminationStore( })); }, removeTooth: (toothContext: ToothContext) => { - set((state) => ({ - dentition: removeTooth(toothContext, state.dentition), - })); + set((state) => removeTooth(toothContext, state.dentition)); }, toggleToothType: (toothContext: ToothContext) => { throw new Error("Not yet implemented"); @@ -100,18 +123,14 @@ export function createDentalExaminationStore( set(setFocus(newFocus)); }, setMainResult: (toothContext: ToothContext, newValue: string) => - set((state) => ({ - dentition: setMainResult(toothContext, newValue, state.dentition), - })), + set((state) => setMainResult(toothContext, newValue, state)), setSecondaryResult1: (toothContext: ToothContext, newValue: string) => - set((state) => ({ - dentition: setSecondaryResult1(toothContext, newValue, state.dentition), - })), + set((state) => setSecondaryResult1(toothContext, newValue, state)), setSecondaryResult2: (toothContext: ToothContext, newValue: string) => - set((state) => ({ - dentition: setSecondaryResult2(toothContext, newValue, state.dentition), - })), + set((state) => setSecondaryResult2(toothContext, newValue, state)), getToothDiagnoses: () => getToothDiagnoses(get().dentition), navigate: (direction) => set((state) => navigate(direction, state)), + navigateTo: (toothContext) => + set((state) => navigateTo(toothContext, state)), })); } diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/factories.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/factories.ts index e9c7a3cf216ce3f7fe26f2fdef6cb1af2bcce88c..27d5c595f02bb28d825fde581024f65b419662f4 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/factories.ts +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/factories.ts @@ -6,9 +6,10 @@ import { ApiTooth } from "@eshg/dental-api"; import { ToothDiagnoses } from "@eshg/dental/api/models/ExaminationResult"; import { ToothDiagnosis } from "@eshg/dental/api/models/ToothDiagnosis"; +import { RELATED_TEETH } from "@eshg/dental/config/teeth"; import { isDefined } from "remeda"; -import { OPTIONAL_TEETH, RELATED_TEETH, TOOTH_TYPES } from "./constants"; +import { OPTIONAL_TEETH, TOOTH_TYPES } from "./constants"; import { AddableTooth, Dentition, diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useElementFocus.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useElementFocus.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fe53c96b3c1e2f3d09895cbed02c4da8136e0dc --- /dev/null +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useElementFocus.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useEffect, useRef } from "react"; +import { useShallow } from "zustand/react/shallow"; + +import { useDentalExaminationStore } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider"; +import { ElementContext } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/types"; + +export function useElementFocus<TElement extends HTMLElement>( + elementContext: ElementContext, + onFocus: (element: TElement) => void, +) { + const elementRef = useRef<TElement>(null); + const isFocused = useDentalExaminationStore( + useShallow((state) => equalsElement(elementContext, state.currentFocus)), + ); + const setFocus = useDentalExaminationStore((state) => state.setFocus); + + useEffect(() => { + if (isFocused && elementRef.current !== null) { + onFocus(elementRef.current); + } + }, [elementRef, isFocused, onFocus]); + + function focusHandler(): void { + setFocus(elementContext); + } + + return { elementRef, isFocused, focusHandler }; +} + +function equalsElement( + elementContext: ElementContext, + currentFocus: ElementContext, +): boolean { + return ( + currentFocus.toothContext.quadrantNumber === + elementContext.toothContext.quadrantNumber && + currentFocus.toothContext.toothIndex === + elementContext.toothContext.toothIndex && + currentFocus.field === elementContext.field + ); +} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useKeyboardNavigationHandler.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useKeyboardNavigationHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..b24650f49805d995e37217b59aed130d0f5f92b5 --- /dev/null +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useKeyboardNavigationHandler.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { KeyboardEvent, KeyboardEventHandler } from "react"; +import { isDefined } from "remeda"; + +import { useDentalExaminationStore } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/DentalExaminationStoreProvider"; +import { NAVIGATE_DIRECTIONS } from "@/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/constants"; + +type KeyboardEventTarget = HTMLInputElement | HTMLButtonElement; + +export function useKeyboardNavigationHandler(): KeyboardEventHandler<KeyboardEventTarget> { + const navigate = useDentalExaminationStore((state) => state.navigate); + + return function handleEvent(event: KeyboardEvent<KeyboardEventTarget>): void { + const direction = NAVIGATE_DIRECTIONS[event.code]; + + if (isDefined(direction)) { + navigate(direction); + event.preventDefault(); + } + }; +} diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/participantExamination/useParticipantExaminationForm.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/participantExamination/useParticipantExaminationForm.ts index 60b4af9c754da2a80cd7f5946a14eed290bbc566..3fc3b35f53f280ba00f2acf4ac9163afd1c7ae27 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/participantExamination/useParticipantExaminationForm.ts +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/participantExamination/useParticipantExaminationForm.ts @@ -11,7 +11,10 @@ import { isEmptyExaminationResult, } from "@eshg/dental/api/models/ExaminationResult"; import { ToothDiagnosis } from "@eshg/dental/api/models/ToothDiagnosis"; -import { mapOptionalValue } from "@eshg/lib-portal/helpers/form"; +import { + mapOptionalValue, + mapRequiredValue, +} from "@eshg/lib-portal/helpers/form"; import { useFormik } from "formik"; import { @@ -24,6 +27,7 @@ import { useProphylaxisSessionStore } from "@/lib/businessModules/dental/feature interface ExaminationInputValues { result?: ExaminationResult; note?: string; + prophylaxisDentitionType?: ApiDentitionType; } interface ExaminationOutputValues { @@ -50,6 +54,7 @@ export function useParticipantExaminationForm( initialValues: mapToExaminationFormValues( initialValues.result, initialValues.note, + initialValues.prophylaxisDentitionType, ), onSubmit: (formValues: ExaminationFormValues) => { onSubmit( @@ -80,11 +85,11 @@ function mapToExaminationResult( if (screening) { result = { type: "screening", + dentitionType: mapRequiredValue(formValues.dentitionType), oralHygieneStatus: mapOptionalValue(formValues.oralHygieneStatus), fluorideVarnishApplied: mapOptionalValue( formValues.fluorideVarnishApplied, ), - dentitionType: ApiDentitionType.Mixed, toothDiagnoses: toothDiagnoses, }; } else { diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/actions.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/actions.ts index 6e7baef74870029c0ea699e08def612ed8863287..5516b488983f7c6c9d314550efcc3135d8ff24bb 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/actions.ts +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/actions.ts @@ -4,6 +4,7 @@ */ import { ExaminationResult } from "@eshg/dental/api/models/ExaminationResult"; +import { mapToExaminationStatus } from "@eshg/dental/api/models/ExaminationStatus"; import { ParticipantFilters } from "./participantFilters"; import { ProphylaxisSessionState } from "./prophylaxisSessionStore"; @@ -40,9 +41,15 @@ export function setExamination( if (participant.examinationId !== examinationId) { return participant; } + if (participant.result?.type === "absence") { + return participant; + } + + const status = mapToExaminationStatus(result); return { ...participant, + status, result, note, }; diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/participantSorting.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/participantSorting.ts index 55bca904037ff51e3d07cd7b0e2b83402a824434..9f4b88d6f5d34f52ebad837f24da360b9a649bcc 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/participantSorting.ts +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/participantSorting.ts @@ -23,6 +23,7 @@ type ParticipantSortAttributes = Omit< | "examinationId" | "examinationVersion" | "allFluoridationConsents" + | "prophylaxisDentitionType" >; export type ParticipantSortKey = keyof ParticipantSortAttributes; export type ParticipantSortDirection = "asc" | "desc"; diff --git a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/useSyncOutgoingProphylaxisSessionChanges.ts b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/useSyncOutgoingProphylaxisSessionChanges.ts index 7d46a095bba8e72e5d8072599249de6371223efb..b1f2990bd0df5b864988b30628fc82aee542341a 100644 --- a/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/useSyncOutgoingProphylaxisSessionChanges.ts +++ b/employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/prophylaxisSessionStore/useSyncOutgoingProphylaxisSessionChanges.ts @@ -92,9 +92,9 @@ function mapScreeningResult( ): ApiExaminationResult { return { type: "ScreeningExaminationResult", + dentitionType: screeningResult.dentitionType, fluorideVarnishApplied: screeningResult.fluorideVarnishApplied, oralHygieneStatus: screeningResult.oralHygieneStatus, - dentitionType: screeningResult.dentitionType, toothDiagnoses: Object.values(screeningResult.toothDiagnoses), }; } diff --git a/employee-portal/src/lib/businessModules/inspection/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/inspection/shared/sideNavigationItem.tsx index e7523f5093552d5adeea1a24cac94633be0fd3e9..8ddfc0c5c058d02139851fee51d3aa504f0b3f0b 100644 --- a/employee-portal/src/lib/businessModules/inspection/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/inspection/shared/sideNavigationItem.tsx @@ -6,19 +6,14 @@ import { ApiUserRole } from "@eshg/base-api"; import { hasUserRole } from "@eshg/lib-employee-portal/helpers/accessControl"; import { + SideNavigationItem, SideNavigationSubItem, - UseSideNavigationItemsResult, } from "@eshg/lib-employee-portal/types/sideNavigation"; import { OtherHousesOutlined } from "@mui/icons-material"; import { routes } from "./routes"; -const sideNavigationItem = { - name: "Begehung", - decorator: <OtherHousesOutlined />, -}; - -const defaultSubItems: SideNavigationSubItem[] = [ +const subItems: SideNavigationSubItem[] = [ { name: "Vorgänge", href: routes.procedures.index, @@ -66,18 +61,13 @@ const defaultSubItems: SideNavigationSubItem[] = [ }, ]; -export function useSideNavigationItems( - enabled: boolean, -): UseSideNavigationItemsResult { - return { - isLoading: false, - items: enabled - ? [ - { - ...sideNavigationItem, - subItems: defaultSubItems, - }, - ] - : [], - }; +export function resolveSideNavigationItems(): SideNavigationItem[] { + return [ + { + type: "SideNavigationParentItem", + name: "Begehung", + decorator: <OtherHousesOutlined />, + subItems, + }, + ]; } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/measlesProtection/shared/sideNavigationItem.tsx index 6e428a5a612e78a679e4400db23428b4ee8ba0cc..83b0da5700314e0c0667bb250bdbd9c302cbf5e2 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/shared/sideNavigationItem.tsx @@ -3,16 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiBaseFeature, ApiUserRole } from "@eshg/base-api"; +import { ApiUserRole } from "@eshg/base-api"; import { hasUserRole } from "@eshg/lib-employee-portal/helpers/accessControl"; import { + SideNavigationItem, + SideNavigationItemsProps, SideNavigationSubItem, - UseSideNavigationItemsResult, } from "@eshg/lib-employee-portal/types/sideNavigation"; import { HubOutlined } from "@mui/icons-material"; -import { useIsNewFeatureEnabled } from "@/lib/baseModule/api/queries/feature"; - import { routes } from "./routes"; const sideNavigationItem = { @@ -39,15 +38,17 @@ const inboxNavigationItem: SideNavigationSubItem = { accessCheck: hasUserRole(ApiUserRole.MeaslesProtectionAdmin), }; -export function useSideNavigationItems( - enabled: boolean, -): UseSideNavigationItemsResult { - const isInboxEnabled = useIsNewFeatureEnabled(ApiBaseFeature.Inbox); +export function resolveSideNavigationItems({ + isInboxEnabled, +}: SideNavigationItemsProps): SideNavigationItem[] { const subItems = isInboxEnabled ? [...defaultSubItems, inboxNavigationItem] : defaultSubItems; - return { - isLoading: false, - items: enabled ? [{ ...sideNavigationItem, subItems }] : [], - }; + return [ + { + type: "SideNavigationParentItem", + ...sideNavigationItem, + subItems, + }, + ]; } diff --git a/employee-portal/src/lib/businessModules/medicalRegistry/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/medicalRegistry/shared/sideNavigationItem.tsx index 3154295ead757df0565494c838973a41d046ab7d..f1c3996efc8a1efa3badd774b09a338eeef1e939 100644 --- a/employee-portal/src/lib/businessModules/medicalRegistry/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/medicalRegistry/shared/sideNavigationItem.tsx @@ -5,25 +5,19 @@ import { ApiUserRole } from "@eshg/base-api"; import { hasUserRole } from "@eshg/lib-employee-portal/helpers/accessControl"; -import { UseSideNavigationItemsResult } from "@eshg/lib-employee-portal/types/sideNavigation"; +import { SideNavigationItem } from "@eshg/lib-employee-portal/types/sideNavigation"; import { MedicalServicesOutlined } from "@mui/icons-material"; import { routes } from "./routes"; -export function useSideNavigationItems( - enabled: boolean, -): UseSideNavigationItemsResult { - return { - isLoading: false, - items: enabled - ? [ - { - name: "Medizinalaufsicht", - decorator: <MedicalServicesOutlined />, - href: routes.procedures.index, - accessCheck: hasUserRole(ApiUserRole.MedicalRegistryAdmin), - }, - ] - : [], - }; +export function resolveSideNavigationItems(): SideNavigationItem[] { + return [ + { + type: "SideNavigationLinkItem", + name: "Medizinalaufsicht", + decorator: <MedicalServicesOutlined />, + href: routes.procedures.index, + accessCheck: hasUserRole(ApiUserRole.MedicalRegistryAdmin), + }, + ]; } diff --git a/employee-portal/src/lib/businessModules/officialMedicalService/components/appointmentBlocks/appointmentBlocksTable/AppointmentBlockGroupTable.tsx b/employee-portal/src/lib/businessModules/officialMedicalService/components/appointmentBlocks/appointmentBlocksTable/AppointmentBlockGroupTable.tsx index 1635f4679f28d944017d2e6fd5dbf50b87a5e085..d3db67e4e25092afcf8946a1a90c9cbb8ba18b36 100644 --- a/employee-portal/src/lib/businessModules/officialMedicalService/components/appointmentBlocks/appointmentBlocksTable/AppointmentBlockGroupTable.tsx +++ b/employee-portal/src/lib/businessModules/officialMedicalService/components/appointmentBlocks/appointmentBlocksTable/AppointmentBlockGroupTable.tsx @@ -20,6 +20,7 @@ import { AppointmentBlockGroup, } from "@/lib/businessModules/officialMedicalService/api/models/AppointmentBlockGroup"; import { useGetAppointmentBlockGroupsQuery } from "@/lib/businessModules/officialMedicalService/api/queries/appointmentBlocksApi"; +import { APPOINTMENT_TYPES } from "@/lib/businessModules/officialMedicalService/components/appointmentBlocks/constants"; import { routes } from "@/lib/businessModules/officialMedicalService/shared/routes"; import { NoAppointmentBlocksAvailable } from "@/lib/shared/components/appointmentBlocks/NoAppointmentBlocksAvailable"; import { Pagination } from "@/lib/shared/components/pagination/Pagination"; @@ -51,6 +52,11 @@ const COLUMNS = [ : formatCalendarWeek(props.getValue()), enableSorting: false, }), + columnHelper.accessor("type", { + header: "Art", + cell: (props) => APPOINTMENT_TYPES[props.getValue()], + enableSorting: true, + }), columnHelper.accessor("start", { header: "Start", cell: (props) => formatDateTime(props.getValue()), diff --git a/employee-portal/src/lib/businessModules/officialMedicalService/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/officialMedicalService/shared/sideNavigationItem.tsx index 8755227c3d836f3b465cb46b99e7baf8f02c084c..e3d5bcca651c10f5fcfa9ac1b7a8d2f0b9714c1a 100644 --- a/employee-portal/src/lib/businessModules/officialMedicalService/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/officialMedicalService/shared/sideNavigationItem.tsx @@ -5,10 +5,7 @@ import { ApiUserRole } from "@eshg/base-api"; import { hasUserRole } from "@eshg/lib-employee-portal/helpers/accessControl"; -import { - SideNavigationItem, - UseSideNavigationItemsResult, -} from "@eshg/lib-employee-portal/types/sideNavigation"; +import { SideNavigationItem } from "@eshg/lib-employee-portal/types/sideNavigation"; import { isPlainObject } from "remeda"; import { StethoscopeIcon } from "@/lib/businessModules/officialMedicalService/components/icons/StethoscopeIcon"; @@ -16,6 +13,7 @@ import { routes } from "@/lib/businessModules/officialMedicalService/shared/rout const NAVIGATION_ITEMS: SideNavigationItem[] = [ { + type: "SideNavigationParentItem", name: "Amtsärztl. Dienst", decorator: <StethoscopeIcon />, subItems: [ @@ -38,11 +36,6 @@ const NAVIGATION_ITEMS: SideNavigationItem[] = [ }, ]; -export function useSideNavigationItems( - enabled: boolean, -): UseSideNavigationItemsResult { - return { - isLoading: false, - items: enabled ? NAVIGATION_ITEMS : [], - }; +export function resolveSideNavigationItems(): SideNavigationItem[] { + return NAVIGATION_ITEMS; } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProcedureFilterSettings.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProcedureFilterSettings.tsx index da5b5237573cc355076afb8c8b88dd43071fb5b1..be80701ecda71d967b7da55974fe16c36873ce85 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProcedureFilterSettings.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProcedureFilterSettings.tsx @@ -17,6 +17,7 @@ import { Label } from "@/lib/businessModules/schoolEntry/api/models/Label"; import { PROCEDURE_TYPE_OPTIONS } from "@/lib/businessModules/schoolEntry/features/procedures/options"; import { LabelAutocomplete } from "@/lib/businessModules/schoolEntry/features/procedures/procedureDetails/LabelAutocomplete"; import { ResetButton } from "@/lib/shared/components/ResetButton"; +import { OverlayBoundary } from "@/lib/shared/components/boundaries/OverlayBoundary"; import { ActiveFilter } from "@/lib/shared/components/filterSettings/ActiveFilter"; import { FilterSettingsContent } from "@/lib/shared/components/filterSettings/FilterSettingsContent"; import { @@ -73,166 +74,174 @@ function evaluateStringAsBoolean(value: string) { export function ProcedureFilterSettings(props: ProcedureFilterSettingsProps) { return ( - <FilterSettingsSheet {...props.filterSettingsSheetProps}> - <FilterSettingsContent - showActiveFilters={props.activeFilters.length > 0} - activeFilters={ - <ActiveFilter - maxVisible={5} - filterValues={props.activeFilters} - deleteAllFilterValues={props.clearFilterValues} - deleteFilterValue={props.deleteFilterValue} - getFilterValueLabel={getFilterLabel} - /> - } - > - <FormControl> - <FormLabel>Untersuchung am</FormLabel> - <Input - type="date" - value={ - props.filterFormValues.dayOfAppointmentFilter !== undefined - ? toDateString(props.filterFormValues.dayOfAppointmentFilter) - : "" - } - onChange={(dayOfAppointment) => { - const value = dayOfAppointment.target.value; - props.setFilterFormValue( - "dayOfAppointmentFilter", - isDateString(value) ? toUtcDate(value) : undefined, - ); - }} - /> - </FormControl> - <FormControl> - <FormLabel>Termin</FormLabel> - <Select - value={evaluateBooleanValue( - props.filterFormValues.hasAppointmentFilter, - )} - onChange={(_, newValue) => { - if (newValue === null) { - return; + <OverlayBoundary> + <FilterSettingsSheet {...props.filterSettingsSheetProps}> + <FilterSettingsContent + showActiveFilters={props.activeFilters.length > 0} + activeFilters={ + <ActiveFilter + maxVisible={5} + filterValues={props.activeFilters} + deleteAllFilterValues={props.clearFilterValues} + deleteFilterValue={props.deleteFilterValue} + getFilterValueLabel={getFilterLabel} + /> + } + > + <FormControl> + <FormLabel>Untersuchung am</FormLabel> + <Input + type="date" + value={ + props.filterFormValues.dayOfAppointmentFilter !== undefined + ? toDateString(props.filterFormValues.dayOfAppointmentFilter) + : "" } - props.setFilterFormValue( - "hasAppointmentFilter", - evaluateStringAsBoolean(newValue), - ); - }} - endDecorator={ - isDefined(props.filterFormValues.hasAppointmentFilter) ? ( - <ResetButton - onReset={() => { - props.setFilterFormValue("hasAppointmentFilter", undefined); - }} - /> - ) : undefined - } - > - <SelectOptions - options={[ - { value: "true", label: "mit Termin" }, - { value: "false", label: "ohne Termin" }, - ]} + onChange={(dayOfAppointment) => { + const value = dayOfAppointment.target.value; + props.setFilterFormValue( + "dayOfAppointmentFilter", + isDateString(value) ? toUtcDate(value) : undefined, + ); + }} /> - </Select> - </FormControl> - <FormControl> - <FormLabel>Einladung versandt</FormLabel> - <Select - value={evaluateBooleanValue( - props.filterFormValues.isInvitationSentFilter, - )} - onChange={(_, newValue) => { - if (newValue === null) { - return; + </FormControl> + <FormControl> + <FormLabel>Termin</FormLabel> + <Select + value={evaluateBooleanValue( + props.filterFormValues.hasAppointmentFilter, + )} + onChange={(_, newValue) => { + if (newValue === null) { + return; + } + props.setFilterFormValue( + "hasAppointmentFilter", + evaluateStringAsBoolean(newValue), + ); + }} + endDecorator={ + isDefined(props.filterFormValues.hasAppointmentFilter) ? ( + <ResetButton + onReset={() => { + props.setFilterFormValue( + "hasAppointmentFilter", + undefined, + ); + }} + /> + ) : undefined + } + > + <SelectOptions + options={[ + { value: "true", label: "mit Termin" }, + { value: "false", label: "ohne Termin" }, + ]} + /> + </Select> + </FormControl> + <FormControl> + <FormLabel>Einladung versandt</FormLabel> + <Select + value={evaluateBooleanValue( + props.filterFormValues.isInvitationSentFilter, + )} + onChange={(_, newValue) => { + if (newValue === null) { + return; + } + props.setFilterFormValue( + "isInvitationSentFilter", + evaluateStringAsBoolean(newValue), + ); + }} + endDecorator={ + isDefined(props.filterFormValues.isInvitationSentFilter) ? ( + <ResetButton + onReset={() => { + props.setFilterFormValue( + "isInvitationSentFilter", + undefined, + ); + }} + /> + ) : undefined } - props.setFilterFormValue( - "isInvitationSentFilter", - evaluateStringAsBoolean(newValue), - ); - }} - endDecorator={ - isDefined(props.filterFormValues.isInvitationSentFilter) ? ( - <ResetButton - onReset={() => { - props.setFilterFormValue( - "isInvitationSentFilter", - undefined, - ); - }} - /> - ) : undefined - } - > - <SelectOptions - options={[ - { value: "true", label: "Ja" }, - { value: "false", label: "Nein" }, - ]} + > + <SelectOptions + options={[ + { value: "true", label: "Ja" }, + { value: "false", label: "Nein" }, + ]} + /> + </Select> + </FormControl> + <FormControl> + <FormLabel>Schuljahr</FormLabel> + <SchoolYearAutocomplete + value={props.filterFormValues.schoolYearFilter ?? null} + onChange={(_, newValue) => { + props.setFilterFormValue( + "schoolYearFilter", + newValue ?? undefined, + ); + }} /> - </Select> - </FormControl> - <FormControl> - <FormLabel>Schuljahr</FormLabel> - <SchoolYearAutocomplete - value={props.filterFormValues.schoolYearFilter ?? null} - onChange={(_, newValue) => { - props.setFilterFormValue( - "schoolYearFilter", - newValue ?? undefined, - ); - }} - /> - </FormControl> - <FormControl> - <FormLabel>Schule</FormLabel> - <SearchInstitutionFilter - institutionId={props.filterFormValues.schoolIdFilter} - onChange={(schoolId) => - props.setFilterFormValue("schoolIdFilter", schoolId) - } - placeholder="Schule suchen" - /> - </FormControl> - <FormControl> - <FormLabel>Art</FormLabel> - <Select - aria-label="Art" - value={props.filterFormValues.procedureTypeFilter ?? ""} - onChange={(_, newValue) => { - if (newValue === null) { - return; + </FormControl> + <FormControl> + <FormLabel>Schule</FormLabel> + <SearchInstitutionFilter + institutionId={props.filterFormValues.schoolIdFilter} + onChange={(schoolId) => + props.setFilterFormValue("schoolIdFilter", schoolId) } - props.setFilterFormValue("procedureTypeFilter", newValue); - }} - endDecorator={ - isDefined(props.filterFormValues.procedureTypeFilter) ? ( - <ResetButton - onReset={() => { - props.setFilterFormValue("procedureTypeFilter", undefined); - }} - /> - ) : undefined - } - > - <SelectOptions options={PROCEDURE_TYPE_OPTIONS} /> - </Select> - </FormControl> - <FormControl> - <FormLabel>Kennungen</FormLabel> - <LabelAutocomplete - name="labels" - value={props.filterFormValues.labelsFilter ?? []} - onChange={(newValue) => { - props.setFilterFormValue( - "labelsFilter", - isEmpty(newValue) ? undefined : newValue, - ); - }} - /> - </FormControl> - </FilterSettingsContent> - </FilterSettingsSheet> + placeholder="Schule suchen" + /> + </FormControl> + <FormControl> + <FormLabel>Art</FormLabel> + <Select + aria-label="Art" + value={props.filterFormValues.procedureTypeFilter ?? ""} + onChange={(_, newValue) => { + if (newValue === null) { + return; + } + props.setFilterFormValue("procedureTypeFilter", newValue); + }} + endDecorator={ + isDefined(props.filterFormValues.procedureTypeFilter) ? ( + <ResetButton + onReset={() => { + props.setFilterFormValue( + "procedureTypeFilter", + undefined, + ); + }} + /> + ) : undefined + } + > + <SelectOptions options={PROCEDURE_TYPE_OPTIONS} /> + </Select> + </FormControl> + <FormControl> + <FormLabel>Kennungen</FormLabel> + <LabelAutocomplete + name="labels" + value={props.filterFormValues.labelsFilter ?? []} + onChange={(newValue) => { + props.setFilterFormValue( + "labelsFilter", + isEmpty(newValue) ? undefined : newValue, + ); + }} + /> + </FormControl> + </FilterSettingsContent> + </FilterSettingsSheet> + </OverlayBoundary> ); } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/schoolEntry/shared/sideNavigationItem.tsx index 97b4410868ea899c296e57888694d8704f3ac5cf..85ffda44dcae7c80622a6a24d78cbc6942d7ba85 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/shared/sideNavigationItem.tsx @@ -3,17 +3,19 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiBaseFeature, ApiUserRole } from "@eshg/base-api"; +import { ApiUserRole } from "@eshg/base-api"; import { hasUserRole } from "@eshg/lib-employee-portal/helpers/accessControl"; import { + SideNavigationItem, + SideNavigationItemsProps, SideNavigationSubItem, - UseSideNavigationItemsResult, + SideNavigationSuspenseItem, } from "@eshg/lib-employee-portal/types/sideNavigation"; import { ApiLocationSelectionMode } from "@eshg/school-entry-api"; import { WcOutlined } from "@mui/icons-material"; -import { useQuery } from "@tanstack/react-query"; +import { useSuspenseQuery } from "@tanstack/react-query"; -import { useIsNewFeatureEnabled } from "@/lib/baseModule/api/queries/feature"; +import { NavigationItem } from "@/lib/baseModule/components/layout/sideNavigation/items/NavigationItem"; import { useConfigApi } from "@/lib/businessModules/schoolEntry/api/clients"; import { getLocationSelectionModeQuery } from "@/lib/businessModules/schoolEntry/api/queries/configApi"; @@ -50,25 +52,25 @@ const inboxNavigationItem: SideNavigationSubItem = { accessCheck: hasUserRole(ApiUserRole.SchoolEntryAdmin), }; -export function useSideNavigationItems( - enabled: boolean, -): UseSideNavigationItemsResult { - const isInboxEnabled = useIsNewFeatureEnabled(ApiBaseFeature.Inbox); +const sideNavigationItem: SideNavigationSuspenseItem = { + type: "SideNavigationSuspenseItem", + name: "Einschulung", + decorator: <WcOutlined />, + accessCheck: hasUserRole(ApiUserRole.SchoolEntryAdmin), + component: SchoolEntrySideNavigationItem, +}; - const configApi = useConfigApi(); - const { - data: locationSelectionMode, - isError: isLocationModeError, - isLoading: isLocationModeLoading, - } = useQuery({ - ...getLocationSelectionModeQuery(configApi), - throwOnError: false, - enabled, - }); +export function resolveSideNavigationItems(): SideNavigationItem[] { + return [sideNavigationItem]; +} - if (!enabled) { - return { isLoading: false, items: [] }; - } +function SchoolEntrySideNavigationItem({ + isInboxEnabled, +}: SideNavigationItemsProps) { + const configApi = useConfigApi(); + const { data: locationSelectionMode } = useSuspenseQuery( + getLocationSelectionModeQuery(configApi), + ); const hasLocationMode = locationSelectionMode !== ApiLocationSelectionMode.None; @@ -80,16 +82,14 @@ export function useSideNavigationItems( ...(isInboxEnabled ? [inboxNavigationItem] : []), ]; - const sideNavigationItem = { - name: "Einschulung", - decorator: <WcOutlined />, - error: isLocationModeError - ? "Bei der Verbindung zum Einschulungsmodul ist ein Fehler aufgetreten." - : undefined, - }; - - return { - isLoading: isLocationModeLoading, - items: [{ ...sideNavigationItem, subItems }], - }; + return ( + <NavigationItem + item={{ + type: "SideNavigationParentItem", + name: sideNavigationItem.name, + decorator: sideNavigationItem.decorator, + subItems, + }} + /> + ); } diff --git a/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ChartsSamplePreview.tsx b/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ChartsSamplePreview.tsx index bb7457a87e9178b08cb178d133cd6645c5501b8b..f4880aa3e7e345b5fa5f389cd477607bf7f80369 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ChartsSamplePreview.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ChartsSamplePreview.tsx @@ -3,8 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { OptionalFieldValue } from "@eshg/lib-portal/types/form"; import { Sheet, Stack, Typography } from "@mui/joy"; import { ReactNode } from "react"; +import { isNumber } from "remeda"; export function ChartsSamplePreview({ chart }: { chart: ReactNode }) { return ( @@ -123,50 +125,54 @@ export const barChartGroupedSampleData = [ }, ]; -export function getHistogramSimpleSampleData(bins: number) { - const histogramSimple = []; - for (let i = 0; i < bins; i++) { - histogramSimple.push({ - min: i, - max: i + 1, - attributes: [ - { - label: "A", - value: 5 + i, - }, - ], - }); - } - return histogramSimple; -} +export function getHistogramSampleData( + isGrouped: boolean, + bins: number, + minBinCenter: OptionalFieldValue<number>, + maxBinCenter: OptionalFieldValue<number>, +) { + const DEFAULT_INTERVAL = 1; + const DEFAULT_MIN = 0; + + const hasMinMax = + isNumber(minBinCenter) && + isNumber(maxBinCenter) && + minBinCenter < maxBinCenter; + const interval = hasMinMax + ? (maxBinCenter - minBinCenter) / (bins - 1) + : DEFAULT_INTERVAL; + const min = hasMinMax ? minBinCenter - interval / 2 : DEFAULT_MIN; -export function getHistogramGroupedSampleData(bins: number) { - const histogramGrouped = []; + const histogramData = []; for (let i = 0; i < bins; i++) { - histogramGrouped.push({ - min: i, - max: i + 1, + histogramData.push({ + min: i * interval + min, + max: (i + 1) * interval + min, attributes: [ { label: "A", value: 5 + i, }, - { - label: "B", - value: 8, - }, - { - label: "C", - value: 3, - }, - { - label: "D", - value: 15 - 0.5 * i, - }, + ...(isGrouped + ? [ + { + label: "B", + value: 8, + }, + { + label: "C", + value: 3, + }, + { + label: "D", + value: 15 - 0.5 * i, + }, + ] + : []), ], }); } - return histogramGrouped; + return histogramData; } export const pieChartSampleData = [ @@ -279,3 +285,67 @@ export const chartSampleConfiguration = { unit: "kg", }, }; + +export const choroplethLandArea = [ + { + name: "Afrika", + value: 30_365_000, + }, + { + name: "Asien", + value: 44_614_000, + }, + { + name: "Australien", + value: 8_510_926, + }, + { + name: "Südamerika", + value: 17_814_000, + }, + { + name: "Europa", + value: 10_000_000, + }, + { + name: "Nordamerika", + value: 24_230_000, + }, +]; + +export const choroplethCountryCount = [ + { + name: "Afrika", + value: 54, + }, + { + name: "Asien", + value: 47, + }, + { + name: "Australien", + value: 14, + }, + { + name: "Südamerika", + value: 12, + }, + { + name: "Europa", + value: 43, + }, + { + name: "Nordamerika", + value: 23, + }, +]; + +export const choroplethAverageLandArea = choroplethLandArea.map((sum) => ({ + name: sum.name, + value: parseFloat( + ( + sum.value / + choroplethCountryCount.find((simple) => simple.name === sum.name)!.value + ).toFixed(2), + ), +})); diff --git a/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ConfigureChoroplethChartStep/ConfigureChoroplethChartStep.tsx b/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ConfigureChoroplethChartStep/ConfigureChoroplethChartStep.tsx index c9b7104035ef7054c7f75bb4f92548c07ef10910..71d6646204b1e0e58a274748a33918d19943f080 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ConfigureChoroplethChartStep/ConfigureChoroplethChartStep.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ConfigureChoroplethChartStep/ConfigureChoroplethChartStep.tsx @@ -5,13 +5,22 @@ import { SingleAutocompleteField } from "@eshg/lib-portal/components/formFields/autocomplete/SingleAutocompleteField"; import { buildEnumOptions } from "@eshg/lib-portal/helpers/form"; +import { ApiCalculation } from "@eshg/statistics-api"; import { Stack } from "@mui/joy"; import { isNonNullish } from "remeda"; import { FlatAttribute } from "@/lib/businessModules/statistics/api/models/flatAttribute"; import { GeoShapeInfo } from "@/lib/businessModules/statistics/api/models/geoShapesTableView"; +import { continentsGeoJSON } from "@/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar//worldContinentsGeoJSON"; +import { + ChartsSamplePreview, + choroplethAverageLandArea, + choroplethCountryCount, + choroplethLandArea, +} from "@/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ChartsSamplePreview"; import { ConfigureChartFormModel } from "@/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/createAnalysisFormModel"; import { mapAttributeToAutocompleteSelectionOption } from "@/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/mapAttribute"; +import { ChoroplethMap } from "@/lib/businessModules/statistics/components/shared/charts/ChoroplethMap"; import { choroplethAggregationMethodValueNames, colorSchemeNames, @@ -62,41 +71,70 @@ export function ConfigureChoroplethChartStep({ value: it.id, })); + const hasSecondAttribute = + isNonNullish(values.secondaryAttribute) && values.secondaryAttribute !== ""; + return ( - <Stack gap={3}> - <SingleAutocompleteField - options={primaryAttributeSelectOptions} - name={fieldName("geoReferencedAttribute")} - placeholder="Bitte wählen" - label="Georeferenziertes Attribut" - required="Bitte wählen Sie ein Attribut aus." - /> - <SingleAutocompleteField - options={secondaryAttributeSelectOptions} - name={fieldName("secondaryAttribute")} - placeholder="Optional" - label="Sekundäres Attribut" - /> - {showGroupedConfigurations && ( - <ToggleButtonGroupField - options={aggregationMethods} - name={fieldName("characteristicParameter")} - label="Darstellung" + <Stack gap={4}> + <Stack gap={3}> + <SingleAutocompleteField + options={primaryAttributeSelectOptions} + name={fieldName("geoReferencedAttribute")} + placeholder="Bitte wählen" + label="Georeferenziertes Attribut" + required="Bitte wählen Sie ein Attribut aus." /> - )} - <SingleAutocompleteField - options={colorSchemes} - name={fieldName("colorScheme")} - placeholder="Bitte wählen" - label="Farbschema" - required="Bitte wählen Sie ein Farbschema aus." - /> - <SingleAutocompleteField - options={districtOptions} - name={fieldName("geoShapeId")} - placeholder="Bitte wählen" - label="Karte" - required="Bitte wählen Sie eine Karte aus." + <SingleAutocompleteField + options={secondaryAttributeSelectOptions} + name={fieldName("secondaryAttribute")} + placeholder="Optional" + label="Sekundäres Attribut" + /> + {showGroupedConfigurations && ( + <ToggleButtonGroupField + options={aggregationMethods} + name={fieldName("characteristicParameter")} + label="Darstellung" + /> + )} + <SingleAutocompleteField + options={colorSchemes} + name={fieldName("colorScheme")} + placeholder="Bitte wählen" + label="Farbschema" + required="Bitte wählen Sie ein Farbschema aus." + /> + <SingleAutocompleteField + options={districtOptions} + name={fieldName("geoShapeId")} + placeholder="Bitte wählen" + label="Karte" + required="Bitte wählen Sie eine Karte aus." + /> + </Stack> + <ChartsSamplePreview + chart={ + <ChoroplethMap + diagramData={ + hasSecondAttribute + ? values.characteristicParameter === ApiCalculation.Mean + ? choroplethAverageLandArea + : choroplethLandArea + : choroplethCountryCount + } + colorScheme={values.colorScheme} + characteristicParameter={ + hasSecondAttribute ? values.characteristicParameter : undefined + } + geoJson={continentsGeoJSON} + additionalEchartsSeriesOptions={{ + roam: false, // disable zoom + layoutCenter: ["40%", "50%"], + layoutSize: 350, // avoid overlapping with visualMap + aspectScale: 1, + }} + /> + } /> </Stack> ); diff --git a/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ConfigureHistogramChartStep/ConfigureHistogramChartStep.tsx b/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ConfigureHistogramChartStep/ConfigureHistogramChartStep.tsx index 9edf43137cdabb0f12b4c6ed50a02b8549f9ffc0..df84ada09951dbd369d9b73b1ffe0327826f3a04 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ConfigureHistogramChartStep/ConfigureHistogramChartStep.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ConfigureHistogramChartStep/ConfigureHistogramChartStep.tsx @@ -12,8 +12,7 @@ import { isDefined, isNonNullish } from "remeda"; import { FlatAttribute } from "@/lib/businessModules/statistics/api/models/flatAttribute"; import { ChartsSamplePreview, - getHistogramGroupedSampleData, - getHistogramSimpleSampleData, + getHistogramSampleData, } from "@/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/ChartsSamplePreview"; import { ConfigureChartFormModel } from "@/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/createAnalysisFormModel"; import { mapAttributeToAutocompleteSelectionOption } from "@/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/mapAttribute"; @@ -101,7 +100,7 @@ export function ConfigureHistogramChartStep({ {showBins && ( <> <SliderField - min={1} + min={2} max={50} name={fieldName("bins")} ariaLabel="Anzahl Bins" @@ -122,19 +121,19 @@ export function ConfigureHistogramChartStep({ </Stack> <ChartsSamplePreview chart={ - showGroupedConfigurations ? ( - <Histogram - key={"groupedHistogram"} - diagramData={getHistogramGroupedSampleData(values.bins)} - grouping={values.grouping} - scaling={values.scaling} - /> - ) : ( - <Histogram - key={"simpleHistogram"} - diagramData={getHistogramSimpleSampleData(values.bins)} - /> - ) + <Histogram + key={ + showGroupedConfigurations ? "groupedHistogram" : "simpleHistogram" + } + diagramData={getHistogramSampleData( + showGroupedConfigurations, + values.bins, + values.minBin, + values.maxBin, + )} + grouping={showGroupedConfigurations ? values.grouping : undefined} + scaling={showGroupedConfigurations ? values.scaling : undefined} + /> } /> </Stack> diff --git a/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/worldContinentsGeoJSON.ts b/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/worldContinentsGeoJSON.ts new file mode 100644 index 0000000000000000000000000000000000000000..351613ab4f37b778e0a573bd8ecb85e8701f9252 --- /dev/null +++ b/employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/worldContinentsGeoJSON.ts @@ -0,0 +1,2377 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const continentsGeoJSON = JSON.stringify({ + type: "FeatureCollection", + crs: { + type: "name", + properties: { name: "urn:ogc:def:crs:OGC:1.3:CRS84" }, + }, + features: [ + { + type: "Feature", + properties: { + name: "Afrika", + FID: 1, + OBJECTID: 1, + CONTINENT: "Africa", + SQMI: 11583462.724, + SQKM: 30001150.784000002, + Shape_Leng: 426.20861174200002, + Shape_Area: 2559.0730977200001, + }, + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [49.356945196864601, -12.090832877268999], + [50.433606194814402, -15.5799978967105], + [49.633605167006699, -15.5574978510394], + [47.133045229097497, -24.928054914109499], + [45.214722126956701, -25.588340009152098], + [44.017083276118903, -24.9808399495044], + [43.238880225770203, -22.282495929848999], + [44.482770250821297, -19.965832936466199], + [43.937217149678901, -17.4794479703327], + [44.460270205150401, -16.1838888764788], + [46.477773182720199, -15.9613909207888], + [47.997081147497298, -14.7672258659129], + [47.905272150103499, -13.596388988743801], + [49.356945196864601, -12.090832877268999], + ], + ], + [ + [ + [1.18250121747968, 36.512218045809099], + [11.1022201262954, 36.9044471943429], + [10.26639015035, 33.748597105145599], + [15.16583720509, 32.398606099556503], + [15.7543111826666, 31.3897151359023], + [18.957501255302901, 30.276379120505801], + [20.0576342644185, 30.8508311798591], + [20.0844451229048, 32.1847210523174], + [21.713886266572999, 32.944447035254498], + [29.0349992659153, 30.824164154832001], + [31.0122181461591, 31.597012174864901], + [34.2166681864841, 31.323331133119499], + [34.903800182196697, 29.4867101294954], + [34.254432180794502, 27.728605095343799], + [32.575617268695197, 30.002707131215899], + [32.340825240307403, 29.594863167574601], + [35.811171210686801, 23.907007089513101], + [35.668890247904898, 22.970692175810701], + [36.8862481965702, 22.0531870910769], + [37.435689159692203, 18.853894100513799], + [38.589030155038202, 18.066799024337701], + [39.718053237547501, 15.0880511736574], + [43.327503261758203, 12.4767280730126], + [42.531525212210099, 11.5119460367522], + [44.892081215160303, 10.421524133044599], + [51.278328196587303, 11.8166681158899], + [51.391242155683599, 10.397368158577899], + [48.000555278735298, 4.52305915842828], + [40.231107185416697, -2.67110891113885], + [39.2030282013217, -4.66962184882136], + [39.392703126999201, -8.90666000188639], + [40.617216203511099, -10.841659906269699], + [40.578327190153502, -15.4988899074924], + [34.626798230403899, -19.618612931729398], + [35.4561122824406, -24.1694448977644], + [32.811111197656899, -25.6120819176009], + [32.3944382401002, -28.531393860336301], + [30.0238921820035, -31.281109894145199], + [25.701948199381, -34.031950947001398], + [19.999998118728499, -34.821997961846101], + [18.420273228055699, -34.323055953840402], + [18.217917153390701, -31.734583932972601], + [15.294168166378, -27.322495934207598], + [14.5113931231107, -22.552783977179502], + [11.7611101373694, -17.961811914915501], + [11.731248098480799, -15.850700010058899], + [13.8494432144548, -10.9561129504315], + [12.246246144978301, -6.1036099180231], + [13.1788802297097, -5.85632600402192], + [12.265002162128299, -5.86471394219103], + [8.71000215456365, -0.641104995500206], + [9.921105191785781, 0.185284107596261], + [9.305136224784009, 0.580969114819071], + [9.722502198097059, 3.86528511394606], + [8.585217241242731, 4.82041002425512], + [5.93583309163841, 4.33833408186615], + [5.63604309198087, 5.53673803282782], + [3.80721625250317, 6.61277809938849], + [0.237636091962202, 6.10486309637048], + [0.663741205188614, 5.75992917191223], + [-2.05889390521315, 4.73083312768296], + [-3.81173376305128, 5.37263210285852], + [-7.71339587587562, 4.35700912983105], + [-12.504167867605201, 7.38860504781836], + [-13.6886399219838, 9.952192036279209], + [-15.5011408256869, 11.3328100132429], + [-14.991110900853601, 11.958058082361999], + [-16.793198798574601, 12.4229170926663], + [-15.6344487934565, 12.531034088562], + [-15.509312846029699, 12.637081089643599], + [-15.5298599082553, 12.782080135994899], + [-15.3918087891036, 12.832921072761801], + [-16.752914868886801, 12.564721125156099], + [-16.709939845357798, 13.471732067084901], + [-16.554725773578099, 13.295071051952201], + [-16.194725881034, 13.2526361582654], + [-16.155908784405899, 13.424860129120701], + [-15.3283317979882, 13.4386121500612], + [-15.305759835587899, 13.451527154148], + [-15.2971739168652, 13.4910550361673], + [-15.5472298938923, 13.528684081835999], + [-16.5133349297537, 13.368610014943499], + [-16.3674267821815, 14.1663881620228], + [-17.532782900486801, 14.7501370931283], + [-16.039448777342201, 17.734582123412899], + [-16.1968049283035, 20.226106034397599], + [-17.101529789953801, 20.837431091751], + [-15.9330509097927, 23.7880541367999], + [-12.9627087669163, 27.920485132114798], + [-10.228202869928101, 29.317915027600399], + [-9.279332871062371, 32.543956180013502], + [-6.79750179293683, 34.062076093831401], + [-5.91874188810169, 35.790643133472003], + [-1.97972079452967, 35.073325054493502], + [1.18250121747968, 36.512218045809099], + ], + ], + ], + }, + }, + { + type: "Feature", + properties: { + name: "Asien", + FID: 2, + OBJECTID: 2, + CONTINENT: "Asia", + SQMI: 17317280.092, + SQKM: 44851729.022, + Shape_Leng: 2331.6237458300002, + Shape_Area: 5432.08522748, + }, + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [-179.999988540843987, 68.980091506370499], + [-175.463631007934993, 67.707469213731798], + [-174.467375930991011, 66.303442089876199], + [-173.761965016058014, 66.449494070907306], + [-174.651156031786002, 67.060117227687002], + [-173.676392992834991, 67.132063126156396], + [-169.694963995303993, 66.068065172283198], + [-172.802645969026997, 65.6747021198846], + [-172.12897799206101, 65.0831771295436], + [-173.191410038778002, 64.254430197077298], + [-176.07806105750899, 65.470267165588197], + [-178.557389969892, 65.514151122700099], + [-178.526150951945993, 66.402910135137205], + [-179.689194000243987, 66.183580203580206], + [-179.32082393916599, 65.530117139551294], + [-179.999988540843987, 65.068917742530303], + [-179.999988540843987, 68.980091506370499], + ], + ], + [ + [ + [179.999988540843987, 65.068909876569407], + [179.459973255275003, 64.8128981346686], + [178.52221833060301, 64.588042181970707], + [176.916069322457986, 65.082493166242401], + [174.450186323910998, 64.686367103273398], + [178.286103302614009, 64.353862200154495], + [178.26109237402099, 63.574507141030203], + [179.59384834261499, 62.748604188319398], + [179.101350341787992, 62.289298188884302], + [176.974407368722012, 62.864020145520598], + [170.242461362712987, 59.910130169881], + [169.264980407829995, 60.619438166886603], + [163.639971337038986, 60.045895208754402], + [161.938584294901005, 58.067623058554503], + [163.209411242262007, 57.839572091995301], + [162.781641315423002, 56.8544320892454], + [163.210797273775, 56.741797079887498], + [163.349964359518992, 56.1959651966447], + [162.393867316077007, 56.3896451633087], + [161.711343355467989, 55.490275164335998], + [162.113013376850006, 54.763534149781201], + [160.005825376156992, 54.139168192154798], + [160.050384244115008, 53.095123191208501], + [158.43942033549601, 53.0272182082714], + [158.27768128735201, 51.941305097932599], + [156.668301390799996, 50.881663051513897], + [155.544129250948004, 55.303597104328297], + [156.760182304923006, 57.738259189840299], + [163.643310351997002, 60.875398188888902], + [164.120517246817997, 62.2770762005539], + [165.629988377638995, 62.442766142589697], + [163.257354387454996, 62.542639201413103], + [163.283310291815013, 61.661800081670698], + [160.138926310918009, 60.589225093892097], + [160.35495326969999, 61.947487193919898], + [157.486635317283998, 61.803316111966602], + [154.231488341289008, 59.878747150838102], + [155.186280322403007, 59.361616077614599], + [154.741635277491014, 59.126932175778101], + [151.308720372226986, 58.839022182800399], + [152.284275333392003, 59.220064172772901], + [149.592042224083002, 59.771233149058403], + [142.587468278647009, 59.237776140060497], + [135.217476210799987, 54.930277194135002], + [136.812186268011999, 54.650197072211398], + [136.760796316584987, 53.768602072438298], + [137.19244522058699, 54.217495065891498], + [137.741139356133999, 54.307414111752102], + [137.345247315903009, 53.525269052329101], + [139.801842327515004, 54.292357196121699], + [141.414831303947011, 53.293609173528402], + [140.713335240721989, 53.115751054980997], + [141.422679279911989, 51.923044115985498], + [140.176089276201992, 48.450124063133799], + [135.422334283569995, 43.756111085551197], + [133.154847245702996, 42.682636049007201], + [131.810517209896005, 43.3255511584128], + [129.760524299145999, 41.730544046551799], + [129.702042251785002, 40.830688064831698], + [127.517625239037002, 39.7395731453677], + [129.429450354542013, 37.059859079038198], + [129.237489348585996, 35.189902072925896], + [126.478314337016997, 34.345270092793299], + [126.688491230281002, 37.833913087303401], + [124.670052329399994, 38.119510177912403], + [125.652069235018004, 38.629432143832702], + [124.373601344701001, 40.093615181117897], + [121.187340201954996, 38.719081124772202], + [122.298660201993997, 40.505617071659898], + [121.177467325815002, 40.9219391627487], + [118.971522232891004, 39.156940180854299], + [117.799425220394994, 39.153322048519399], + [117.672210225699004, 38.3866571908382], + [118.837764324557, 38.1529090446748], + [119.158866318926002, 37.171099172065901], + [120.737061344626994, 37.834993179349603], + [122.597217223691004, 37.209988185423498], + [120.088854236817994, 36.1999810875393], + [119.200392279029998, 35.029576046023102], + [121.896522302774997, 31.741525070830701], + [119.631654274381006, 32.262769143953598], + [121.882761229379, 30.9798551569037], + [120.149982283380993, 30.196936112539099], + [122.119560220669001, 29.882107134524201], + [121.146381361422002, 28.842148143749299], + [121.641660310090998, 28.3472200615475], + [119.097495197171, 26.140411129678998], + [119.645397242865997, 25.353325105958302], + [116.481717282613999, 22.939021154573599], + [114.296103334088002, 22.2605650992245], + [113.683590225775006, 23.152699168803601], + [113.552685349653004, 22.1870080313223], + [110.394351199623003, 21.3732191080286], + [110.278872210510002, 20.246113134419101], + [109.573317294481001, 21.7233281871599], + [106.646652243345002, 21.021661133110499], + [105.638562254361005, 18.890659139600899], + [108.829161199650002, 15.4219421082891], + [109.461861283627002, 12.8609741293019], + [109.021680272720999, 11.362258153751499], + [106.763031281224002, 10.680553105084201], + [106.607115308869993, 9.81086510171928], + [106.114806236142002, 10.234063035640199], + [106.543026271184004, 9.58360605737442], + [105.823719334214005, 10.0042300764609], + [106.194546298757999, 9.36847009490219], + [105.121296232495993, 8.6250611629224], + [104.981769227829005, 10.104445116934899], + [103.129704232284993, 10.8830530132081], + [102.060675292474002, 12.5662421053106], + [100.932210277078994, 12.6113950825668], + [100.976346194201, 13.4628130515291], + [99.956790264555096, 13.2910750634212], + [99.237213262664397, 9.257293033786659], + [99.845397285613203, 9.300421110867999], + [100.424826183809998, 7.15777313341595], + [103.181931200565003, 5.28277608891004], + [104.211639291365003, 1.34062301338855], + [101.285739340353999, 2.84354201047904], + [100.090179201509002, 6.53343416551774], + [98.274159239567396, 8.274448163770611], + [98.883729294029095, 11.6972201279739], + [97.737291298881701, 16.560766150208899], + [96.878034238771804, 17.450002092938], + [95.428593292832403, 15.729715032553401], + [94.2509612067402, 15.958882133342399], + [93.985713197803094, 19.457146043982402], + [90.593829217439804, 23.597965145104499], + [90.269991202228596, 21.8469431544322], + [90.000000209547693, 22.483756070670701], + [88.257492220868698, 21.548746051658501], + [88.199253248700302, 22.1519081411247], + [87.906096184320205, 22.4204051433797], + [86.421231303805897, 19.984924145925199], + [80.279433309080204, 15.699160145546299], + [79.858107221781694, 10.2858310109009], + [78.909012252634, 9.47388711213938], + [79.009155208740395, 9.33166012881394], + [79.331661171895206, 9.264160159439481], + [79.446231227425798, 9.159931025523569], + [77.486103215442398, 8.078059165992951], + [73.447479295605902, 16.058611023430799], + [72.914769281820995, 22.271113053848101], + [70.825131310045705, 20.695960156094099], + [68.945958233727893, 22.289302119065599], + [70.169985159854505, 22.550824036754602], + [70.509717275187398, 23.098195172700699], + [68.752494184890494, 23.089168030593701], + [68.741361173423698, 23.844160082242201], + [67.519215314013096, 23.876542056508299], + [66.438036302601105, 25.593328163564902], + [61.7608261910854, 25.032079110492301], + [57.319092238896403, 25.7714561793954], + [56.808882270782398, 27.123607033801001], + [53.747775180374497, 26.709166177066098], + [48.978882135288899, 30.5115310678169], + [47.707218171074302, 29.3758301230292], + [50.155002293682301, 26.663059171587999], + [50.775408137323303, 24.720823170172899], + [51.036948159923099, 26.0424281900269], + [51.567219207515301, 25.907707033378198], + [51.282351174846603, 24.300001170532799], + [54.123741257407701, 24.141664169259201], + [56.404206163636097, 26.3687051714309], + [56.6197831818548, 24.477562066792899], + [59.809167254094099, 22.223323129845401], + [57.828195292874, 20.216242043074999], + [57.805695247203097, 18.9709750389666], + [55.031940250296003, 17.0147171193282], + [52.596378283656499, 16.4772190271601], + [52.189164136221699, 15.6052721008831], + [48.698055185790302, 14.040001131724599], + [43.478883283376803, 12.674998137506201], + [42.307083157892301, 17.447635043474701], + [39.174858133412997, 21.104029090017299], + [38.446875256080403, 23.789089134207], + [34.572150233204098, 28.095904116831498], + [35.0036821258375, 29.528191162597999], + [34.2166681864841, 31.323331133119499], + [35.983611267006701, 34.527502056068002], + [36.1933291672511, 36.791938081171203], + [32.7716641171844, 36.028882135731202], + [30.694374197563398, 36.881587062110803], + [29.677221191926201, 36.1183331361173], + [27.374715233046199, 36.684028118284601], + [28.324791220144199, 37.038178112948003], + [27.254997180208999, 36.964999068879997], + [27.268326250313901, 37.953604150412303], + [26.275824254346499, 38.264437139896302], + [27.156456173441899, 38.452798118428099], + [26.197353211874901, 40.002832128675699], + [29.933955198869899, 40.722211150012598], + [29.160000264700599, 41.224573142161603], + [34.978878230253301, 42.091948075519802], + [38.3558311911723, 40.910275073895299], + [41.384952268421003, 41.373712178140998], + [41.484438250954902, 42.668740194608198], + [40.169988191309301, 43.581241115494002], + [45.1651232206058, 42.703327112329902], + [47.915469238259, 41.224987040541698], + [49.760631233604798, 42.710752137459501], + [48.686157241837698, 44.754346135668598], + [50.038497191979403, 45.858484185770799], + [46.499166146616602, 48.417499181312799], + [47.319642152282299, 50.296096085604603], + [48.790557198940803, 49.939435208018999], + [48.697488233858003, 50.591935191370503], + [50.7733022679637, 51.769180201173], + [53.423748237065197, 51.492637074761397], + [54.523935225637402, 50.528836056451297], + [54.647217263714801, 51.036949207661003], + [61.422354211079302, 50.800618094207799], + [60.144156218044998, 52.423732074427598], + [58.789296164887702, 52.450678049193201], + [58.921524208248897, 53.932906150599301], + [59.9363101644228, 54.861589173800802], + [57.153042210299198, 54.853192183176198], + [59.641659277254597, 55.558675182476101], + [57.466386250343596, 56.121940083267901], + [57.221694188449497, 56.850967178101101], + [59.449131151728402, 58.488049097087298], + [58.310964251019499, 59.460409212030001], + [59.473602285755398, 60.809572085583099], + [59.483115242971998, 64.487971142593693], + [66.108870246802596, 67.481227229520997], + [64.522215198564993, 68.903047122211007], + [64.937340186238998, 69.262219217994897], + [68.528592260414896, 68.277205111431201], + [69.217479259018901, 68.955823105150202], + [66.793725214692003, 69.580270199599696], + [67.337001288011194, 70.758046119150904], + [66.619566197664, 71.045119095275197], + [69.379290223892397, 72.961651152340593], + [72.823869218010003, 72.711379089849004], + [71.806230229625299, 71.4633311378972], + [72.839430221298798, 70.868314079045405], + [72.5538602880561, 68.976658169569802], + [73.513314283729201, 68.586931186778799], + [73.638882234446797, 68.441509189592097], + [71.550954170924101, 66.644290171801103], + [68.971644240804807, 66.806371201595596], + [72.000000217929497, 66.219436141446295], + [74.739969271784602, 67.691656082794907], + [74.640546153524895, 68.7691452127807], + [76.582494320383901, 68.970547091585402], + [77.320827171786505, 68.518387167541704], + [77.084298245417898, 67.784842226884393], + [77.785533296176595, 67.561921152720998], + [79.041177264083302, 67.573432188021997], + [77.466384285252801, 67.759156219807494], + [78.171021215243897, 68.268043188320704], + [77.644152214521995, 68.904712103462401], + [73.748457178584999, 69.171238184999098], + [74.320542188474803, 70.655257163842606], + [73.016379238625504, 71.418529194746995], + [74.974428203339201, 72.122212096515796], + [74.829987224102993, 72.834157207905903], + [75.712419240729403, 72.558172148609302], + [75.267342192526698, 71.361235198345398], + [76.915818303083498, 71.069716125488398], + [78.436098233355494, 70.885819180962102], + [79.031097187296297, 70.934707134283499], + [79.108515295088196, 71.007076151226499], + [76.097493261416702, 71.928595161764207], + [78.538032234537596, 72.403588228311904], + [83.261646238542994, 71.721082204975104], + [82.080963307324296, 70.564564133040705], + [83.109699264992102, 70.890679176074897], + [82.643049209612101, 70.233598147349099], + [83.106936254421498, 70.068601221069798], + [83.626362292740694, 71.625403115236097], + [80.726373319087799, 72.523036216227894], + [80.518599182195103, 73.573462192441397], + [87.047478328148102, 73.870120210149807], + [85.785957304994497, 73.470547173309001], + [85.839570304787301, 73.323748197064802], + [86.783598245813295, 72.994078221245999], + [85.841802237970995, 73.451098140402607], + [87.183306231295305, 73.619425196822803], + [87.666102179169101, 73.890829210745196], + [85.949640284128293, 74.282626188350207], + [87.132753296722399, 74.369134134885002], + [86.035608268459001, 74.811016169226406], + [94.155192262998995, 75.946780145926098], + [92.865267316837304, 75.948319230991601], + [93.181095250075501, 76.099717189882995], + [99.276930240419901, 76.214152129133794], + [99.765621180813497, 76.034287215322294], + [99.223452189268599, 75.758599210675001], + [99.093042180711194, 75.561931095520805], + [100.186254252354999, 75.168532168576306], + [99.174213201841198, 75.5686631048941], + [99.877203255491494, 76.091662180908799], + [98.836110193213898, 76.506103205281804], + [102.240819323661995, 76.379005221954202], + [100.849716256348998, 76.878586098621795], + [104.302197205888007, 77.730814094709103], + [106.282485203806999, 77.366089155693899], + [104.123322194843993, 77.089573186648593], + [107.498169286402998, 76.918393098017503], + [106.392906217253994, 76.512079166986297], + [111.103866229638001, 76.755268185998801], + [113.891373247485006, 75.845053177753101], + [112.349700353317004, 75.847132225022705], + [113.717484295378995, 75.408598090456906], + [107.138880346888996, 73.611505136490194], + [105.211926231386002, 72.764713139903407], + [110.914317200146996, 73.696681198606498], + [109.526931339263996, 73.779985078824694], + [110.200410220494007, 74.024443117981505], + [112.887774282235, 73.964998157580993], + [113.424426305094002, 73.640827213174603], + [113.135265229244993, 73.448173191462303], + [113.519709266088, 73.112761109038004], + [113.089572289784996, 72.835129173400802], + [113.185395212982996, 72.719425214005597], + [113.533866300591001, 72.634987094646604], + [114.044706252549005, 72.597214215519202], + [113.155686227646996, 72.819982068492195], + [114.027894334026996, 73.344439092749397], + [113.470542195389996, 73.500967111674498], + [118.634427231941999, 73.571653210093103], + [126.364986311902996, 72.352198109247496], + [127.215261324544997, 71.394571200833596], + [126.720117324517005, 72.387478210363795], + [127.658601307129004, 72.347491167687096], + [131.130243287381006, 70.731091091929798], + [132.718446305501004, 71.9410872150156], + [137.984958358991008, 71.106940157594494], + [139.932468253899003, 71.484985114259104], + [139.33663228310499, 71.945524091654804], + [140.194269288964989, 72.206371098498195], + [139.087467302388006, 72.230266228137495], + [141.02413235566101, 72.585820191586805], + [140.593014361407, 72.8874910903222], + [144.339417307812994, 72.637777094945505], + [146.91732326914601, 72.299845077143601], + [144.920673293269004, 71.693308098173304], + [148.266936250901011, 72.319447163602504], + [152.224119252323987, 70.876648205396094], + [159.151644396623993, 70.844986068976496], + [160.035246359299009, 70.409017132065898], + [159.729669326035008, 69.834718126264804], + [160.999209315978987, 69.579784216852104], + [160.915257347005991, 68.519440102221395], + [162.323298396664995, 69.662197100760196], + [167.777208348009992, 69.776092077806794], + [170.611938337927995, 68.756338167607197], + [170.420967402378011, 70.126093198025004], + [176.084397273413003, 69.892912171432101], + [179.999988540843987, 68.980095942391401], + [179.999988540843987, 65.068909876569407], + ], + ], + [ + [ + [106.163307280812006, -6.01416797009251], + [112.560255270414999, -6.91221496946429], + [112.847913303382001, -7.60068790205044], + [114.448320204921998, -7.80055086342799], + [114.621651257552003, -8.74389484115278], + [105.243318302884006, -6.81028096828238], + [106.163307280812006, -6.01416797009251], + ], + ], + [ + [ + [133.574967344768993, -0.753892890772364], + [135.004158283132, -3.34139294614533], + [137.860110246120001, -1.47166091031461], + [144.513738263210001, -3.82221797330096], + [145.766394250264995, -5.48527390155801], + [147.826359278887992, -6.33721389545149], + [146.945664327879996, -6.95666587850856], + [148.588866377306005, -9.070279949108119], + [150.878016276353009, -10.2300019197213], + [147.952458306992014, -10.1458338652834], + [144.522900353958988, -7.50292088022474], + [142.139007288484009, -8.223892966083779], + [143.392761304857999, -8.77028291644149], + [142.638885219802006, -9.334717930919959], + [141.119964331314009, -9.230974947389621], + [140.147901271020999, -7.88583488445762], + [138.910266325050998, -8.29833197784064], + [138.66219937365301, -7.20097086411278], + [139.050540324931006, -7.25166796742081], + [139.175811220999009, -7.2388879119755], + [139.222476293591995, -7.16249591677332], + [138.562902319216988, -6.90652684231554], + [139.186647345454986, -6.96757391969346], + [138.065247268629008, -5.40895399072332], + [134.212059210745991, -3.95999885989353], + [133.828857371957014, -2.96166490331838], + [132.912198356532002, -4.09791486276557], + [131.956911340214987, -2.78701990354303], + [133.678593316930005, -2.71805293347046], + [133.934283274011989, -2.10409998182614], + [132.299001284918006, -2.2684759767164], + [130.963590264667005, -1.40305385916559], + [132.269715250417988, -0.384163955547426], + [133.574967344768993, -0.753892890772364], + ], + ], + [ + [ + [125.141661286350001, 1.42138902095598], + [124.246089295355006, 0.375004127912691], + [120.242196294337006, 0.344998087927308], + [120.664575316316004, -1.39389193605501], + [121.622211277185002, -0.805003892460323], + [123.446781283304006, -0.837637994374805], + [121.298715243624002, -1.8004219525508], + [122.893740292757997, -4.39805589652917], + [121.552416342713997, -4.74568996728391], + [120.771927212086993, -2.61250096759176], + [120.201939354377998, -2.96333893702525], + [120.463461272066994, -5.61978785755974], + [119.465001251667005, -5.5636099953881], + [119.506104260935999, -3.52721588438868], + [118.759167218261993, -2.7741679313682], + [120.032766228648995, 0.712576059153357], + [125.141661286350001, 1.42138902095598], + ], + ], + [ + [ + [95.738571328190204, 5.58527511968135], + [97.5148292177375, 5.24944913887714], + [102.932451227925, 0.694999040507294], + [102.539961234564004, 0.166663039087593], + [103.739688185125999, 0.281107030793923], + [103.360806211607994, -0.702214937152087], + [104.377761236691995, -1.03930993810065], + [104.531787257511994, -2.7713869835248], + [105.606369208193001, -2.3933329744047], + [106.055253316828001, -3.03138792105938], + [105.728877261329004, -5.89826586250582], + [104.560776237363996, -5.92974795564464], + [101.626920183836006, -3.24610998515163], + [98.770716260837006, 1.74861114576469], + [95.531085193491293, 4.6827731387595], + [95.738571328190204, 5.58527511968135], + ], + ], + [ + [ + [113.010534295381007, 3.16055816231476], + [117.178308256085998, 6.99027404139376], + [117.503055204879999, 5.89610810916531], + [119.275821361192996, 5.34500216479195], + [117.031383216018, 3.60069407858186], + [119.009016329917998, 0.983890051435373], + [117.893052252592, 1.11778307604818], + [115.975809242498002, -3.60110588148593], + [114.637077312198997, -4.18507089805581], + [113.064696309832996, -2.99389399167025], + [111.896379200403999, -3.5738898417992], + [111.751686261157005, -2.74982286116512], + [110.261934228163994, -3.00249784766573], + [109.066932324072994, 1.53221504796556], + [109.648566333362993, 2.07341207401554], + [109.928034241077, 1.69048013250976], + [110.330541279312996, 1.80153112980103], + [110.687472221820002, 1.44471703102487], + [111.378312203869001, 1.34596915888675], + [111.448458340084002, 2.69471813406015], + [113.010534295381007, 3.16055816231476], + ], + ], + [ + [ + [125.440263235047993, 9.80916407828424], + [126.581868224811004, 7.28431304962851], + [126.191655259273006, 6.27221801965754], + [125.651511335541002, 7.23457902699817], + [125.405550253502, 5.56333314112549], + [124.183449321092994, 6.21388902584891], + [123.676659337122004, 7.81249616513378], + [121.921101228077006, 6.99416207101149], + [123.378588298173, 8.72527603575816], + [123.667200359361999, 7.95444403108269], + [125.514711299260995, 9.00666105237179], + [125.440263235047993, 9.80916407828424], + ], + ], + [ + [ + [120.571659237147003, 18.493192139758001], + [122.238873259943006, 18.5149991696724], + [122.533047216819, 17.1013781194156], + [121.378860319802001, 15.3322210429821], + [121.735539302299003, 14.1684760941097], + [123.924987300986004, 13.7891621173007], + [123.53317221847, 13.571245039347], + [124.190946262951996, 13.0650040700357], + [124.083117269249996, 12.5416630123703], + [122.561001199263998, 13.9365640876609], + [122.607486228575993, 13.163887058453399], + [121.749921307083994, 13.964788135026501], + [120.662415299860996, 13.7688941724517], + [120.956643236193997, 14.6369440166552], + [120.087342309118995, 14.7834730956165], + [119.785662357928004, 16.318468127506499], + [120.421917207052005, 16.155829030597101], + [120.571659237147003, 18.493192139758001], + ], + ], + [ + [ + [141.270813275546004, 41.3424910974677], + [142.069698336761007, 39.546667163645303], + [140.953581373522013, 38.148049049562097], + [140.332185291836993, 35.129854118409199], + [139.772061257811998, 34.951384120928097], + [139.96856726695799, 35.660818181757797], + [138.850389361358992, 34.593184158276998], + [136.849833250952997, 35.079031118915204], + [135.772218224781, 33.454990100201798], + [135.064683335577996, 33.875542034920798], + [135.333594236213003, 34.718320105703803], + [130.893309347448991, 33.921667145309897], + [133.091064315121002, 35.582500192838403], + [136.072755219652009, 35.648605078244799], + [136.786923211769988, 37.362205056257302], + [137.356353337640996, 37.5047200417767], + [137.302461220471997, 36.746380090352403], + [138.580533317320004, 37.398610176420902], + [140.022765323594001, 39.378601119690799], + [139.852323345358997, 40.598191169177802], + [141.270813275546004, 41.3424910974677], + ], + ], + ], + }, + }, + { + type: "Feature", + properties: { + name: "Australien", + FID: 3, + OBJECTID: 3, + CONTINENT: "Australia", + SQMI: 2973612.2055000002, + SQKM: 7701651.076, + Shape_Leng: 252.16531086000001, + Shape_Area: 695.53992064399995, + }, + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [142.51629636511899, -10.8582649594209], + [143.782200285454991, -14.413336883715001], + [145.315800233375995, -14.945551862297], + [146.27762127851301, -18.887020974517299], + [148.768893229487986, -20.232466976916101], + [149.669478269147987, -22.495174988855702], + [150.634557360058011, -22.3430568568414], + [153.181917319202, -25.949446983470601], + [153.624195314649995, -28.661038936407898], + [153.05246133886601, -31.034995926192501], + [150.162471305177007, -35.940553891278697], + [149.971635318268994, -37.522213995761803], + [146.394135265032986, -39.147226979970299], + [144.917685312416012, -37.868543004188602], + [143.542953275516993, -38.859236017807703], + [141.571359322872013, -38.417219034824697], + [139.814415349950991, -37.299724924889297], + [139.356612225759989, -35.374445010731499], + [138.093156324071998, -35.619163894715797], + [138.092256275305999, -34.134928998045801], + [137.748177304975002, -35.132786024329], + [136.831500352277004, -35.251802008954101], + [137.948580228556011, -33.5592979401885], + [137.816280268464993, -32.566876913405999], + [135.956412223956988, -35.008234966107402], + [134.184141270484986, -32.486659920497999], + [131.148594290967992, -31.474024928323001], + [125.972271348259, -32.266735879642503], + [123.540822213881, -33.905824929168801], + [120.004992289485003, -33.928882874316599], + [117.934146209405995, -35.125342894288401], + [115.008948326606003, -34.262431994936001], + [115.739982216702998, -31.868054006749901], + [113.224428227437002, -26.239165958796701], + [113.855814196631002, -26.507501022681801], + [113.391108239879003, -25.710415891358899], + [113.469435281252998, -25.5408379197054], + [114.221448236867005, -26.292500008851], + [113.389713323547994, -24.429446002936999], + [114.030270268308001, -21.841666997825499], + [114.153876350762999, -22.527781933403698], + [116.707491221349997, -20.649166924201001], + [121.027482220527006, -19.592224856440598], + [122.920254264232995, -16.414586009877802], + [123.575274350599997, -17.5975008742802], + [123.570900338235006, -16.171666888148302], + [123.891372348759006, -16.378892010381499], + [124.893045320292998, -16.406701991728902], + [124.45726531148, -15.478261876082], + [125.181810267396997, -15.520679000134001], + [126.017595316341001, -13.9265270100374], + [127.425258342166003, -13.95403088428], + [128.020842352950012, -15.498223881464201], + [128.535966295634012, -14.758468956365], + [129.731967322586002, -15.182188915217001], + [129.370239249240996, -14.3333359762716], + [130.579272291649005, -12.4046538476052], + [132.748992307690003, -12.135418902592701], + [131.770953285692002, -11.317633990768], + [131.984325360456012, -11.127427987703999], + [135.231354295565012, -12.294448919622701], + [135.912753237126992, -11.765554851088099], + [136.039698334539992, -12.4716680018704], + [136.562193322897002, -11.9344488594407], + [136.97836236043301, -12.358150881019901], + [135.451359305604996, -14.932780859307201], + [140.494968222204989, -17.6408359843706], + [142.51629636511899, -10.8582649594209], + ], + ], + ], + }, + }, + { + type: "Feature", + properties: { + name: "Südamerika", + FID: 5, + OBJECTID: 5, + CONTINENT: "South America", + SQMI: 6856255.3355, + SQKM: 17757690.859000001, + Shape_Leng: 622.55258190300003, + Shape_Area: 1539.31293336, + }, + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [-69.151670925348, -52.684442037153602], + [-67.3591769540283, -54.028888916691002], + [-65.140073949462007, -54.653263926772802], + [-66.4580698894367, -55.0516759023823], + [-71.967779846756798, -54.643274039264298], + [-68.994872841501603, -54.467648021538501], + [-70.178345940657195, -53.836946015646099], + [-69.361118928309295, -53.345483012226602], + [-70.487225946695801, -53.230831987511301], + [-69.151670925348, -52.684442037153602], + ], + ], + [ + [ + [-71.528624948440296, 12.446110154093599], + [-71.689175945624797, 9.06339714929651], + [-71.055971941627405, 9.33874317229316], + [-71.492858864576505, 10.961038072932199], + [-69.801389960856, 11.427220082837501], + [-70.014311927417396, 12.1975030728536], + [-68.114240922406196, 10.484929039792499], + [-65.081393921547502, 10.0605521073677], + [-63.697499961537503, 10.485559023636901], + [-64.235690901824199, 10.514377012662999], + [-64.264589860035201, 10.6577741096749], + [-61.879589880424099, 10.7283250918139], + [-63.015974956150998, 10.0956880397489], + [-60.959726966320503, 9.532504108141881], + [-61.598888827111601, 8.55499616353074], + [-59.129306894682799, 8.03999811703347], + [-58.637087843594898, 6.42194211248011], + [-57.986387957774497, 6.79055508111321], + [-57.2485048792974, 5.48611301388733], + [-55.047365802028899, 6.00181312859654], + [-52.937783929782199, 5.4583390747237], + [-51.259310831695302, 4.15250209116732], + [-49.888889942887303, 1.58055415426571], + [-52.7097239107676, -1.56555783979536], + [-50.826464824278403, -0.928744923556798], + [-50.667227941877499, -1.77166699543671], + [-51.336602875663097, -1.64743092913739], + [-51.521948883614201, -2.04639191940646], + [-51.478190822688497, -2.23812795508042], + [-51.388415945562897, -2.31923594429839], + [-51.308225942383302, -1.76690590678174], + [-50.991119936545303, -2.02958000088433], + [-51.029162880594001, -2.34500292056004], + [-50.996105827844097, -2.41778583588286], + [-50.843816872642599, -2.50749784873449], + [-50.677955939781597, -1.81044788224302], + [-49.280912952947602, -1.71771184635532], + [-49.4900099218032, -2.56499887814511], + [-48.056390823980202, -0.708055950215291], + [-46.826675938342902, -0.713194895066523], + [-44.538893798260098, -1.83221886997363], + [-44.7863937977258, -3.29749993657811], + [-44.063333947417902, -2.40583391247348], + [-44.340353836483601, -2.82736686514285], + [-39.998753787945503, -2.84652789585551], + [-37.174445856465603, -4.91860695389567], + [-35.479727791789102, -5.16610695336141], + [-34.792919840454303, -7.17278285893122], + [-35.327510920955099, -9.22888684766464], + [-38.317913808033403, -12.9372289127486], + [-38.917223910065601, -12.743611977996901], + [-39.132224923896402, -17.686321890821699], + [-40.987223920836399, -22.008607855094699], + [-42.034445830732999, -22.9191738974462], + [-44.675207851792798, -23.055703868805299], + [-48.718610797557098, -25.424728946747699], + [-48.761810959005999, -28.4906958646303], + [-50.749451897687301, -31.0811119841261], + [-52.069652948755603, -32.171947953851699], + [-50.5681018782671, -30.457547001169502], + [-51.275033940992003, -30.010553011704999], + [-54.140759873662702, -34.664651030978497], + [-57.8368708979609, -34.492777926590897], + [-58.200002940092197, -32.4483380267109], + [-58.469723867852302, -34.539731001378101], + [-57.1883399134122, -35.320552893562201], + [-56.663063809574297, -36.900556901611303], + [-58.301117861693598, -38.4849979539378], + [-62.385146886434399, -38.8026439219799], + [-62.390006881547102, -40.901947904524199], + [-65.130146926227496, -40.844176977830401], + [-65.013623887251796, -42.092224929782297], + [-63.750833843953799, -42.090001881415901], + [-63.628640950378298, -42.764830919612997], + [-64.953755976015799, -42.661114925810999], + [-64.296530946193101, -42.991181030375003], + [-65.326805821288204, -43.661806879394099], + [-65.611115954479999, -45.020564014624803], + [-67.617926976329301, -46.071395004400799], + [-65.775284916361301, -47.195206890053299], + [-65.789792817332099, -47.965832029357799], + [-67.897232945673395, -49.9858370050326], + [-68.273342856165598, -50.123338941886701], + [-68.592518856817705, -49.928606040543002], + [-69.002855933746304, -50.009650997848802], + [-68.373197977716302, -50.155207943677198], + [-69.6095189765405, -51.624170006890402], + [-68.990147962668502, -51.624448956628797], + [-68.420915817351201, -52.372511018350501], + [-70.811675840840493, -52.732510910894497], + [-71.284724976398493, -53.886392036082597], + [-72.4525108408032, -53.404307041238098], + [-71.166536956171399, -52.810703003627602], + [-72.517094913693001, -53.062360929994099], + [-72.189521922416404, -53.182708966675598], + [-72.647783871989006, -53.325970947408003], + [-72.400283872523303, -53.540278945482299], + [-73.298888939010098, -53.160766988119697], + [-72.710279845153906, -53.294038913705499], + [-72.915848870952999, -52.824716037033603], + [-71.543897949534994, -52.5588920237088], + [-72.789794937487898, -52.542853922490004], + [-72.678347979089395, -52.662157909309101], + [-73.005848885998503, -52.854172894721501], + [-72.980423891387403, -53.064727979457501], + [-73.449341922134707, -53.002042910556398], + [-72.890630909350904, -52.517635960887901], + [-73.6936108655553, -52.722089020095197], + [-73.731671914514905, -52.037513001944198], + [-73.321820820333798, -52.223758891022896], + [-72.986255851995097, -52.070624034150697], + [-72.986678970468603, -52.187497939594301], + [-72.866393965699103, -52.263889934796403], + [-72.694232859117704, -51.989101978914597], + [-72.899441965993603, -52.458613951322697], + [-72.468899976127901, -51.7891759856251], + [-72.712646894617095, -51.583894962019798], + [-73.242512928646804, -51.462979973406], + [-72.566045898926504, -51.788959900160599], + [-73.283903940109198, -51.613540915443899], + [-72.926954892691498, -51.861121884094501], + [-73.380671839073599, -51.669889936078903], + [-73.286945900418203, -52.160002950169201], + [-73.545290909156606, -52.056403967735697], + [-74.247362976768798, -50.927704929604097], + [-73.533131952738302, -50.714233948383303], + [-73.571732963901994, -50.404345934665898], + [-74.688758860724604, -50.207092930306501], + [-73.712510883802295, -49.757227971358397], + [-74.656880974116802, -48.029722918853203], + [-73.226600891252104, -48.003028904097697], + [-74.741390842567, -47.711464904239399], + [-74.046671883355202, -47.620204921505099], + [-74.265776844630196, -46.787569959421198], + [-75.7175219757588, -46.725281019265097], + [-74.368619947033196, -45.792367984795298], + [-73.846547909512097, -46.592089895226401], + [-72.613889970023706, -44.472778980298699], + [-73.289384866610902, -44.146870970273397], + [-72.310004907739398, -41.435836916813102], + [-73.745567936552206, -41.754373881165201], + [-73.994318851251194, -40.966945875794003], + [-73.224035861235194, -39.416884875818198], + [-73.677086949227103, -37.347289878610098], + [-71.446670889913406, -32.665004021793202], + [-70.31201382735, -18.4374979972199], + [-75.933890916191601, -14.651872940623701], + [-78.994592825398797, -8.21965390235999], + [-81.174725847422707, -6.0866628832215], + [-81.355148828349002, -4.68749592211674], + [-79.726814934093298, -2.59694884912052], + [-79.762922831969505, -2.01402788241307], + [-80.252225986572697, -2.73319098592374], + [-80.975006887142101, -2.21694185655497], + [-80.100836912498707, 0.77028412157307], + [-78.889292987168304, 1.238365135467], + [-77.032718862979195, 3.91840307853595], + [-77.889725884994505, 7.22889106748732], + [-77.475770843369105, 8.52111114638285], + [-76.757921854641907, 7.91916409760444], + [-75.264137958571794, 10.7989930853216], + [-71.528624948440296, 12.446110154093599], + ], + ], + ], + }, + }, + { + type: "Feature", + properties: { + name: "Europa", + FID: 7, + OBJECTID: 7, + CONTINENT: "Europe", + SQMI: 3821854.3456899999, + SQKM: 9898596.9251000006, + Shape_Leng: 1595.02087869, + Shape_Area: 1444.3956132200001, + }, + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [-10.1122197931565, 54.229996171598401], + [-6.10125280111504, 55.209448214837401], + [-5.43131980021451, 54.485965078417998], + [-6.38138690249514, 53.951527051469697], + [-6.35944475630112, 52.179031127716001], + [-10.132496790460999, 51.593329213165298], + [-9.58097678006936, 51.872356065132799], + [-10.4633368799664, 52.180210126219997], + [-8.81833490064753, 52.665553166837903], + [-9.93236376416235, 52.555204070120404], + [-8.941112851066411, 53.264161200658101], + [-10.1765249163078, 53.409718146486199], + [-9.56139279852135, 53.859718054075699], + [-10.1122197931565, 54.229996171598401], + ], + ], + [ + [ + [-3.01472088373401, 58.638187088290003], + [-4.43152789539687, 57.5727761131756], + [-1.77333285678799, 57.458053171730903], + [-3.72375881581866, 56.027359190486202], + [-1.63597475339282, 55.581940160632897], + [-1.29750277338661, 54.763606066510597], + [-0.56471388039779, 54.479998169168802], + [-0.457496765629571, 54.376939148939499], + [-0.42166781749188, 54.332218174974301], + [-0.393335811213319, 54.272773214573803], + [-0.393470759854722, 54.267499153442998], + [-0.38194178728088, 54.255556114850997], + [-0.079307807867963, 54.113401215893198], + [-0.212219814530386, 54.008326180306398], + [0.126387114117392, 53.6452751073599], + [0.142083233685582, 53.580556085828697], + [-0.272501791784237, 53.735554072143898], + [-0.716525905307208, 53.696386109047999], + [0.235557212330889, 53.399440089145699], + [0.002106120816848, 52.879852112456703], + [1.6752781680446, 52.748056072386099], + [0.388935144395816, 51.448222207900898], + [1.40763611991594, 51.1838921850022], + [-5.67778480227315, 50.038606198630497], + [-4.22805585414, 51.187771162164601], + [-3.02833778839482, 51.206113113296702], + [-2.37999589958218, 51.761737071132302], + [-5.24694575775817, 51.730273082904503], + [-4.13083784697386, 52.334722129787799], + [-4.75848884010196, 52.787260077665898], + [-4.19639388535853, 53.206111156587902], + [-3.17083483664123, 53.400277105999201], + [-2.86221584306816, 53.282773216711099], + [-2.70492289165702, 53.350615167736201], + [-2.81360683948509, 54.222769127022303], + [-3.6326337817257, 54.512218205065203], + [-3.04138790876129, 54.978886197717898], + [-4.95013485413546, 54.654994203050101], + [-4.48541984492812, 55.923607154500402], + [-5.03221480884844, 56.232496212994498], + [-5.7816629020831, 55.299016058954003], + [-5.12083784483644, 56.814715111489903], + [-5.67694778541966, 56.493892066859402], + [-6.23451584188342, 56.716732171837897], + [-5.00152480556198, 58.624165170066703], + [-3.01472088373401, 58.638187088290003], + ], + ], + [ + [ + [-18.295145830170298, 66.175273066957899], + [-14.7108327983766, 66.367216135641002], + [-14.8483349028688, 65.731384069714494], + [-13.4994419265985, 65.069155211320194], + [-18.774998891830901, 63.3913930662628], + [-22.686254931056698, 63.804574122946804], + [-21.362777896942301, 64.384993091549205], + [-24.059537904531101, 64.8908831943926], + [-21.698612930202, 65.449153151430195], + [-24.538400895785902, 65.500273205573706], + [-23.2294408150977, 65.740546160463197], + [-23.873336774815499, 65.868733120654298], + [-23.4687508569477, 66.199717211256797], + [-22.4247237932743, 65.847493210310304], + [-22.941809771858502, 66.465694110495903], + [-21.399443861933399, 66.027214123024905], + [-21.084443893093201, 65.159164111183102], + [-20.422637817896099, 66.084445087514496], + [-18.069443808163498, 65.643328153297006], + [-18.295145830170298, 66.175273066957899], + ], + ], + [ + [ + [9.43944314777673, 54.807985058825402], + [10.818540144585899, 53.890057190894403], + [12.5269471982906, 54.474166208561201], + [12.3743702408954, 54.262423072865701], + [14.5250011429541, 53.660557160910201], + [13.768470173878301, 54.165826164726603], + [18.043326133072, 54.834031152463602], + [19.836378171506698, 54.600004056561701], + [19.371105262822301, 54.268597182762001], + [20.397294127746001, 54.675064167345603], + [19.8727112073027, 54.640549166353402], + [21.0920132545961, 55.714015150441902], + [20.5317001248347, 54.964360191836199], + [21.2450671422833, 54.955063152446201], + [21.052224192472899, 56.817487174515698], + [21.729024151388199, 57.574720211803502], + [24.4041662247988, 57.251242115515502], + [24.5545112490103, 58.324717152059598], + [23.728608128661399, 58.370824157537903], + [23.5052731561182, 59.226787129690699], + [30.2180581314148, 59.8969362160557], + [28.676727218896598, 60.735826089582801], + [22.900275262213398, 59.806801084730601], + [23.084658189487001, 60.345199058026402], + [21.4241671236089, 60.5792980706576], + [21.4968692373848, 63.203536175389303], + [25.4442331744869, 64.953388220012997], + [24.6891601536535, 65.896102213893499], + [22.6758241554401, 65.902222176695105], + [21.264867209295701, 65.338255207691404], + [21.584997238170001, 64.439713173116701], + [20.774025137265401, 63.8670791485672], + [17.700552225068201, 62.992711193370297], + [17.152497125821, 60.942772094439498], + [19.0726382627658, 59.7381942012195], + [16.0324651432351, 59.490163124367299], + [18.6431582600353, 59.321872110130897], + [16.193610249718301, 58.6274951325694], + [16.933608250010501, 58.488328214463898], + [16.379829149068801, 56.663326205053998], + [14.193549174515001, 55.386145104515897], + [12.982221167011099, 55.400554099029002], + [10.7411852363443, 59.890348207779503], + [8.12750413869173, 58.098880181411403], + [5.50847723081671, 58.667635061160503], + [6.46889413952931, 59.5552692221884], + [5.17888822418269, 59.506804219702097], + [7.1020081218864, 60.496111201808198], + [5.74722015309641, 59.9866661661802], + [5.01228918092602, 61.040269218982097], + [7.11388812856632, 60.860269188891102], + [7.56246613009716, 61.470334110917698], + [5.11583418390296, 61.141663090321998], + [5.79624322269763, 61.447978066343801], + [4.98292217724043, 61.739992174403703], + [5.41721724808241, 61.911244179764203], + [5.74471815499152, 61.842214177779603], + [6.46916420445052, 61.800823166317201], + [6.7636082262477, 61.868044185953401], + [5.43360621576903, 61.935004193124001], + [5.15457919616352, 61.892497215069596], + [5.08014018440679, 62.1766631795265], + [7.06874420376562, 62.091244209855297], + [6.25298419211572, 62.577775133794503], + [10.911798205404899, 63.458047133966403], + [11.4879152460095, 64.005544166098801], + [9.54527423103201, 63.766108060424699], + [12.9392372586648, 65.320885222054301], + [12.2490272604596, 65.231803193047099], + [13.1694482416782, 65.849842154862898], + [12.673746174534999, 66.064438155131], + [14.1382621410153, 66.320236071125507], + [13.0284002568568, 66.188944118713394], + [13.551390112778099, 66.928465188713403], + [15.7355551655168, 67.175758155170101], + [14.3289001473703, 67.239199104101701], + [15.8874662645218, 67.5564491110365], + [14.756796138034, 67.802626110901301], + [16.4958301631133, 67.7941572035473], + [16.137639252917701, 68.306104069642203], + [17.3580572668027, 68.176369139568294], + [16.467012174087099, 68.511727074898005], + [18.2569411153898, 69.486373102480897], + [19.440558215642302, 69.2258231502868], + [19.246662163513999, 69.771943203361602], + [20.3872142185973, 69.912217203241596], + [19.9469431860506, 69.256099087555398], + [22.099581218649099, 69.7437461457246], + [21.5163901870208, 70.303879232205205], + [23.317632183071101, 69.942358191868607], + [24.588477235342999, 70.960960093292798], + [25.903323256095501, 70.888735077446896], + [25.089867261997, 70.506559183609696], + [25.267635191266301, 70.395958126881993], + [24.9840271262863, 70.226110090307301], + [25.039998123087098, 70.061653126232201], + [28.207773145965501, 71.079994182828898], + [28.038888190068501, 70.061932075970702], + [28.853883269232401, 70.880554172286807], + [31.0735351208201, 70.285546165890494], + [28.599093193278598, 70.160752200114203], + [29.4919472683424, 69.660199190313705], + [32.055831143814501, 69.959431122856301], + [33.516171246121402, 69.422482212985599], + [33.516657228868702, 69.181363188787302], + [33.0433291435724, 69.070411097953595], + [33.010596135200103, 68.967163149626103], + [35.8366592372101, 69.198868123065694], + [40.992219199866803, 67.716091174638294], + [41.219712266949003, 66.837772157911502], + [38.607777287370702, 66.052198061889598], + [34.4790182362092, 66.532348178199697], + [32.667777132556999, 67.119427071808005], + [31.8588121621924, 67.152835158663805], + [34.850133202947198, 65.898739160639906], + [34.7877721784239, 64.547767137100195], + [37.388736252543303, 63.803323207713298], + [37.978875211371196, 64.316665157776796], + [36.4395691990279, 64.937899133453797], + [40.497219200935596, 64.535131082751704], + [39.751389240035699, 65.550808202873796], + [42.1751342319074, 66.524428117867203], + [44.174439259442202, 65.874691145086103], + [44.244918157213597, 68.264569224720802], + [43.311663141093199, 68.684986210798499], + [46.5273541517981, 68.138173141967201], + [44.9129072271243, 67.365676156040195], + [46.381932154611498, 66.741094112949398], + [49.0972141566129, 67.632616135956894], + [48.5952752829376, 67.930462204624604], + [53.775558171993602, 68.966938179344297], + [54.559152294841603, 68.995675199185499], + [53.209152236797003, 68.2647132258178], + [58.898331146821597, 68.999707229900096], + [59.842638205223899, 68.369428174843094], + [60.914439208061097, 68.904712103462401], + [60.145272184636802, 69.573106186935505], + [60.931665192601201, 69.863032195270605], + [64.959156268608794, 69.319990144688703], + [64.522215198564993, 68.903047122211007], + [66.210534183063402, 67.696093127072103], + [59.650695304179202, 64.778041152025807], + [59.473602285755398, 60.809572085583099], + [58.310964251019499, 59.460409212030001], + [59.449131151728402, 58.488049097087298], + [57.221694188449497, 56.850967178101101], + [57.466386250343596, 56.121940083267901], + [59.292351172792898, 56.134090154868801], + [59.641659277254597, 55.558675182476101], + [58.810275230403903, 55.019719142065298], + [57.160125253778503, 54.824437058424202], + [59.9363101644228, 54.861589173800802], + [58.921524208248897, 53.932906150599301], + [58.789296164887702, 52.450678049193201], + [60.144156218044998, 52.423732074427598], + [61.422354211079302, 50.800618094207799], + [54.647217263714801, 51.036949207661003], + [54.523935225637402, 50.528836056451297], + [53.423748237065197, 51.492637074761397], + [50.7733022679637, 51.769180201173], + [48.697488233858003, 50.591935191370503], + [48.833883256575596, 49.959163190663801], + [47.599722274205597, 50.460823114600899], + [46.931382170189103, 49.865824160660097], + [46.499166146616602, 48.417499181312799], + [50.038497191979403, 45.858484185770799], + [48.686157241837698, 44.754346135668598], + [49.760631233604798, 42.710752137459501], + [47.915469238259, 41.224987040541698], + [45.1651232206058, 42.703327112329902], + [39.945537252174198, 43.396939157405299], + [36.625545190823701, 45.1273421691225], + [37.734633257678901, 45.298810092309402], + [38.570859194731398, 46.0911250825217], + [37.737486122251802, 46.667107174484897], + [39.251520193536102, 47.263186052833902], + [33.681636217875202, 46.221652102447898], + [36.636795213659099, 45.377911118625399], + [33.930270121205602, 44.379154043576698], + [32.481099240187497, 45.3940211365735], + [33.614019237131998, 46.142623160499298], + [31.790133194313999, 46.284166180523599], + [32.641614195188197, 46.642285173989897], + [31.751514245877399, 47.252359148471797], + [31.908195150716899, 46.653589176281898], + [28.869192144872802, 44.940511055562503], + [27.449586248093901, 42.4729811806748], + [28.013058181894699, 41.982220077829297], + [26.3813222407687, 41.822002177477799], + [26.0905411106692, 40.736107172049998], + [23.722011235567798, 40.7446481637716], + [23.988537149466399, 39.952639113026002], + [22.585212261460999, 40.4650000451389], + [23.343678109071199, 39.181798055895001], + [22.5238592446163, 38.866069196752598], + [24.0740282035052, 38.195002124349102], + [22.725558178070401, 37.563400069691198], + [23.201451125745798, 36.440200062972302], + [21.703608209232499, 36.816661175208502], + [21.384441261035899, 38.211391092035903], + [23.224644187173102, 38.153404079877703], + [21.108744203933099, 38.355553121533603], + [19.289790240595401, 40.421458069677598], + [19.5977071659565, 41.8061080773561], + [15.9883381109306, 43.504435054273799], + [14.4827731146381, 45.311104165007201], + [13.904442245760499, 44.7725621906147], + [13.54263320323, 45.783325168529302], + [12.1542301145754, 45.301960179169498], + [12.368340132096099, 44.246665155387802], + [18.512496123829699, 40.134718190386501], + [16.919298161955101, 40.450555176080201], + [16.490556101982399, 39.749167071769101], + [17.169165210884199, 38.963332130919902], + [16.0572871437302, 37.924237146726803], + [15.6637442156888, 40.033054086487503], + [8.74721713421428, 44.428051049353698], + [6.16527914216601, 43.050556169521897], + [3.2580542391197, 43.227487081937802], + [3.20166012384539, 41.892769077443198], + [0.703611236496556, 40.796875132051397], + [-0.326105906759665, 39.4947191451039], + [0.207216153596733, 38.732212046685397], + [-2.12291991098806, 36.733465086265603], + [-5.61360591058394, 36.006103140321997], + [-6.95992480438179, 37.221832149919599], + [-8.989235871902, 37.026316043540902], + [-8.67333585429623, 38.4138911677978], + [-9.49083276392691, 38.793880055452298], + [-8.57986187565098, 42.349015179296501], + [-9.208196832080089, 43.155136169905802], + [-7.89805792052596, 43.7641660945254], + [-1.7808748932863, 43.359931043125599], + [-1.08944080193805, 45.558613049290301], + [-0.539927754448513, 44.895619090772001], + [-2.12542190909289, 46.830970196899301], + [-1.73500191054541, 47.208673171913702], + [-4.36736677334244, 47.808748206431702], + [-4.72888781367902, 48.558880095329201], + [-1.36889077237907, 48.6436060492439], + [-1.92202178456546, 49.726459094363001], + [-1.1095828506011, 49.369438130215798], + [-0.228338884933976, 49.2836050945265], + [0.465768195344367, 49.4688161538364], + [1.73958312355774, 50.9452751589087], + [4.30967715110995, 51.262030130640802], + [3.44423709628632, 51.529375124119902], + [5.81373910452061, 52.428466173354302], + [5.36986821218818, 53.070409149626798], + [4.58200820352602, 52.477084061754802], + [5.59916120916336, 53.300278150989698], + [9.829062171654581, 53.541703114655], + [8.29083625135752, 54.742807211913103], + [8.68944620752063, 55.1602720916839], + [8.09291722096987, 55.556209058916899], + [8.395695201479731, 55.894717081106698], + [8.10832517070622, 56.017774148902198], + [8.12118619533658, 56.548261114320702], + [8.22072615732691, 56.707426079992302], + [9.974277135832271, 57.071728068172], + [10.961946126415301, 56.442214113239103], + [9.554580155239419, 55.7029812130708], + [9.43944314777673, 54.807985058825402], + ], + ], + [ + [ + [53.153325241093199, 73.153540073928895], + [56.586033280986399, 73.132345090586099], + [55.621521142009101, 72.963460134688802], + [56.256111177519301, 72.968320129801697], + [55.221021234312701, 71.925607180911697], + [57.633120242241702, 70.728112163532998], + [53.463609215917799, 70.813873114854701], + [54.236025232659799, 71.124778188706301], + [51.4187371451087, 71.731369147132995], + [53.153325241093199, 73.153540073928895], + ], + ], + [ + [ + [57.992220253658303, 75.6715062072969], + [68.861097163533699, 76.541923100963999], + [58.191948266394299, 74.5752340704327], + [58.724163244976303, 74.235808229842604], + [56.567835163313397, 73.880119150113799], + [57.613608177423103, 73.662202072160198], + [56.7495811438408, 73.2452681021377], + [53.632701204823903, 73.758943149034295], + [55.862001297545497, 74.108170116672895], + [55.075275192748201, 74.261386110367994], + [56.984580205237599, 74.687149074305793], + [55.803330154448403, 75.149425117320305], + [57.992220253658303, 75.6715062072969], + ], + ], + [ + [ + [15.646941181984101, 79.839784088022299], + [21.5405552139432, 78.759919191393806], + [18.969930109004299, 78.452002098394601], + [16.612776152924301, 76.5705431094365], + [13.914162235986099, 77.524993108899807], + [16.2230491701334, 77.434984209036898], + [14.745627252021499, 77.659291147075095], + [16.800552242251001, 77.806927140172903], + [17.006112215594701, 77.931361187025701], + [13.951665217830699, 77.718043091719196], + [13.5922141723081, 78.052204091272102], + [17.295201207076499, 78.421789193038293], + [16.330275169718998, 78.450958216170207], + [16.810830131953502, 78.674995089287506], + [13.0066651436718, 78.197482087361905], + [11.337012154683199, 78.9605382004398], + [12.5006942392816, 78.911992228768796], + [12.1152691844883, 79.293322220935195], + [10.6823521548773, 79.546087229076093], + [13.8247201204174, 79.875262169692107], + [12.4493041202171, 79.568884161758405], + [14.0588912174164, 79.260274220640795], + [14.585004170469499, 79.804153120438201], + [16.450002107373599, 78.903874187882707], + [15.646941181984101, 79.839784088022299], + ], + ], + [ + [ + [17.7325075797688, 80.131159758725204], + [22.226382147327399, 79.979158206774997], + [22.784730354672501, 80.508202648094297], + [27.229463788158998, 80.057748205650995], + [23.613256863539998, 79.219393345622095], + [17.7325075797688, 80.131159758725204], + ], + ], + ], + }, + }, + { + type: "Feature", + properties: { + name: "Nordamerika", + FID: 8, + OBJECTID: 8, + CONTINENT: "North America", + SQMI: 9339528.4866000004, + SQKM: 24189364.532000002, + Shape_Leng: 3955.35871365, + Shape_Area: 3708.7527567000002, + }, + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [-82.005011864954199, 23.188195187746299], + [-80.037647923577296, 22.9512431429042], + [-74.133305893713995, 20.191312083666698], + [-77.681114898886406, 19.821943067365201], + [-77.236388884789704, 20.663056156896701], + [-81.885005977561306, 22.680838084205099], + [-82.760561983717594, 22.700836131770998], + [-84.028760869149707, 21.914029057788699], + [-84.951539847376097, 21.856168109454799], + [-82.005011864954199, 23.188195187746299], + ], + ], + [ + [ + [-55.882214927661899, 51.494995071769303], + [-56.848877863000197, 49.544443048914403], + [-56.122631883648403, 50.1544990861236], + [-55.383326899112802, 49.040839193987701], + [-53.511119854905502, 49.277215066803898], + [-54.096110816426901, 48.812212055402703], + [-52.977077956702402, 48.597832140598797], + [-53.944721909991003, 48.171385045722097], + [-53.6311078050256, 47.543329206669398], + [-53.274446927440003, 48.013327161824897], + [-52.923473841726903, 48.170683145148203], + [-52.834652825185302, 48.099646180261701], + [-53.096660893259802, 46.639990208893998], + [-54.186875931596397, 46.821871138063003], + [-54.1961818558039, 47.841247192066497], + [-55.3920838086608, 46.865827179542201], + [-55.981628825828103, 46.9492661760397], + [-54.843749927313098, 47.560123187918599], + [-54.941453917226902, 47.777626199854303], + [-59.303474964166099, 47.611666192897601], + [-57.378608948388198, 50.687767167023601], + [-55.882214927661899, 51.494995071769303], + ], + ], + [ + [ + [-94.390766993507398, 71.933653137430397], + [-91.513484930896297, 70.156297218564006], + [-92.918879981353896, 69.676993171562998], + [-90.313397893772404, 69.448105188150393], + [-91.433744868280499, 69.349492097015698], + [-90.263195993305402, 68.235814099968806], + [-89.314442838170194, 69.249304213908104], + [-88.045271987242998, 68.818600118034695], + [-88.3712428615421, 67.962826073979997], + [-87.510419894276595, 67.114630108607301], + [-84.534299937887099, 69.014854167170796], + [-85.554989939036304, 69.859702065129795], + [-81.3356999630806, 69.184981153483903], + [-82.049786986014098, 68.877136144852301], + [-81.259037902957402, 68.641795101805002], + [-82.632509972167895, 68.500954149992495], + [-81.502217869514396, 67.000960101842296], + [-83.402216957796099, 66.347488152995993], + [-83.912093829076994, 66.878866114724502], + [-84.908195852468396, 67.059280210833506], + [-85.227083850926505, 66.874690082912807], + [-83.7678959250335, 66.168640131680604], + [-86.757929840732999, 66.528451096126503], + [-85.897259927019803, 66.168019200291596], + [-87.395840953928797, 65.321380089618998], + [-89.668187986909004, 65.9371961707065], + [-91.429172875361601, 65.951101077561205], + [-86.9351309857079, 65.142910092137896], + [-90.2363938519985, 63.6072222121295], + [-93.772709926780706, 64.190251137750096], + [-90.627488929029795, 63.059437177803296], + [-93.616100938670598, 61.939990084422803], + [-94.819166904191405, 59.636386096223603], + [-94.3633259835389, 58.218886068804302], + [-93.153887927567993, 58.739014175335598], + [-92.418740869933302, 57.332701138839603], + [-92.868335931598395, 56.906659057525303], + [-90.824714943660894, 57.256525061463797], + [-85.419962842920697, 54.999091110655002], + [-82.307771908191199, 55.148878067751497], + [-82.273886891043404, 52.956379056300399], + [-80.438192925913, 51.466375063296603], + [-81.009170854028397, 51.033466191605903], + [-79.345934882490695, 50.734954096909902], + [-79.326953895058793, 51.662341110239097], + [-79.022501880842, 51.474844138288901], + [-78.954875847643194, 51.223042210825398], + [-78.844724899117395, 51.163606135242297], + [-78.5070899359647, 52.457491195389402], + [-79.761806865377594, 54.651655188091901], + [-76.5316709856138, 56.318743147063699], + [-76.861934903093399, 57.719152138584299], + [-78.572987956, 58.628881164082401], + [-76.758902872592301, 60.159151149501199], + [-78.179930843068107, 60.7879450989494], + [-77.477147822426602, 61.541524129356603], + [-78.153263985679004, 62.280064181406402], + [-77.508341913371495, 62.561665115846203], + [-73.683467924494394, 62.4799901746958], + [-71.389997824900703, 61.137775060704101], + [-69.513443925694602, 61.066063184972101], + [-69.630830971251996, 60.066379071429701], + [-70.945829877918996, 60.063049108926897], + [-69.600554866345405, 59.833054211377799], + [-69.815969946194301, 58.588885068950397], + [-68.358599887467093, 58.7701271294574], + [-69.363647916142597, 57.767770194622898], + [-66.388607884161303, 58.850542102918901], + [-66.058883928885805, 58.320271055326799], + [-64.989431870601095, 59.374432175243797], + [-65.527784916895698, 59.716936185964599], + [-64.844792910811194, 60.3611380851492], + [-64.471265967608801, 60.280111065116103], + [-63.359657965375803, 59.199364056995002], + [-64.044017898062194, 59.018194080855501], + [-62.846378879586602, 58.687768057367897], + [-63.581012964745597, 58.301731123641197], + [-62.556947906454397, 58.480264153034398], + [-63.335285905444401, 57.980125209251803], + [-62.450621955634197, 58.168495072601097], + [-61.884863941554897, 57.633121122341599], + [-62.526941866468903, 57.488608058737697], + [-61.361117870327, 57.092356099582503], + [-62.5720859589076, 56.795689197056802], + [-60.3327688213108, 55.781308086807499], + [-60.683327841005898, 54.994987163210702], + [-57.349682832810899, 54.574957085785002], + [-60.856946895829701, 53.792776151816099], + [-60.407774952638199, 53.267221098239801], + [-58.177781844159803, 54.236935213980502], + [-55.745837842217099, 53.249437214222802], + [-56.487734846497901, 52.594291064031403], + [-55.699307885903501, 52.085269146876897], + [-60.005006901936198, 50.248882165989897], + [-66.449024977694805, 50.267773131781397], + [-69.678323840605202, 48.140830158715097], + [-69.968330818125196, 48.271933183029198], + [-70.779995934786299, 48.435544077795399], + [-71.039582973940796, 48.443878204146102], + [-69.732503959968497, 48.107566072956097], + [-71.299160960639796, 46.742221097087302], + [-68.202224922094302, 48.639853135905803], + [-64.825559963369201, 49.187764066417898], + [-64.246382857544603, 48.488041110996299], + [-66.843701977238894, 47.996650191944497], + [-64.803878829640794, 47.808181086861197], + [-65.3661808173932, 47.085481155476799], + [-64.504178851623706, 46.2402731709563], + [-60.9702749209441, 45.269722206000402], + [-64.200833919181406, 44.576380058207903], + [-65.481380856768098, 43.464439126779801], + [-66.194855833130006, 44.418043056934401], + [-63.369620863156101, 45.359857169687302], + [-64.935341940516295, 45.331723143962201], + [-64.274309850260707, 45.805825046561999], + [-64.750823896962999, 46.086652163698602], + [-68.796863957111796, 44.574607118043303], + [-71.044865919889006, 42.367222181787099], + [-69.935408881654894, 41.672503054937103], + [-73.934153885366001, 40.798045078099904], + [-73.866095848876398, 41.0891500849389], + [-73.951280963448298, 41.304439100963698], + [-74.908331867474303, 38.927359181685198], + [-75.459446864303203, 39.788326150047702], + [-75.044582889094897, 38.4172120778451], + [-75.9608278385018, 37.1522170910917], + [-75.835340856969097, 39.571939104704001], + [-76.610519884281601, 39.250369064860401], + [-76.312340886418795, 38.047303099339402], + [-77.061689905557699, 38.904571133820497], + [-76.267718986549397, 37.086310186239103], + [-76.389722952026901, 36.973315090319801], + [-76.653881984100195, 37.226935052587002], + [-76.990274916836896, 37.312912089373199], + [-77.232230957888802, 37.296388173045003], + [-76.293611858997195, 36.843328200235803], + [-75.987287830519804, 36.909226052632903], + [-75.532643845644102, 35.801515132473099], + [-75.945311929852295, 36.7124141040197], + [-75.793157923292398, 36.073846184889398], + [-76.7460958274186, 36.2281781451764], + [-75.720833833350795, 35.814511105744899], + [-77.049584928595905, 35.526934041962399], + [-76.343264912442606, 34.881940052925401], + [-80.832365837798704, 32.519989133644899], + [-81.488438858845697, 31.113460179322502], + [-80.035334853570603, 26.795692060873801], + [-80.397512867479804, 25.186654145972199], + [-81.146672958520497, 25.160410071780301], + [-82.396394944179207, 26.962354136042499], + [-82.802780959578001, 29.154988096135298], + [-86.259995840406006, 30.495826063431199], + [-90.2362498509017, 30.3765311290678], + [-89.399447909461301, 30.0508301520515], + [-89.008757845992804, 29.174167064120802], + [-89.405000920330593, 28.926667064655], + [-91.838816936533803, 29.828260111993998], + [-95.060060958108096, 29.715067035521201], + [-97.769339841038601, 27.449722076835201], + [-97.137512983736897, 25.933195059901301], + [-97.888265971661497, 22.5988211614041], + [-95.911388905430101, 18.825274092041401], + [-94.469183888884402, 18.146242032304201], + [-91.495709931696695, 18.435556161705701], + [-90.332648946125204, 21.0275021461735], + [-87.005429840198801, 21.579715172321599], + [-88.910576925787097, 15.8936050446962], + [-84.996962889553402, 15.991282044881499], + [-83.393477985521002, 15.256387114895], + [-83.410838918702495, 10.3969450401042], + [-81.507293950091494, 8.79312703923874], + [-79.534457967030406, 9.620137073724139], + [-78.034382949695299, 9.22881702641109], + [-77.198336888285496, 7.99944412242445], + [-77.889725884994505, 7.22889106748732], + [-78.432785872849394, 8.048890142860939], + [-77.779169922906107, 8.15500000821657], + [-78.979103906477704, 9.138538061627051], + [-80.471267915935599, 8.215552050391629], + [-80.436869926311999, 7.2445781346002], + [-83.595563827626805, 8.46833516344361], + [-84.747572938554498, 9.967159097907301], + [-85.656680865025507, 9.904996053937399], + [-87.398333899578105, 13.412368075869299], + [-91.384739903590201, 13.9788910224345], + [-93.938615954280905, 16.093891124547], + [-94.364378918218705, 16.2874991744815], + [-96.557777978435396, 15.656392155026399], + [-103.478336922986998, 18.3293021276152], + [-105.527780986714006, 20.043892176033602], + [-105.820424910981004, 22.664026165683001], + [-112.161941865987004, 28.971388038620301], + [-113.091668939051004, 31.229722038194598], + [-115.031141929895995, 31.960000048261101], + [-114.545294969257, 30.0011051818765], + [-109.438901909660999, 23.225833118232501], + [-109.998188926832, 22.881943075998599], + [-112.087511906684995, 24.7561121561065], + [-112.378347016242003, 26.254999122482399], + [-115.023059931193004, 27.768610075293299], + [-113.985008996862007, 27.700840040997701], + [-114.061319855242004, 28.517572185780502], + [-115.696673928703007, 29.774233046183198], + [-117.409436889861993, 33.244156065727203], + [-120.605822868757002, 34.558597072917202], + [-122.490278891022996, 37.529992150473497], + [-121.427217028098994, 38.012905109715902], + [-122.956388984198995, 38.058049202154599], + [-124.331183885371999, 40.272454149978003], + [-123.951950877748004, 46.181107160294403], + [-123.506252898156006, 46.250137162279003], + [-123.263468892706996, 46.144855093683198], + [-123.163569011793001, 46.195192110429701], + [-123.430409917614, 46.2869380759115], + [-124.000001981853998, 46.323613093358098], + [-124.714304922614005, 48.397069130455897], + [-122.749433926886994, 48.153943143355598], + [-122.639714981652006, 47.151928190171603], + [-122.855138946317993, 49.438117098094402], + [-124.741943913135998, 49.958326173810498], + [-124.806932999588994, 50.919580099376397], + [-125.703683989088006, 50.429503127469999], + [-126.270206935652993, 50.627359125945603], + [-125.738324886266, 50.682205103698799], + [-125.620136866039005, 50.752081174992398], + [-125.507852890787007, 50.941486203386702], + [-125.610821889375998, 51.087772207155098], + [-127.789991998359994, 51.165541181414902], + [-126.620001022861999, 51.679990213252701], + [-127.876112868606, 51.668596189320297], + [-126.675404900093, 51.990895119466103], + [-126.974159902343004, 52.833673190249101], + [-128.393892030583999, 52.291378134880098], + [-128.969576900259995, 53.553178107772297], + [-127.870928996752994, 53.237170131253102], + [-128.606174960846005, 54.029782176114999], + [-129.27279593651599, 53.379154207023902], + [-130.100264964020994, 53.944291122076102], + [-129.474288004599003, 54.239365127717903], + [-130.481108973440001, 54.364717160609203], + [-129.478184919034987, 55.470754214700001], + [-130.109030926023991, 54.993943113348202], + [-129.946382944297, 55.285408206748698], + [-129.995406013897991, 55.926595135352599], + [-130.837770018663008, 54.767278178302099], + [-131.012792874635011, 56.106514196258701], + [-132.168060031336012, 55.584433106282297], + [-131.769746962184001, 56.196937162139697], + [-133.488594011598991, 57.1661020955829], + [-133.640567974877996, 57.696382195630598], + [-133.006427879932005, 57.513952084164103], + [-133.774037881017989, 58.515004124308497], + [-134.757648018796004, 58.381975106277501], + [-135.347831904624996, 59.464153072912801], + [-135.085536001994996, 58.233043103307203], + [-137.064303019758995, 59.061862120140802], + [-136.028340017515006, 58.385269194234603], + [-136.658906907127999, 58.216519186978999], + [-139.710536914865003, 59.495824094149697], + [-138.899735972423997, 59.805334084032701], + [-139.499999935039995, 60.0330521213968], + [-143.900576898503999, 59.991103210457403], + [-147.715685970154993, 61.275412113837902], + [-148.692365950368014, 60.788116089774803], + [-147.93785099664899, 60.451381175387603], + [-148.438521017818999, 59.948461116123703], + [-151.976249946204007, 59.275828136564598], + [-150.997887047466008, 59.780827075459797], + [-151.878311933552993, 59.759992178678502], + [-151.406739018785998, 60.728113062259297], + [-149.029739901054995, 60.851656112802097], + [-150.062013021871991, 61.157773108270298], + [-149.634854973965986, 61.487353062448904], + [-153.078894005879988, 60.298192171420602], + [-152.587494034372014, 60.046381191501702], + [-154.256957927624995, 59.132764136385802], + [-153.261179948610987, 58.859569077388002], + [-158.505281978070997, 55.988884075508501], + [-163.352906910455999, 54.809722124444399], + [-157.397921924379006, 57.492775205732102], + [-156.781818008735996, 59.151241203797397], + [-158.190965972532013, 58.606804069246898], + [-158.538617980558996, 59.1737410818303], + [-158.897520011421989, 58.395547083937103], + [-160.322768940711001, 59.058325124628901], + [-161.632196899235993, 58.599163126290897], + [-162.165968900156003, 58.655125070636103], + [-161.568404916198006, 59.106655178473503], + [-162.369584942509988, 60.169438091659003], + [-161.879427001419003, 60.702220189811598], + [-164.06527497268101, 59.8241621855503], + [-165.42243904339, 60.5521450628829], + [-163.555128036479005, 60.8971061447076], + [-166.197357058237003, 61.590268081581101], + [-164.401127943213993, 63.214993063595699], + [-161.151668926729997, 63.512497150612703], + [-161.529173921189994, 64.418869223879796], + [-160.782794945630997, 64.719154091102098], + [-166.121369908958997, 64.574713111865805], + [-166.422249053119003, 64.9191431163039], + [-166.696676922438996, 64.995895198067302], + [-166.722488993339994, 65.055403190379906], + [-166.846931925010011, 65.088046177111707], + [-166.919732945243993, 65.131363182291295], + [-166.95979190464999, 65.1799091539622], + [-166.054176046690003, 65.250028132810399], + [-167.462352044990013, 65.420119076939798], + [-168.131951949057992, 65.6629570618462], + [-164.361932990389988, 66.593881070687203], + [-163.656396011633007, 66.070549065477294], + [-161.015561906205988, 66.183877090591494], + [-161.913483009391996, 66.276649168662999], + [-162.329570910104991, 66.955744092673996], + [-161.630279957974011, 66.456100184094495], + [-160.235001026486998, 66.398041087568799], + [-166.823630904670011, 68.348737111520606], + [-161.942228914049991, 70.307209194707895], + [-156.596723960795003, 71.351434071296794], + [-155.592503896517002, 71.168320164167497], + [-155.973600033585001, 70.755823070784501], + [-143.215541887334012, 70.110262129815197], + [-135.160002003480002, 68.657212103996599], + [-135.617553000023008, 68.886586070156696], + [-129.407471998526006, 70.103179086335999], + [-132.001820907003008, 69.529294146553198], + [-133.491923974102008, 68.824153128903802], + [-130.937229011468986, 69.134428218910898], + [-127.516256977157994, 70.223590154929596], + [-128.001797998330005, 70.589575229271802], + [-125.420831971777005, 69.312601161742705], + [-124.436105867406994, 70.151095074162797], + [-124.446662874485995, 69.3672042319414], + [-121.683878950674995, 69.793588127268194], + [-114.066521999643001, 68.469697194773602], + [-115.108604964688993, 67.797622114691606], + [-110.077370922333003, 68.005558189954002], + [-107.228619014192006, 66.348874184508801], + [-107.887913871191003, 68.084992145465193], + [-106.426665002939998, 68.154571162109306], + [-105.651800967548994, 68.636098089838796], + [-108.815975963004007, 68.266099089692702], + [-108.345284992092004, 68.601925070497003], + [-106.208063961684999, 68.940946065162706], + [-104.500493924834004, 68.033386108574305], + [-97.138898847611699, 67.674142096060905], + [-98.710208978024994, 68.362003149713402], + [-97.844444878823595, 68.541373195960006], + [-95.9802839587731, 68.254714118215801], + [-96.454862959302901, 67.474567136877297], + [-95.340284913490393, 66.982618150710394], + [-96.455555975059397, 67.064149090763706], + [-95.741378930485695, 66.638044145175499], + [-95.2247249551924, 66.977965188606603], + [-95.470343887942093, 68.059423149757194], + [-93.619151951435001, 68.5441450913479], + [-94.599098862239003, 68.961862098767], + [-93.367574994253303, 69.373936073676504], + [-96.0280559455028, 69.809158183012201], + [-96.569450952106095, 70.259842221540893], + [-95.816663843913304, 70.709707180488905], + [-96.5556179619809, 71.133661162078198], + [-94.390766993507398, 71.933653137430397], + ], + ], + [ + [ + [-114.004304976217, 72.798040089936094], + [-114.597350946711998, 72.604009089166397], + [-111.229928880282003, 72.7234570770825], + [-111.899096948696993, 72.3529720941889], + [-111.663890854290997, 72.276382118433006], + [-109.659437997088006, 72.924994072166797], + [-107.837081986879994, 71.604145099981295], + [-107.261423939294005, 71.889634231676993], + [-108.293192972452999, 73.149157176746101], + [-106.754939894789999, 73.289917159373701], + [-105.323471928603993, 72.738802162544602], + [-104.578055866084, 71.0624890809123], + [-100.870001970832007, 69.788314066137403], + [-103.481802001768997, 69.689143075525806], + [-101.753063971303007, 69.1629042262865], + [-102.894722940522996, 68.799988101981597], + [-106.611110962124997, 69.496993141471904], + [-113.260418946306999, 68.453047214621506], + [-113.553603000414995, 69.187195149394796], + [-117.432566919376995, 69.983452148681593], + [-111.494087912355994, 70.339771212254902], + [-117.551519872089997, 70.596235154277593], + [-118.410902996024006, 71.000263172668298], + [-115.066943888305005, 71.523874127616807], + [-119.134457881534999, 71.774569140943797], + [-117.353609894157998, 72.916381163715798], + [-114.561665999671007, 73.375534109598405], + [-114.004304976217, 72.798040089936094], + ], + ], + [ + [ + [-89.039582965558907, 73.254988092363405], + [-84.840281984713798, 73.738729183641894], + [-86.651747891009805, 72.869761185761803], + [-86.423192836792296, 72.018883179003495], + [-84.836870885388095, 71.281063132438604], + [-86.816249949724295, 70.9878250988736], + [-84.748193869943407, 70.981795157712298], + [-84.634577842634997, 71.669152123706596], + [-86.049161973569596, 72.012502203736204], + [-84.167918902437805, 72.022627207524394], + [-85.685066850760705, 72.897283164915294], + [-83.637917919766807, 72.985672178165899], + [-85.115825988264206, 73.314415115491101], + [-81.5452738622282, 73.715545174669899], + [-80.252774833594202, 72.727480222979807], + [-81.372365928071801, 72.241651199614495], + [-80.520983833655094, 72.505954232784902], + [-80.977769897712506, 71.888536202357997], + [-79.799165846125305, 72.501391124683394], + [-77.785550898173298, 71.7874931974861], + [-78.869987955358596, 72.228178128412495], + [-77.006627842339896, 72.129205118354704], + [-78.560135983825106, 72.441505108536504], + [-77.576399949861099, 72.755551216792696], + [-75.223484889866896, 72.499015190402602], + [-76.348052823187999, 71.891668184307406], + [-74.122226861703993, 71.983603077887295], + [-75.390209829309697, 71.676919130486695], + [-74.6313838951381, 71.656030086610699], + [-75.081392855183097, 71.179426185905896], + [-73.748042944928798, 71.776927137951603], + [-74.231567950742701, 71.204113070121394], + [-71.123597974826197, 71.261101127056307], + [-72.5694568306143, 70.609978122762399], + [-70.603055969915104, 71.053723118908707], + [-71.803052817760701, 70.428322163875507], + [-71.173043827624596, 70.532344097144104], + [-71.533367932184404, 70.027138125239802], + [-71.001539862254006, 70.621372146694696], + [-69.907292960840806, 70.879906083531495], + [-69.774650851461303, 70.857199172489601], + [-70.454726961060899, 70.627762174417498], + [-70.487918962452298, 70.483897199568901], + [-68.319728979020297, 70.564843082779007], + [-68.452010834200294, 70.375330095471497], + [-69.668063888175695, 70.198588111153896], + [-70.465832815161306, 69.843466150995596], + [-67.801382865109304, 70.260688123211693], + [-67.127768867599897, 69.726925174746995], + [-70.028891912755597, 69.532624109056101], + [-66.790412854185803, 69.339142122945702], + [-69.390134897889098, 68.864698071057305], + [-65.925692972484896, 68.160043203793606], + [-66.002021935775005, 67.628800190706698], + [-64.726181939748599, 67.988827072978907], + [-65.015288868503205, 67.862494189775205], + [-65.203757973586306, 67.651237204465403], + [-63.907910877538498, 67.301686192449196], + [-64.7923949517061, 67.355398098699695], + [-63.9711449610991, 67.275856184275398], + [-64.611935928595898, 67.132477192174306], + [-64.696976874432906, 67.009087195183994], + [-61.264583826461802, 66.626092221766001], + [-62.883548932235897, 66.333736132055705], + [-61.959788936059198, 66.021580142970805], + [-63.7185688810562, 65.678212125668196], + [-63.546956956772497, 64.887211082601297], + [-65.501810907625, 65.748880119175794], + [-64.362320839677096, 66.343222099543794], + [-68.843888948628205, 66.188728200886899], + [-64.634093824978294, 63.9746110876195], + [-64.534580852716303, 63.249031134295798], + [-64.767500866843605, 63.323884212070702], + [-64.942487848269494, 63.632206150994399], + [-65.297330858689307, 63.810163175999698], + [-64.630619861378506, 62.899021129260298], + [-65.1875039545411, 62.562205078050198], + [-68.993045921880395, 63.7465151940591], + [-66.061934941650193, 61.869151100089802], + [-71.624996886297495, 63.140824116759902], + [-72.140003817612296, 63.443116114522297], + [-71.607230939553403, 63.4241621168187], + [-71.229851841278801, 63.602623061844298], + [-73.301525885756405, 64.657837116441101], + [-77.747228836748107, 64.337770119479103], + [-78.147287856336206, 64.947961105329895], + [-77.385554910497206, 65.468044117221794], + [-75.373046876681599, 64.714996164201395], + [-75.943322904223194, 65.319571107270704], + [-73.500560882735996, 65.474425092489099], + [-74.470859887681897, 66.134845136173098], + [-72.258344891391801, 67.248028098017201], + [-74.652497909296102, 69.040261224509194], + [-76.667498888761003, 68.704849142084896], + [-75.591674907664597, 69.221647118475104], + [-79.017362935990803, 70.679980090241699], + [-79.589231860416305, 70.410889146326298], + [-78.7919489161781, 69.891103189083694], + [-85.726943844970606, 69.990535192160806], + [-89.549729901760799, 71.088589154006996], + [-87.005213922372306, 70.992694146441707], + [-89.966663871783098, 71.414155182381705], + [-89.039582965558907, 73.254988092363405], + ], + ], + [ + [ + [-100.981043915667996, 76.495951211765302], + [-97.518050886051, 76.200130210910501], + [-97.934579842510601, 75.744145121522806], + [-97.288262853872794, 75.3988781002314], + [-98.164997858533098, 75.331657080595207], + [-97.619444925028901, 75.118591112936898], + [-102.874265899937001, 75.612889211294501], + [-101.182220991828999, 75.779713224832804], + [-101.885831976868005, 76.444975158718904], + [-99.888335931682207, 75.886381157303504], + [-99.423809850572894, 76.156579182993596], + [-100.981043915667996, 76.495951211765302], + ], + ], + [ + [ + [-108.653264949364996, 76.8109511806055], + [-108.022230014277, 75.782341119123799], + [-105.390143933581001, 75.647629182568494], + [-112.753062010402999, 74.4013811605097], + [-114.441866977648999, 74.664415173535801], + [-110.916107909946007, 75.231433155303804], + [-117.677222939087002, 75.2463101952915], + [-115.000775970986993, 75.694069117241796], + [-117.248048875823997, 75.591802186864797], + [-114.838604919551997, 75.874420181438794], + [-116.733888013818003, 75.925540235582204], + [-114.909579020164003, 76.515679194410296], + [-108.896661001385993, 75.477556175711996], + [-110.393063906929996, 76.391938163313995], + [-108.653264949364996, 76.8109511806055], + ], + ], + [ + [ + [-96.954227918144099, 76.727422162467704], + [-89.299025835978398, 76.297627167815094], + [-91.601954913331895, 76.262077169415804], + [-88.957223893469404, 75.4308191863894], + [-81.536948955970999, 75.809422210169004], + [-79.341182846291105, 74.900188219873897], + [-91.546667879832199, 74.647351127365397], + [-93.077783934559903, 76.355677211885293], + [-96.954227918144099, 76.727422162467704], + ], + ], + [ + [ + [-92.7277829819798, 81.305542084918798], + [-87.675137870817395, 80.407063082256101], + [-86.963399960074398, 79.905340126407097], + [-87.462512958905407, 79.534720194872094], + [-84.9055679581775, 79.271029208273305], + [-88.162505979248095, 78.990535187969797], + [-88.817777857987394, 78.154435147103499], + [-92.058335963826806, 78.208885163749699], + [-92.972771930885202, 78.485959199909999], + [-91.637846893381706, 78.544846093195503], + [-94.288049954928596, 78.983740146684696], + [-90.370538836621606, 79.245577223933793], + [-95.087555947533303, 79.270750090896897], + [-95.779025913427205, 79.419430133856807], + [-94.287698920822606, 79.761655194839307], + [-96.802082964039698, 80.0888771520099], + [-94.385699965385697, 79.985116231206703], + [-96.671528954385096, 80.344567109091301], + [-93.786659928274602, 80.5287971504506], + [-95.527214933508304, 80.8192901107181], + [-92.7277829819798, 81.305542084918798], + ], + ], + [ + [ + [-70.1119349481461, 83.109421195353306], + [-61.076393838755202, 82.320823243933702], + [-69.297209933903304, 81.714574099519297], + [-66.623120962810603, 81.513811089376404], + [-70.208198927090194, 81.176788172795298], + [-64.443941969009003, 81.481987182225097], + [-72.416672840209998, 80.209162156779399], + [-70.503821947391501, 80.093809231490397], + [-71.183879952080105, 79.777486095410893], + [-78.051320932041406, 79.350814197890301], + [-74.496941855865799, 79.224994119524496], + [-74.442833820870106, 79.059070154751595], + [-77.777495889199201, 79.208884101576203], + [-76.083479892734601, 79.096519157139397], + [-78.885413842367896, 79.061779185865305], + [-74.723471842270399, 78.704434177340502], + [-78.6905548772, 77.315536221120894], + [-81.927008867957099, 77.683582237821597], + [-77.784443984036798, 76.786516088762298], + [-81.053324876061296, 76.128040143706102], + [-80.778203990984395, 76.421512200008394], + [-82.725011985318304, 76.819159075493999], + [-82.131587990988507, 76.445119159815803], + [-89.672633916003903, 76.566925144739798], + [-86.739992903163596, 77.174146087010996], + [-88.068068919925196, 77.820265095095095], + [-84.4794449076783, 77.294440144235793], + [-83.467565963171893, 77.349295174444606], + [-82.325204925740294, 78.0726881215856], + [-83.898341975774798, 77.490532087364201], + [-85.673537878186707, 77.938588231601798], + [-84.127220906731793, 78.175540108806004], + [-84.969998977514905, 78.210829094739594], + [-84.625973986639494, 78.5892881174222], + [-85.486121875421503, 78.102478076106607], + [-87.532361872832595, 78.1406112094337], + [-86.856947945430207, 78.734980179529998], + [-82.308338860123598, 78.568885223931801], + [-83.252717835255496, 78.833593103026701], + [-81.705833911868098, 78.841234213620695], + [-81.484028971934805, 79.045741084646494], + [-82.503062876649906, 78.882751121269195], + [-84.748040984029103, 79.031935084249696], + [-84.5036189870561, 79.144435144965996], + [-83.896388992329406, 79.038037109778301], + [-83.474720923380403, 79.024159192652107], + [-83.371733987518596, 79.047766152459303], + [-86.514722884447707, 80.299144234552202], + [-81.706661876266097, 79.586659160958007], + [-79.898606901657899, 79.648048219986507], + [-83.197214883929306, 80.314705237840698], + [-78.038054893848596, 80.5672181183335], + [-79.9604189115217, 80.611363087910902], + [-76.511672938047795, 80.854426210737401], + [-78.935687827202401, 80.878447236562707], + [-76.762709933025306, 81.437923181832602], + [-85.066955972215496, 80.505262107372801], + [-86.738957905756607, 80.603317131392302], + [-82.368332835183594, 81.1770671225337], + [-87.594722897355993, 80.628580187633801], + [-89.454446940767198, 80.910019183703895], + [-86.671934866673993, 81.005266102513801], + [-84.737294881213899, 81.284293122119294], + [-89.820845913489293, 81.010819113382894], + [-90.351944925479401, 81.167482080949796], + [-87.248060959734204, 81.488872077512696], + [-91.953044842775597, 81.660403200249604], + [-84.613463828477194, 81.888454166808799], + [-86.868467865548595, 82.197478173944305], + [-85.046957924649405, 82.481932140595106], + [-79.236809878916304, 81.816085149865998], + [-82.728674877016203, 82.398394237639806], + [-70.1119349481461, 83.109421195353306], + ], + ], + [ + [ + [-38.856392918152103, 83.431657093587106], + [-25.653401892433699, 83.290474160124205], + [-33.718925897830403, 83.150542141894704], + [-32.803356026907302, 83.043874209424203], + [-35.643344497473002, 82.910176625715394], + [-21.316391774087499, 82.610956117639503], + [-32.724092822582499, 81.783199858563094], + [-21.036981296382798, 81.917031162387005], + [-23.982659018004998, 80.570410048740797], + [-15.7363832673594, 81.820370517163099], + [-11.4618891772234, 81.450238431299397], + [-20.761521871284099, 80.528469230353295], + [-15.715452523928899, 80.413407273179899], + [-20.1904943603611, 80.071189361009502], + [-17.1672942519412, 80.002079227705707], + [-19.347922309167899, 79.606871150775007], + [-19.6734432429033, 79.102745270916799], + [-19.368323812064698, 79.274728142154999], + [-18.8342634039413, 79.240470601874804], + [-18.888301317810502, 79.153316142762705], + [-21.028701617261198, 78.769343807917096], + [-21.9620142859571, 77.639545577488803], + [-19.2598476309836, 77.725556808419199], + [-21.069423585746399, 77.507360780727097], + [-18.486246116248701, 77.300855556805203], + [-18.419574278909799, 76.756139640154402], + [-22.702628533661098, 76.686324415589695], + [-19.872970091391601, 76.236937666343493], + [-21.981527811787402, 75.990538206851994], + [-19.3368058443804, 75.402073114092701], + [-22.4258308750487, 75.159145107545996], + [-18.978191815711199, 74.483461115222596], + [-22.479164925103198, 74.309698226939901], + [-20.501117912823201, 73.452772174109398], + [-25.688744857824101, 73.952344165959701], + [-24.6754168498925, 73.518391076768197], + [-27.728333815046799, 73.131787191108998], + [-25.051256863101798, 73.080964191614996], + [-27.387782787771101, 72.840682184270094], + [-21.898052773105999, 71.738308189515095], + [-22.503473785484299, 71.5519360689742], + [-22.471109916128999, 71.260687228676403], + [-21.805550927593799, 71.509429090919895], + [-22.328189917047101, 71.053804088093599], + [-21.4758358571356, 70.541641136534395], + [-23.349023919293199, 70.439986085090794], + [-24.740000922782301, 71.332219228765695], + [-28.639718936978898, 72.124417207609298], + [-27.3268077947607, 71.7126310672557], + [-28.4669457838258, 71.552476198816393], + [-25.412363837420202, 71.350966193460394], + [-28.405421776155901, 70.977061226423501], + [-27.915686785899901, 70.869565161916896], + [-29.203685905982599, 70.393033177941703], + [-26.326943805575599, 70.378804226709306], + [-28.542779879551102, 70.044706091430598], + [-22.081112868417001, 70.137136187851496], + [-29.376944874245002, 68.199418199898702], + [-32.475824843780501, 68.621914065607498], + [-32.131943854002003, 67.848877117476505], + [-34.719992924034599, 66.33831717743], + [-37.193057872518501, 65.769148064024805], + [-37.810763905139297, 66.029293170294295], + [-37.1838868969526, 66.340549110613694], + [-38.106107808064003, 66.386935065830301], + [-38.246525809041003, 65.628307112212397], + [-40.0963949135853, 65.567215107833405], + [-41.155415860977001, 64.962343110114503], + [-40.3570169501468, 64.354222119077903], + [-41.567993923544897, 64.263880122381806], + [-40.515974882809402, 63.699571171727499], + [-43.144442831171297, 62.758612180738901], + [-42.165341822038201, 62.382628166432802], + [-42.980417870386901, 62.510968180176199], + [-42.115832937327603, 62.006662089399398], + [-43.2444419538189, 61.337359072343297], + [-42.634025830048202, 61.101172127625098], + [-43.612361906695497, 61.126381204409903], + [-42.751871868624796, 60.684229105147402], + [-44.199584801400498, 60.590890075143598], + [-43.137503788788997, 60.079438076613599], + [-45.152783885630498, 60.0741641831208], + [-44.824445794229902, 60.189994206340003], + [-44.470835929408601, 60.5572120910048], + [-45.977984937767502, 60.577912206782699], + [-45.252782840640101, 60.905818127254399], + [-46.208609819160799, 60.743323199079903], + [-45.489725833026696, 60.989158217294303], + [-45.200618904272197, 61.189633057605199], + [-46.065554871437499, 60.921100180804601], + [-45.6536067927141, 61.142221157437099], + [-45.774161862404497, 61.334164058482003], + [-48.226940928766801, 60.816097061947403], + [-47.920697869474502, 61.322563169178601], + [-49.438610917921302, 61.841305076558598], + [-48.842567914118, 62.076106157401902], + [-50.315624872320001, 62.494300095113097], + [-49.699718937230301, 63.0552611459918], + [-51.558191897770101, 63.707221167138997], + [-50.0480639610143, 64.1956691999779], + [-51.445826953333203, 64.078048131683204], + [-51.761807940123802, 64.188514072131298], + [-50.176178836837998, 64.446787164140204], + [-50.856659960000101, 64.633042105674505], + [-49.584014810197097, 64.339786134836402], + [-49.998815921131403, 64.864702152112898], + [-50.981948960979302, 65.216800089235306], + [-50.6340988047601, 64.7583311066536], + [-52.004726894215302, 64.201798215235002], + [-51.2498609063909, 65.015470127159901], + [-52.562150949581998, 65.320057090018295], + [-50.5469429371079, 65.707687088266994], + [-52.495901895440802, 65.3879711254108], + [-53.264996834497403, 65.742823188286096], + [-51.813890907306799, 65.964709097404494], + [-51.831944856244903, 66.055816194224505], + [-53.461313915545702, 66.028798135091407], + [-50.001110886227103, 66.976435155996697], + [-53.4766678858253, 66.098872186938394], + [-52.232561942948003, 66.837484155717505], + [-53.956664948582898, 67.0972151959689], + [-52.151390921817999, 67.369978084037996], + [-51.189434928040001, 67.123603103619701], + [-50.350490907418198, 67.184317084164505], + [-51.1516618812744, 67.423312134092299], + [-53.252225831507502, 67.320541116056901], + [-53.7983368321268, 67.202776214303199], + [-53.8806868517607, 67.260421077172794], + [-52.4961089284498, 67.769722111703899], + [-50.700554891910301, 67.4916581727758], + [-50.195825850298, 67.467349144756597], + [-50.070968852609703, 67.511386155420794], + [-51.059996884977402, 67.974157066000103], + [-53.751185944424101, 67.604626111328699], + [-53.191385954776599, 68.041648150557506], + [-53.081675894359002, 68.062762164715394], + [-52.748333806748597, 67.968325105392594], + [-52.212221913731497, 67.923037179494798], + [-52.062353819812003, 67.976515230646299], + [-53.321111832394998, 68.184415096086894], + [-51.188606963642101, 68.063599181568904], + [-50.5694518675963, 67.900816083562404], + [-50.144165833950602, 67.939417094726096], + [-53.388611801769599, 68.329027066148498], + [-50.217497931570897, 68.956642184730896], + [-51.120269809775898, 69.2003981556757], + [-50.211944920701697, 70.015276223470906], + [-54.627218880872498, 70.6530431679316], + [-54.063323828598101, 70.829713067881798], + [-50.676389864988003, 70.322770197996604], + [-50.491529839784199, 70.5115180851801], + [-52.555139822832302, 71.176231172044595], + [-51.345971831782599, 71.484157149861204], + [-52.986185900356297, 71.418034159544206], + [-51.8027848855683, 71.594434162211101], + [-51.643412886887702, 71.708959123102403], + [-53.250551965438703, 71.702776128388805], + [-52.687142895911798, 71.9999831607565], + [-53.557559957216903, 72.352900177459404], + [-53.915552886858997, 71.4419291215454], + [-55.905272872809697, 71.679979195706395], + [-54.826523942773001, 71.917075074007499], + [-54.536660966349999, 72.041365119763398], + [-54.389312807808302, 72.222976151649405], + [-55.626389854301102, 72.457489230298705], + [-54.299312960400798, 72.481231138747503], + [-55.695131854091798, 73.064152105454795], + [-55.089620820073002, 73.354285146799], + [-56.129165912467997, 74.278315207896995], + [-57.323339852161403, 74.104705205528504], + [-56.192912968504302, 74.5502682364788], + [-60.878609924647201, 76.152484120366907], + [-68.500565858326794, 76.086928081982094], + [-69.631514934553095, 76.373659076455795], + [-67.982228964509204, 76.679425205456198], + [-71.375273838465503, 77.056093183063197], + [-66.055553966382803, 77.491360219400207], + [-73.053602935455402, 78.1572072101293], + [-65.976668857893202, 79.101658101990793], + [-63.784871914654097, 80.144857201266007], + [-67.481945851991597, 80.324155163145207], + [-61.0566658561102, 81.119710094219997], + [-61.452224967146897, 81.753058099314401], + [-60.8066638585397, 81.879976206999203], + [-56.478338900650002, 81.332488227322401], + [-59.465825891243597, 81.992764102271494], + [-54.505277947307, 82.365535165443504], + [-53.634446819983999, 81.513316221811493], + [-49.619789946516299, 81.640198119674693], + [-51.066386912700104, 81.935254188043501], + [-49.435136954321401, 81.929008161417798], + [-51.117497914388103, 82.491364128626799], + [-50.317505939035897, 82.518328208303203], + [-44.639999835044101, 81.754165181088894], + [-44.6180578564882, 82.276651116989797], + [-42.3008367959902, 82.214992160678406], + [-45.763334958042599, 82.761940178151093], + [-39.753332835749298, 82.401517167133505], + [-46.889027910410903, 82.962775105023596], + [-38.574440949606299, 82.744138189223094], + [-39.1472188076146, 82.979425085175805], + [-36.877913902581298, 83.148607095722298], + [-38.856392918152103, 83.431657093587106], + ], + ], + ], + }, + }, + ], +}); diff --git a/employee-portal/src/lib/businessModules/statistics/components/geoshapes/ImportGeoShapeSidebar/ImportGeoShapeStep.tsx b/employee-portal/src/lib/businessModules/statistics/components/geoshapes/ImportGeoShapeSidebar/ImportGeoShapeStep.tsx index 9ec1f29d4d9996f99ed1455715413d125325e94e..7485bd0769f982b4438461dd257f37a3451be632 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/geoshapes/ImportGeoShapeSidebar/ImportGeoShapeStep.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/geoshapes/ImportGeoShapeSidebar/ImportGeoShapeStep.tsx @@ -20,7 +20,7 @@ export function ImportGeoShapeStep({ <DeletableFileField name={fieldName("file")} label="Karten-Datei für den Import auswählen:" - required="Bitte Shapefile importieren." + required="Bitte GeoJSON-Datei importieren." accept={FileType.Geojson} placeholder=".geojson" /> diff --git a/employee-portal/src/lib/businessModules/statistics/components/shared/charts/ChoroplethMap.tsx b/employee-portal/src/lib/businessModules/statistics/components/shared/charts/ChoroplethMap.tsx index a12eea003a14f5170ac68fd8d3acb53449065e92..f973458f3aab5695229a3b7d0414746fe8abeba2 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/shared/charts/ChoroplethMap.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/shared/charts/ChoroplethMap.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { EChartsOption, registerMap } from "echarts"; +import { EChartsOption, MapSeriesOption, registerMap } from "echarts"; import { useState } from "react"; import { isNonNullish, randomString } from "remeda"; @@ -24,6 +24,7 @@ export interface ChoroplethMapProps { characteristicParameter?: DiagramCharacteristicParameter; geoJson: string; eChartApi?: (eChartApi: ChartApi) => void; + additionalEchartsSeriesOptions?: Partial<MapSeriesOption>; } export function getDefinedDiagramValues( @@ -85,15 +86,15 @@ export function ChoroplethMap(props: ChoroplethMapProps) { series: [ { name: getChoroplethAggregationMethod(props.characteristicParameter), - type: "map", + type: "map" as const, map: mapId, data: props.diagramData, roam: true, select: { disabled: true }, emphasis: { label: { show: false } }, + ...(props.additionalEchartsSeriesOptions ?? {}), }, ], - legend: undefined, }; diff --git a/employee-portal/src/lib/businessModules/statistics/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/statistics/shared/sideNavigationItem.tsx index 893f8bbba6ee375f1db7a1a7881f6d2eb6cbcf1b..161c22313e6139147f86d2fa1d087c75f2983d4f 100644 --- a/employee-portal/src/lib/businessModules/statistics/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/statistics/shared/sideNavigationItem.tsx @@ -19,6 +19,7 @@ export function useSideNavigationItems(): UseSideNavigationItemsResult { isLoading: false, items: [ { + type: "SideNavigationParentItem", name: "Statistik", decorator: <BarChartOutlined />, subItems: [ diff --git a/employee-portal/src/lib/businessModules/stiProtection/api/queries/procedures.ts b/employee-portal/src/lib/businessModules/stiProtection/api/queries/procedures.ts index 0b8e0cdc94e3efe07c2ab177e6c3c252e0ead7f3..d0f7ddafee22821b8400aa091f78d03d7350c016 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/api/queries/procedures.ts +++ b/employee-portal/src/lib/businessModules/stiProtection/api/queries/procedures.ts @@ -64,6 +64,16 @@ export function useStiProceduresQuery( mapSortOrder(sortState?.desc), page.pageNumber, page.pageSize, + undefined, // startDate + undefined, // endDate + undefined, // yearOfBirth + undefined, // appointmentStart + undefined, // appointmentEnd + undefined, // gender + undefined, // concern + undefined, // procedureStatus + undefined, // labStatus + undefined, // createdBy { signal }, ), diff --git a/employee-portal/src/lib/businessModules/stiProtection/components/textTemplates/TextTemplatesOverviewTable.tsx b/employee-portal/src/lib/businessModules/stiProtection/components/textTemplates/TextTemplatesOverviewTable.tsx index 082df986760072790236c73b1fa13324c486c4f5..7dd637a870f003b0d2134997d81ad243d1c2efd8 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/components/textTemplates/TextTemplatesOverviewTable.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/components/textTemplates/TextTemplatesOverviewTable.tsx @@ -41,12 +41,12 @@ export function TextTemplatesOverviewTable() { const addMutation = useCreateTextTemplate({ onSuccess: () => { - snackbar.confirmation("Die Vorlage wurde erzeugt."); + snackbar.confirmation("Die Vorlage wurde erstellt."); }, }); const updateMutation = useUpdateTextTemplate({ onSuccess: () => { - snackbar.confirmation("Der Vorlage wurde aktualisiert."); + snackbar.confirmation("Die Vorlage wurde aktualisiert."); }, }); const deleteMutation = useDeleteTextTemplate({ diff --git a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/ProcedureDetails.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/ProcedureDetails.tsx index e3edb70d8d800835447f1e08245fcdcb2a0f6637..0574556905f1a1952ff58fe74e0711521fcddecb 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/ProcedureDetails.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/ProcedureDetails.tsx @@ -7,6 +7,7 @@ import { ApiStiProtectionProcedure } from "@eshg/sti-protection-api"; import { Grid, Stack } from "@mui/joy"; +import { isDefined } from "remeda"; import { AdditionalDataSection } from "./AdditionalDataSection"; import { AnonIdentityDocumentCard } from "./AnonIdentityDocumentCard"; @@ -21,6 +22,7 @@ import { WaitingRoomSection } from "./WaitingRoomSection"; export function ProcedureDetails({ procedure, }: Readonly<{ procedure: ApiStiProtectionProcedure }>) { + const hasAccessCode = isDefined(procedure.person.accessCode); return ( <> <Grid container spacing={2}> @@ -28,9 +30,11 @@ export function ProcedureDetails({ <Grid xxs={12} mb={2}> <PersonDetails procedure={procedure} /> </Grid> - <Grid xxs={12} mb={2}> - <AnonIdentityDocumentCard procedure={procedure} /> - </Grid> + {hasAccessCode && ( + <Grid xxs={12} mb={2}> + <AnonIdentityDocumentCard procedure={procedure} /> + </Grid> + )} <Grid xxs={12}> <AppointmentDetails procedure={procedure} /> </Grid> @@ -38,7 +42,7 @@ export function ProcedureDetails({ <Grid xxs={12} lg={4}> <Stack spacing={2}> <AdditionalDataSection procedure={procedure} /> - <CheckPinSection procedure={procedure} /> + {hasAccessCode && <CheckPinSection procedure={procedure} />} <WaitingRoomSection procedure={procedure} /> <FinalProcedureActionPanel procedure={procedure} /> </Stack> diff --git a/employee-portal/src/lib/businessModules/stiProtection/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/stiProtection/shared/sideNavigationItem.tsx index 75528b61624556ee11f8b0e23723331943094a10..9cc0d87cbd6ca77f32789f376652dd20df7a2955 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/shared/sideNavigationItem.tsx @@ -6,8 +6,8 @@ import { ApiUserRole } from "@eshg/base-api"; import { hasUserRole } from "@eshg/lib-employee-portal/helpers/accessControl"; import { + SideNavigationItem, SideNavigationSubItem, - UseSideNavigationItemsResult, } from "@eshg/lib-employee-portal/types/sideNavigation"; import { HivOutlined } from "@/lib/shared/components/icons/HivOutlined"; @@ -17,7 +17,6 @@ import { routes } from "./routes"; const sideNavigationItem = { name: "HIV-STI", decorator: <HivOutlined />, - accessCheck: hasUserRole(ApiUserRole.StiProtectionUser), }; const defaultSubItems: SideNavigationSubItem[] = [ @@ -48,12 +47,13 @@ const defaultSubItems: SideNavigationSubItem[] = [ }, ]; -export function useSideNavigationItems( - enabled: boolean, -): UseSideNavigationItemsResult { +export function resolveSideNavigationItems(): SideNavigationItem[] { const subItems = defaultSubItems; - return { - isLoading: false, - items: enabled ? [{ ...sideNavigationItem, subItems }] : [], - }; + return [ + { + type: "SideNavigationParentItem", + ...sideNavigationItem, + subItems, + }, + ]; } diff --git a/employee-portal/src/lib/businessModules/travelMedicine/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/travelMedicine/shared/sideNavigationItem.tsx index 507ee4b1a733b95f9a6d3eeca38b22eeff173cd6..ac5de3411d4ca50dceccadf8514e5af1fad6c90d 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/shared/sideNavigationItem.tsx @@ -3,22 +3,20 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiBaseFeature, ApiUserRole } from "@eshg/base-api"; +import { ApiUserRole } from "@eshg/base-api"; import { hasUserRole } from "@eshg/lib-employee-portal/helpers/accessControl"; -import { UseSideNavigationItemsResult } from "@eshg/lib-employee-portal/types/sideNavigation"; +import { + SideNavigationItem, + SideNavigationItemsProps, +} from "@eshg/lib-employee-portal/types/sideNavigation"; import { VaccinesOutlined } from "@mui/icons-material"; import { isPlainObject } from "remeda"; -import { useIsNewFeatureEnabled } from "@/lib/baseModule/api/queries/feature"; - import { routes } from "./routes"; -export function useSideNavigationItems( - enabled: boolean, -): UseSideNavigationItemsResult { - // their toggles - const isInboxEnabled = useIsNewFeatureEnabled(ApiBaseFeature.Inbox); - +export function resolveSideNavigationItems({ + isInboxEnabled, +}: SideNavigationItemsProps): SideNavigationItem[] { const SUB_NAVIGATION_ITEMS = [ { name: "Vorgänge", @@ -72,16 +70,12 @@ export function useSideNavigationItems( }, ]; - return { - isLoading: false, - items: enabled - ? [ - { - name: "Impfberatung", - decorator: <VaccinesOutlined />, - subItems: SUB_NAVIGATION_ITEMS.filter(isPlainObject), - }, - ] - : [], - }; + return [ + { + type: "SideNavigationParentItem", + name: "Impfberatung", + decorator: <VaccinesOutlined />, + subItems: SUB_NAVIGATION_ITEMS.filter(isPlainObject), + }, + ]; } diff --git a/employee-portal/src/lib/shared/components/archiving/shared/sideNavigationItem.tsx b/employee-portal/src/lib/shared/components/archiving/shared/sideNavigationItem.tsx index 226e5b22711560c12994b04e1640918b1caf42eb..c5e099cda0075dd1946e81861c57a27f78d3dd30 100644 --- a/employee-portal/src/lib/shared/components/archiving/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/shared/components/archiving/shared/sideNavigationItem.tsx @@ -12,6 +12,7 @@ import { routes } from "./routes"; export const sideNavigationItems: SideNavigationItem[] = [ { + type: "SideNavigationParentItem", name: "Archivierung", decorator: <Inventory2Outlined />, subItems: [ @@ -48,6 +49,7 @@ export const sideNavigationItems: SideNavigationItem[] = [ ], }, { + type: "SideNavigationParentItem", name: "Archiv-Admin", decorator: <Inventory2Outlined />, subItems: [ diff --git a/employee-portal/src/lib/shared/components/chip/ChipWithTooltip.tsx b/employee-portal/src/lib/shared/components/chip/ChipWithTooltip.tsx index a89b55021e52a6e48247bf38ffa48820674aa0a1..bec3c6795a36f3ac6f580219b94db96358a5ca36 100644 --- a/employee-portal/src/lib/shared/components/chip/ChipWithTooltip.tsx +++ b/employee-portal/src/lib/shared/components/chip/ChipWithTooltip.tsx @@ -59,6 +59,7 @@ export function ChipWithTooltip(props: Props) { onClick={() => { setOpen(true); }} + onKeyDown={(event) => event.stopPropagation()} > {props.name} </StyledChip> diff --git a/employee-portal/src/lib/shared/components/filterSettings/ActiveFilter.tsx b/employee-portal/src/lib/shared/components/filterSettings/ActiveFilter.tsx index 420c9f54cd1a7e8835467f4e6b58fb1a0c1a1e95..4c0090585c036d568816c2d593df68646d79d6c0 100644 --- a/employee-portal/src/lib/shared/components/filterSettings/ActiveFilter.tsx +++ b/employee-portal/src/lib/shared/components/filterSettings/ActiveFilter.tsx @@ -4,7 +4,7 @@ */ import { ButtonLink } from "@eshg/lib-portal/components/buttons/ButtonLink"; -import { Chip, ChipDelete, Stack, Typography } from "@mui/joy"; +import { Chip, ChipDelete, List, ListItem, Stack, Typography } from "@mui/joy"; import { useState } from "react"; export interface ActiveFilter<TKey extends string = string> { @@ -58,7 +58,14 @@ function ActiveFilterList<TKey extends string = string>( return ( <Stack gap={1} data-testid="activeFilterList"> - <Stack gap={1} flexDirection="row" flexWrap="wrap"> + <Stack + aria-label="Filter" + component={List} + sx={{ "--List-padding": 0 }} + gap={1} + flexDirection="row" + flexWrap="wrap" + > {props.filterValues .slice(0, showAll ? props.filterValues.length : props.maxVisible) .map((filterValue) => ( @@ -89,6 +96,7 @@ function ActiveFilterChip<TKey extends string = string>( ) { return ( <Chip + component={ListItem} variant={"soft"} color={"primary"} endDecorator={ @@ -98,6 +106,8 @@ function ActiveFilterChip<TKey extends string = string>( sx={{ alignItems: "flex-start", gap: 0.75, + "--ListItem-paddingX": 0, + "--ListItem-paddingY": 0, }} slotProps={{ label: { diff --git a/employee-portal/src/lib/shared/components/pagination/IconButton.tsx b/employee-portal/src/lib/shared/components/pagination/IconButton.tsx index 21883706c61e7ff7eeaf4ed51f4ce578e3a29ed3..8b23b3ac86d5b7498b0ff13abb30aee7054d2b1a 100644 --- a/employee-portal/src/lib/shared/components/pagination/IconButton.tsx +++ b/employee-portal/src/lib/shared/components/pagination/IconButton.tsx @@ -3,28 +3,40 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ButtonProps, IconButton as JoyIconButton } from "@mui/joy"; -import { SxProps } from "@mui/joy/styles/types"; +import { ButtonProps, IconButton as JoyIconButton, useTheme } from "@mui/joy"; +import { SxProps, Variants } from "@mui/joy/styles/types"; import { PropsWithChildren } from "react"; export function IconButton( props: PropsWithChildren<{ - disabled: boolean; + disabled?: boolean; + ariaDisabled?: boolean; label: string; onClick: () => void; sx?: SxProps; variant?: ButtonProps["variant"]; }>, ) { + const theme = useTheme(); + + // Extract the disabled styles from JoyIconButton + const variant = props.variant ?? "soft"; + const disabledStyles = + theme.variants[`${variant}Disabled` satisfies keyof Variants].primary; + return ( <JoyIconButton aria-label={props.label} + aria-disabled={props.ariaDisabled} disabled={props.disabled} color="primary" - variant={props.variant ?? "soft"} + variant={variant} size="sm" onClick={props.onClick} - sx={props.sx} + sx={{ + '&[aria-disabled="true"]': disabledStyles, + ...(props.sx ?? {}), + }} > {props.children} </JoyIconButton> diff --git a/employee-portal/src/lib/shared/components/pagination/Pagination.tsx b/employee-portal/src/lib/shared/components/pagination/Pagination.tsx index 3e44379341fe782955846cfca40519b3021d4b1a..cde71397dc8141be62ee7a8710e4570af2054bdb 100644 --- a/employee-portal/src/lib/shared/components/pagination/Pagination.tsx +++ b/employee-portal/src/lib/shared/components/pagination/Pagination.tsx @@ -48,10 +48,18 @@ export function Pagination(props: Readonly<PaginationProps>) { } function goToPreviousPage() { + if (props.pageNumber === 0) { + return; + } + props.onPageChange(props.pageNumber - 1); } function goToNextPage() { + if (props.pageNumber === lastPage) { + return; + } + props.onPageChange(props.pageNumber + 1); } @@ -96,14 +104,14 @@ export function Pagination(props: Readonly<PaginationProps>) { > <IconButton label={"Zur ersten Seite"} - disabled={isFirstPage} + ariaDisabled={isFirstPage} onClick={goToFirstPage} > <SkipPrevious /> </IconButton> <IconButton label={"Zur vorherigen Seite"} - disabled={isFirstPage} + ariaDisabled={isFirstPage} onClick={goToPreviousPage} > <ChevronLeft /> @@ -135,14 +143,14 @@ export function Pagination(props: Readonly<PaginationProps>) { > <IconButton label={"Zur nächsten Seite"} - disabled={isLastPage} + ariaDisabled={isLastPage} onClick={goToNextPage} > <ChevronRight /> </IconButton> <IconButton label={"Zur letzten Seite"} - disabled={isLastPage} + ariaDisabled={isLastPage} onClick={goToLastPage} > <SkipNext /> @@ -156,14 +164,14 @@ export function Pagination(props: Readonly<PaginationProps>) { > <IconButton label={"Zur vorherigen Seite"} - disabled={isFirstPage} + ariaDisabled={isFirstPage} onClick={goToPreviousPage} > <ChevronLeft /> </IconButton> <IconButton label={"Zur nächsten Seite"} - disabled={isLastPage} + ariaDisabled={isLastPage} onClick={goToNextPage} > <ChevronRight /> diff --git a/lib-portal/src/components/formFields/appointmentPicker/AppointmentCalendar.tsx b/lib-portal/src/components/formFields/appointmentPicker/AppointmentCalendar.tsx index 345c2d7577b32e3d66b4be78764ec45586ac11c1..5799d7719b3403926f389675ed79d1c53975890f 100644 --- a/lib-portal/src/components/formFields/appointmentPicker/AppointmentCalendar.tsx +++ b/lib-portal/src/components/formFields/appointmentPicker/AppointmentCalendar.tsx @@ -11,6 +11,7 @@ import { Day, DaysGrid } from "./Day"; import { MonthSelection, MonthSelectionProps } from "./MonthSelection"; import { WeekdayHeaders } from "./WeekdayHeaders"; import { + Weekday, getDaysInAndAroundMonth, getMonthInterval, monthLabel, @@ -18,7 +19,7 @@ import { export type MonthSelectionPassThroughProps = Omit< MonthSelectionProps, - "label" | "nextMonthLabel" | "prevMonthLabel" + "label" | "nextMonthLabel" | "prevMonthLabel" | "slots" >; export interface AppointmentCalendarProps extends MonthSelectionPassThroughProps { @@ -28,6 +29,12 @@ export interface AppointmentCalendarProps monthSelectionLabel: string; nextMonthLabel: string; prevMonthLabel: string; + showWeekdays?: Weekday[]; + padDays?: boolean; + errorMessageId?: string; + slots?: { + monthSelection?: MonthSelectionProps["slots"]; + }; } export function AppointmentCalendar({ selectedDay, @@ -38,6 +45,10 @@ export function AppointmentCalendar({ monthSelectionLabel, nextMonthLabel, prevMonthLabel, + showWeekdays, + padDays, + slots, + errorMessageId, }: AppointmentCalendarProps) { return ( <Box sx={{ width: "min-content" }}> @@ -48,12 +59,16 @@ export function AppointmentCalendar({ label={monthSelectionLabel} nextMonthLabel={nextMonthLabel} prevMonthLabel={prevMonthLabel} + slots={slots?.monthSelection} /> <MonthGrid + errorMessageId={errorMessageId} currentMonth={currentMonth} selectedDay={selectedDay} onDateSelected={onDateSelected} appointments={appointments} + padDays={padDays} + showWeekdays={showWeekdays} /> </Row> </Box> @@ -65,18 +80,36 @@ export function MonthGrid({ selectedDay, onDateSelected, currentMonth, + showWeekdays, + padDays, + errorMessageId, }: Pick< AppointmentCalendarProps, - "selectedDay" | "onDateSelected" | "currentMonth" | "appointments" + | "selectedDay" + | "onDateSelected" + | "currentMonth" + | "appointments" + | "showWeekdays" + | "padDays" + | "errorMessageId" >) { const currentInterval = getMonthInterval(currentMonth); - const days = getDaysInAndAroundMonth(currentInterval); + const days = getDaysInAndAroundMonth(currentInterval, { + showWeekdays, + padDays, + }); return ( - <DaysGrid role="grid" aria-label={monthLabel(currentMonth)}> - <WeekdayHeaders /> - {days.map((t) => ( + <DaysGrid + role="grid" + columns={showWeekdays?.length} + padDays={padDays} + aria-label={monthLabel(currentMonth)} + aria-describedby={errorMessageId} + > + <WeekdayHeaders showWeekdays={showWeekdays} /> + {days.map((t, index) => ( <Day - key={t.toString()} + key={index} date={t} appointments={appointments} selectedDay={selectedDay} diff --git a/lib-portal/src/components/formFields/appointmentPicker/AppointmentPickerField.tsx b/lib-portal/src/components/formFields/appointmentPicker/AppointmentPickerField.tsx index 64c088beac9b7c0f40c1ffec4a3512adb8660c94..7710b7c783655e40b05306ee901b2b7ce672f10a 100644 --- a/lib-portal/src/components/formFields/appointmentPicker/AppointmentPickerField.tsx +++ b/lib-portal/src/components/formFields/appointmentPicker/AppointmentPickerField.tsx @@ -7,7 +7,7 @@ import { FormControl, FormHelperText, Stack } from "@mui/joy"; import { SxProps } from "@mui/joy/styles/types"; import { isSameDay } from "date-fns"; import { useFormikContext } from "formik"; -import { ReactNode, useState } from "react"; +import { ReactNode, useEffect, useId, useState } from "react"; import { isDate } from "remeda"; import { getPropertyIf } from "../../../helpers/getProperty"; @@ -15,6 +15,7 @@ import { useBaseField } from "../BaseField"; import { AppointmentCalendar, + AppointmentCalendarProps, MonthSelectionPassThroughProps, } from "./AppointmentCalendar"; import { @@ -23,18 +24,22 @@ import { AppointmentListProps, useAppointmentList, } from "./AppointmentListForDate"; +import { Weekday } from "./helpers"; export { FIELD_LABELS_DE } from "./labels"; export interface Appointment { start: Date; + end?: Date; } export interface AppointmentPickerLayoutProps { calendar: ReactNode; + calendarError?: ReactNode; appointmentList: ReactNode; sx?: SxProps; className?: string; + labels: AppointmentPickerFieldLabels; } export interface AppointmentPickerFieldLabels { @@ -44,6 +49,8 @@ export interface AppointmentPickerFieldLabels { prevMonth: string; requiredDay: string; requiredAppointment: string; + calendarLabel?: string; + availableLegend?: string; } export interface AppointmentPickerFieldProps<T extends Appointment> @@ -55,10 +62,17 @@ export interface AppointmentPickerFieldProps<T extends Appointment> active?: boolean; monthAppointments: T[]; onAppointmentSelected?: (d: T) => unknown; + onDateSelected?: (d: Date) => unknown; isAppointmentEqual?: (apt1: T, apt2: T) => boolean; layout?: (props: AppointmentPickerLayoutProps) => ReactNode; appointmentList?: (props: AppointmentListProps<T>) => ReactNode; labels: AppointmentPickerFieldLabels; + showWeekdays?: Weekday[]; + padDays?: boolean; + autoSelectFirst?: true; + slots?: { + calendar?: AppointmentCalendarProps["slots"]; + }; } export function AppointmentPickerField<T extends Appointment>({ @@ -74,6 +88,11 @@ export function AppointmentPickerField<T extends Appointment>({ appointmentList: AppointmentListOverride, layout, labels, + showWeekdays, + slots, + padDays, + onDateSelected, + autoSelectFirst, ...props }: AppointmentPickerFieldProps<T>) { const { @@ -84,11 +103,11 @@ export function AppointmentPickerField<T extends Appointment>({ requiredDay: requiredDayWarning, requiredAppointment: requiredAppointmentWarning, } = labels; - const { getFieldMeta } = useFormikContext(); - const { value } = getFieldMeta(props.name); - const [selectedDay, setSelectedDayRaw] = useState<Date | undefined>( - getPropertyIf(value, "start", isDate), - ); + const { getFieldMeta, getFieldHelpers } = useFormikContext(); + const { value, error } = getFieldMeta(props.name); + const { setValue } = getFieldHelpers(props.name); + const start = getPropertyIf(value, "start", isDate); + const [selectedDay, setSelectedDayRaw] = useState<Date | undefined>(start); const requiredWarning = selectedDay == null ? requiredDayWarning : requiredAppointmentWarning; const field = useBaseField<T | null>({ @@ -107,17 +126,51 @@ export function AppointmentPickerField<T extends Appointment>({ if (!selectedDay || !isSameDay(d, selectedDay)) { void field.helpers.setValue(null); } + onDateSelected?.(d); } + // When auto select first is on + // auto-select the first appointment in the list + useEffect(() => { + const appt = monthAppointments[0]; + if (autoSelectFirst == null || selectedDay != null || appt == null) { + return; + } + setSelectedDayRaw(appt.start); + onDateSelected?.(appt.start); + void setValue(appt); + onAppointmentSelected?.(appt); + }, [ + selectedDay, + setValue, + monthAppointments, + autoSelectFirst, + onDateSelected, + onAppointmentSelected, + ]); + const dateAppointments = monthAppointments.map((t) => t.start); const Layout = layout ?? DefaultLayout; const AppointmentList = AppointmentListOverride ?? AppointmentListForDate; + const calendarErrorId = useId(); + const calendarError = + selectedDay == null && error ? ( + <FormHelperText + component="p" + sx={(theme) => ({ my: 1, color: theme.palette.danger.plainColor })} + id={calendarErrorId} + aria-live="polite" + > + {error} + </FormHelperText> + ) : undefined; return ( <Layout className={className} sx={sx} + labels={labels} calendar={ <AppointmentCalendar selectedDay={active ? selectedDay : undefined} @@ -128,10 +181,19 @@ export function AppointmentPickerField<T extends Appointment>({ monthSelectionLabel={monthSelectionLabel} nextMonthLabel={nextMonthLabel} prevMonthLabel={prevMonthLabel} + showWeekdays={showWeekdays} + slots={slots?.calendar} + padDays={padDays} + errorMessageId={calendarErrorId} /> } + calendarError={calendarError} appointmentList={ - <FormControl error={field.error} required={field.required}> + <FormControl + error={field.error} + required={field.required} + sx={{ flex: 1 }} + > <AppointmentList {...listProps} field={field} @@ -140,7 +202,7 @@ export function AppointmentPickerField<T extends Appointment>({ isAppointmentEqual={isAppointmentEqual} /> {field.helperText != null && ( - <FormHelperText component="p" sx={{ my: 1 }}> + <FormHelperText component="p" sx={{ my: 1 }} aria-live="polite"> {field.helperText} </FormHelperText> )} @@ -154,6 +216,7 @@ function DefaultLayout({ sx, className, calendar, + calendarError, appointmentList, }: AppointmentPickerLayoutProps) { const givenSx = sx == null ? [] : sx instanceof Array ? sx : [sx]; @@ -166,6 +229,7 @@ function DefaultLayout({ aria-label={"Terminkalender"} > {calendar} + {calendarError} {appointmentList} </Stack> ); diff --git a/lib-portal/src/components/formFields/appointmentPicker/Day.tsx b/lib-portal/src/components/formFields/appointmentPicker/Day.tsx index 97128cbb154dcdb623ee2227d93c54d2612223c5..d242a978896c2e33ef44ec8a3372655c01399c18 100644 --- a/lib-portal/src/components/formFields/appointmentPicker/Day.tsx +++ b/lib-portal/src/components/formFields/appointmentPicker/Day.tsx @@ -23,19 +23,24 @@ export interface DayProps AppointmentCalendarProps, "monthSelectionLabel" | keyof MonthSelectionProps > { - date: Date; + date: Date | null; currentInterval: Interval; } -export const DaysGrid = styled("div")` - display: grid; - gap: 8px; - grid-template-columns: repeat(7, 36px); - grid-template-rows: repeat(7, 40px); - text-align: center; - justify-content: space-between; - width: 320px; -`; +export const DaysGrid = styled("div", { + shouldForwardProp: (propName) => + !["columns", "padDays"].includes(propName as string), +})<{ columns?: number; padDays?: boolean }>( + ({ columns = 7, padDays = true }) => ({ + display: "grid", + gap: "8px", + gridTemplateColumns: `repeat(${columns}, 36px)`, + gridTemplateRows: `repeat(${padDays ? 7 : 6}, 40px)`, + textAlign: "center", + justifyContent: "space-between", + width: "320px", + }), +); export function Day({ date, @@ -45,6 +50,9 @@ export function Day({ appointments: monthAppointments, }: DayProps) { const theme = useTheme(); + if (date == null) { + return <div></div>; + } const boldProp = isSunday(date) ? { fontWeight: "bold" } : { fontWeight: "normal" }; @@ -91,7 +99,9 @@ export function Day({ > {date.getDate()} </Button> - {hasAppointments && <AppointmentMarker aria-hidden />} + {hasAppointments && !isSelected ? ( + <AppointmentMarker aria-hidden /> + ) : null} </Stack> ); } diff --git a/lib-portal/src/components/formFields/appointmentPicker/MonthSelection.tsx b/lib-portal/src/components/formFields/appointmentPicker/MonthSelection.tsx index 66c1378b0968e1adeb9e4b73cac35272f2f84f6c..0b747b2d82863b4e9364c5bcfcfdc4aaedac264d 100644 --- a/lib-portal/src/components/formFields/appointmentPicker/MonthSelection.tsx +++ b/lib-portal/src/components/formFields/appointmentPicker/MonthSelection.tsx @@ -4,8 +4,8 @@ */ import { ChevronLeft, ChevronRight } from "@mui/icons-material"; -import { IconButton, Typography } from "@mui/joy"; -import { addMonths } from "date-fns"; +import { IconButton, IconButtonProps, Typography } from "@mui/joy"; +import { addMonths, startOfMonth } from "date-fns"; import { useId } from "react"; import { Row } from "../../Row"; @@ -18,6 +18,9 @@ export interface MonthSelectionProps { label: string; nextMonthLabel: string; prevMonthLabel: string; + slots?: { + arrows?: IconButtonProps; + }; } export function MonthSelection({ currentMonth, @@ -25,8 +28,12 @@ export function MonthSelection({ label, nextMonthLabel, prevMonthLabel, + slots, }: MonthSelectionProps) { const monthYearId = useId(); + const previousMonth = addMonths(currentMonth, -1); + const now = new Date(); + const nowMonth = startOfMonth(now); return ( <Row justifyContent="space-between" width="100%" alignItems="center"> <Typography level="title-md" id={monthYearId} aria-label={label}> @@ -39,7 +46,9 @@ export function MonthSelection({ variant="outlined" title={prevMonthLabel} aria-controls={monthYearId} - onClick={() => setCurrentMonth(addMonths(currentMonth, -1))} + onClick={() => setCurrentMonth(previousMonth)} + disabled={previousMonth < nowMonth} + {...slots?.arrows} > <ChevronLeft /> </IconButton> @@ -50,6 +59,7 @@ export function MonthSelection({ title={nextMonthLabel} aria-controls={monthYearId} onClick={() => setCurrentMonth(addMonths(currentMonth, 1))} + {...slots?.arrows} > <ChevronRight /> </IconButton> diff --git a/lib-portal/src/components/formFields/appointmentPicker/WeekdayHeaders.tsx b/lib-portal/src/components/formFields/appointmentPicker/WeekdayHeaders.tsx index 2d114146cbd5184c9419869f41683d8eab311ed5..d2013553d3ae44c45e24448e3267177992c1cf97 100644 --- a/lib-portal/src/components/formFields/appointmentPicker/WeekdayHeaders.tsx +++ b/lib-portal/src/components/formFields/appointmentPicker/WeekdayHeaders.tsx @@ -6,10 +6,10 @@ import { Box } from "@mui/joy"; import { PropsWithChildren } from "react"; -import { getWeekdayShortCodes } from "./helpers"; +import { Weekday, getWeekdayShortCodes } from "./helpers"; -export function WeekdayHeaders() { - const weekdayShortCodes = getWeekdayShortCodes(); +export function WeekdayHeaders({ showWeekdays }: { showWeekdays?: Weekday[] }) { + const weekdayShortCodes = getWeekdayShortCodes(showWeekdays); return ( <> {weekdayShortCodes.map((w) => ( diff --git a/lib-portal/src/components/formFields/appointmentPicker/helpers.ts b/lib-portal/src/components/formFields/appointmentPicker/helpers.ts index daf1b4f782a191cf87efd0fad38fb087a8adafc7..29088d737971be09472755f42c9f61ba0b943337 100644 --- a/lib-portal/src/components/formFields/appointmentPicker/helpers.ts +++ b/lib-portal/src/components/formFields/appointmentPicker/helpers.ts @@ -3,7 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { addDays, eachDayOfInterval, endOfMonth, startOfMonth } from "date-fns"; +import { + addDays, + eachDayOfInterval, + endOfMonth, + isSameSecond, + isWithinInterval, + startOfMonth, +} from "date-fns"; + +import { Appointment } from "./AppointmentPickerField"; export const dateInMonthForm = Intl.DateTimeFormat(undefined, { day: "numeric", @@ -15,17 +24,46 @@ export function getMonthInterval(date: Date) { const end = endOfMonth(date); return { start, end }; } +const allWeekdays = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +] as const satisfies Weekday[]; +export function getDaysInAndAroundMonth( + interval: { start: Date; end: Date }, + { + showWeekdays, + padDays = true, + }: { showWeekdays?: Weekday[]; padDays?: boolean } = {}, +) { + const daysInWeek = showWeekdays?.length ?? 7; + const weekdayValues: number[] = getWeekdayValues(showWeekdays); + const firstDayOfTheWeek = weekdayValues[0]; // True for Germany + + let start = + eachDayOfInterval(interval).find((d) => + weekdayValues.includes(d.getDay()), + ) ?? interval.start; + + if (firstDayOfTheWeek == null) { + throw Error("showWeekdays must include at least one day"); + } -export function getDaysInAndAroundMonth(interval: { start: Date; end: Date }) { - let { start } = interval; - const firstDayOfTheWeek = 1; // True for Germany const startDiff = start.getDay() - firstDayOfTheWeek; if (startDiff != 0) { - start = addDays(start, (startDiff > 0 ? 0 : -7) - startDiff); + start = addDays(start, (startDiff > 0 ? 0 : -daysInWeek) - startDiff); } - let days = eachDayOfInterval({ start, end: interval.end }); - const requiredPadding = Math.ceil(days.length / 7) * 7 - days.length; - if (requiredPadding > 0) { + let days = eachDayOfInterval({ start, end: interval.end }) + .filter((date) => weekdayValues.includes(date.getDay())) + .map((d) => (padDays || isWithinInterval(d, interval) ? d : null)); + + const requiredPadding = + Math.ceil(days.length / daysInWeek) * daysInWeek - days.length; + if (requiredPadding > 0 && padDays) { const last = days[days.length - 1]; const paddingDays = new Array(requiredPadding) .fill(last) @@ -43,9 +81,36 @@ export function monthLabel(currentMonth: Date) { const weekdaySortCodeForm = Intl.DateTimeFormat([], { weekday: "short" }); const startMonday = new Date("2024-09-30"); -const weekdays = [1, 2, 3, 4, 5, 6, 7].map((d) => addDays(startMonday, d - 1)); -export function getWeekdayShortCodes() { - return weekdays.map((d) => weekdaySortCodeForm.format(d)); +export type Weekday = + | "monday" + | "tuesday" + | "wednesday" + | "thursday" + | "friday" + | "saturday" + | "sunday"; + +const weekdays = [1, 2, 3, 4, 5, 6, 0].map((_, d) => addDays(startMonday, d)); +const weekdayValueMap = { + monday: 1, + tuesday: 2, + wednesday: 3, + thursday: 4, + friday: 5, + saturday: 6, + sunday: 0, +} as const satisfies Record<Weekday, number>; + +function getWeekdayValues(givenDays: Weekday[] = allWeekdays) { + return givenDays.map((t) => weekdayValueMap[t]); +} + +export function getWeekdayShortCodes(showWeekdays?: Weekday[]) { + const showWeekdayValues = new Set(getWeekdayValues(showWeekdays)); + + return weekdays + .filter((d) => (showWeekdayValues as Set<number>).has(d.getDay())) + .map((d) => weekdaySortCodeForm.format(d)); } export const timeForm = Intl.DateTimeFormat(undefined, { timeStyle: "short" }); @@ -55,3 +120,25 @@ export const dateFullForm = Intl.DateTimeFormat(undefined, { weekday: "long", year: "numeric", }); + +export function isSameAppointment( + apt1: Appointment | null, + apt2: Appointment | null, +) { + if (apt1 === apt2) { + return true; + } + if (apt1 == null || apt2 == null) { + return false; + } + if (!isSameSecond(apt1.start, apt2.start)) { + return false; + } + if (apt1.end === apt2.end) { + return true; + } + if (apt1.end == null || apt2.end == null) { + return false; + } + return isSameSecond(apt1.end, apt2.end); +} diff --git a/lib-portal/src/components/formFields/appointmentPicker/labels.ts b/lib-portal/src/components/formFields/appointmentPicker/labels.ts index 31b61add2b52fb6e63f931382cb61af27890dcb0..e54f0ae57ef42d6e8267471cc1bede7e8c6ae88b 100644 --- a/lib-portal/src/components/formFields/appointmentPicker/labels.ts +++ b/lib-portal/src/components/formFields/appointmentPicker/labels.ts @@ -11,6 +11,7 @@ const dateFormatter = Intl.DateTimeFormat(undefined, { month: "long", year: "numeric", }); + export const FIELD_LABELS_DE = { requiredAppointment: "Bitte einen Termin auswählen", requiredDay: "Bitte einen Tag auswählen", diff --git a/lib-portal/src/errorHandling/errorResolvers.ts b/lib-portal/src/errorHandling/errorResolvers.ts index 717043c794e0dff8bee199cc6e6671ec126abc87..40c875f5e7ecfd0a0d2df90bd78a46ab29218fc8 100644 --- a/lib-portal/src/errorHandling/errorResolvers.ts +++ b/lib-portal/src/errorHandling/errorResolvers.ts @@ -23,6 +23,7 @@ const STATUS_MAPPING: Record<number, PortalErrorCode> = { const ERROR_MAPPING: Record<ApiErrorCode, PortalErrorCode> = { [ApiErrorCode.NotFound]: PortalErrorCode.NotFound, [ApiErrorCode.Conflict]: PortalErrorCode.Conflict, + [ApiErrorCode.InternalServerError]: PortalErrorCode.UnexpectedError, [ApiErrorCode.Unauthorized]: PortalErrorCode.Unauthorized, [ApiErrorCode.InsufficientUserRights]: PortalErrorCode.InsufficientUserRights, [ApiErrorCode.Timeout]: PortalErrorCode.Timeout, diff --git a/packages/dental/src/api/models/ChildExamination.ts b/packages/dental/src/api/models/ChildExamination.ts index 1b10ea3945a2b6df2ce24cd45109e3e472b33a1f..c5b31e1fc1b222b84188bacfda6e5d3b43f16617 100644 --- a/packages/dental/src/api/models/ChildExamination.ts +++ b/packages/dental/src/api/models/ChildExamination.ts @@ -4,6 +4,7 @@ */ import { + ApiDentitionType, ApiFluoridationConsent, ApiGender, ApiProphylaxisSessionChildExamination, @@ -27,11 +28,13 @@ export interface ChildExamination { readonly status: ExaminationStatus; readonly result?: ExaminationResult; readonly note?: string; + readonly prophylaxisDentitionType?: ApiDentitionType; } export function mapChildExamination( response: ApiProphylaxisSessionChildExamination, ): ChildExamination { + const result = mapOptional(response.result, mapExaminationResult); return { childId: response.childId, examinationId: response.examinationId, @@ -43,8 +46,9 @@ export function mapChildExamination( gender: response.gender, currentFluoridationConsent: response.allFluoridationConsents[0], allFluoridationConsents: response.allFluoridationConsents, - status: mapToExaminationStatus(response.result), - result: mapOptional(response.result, mapExaminationResult), + result: result, + status: mapToExaminationStatus(result), note: response.note, + prophylaxisDentitionType: response.prophylaxisDentitionType, }; } diff --git a/packages/dental/src/api/models/Examination.ts b/packages/dental/src/api/models/Examination.ts index 11345c2b425e1599dd0fe261cb892eccc67265af..338d9c469509979a102a5d7be24841ed2ba28c44 100644 --- a/packages/dental/src/api/models/Examination.ts +++ b/packages/dental/src/api/models/Examination.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiExamination, ApiProphylaxisType } from "@eshg/dental-api"; +import { + ApiDentitionType, + ApiExamination, + ApiProphylaxisType, +} from "@eshg/dental-api"; import { BaseEntity, mapBaseEntity, @@ -21,6 +25,7 @@ export interface Examination extends BaseEntity, Versioned { readonly dateAndTime: Date; readonly prophylaxisType: ApiProphylaxisType; readonly screening: boolean; + readonly prophylaxisDentitionType?: ApiDentitionType; readonly fluoridation: boolean; readonly fluoridationConsentGiven?: boolean; readonly note?: string; @@ -29,16 +34,18 @@ export interface Examination extends BaseEntity, Versioned { } export function mapExamination(response: ApiExamination): Examination { + const result = mapOptional(response.result, mapExaminationResult); return { ...mapBaseEntity(response), ...mapVersioned(response), dateAndTime: response.dateAndTime, prophylaxisType: response.prophylaxisType, screening: response.isScreening, + prophylaxisDentitionType: response.prophylaxisDentitionType, fluoridation: response.isFluoridation, fluoridationConsentGiven: response.fluoridationConsentGiven, note: response.note, - result: mapOptional(response.result, mapExaminationResult), - status: mapToExaminationStatus(response.result), + result: result, + status: mapToExaminationStatus(result), }; } diff --git a/packages/dental/src/api/models/ExaminationResult.ts b/packages/dental/src/api/models/ExaminationResult.ts index cd7fcd93321e2257c459eae57ed17fa1ba0e6986..ccf7a7139dbc90f7d4b4931b82aa245a6b19e957 100644 --- a/packages/dental/src/api/models/ExaminationResult.ts +++ b/packages/dental/src/api/models/ExaminationResult.ts @@ -29,9 +29,9 @@ export interface FluoridationExaminationResult { export interface ScreeningExaminationResult { readonly type: "screening"; + readonly dentitionType: ApiDentitionType; readonly oralHygieneStatus?: ApiOralHygieneStatus; readonly fluorideVarnishApplied?: boolean; - readonly dentitionType: ApiDentitionType; readonly toothDiagnoses: ToothDiagnoses; } @@ -69,9 +69,9 @@ function mapScreeningExaminationResult( ): ScreeningExaminationResult { return { type: "screening", + dentitionType: response.dentitionType, oralHygieneStatus: response.oralHygieneStatus, fluorideVarnishApplied: response.fluorideVarnishApplied, - dentitionType: response.dentitionType, toothDiagnoses: mapToObj( response.toothDiagnoses, (toothDiagnosisResponse) => [ diff --git a/packages/dental/src/api/models/ExaminationStatus.ts b/packages/dental/src/api/models/ExaminationStatus.ts index 862b3c0e2121d831bd4ffe75112c28661c063ce7..218e76506a49e3df1c2cfd9fd66791c8b6ecb843 100644 --- a/packages/dental/src/api/models/ExaminationStatus.ts +++ b/packages/dental/src/api/models/ExaminationStatus.ts @@ -3,16 +3,78 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiExaminationResult } from "@eshg/dental-api"; +import { ApiTooth } from "@eshg/dental-api"; import { isDefined } from "remeda"; +import { + ExaminationResult, + FluoridationExaminationResult, + ScreeningExaminationResult, +} from "@/api/models/ExaminationResult"; +import { ToothDiagnosis } from "@/api/models/ToothDiagnosis"; +import { RELATED_TEETH } from "@/config/teeth"; + export type ExaminationStatus = "OPEN" | "CLOSED" | "NOT_PRESENT"; export function mapToExaminationStatus( - examinationResult: ApiExaminationResult | undefined, + examinationResult: ExaminationResult | undefined, ): ExaminationStatus { - if (examinationResult?.type === "AbsenceExaminationResult") { + if (examinationResult === undefined) { + return "OPEN"; + } + if (examinationResult.type === "absence") { return "NOT_PRESENT"; } - return isDefined(examinationResult) ? "CLOSED" : "OPEN"; + return requiredFieldsDefined(examinationResult) ? "CLOSED" : "OPEN"; +} + +function requiredFieldsDefined( + examinationResult: ScreeningExaminationResult | FluoridationExaminationResult, +) { + switch (examinationResult.type) { + case "screening": + return ( + isDefined(examinationResult.fluorideVarnishApplied) && + allRequiredDiagnosesSet(examinationResult.toothDiagnoses) + ); + case "fluoridation": + return isDefined(examinationResult.fluorideVarnishApplied); + default: + return false; + } +} + +const requiredSecondaryTeeth = new Set<ApiTooth>([ + "T11", + "T12", + "T13", + "T14", + "T15", + "T21", + "T22", + "T23", + "T24", + "T25", + "T31", + "T32", + "T33", + "T34", + "T35", + "T41", + "T42", + "T43", + "T44", + "T45", +]); + +function allRequiredDiagnosesSet( + diagnoses: Partial<Record<ApiTooth, ToothDiagnosis>>, +) { + return requiredSecondaryTeeth.values().every((secondaryTooth) => { + const primaryTooth = RELATED_TEETH[secondaryTooth]; + return ( + isDefined(diagnoses[secondaryTooth]) || + (isDefined(primaryTooth) && isDefined(diagnoses[primaryTooth])) + ); + }); } diff --git a/packages/dental/src/config/teeth.ts b/packages/dental/src/config/teeth.ts new file mode 100644 index 0000000000000000000000000000000000000000..89c33472f2f782911143581debe77e717e4279eb --- /dev/null +++ b/packages/dental/src/config/teeth.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2025 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiTooth } from "@eshg/dental-api"; + +/** + * Defines a mapping from milk teeth to permanent teeth and vice versa + */ +export const RELATED_TEETH: Partial<Record<ApiTooth, ApiTooth>> = { + T11: "T51", + T12: "T52", + T13: "T53", + T14: "T54", + T15: "T55", + + T21: "T61", + T22: "T62", + T23: "T63", + T24: "T64", + T25: "T65", + + T31: "T71", + T32: "T72", + T33: "T73", + T34: "T74", + T35: "T75", + + T41: "T81", + T42: "T82", + T43: "T83", + T44: "T84", + T45: "T85", + + T51: "T11", + T52: "T12", + T53: "T13", + T54: "T14", + T55: "T15", + + T61: "T21", + T62: "T22", + T63: "T23", + T64: "T24", + T65: "T25", + + T71: "T31", + T72: "T32", + T73: "T33", + T74: "T34", + T75: "T35", + + T81: "T41", + T82: "T42", + T83: "T43", + T84: "T44", + T85: "T45", +}; diff --git a/packages/dental/src/shared/useSideNavigationItems.tsx b/packages/dental/src/shared/sideNavigationItem.tsx similarity index 93% rename from packages/dental/src/shared/useSideNavigationItems.tsx rename to packages/dental/src/shared/sideNavigationItem.tsx index 4027ed7dd2f378a78c41019e13868d940b55d317..6c9e703466d511686bfa5b8b39256c015ce90813 100644 --- a/packages/dental/src/shared/useSideNavigationItems.tsx +++ b/packages/dental/src/shared/sideNavigationItem.tsx @@ -6,8 +6,8 @@ import { ApiUserRole } from "@eshg/base-api"; import { hasUserRole } from "@eshg/lib-employee-portal/helpers/accessControl"; import { + SideNavigationItem, SideNavigationSubItem, - UseSideNavigationItemsResult, } from "@eshg/lib-employee-portal/types/sideNavigation"; import { SvgIcon, SvgIconProps } from "@mui/joy"; @@ -31,14 +31,15 @@ const defaultSubItems: SideNavigationSubItem[] = [ }, ]; -export function useSideNavigationItems( - enabled: boolean, -): UseSideNavigationItemsResult { +export function resolveSideNavigationItems(): SideNavigationItem[] { const subItems = defaultSubItems; - return { - isLoading: false, - items: enabled ? [{ ...sideNavigationItem, subItems }] : [], - }; + return [ + { + type: "SideNavigationParentItem", + ...sideNavigationItem, + subItems, + }, + ]; } function DentalSidenavIcon(props: SvgIconProps) { diff --git a/packages/lib-employee-portal/src/types/sideNavigation.ts b/packages/lib-employee-portal/src/types/sideNavigation.ts index a0cae2c9098f0d077473031636f08860d743cf61..a1cee2b78d6b47f2cf526faaff3b7ebbab5f26bd 100644 --- a/packages/lib-employee-portal/src/types/sideNavigation.ts +++ b/packages/lib-employee-portal/src/types/sideNavigation.ts @@ -7,7 +7,8 @@ import { ReactNode } from "react"; import { AccessCheck } from "@/helpers/accessControl"; -export interface SideNavigationItemWithoutSubItems { +export interface SideNavigationLinkItem { + type: "SideNavigationLinkItem"; name: string; href: string; decorator: ReactNode; @@ -15,16 +16,19 @@ export interface SideNavigationItemWithoutSubItems { chip?: ReactNode; } -export interface SideNavigationItemWithSubItems { +export interface SideNavigationParentItem { + type: "SideNavigationParentItem"; name: string; decorator: ReactNode; subItems: SideNavigationSubItem[]; - /** - * Errors can occur when resolving the navigation items. - * This can happen, for example, when querying feature toggles of a module that's currently not available. - * In this case, the main navigation item is deactivated and an error icon with tooltip is displayed. - */ - error?: string; +} + +export interface SideNavigationSuspenseItem { + type: "SideNavigationSuspenseItem"; + name: string; + decorator: ReactNode; + accessCheck: AccessCheck; + component: (props: SideNavigationItemsProps) => ReactNode; } export interface SideNavigationSubItem { @@ -34,10 +38,15 @@ export interface SideNavigationSubItem { } export type SideNavigationItem = - | SideNavigationItemWithoutSubItems - | SideNavigationItemWithSubItems; + | SideNavigationLinkItem + | SideNavigationParentItem + | SideNavigationSuspenseItem; export interface UseSideNavigationItemsResult { isLoading: boolean; items: SideNavigationItem[]; } + +export interface SideNavigationItemsProps { + isInboxEnabled: boolean; +} diff --git a/packages/lib-vitest/src/matchers/toMatchValidationFile/toMatchValidationFile.ts b/packages/lib-vitest/src/matchers/toMatchValidationFile/toMatchValidationFile.ts index ca2f35c484a31111144931c202777370b148fbe9..12b230874286c52c4bbb15ff98e74d800160ec8e 100644 --- a/packages/lib-vitest/src/matchers/toMatchValidationFile/toMatchValidationFile.ts +++ b/packages/lib-vitest/src/matchers/toMatchValidationFile/toMatchValidationFile.ts @@ -86,6 +86,7 @@ function normalizeTestName(name: string): string { return name .replaceAll(/[ .:]/g, "_") .replaceAll(/'(\w+)'/g, "$1") + .replaceAll(/\+0/g, "0") .replaceAll(/'/g, "_") .replaceAll(/,/g, ""); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04b3039f5cca4bc58c60f7bc94a27b39255aa3d4..f4f2c68d767111abcd3fccb9539de0593bffbc6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -698,7 +698,7 @@ importers: dependencies: '@ducanh2912/next-pwa': specifier: 10.2.9 - version: 10.2.9(@types/babel__core@7.20.5)(next@14.2.14(@babel/core@7.26.0)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.92.1(esbuild@0.24.2)) + version: 10.2.9(@types/babel__core@7.20.5)(next@14.2.14(@babel/core@7.26.0)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.92.1) '@emotion/react': specifier: catalog:joy version: 11.14.0(@types/react@18.3.12)(react@18.3.1) @@ -1364,6 +1364,9 @@ importers: '@eshg/medical-registry-api': specifier: workspace:* version: link:../packages/medical-registry-api + '@eshg/official-medical-service-api': + specifier: workspace:* + version: link:../packages/official-medical-service-api '@eshg/school-entry-api': specifier: workspace:* version: link:../packages/school-entry-api @@ -9195,15 +9198,15 @@ snapshots: '@drauu/core@0.4.2': {} - '@ducanh2912/next-pwa@10.2.9(@types/babel__core@7.20.5)(next@14.2.14(@babel/core@7.26.0)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.92.1(esbuild@0.24.2))': + '@ducanh2912/next-pwa@10.2.9(@types/babel__core@7.20.5)(next@14.2.14(@babel/core@7.26.0)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.92.1)': dependencies: fast-glob: 3.3.2 next: 14.2.14(@babel/core@7.26.0)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: 7.6.3 - webpack: 5.92.1(esbuild@0.24.2) + webpack: 5.92.1 workbox-build: 7.1.1(@types/babel__core@7.20.5) workbox-core: 7.1.0 - workbox-webpack-plugin: 7.1.0(@types/babel__core@7.20.5)(webpack@5.92.1(esbuild@0.24.2)) + workbox-webpack-plugin: 7.1.0(@types/babel__core@7.20.5)(webpack@5.92.1) workbox-window: 7.1.0 transitivePeerDependencies: - '@types/babel__core' @@ -12265,7 +12268,7 @@ snapshots: debug: 4.4.0 enhanced-resolve: 5.17.1 eslint: 9.19.0 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.22.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.19.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.22.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.22.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.19.0))(eslint@9.19.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-bun-module: 1.1.0 @@ -12278,7 +12281,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.22.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.19.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.22.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.22.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.19.0))(eslint@9.19.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -12300,7 +12303,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.19.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.22.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.19.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.22.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.22.0(eslint@9.19.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.19.0))(eslint@9.19.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -15488,16 +15491,14 @@ snapshots: type-fest: 0.16.0 unique-string: 2.0.0 - terser-webpack-plugin@5.3.10(esbuild@0.24.2)(webpack@5.92.1(esbuild@0.24.2)): + terser-webpack-plugin@5.3.10(webpack@5.92.1): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.92.1(esbuild@0.24.2) - optionalDependencies: - esbuild: 0.24.2 + webpack: 5.92.1 terser@5.36.0: dependencies: @@ -16003,7 +16004,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.92.1(esbuild@0.24.2): + webpack@5.92.1: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.6 @@ -16026,7 +16027,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(esbuild@0.24.2)(webpack@5.92.1(esbuild@0.24.2)) + terser-webpack-plugin: 5.3.10(webpack@5.92.1) watchpack: 2.4.2 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -16275,12 +16276,12 @@ snapshots: workbox-sw@7.1.0: {} - workbox-webpack-plugin@7.1.0(@types/babel__core@7.20.5)(webpack@5.92.1(esbuild@0.24.2)): + workbox-webpack-plugin@7.1.0(@types/babel__core@7.20.5)(webpack@5.92.1): dependencies: fast-json-stable-stringify: 2.1.0 pretty-bytes: 5.6.0 upath: 1.2.0 - webpack: 5.92.1(esbuild@0.24.2) + webpack: 5.92.1 webpack-sources: 1.4.3 workbox-build: 7.1.0(@types/babel__core@7.20.5) transitivePeerDependencies: diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 45bde8e4a33d0387b61a743cd775edb5dc9bd001..0000000000000000000000000000000000000000 --- a/vitest.config.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright 2025 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// eslint-disable-next-line no-restricted-imports -import { defineConfig } from "vitest/config"; - -import { VITEST_COVERAGE_EXCLUDES, VITEST_OUT_DIR } from "./config/vitest.base"; - -// https://vitejs.dev/config/ -export default defineConfig({ - test: { - workspace: [ - "employee-portal", - "citizen-portal", - "admin-portal", - "lib-portal", - "packages/*", - ], - environment: "node", - reporters: ["default", "junit"], - outputFile: { - junit: `${VITEST_OUT_DIR}/junit.xml`, - }, - coverage: { - provider: "istanbul", - all: true, - reportsDirectory: `${VITEST_OUT_DIR}/coverage`, - reporter: ["text", "html", "cobertura"], - include: ["**/src/**/*"], - exclude: [ - "packages/*-api", - "e2e", - "performance-test", - "**/build", - "**/.next", - ...VITEST_COVERAGE_EXCLUDES, - ], - }, - }, -});