From c29d3bc49cb07470ac194521466474c08f8a8483 Mon Sep 17 00:00:00 2001 From: cronn Bot <no-reply@cronn.de> Date: Mon, 24 Feb 2025 12:52:19 +0000 Subject: [PATCH] Update source code --- .gitignore | 2 + README.adoc | 1 + backend/auditlog/gradle.lockfile | 34 +- .../de/eshg/auditlog/AuditLogController.java | 18 +- .../auditlog/crypto/AsymmetricEncryption.java | 10 +- backend/base/gradle.lockfile | 32 +- backend/base/openApi.json | 8 +- .../task/TaskAggregationController.java | 14 +- .../task/TaskAggregationService.java | 22 +- .../task/TaskAggregationSpecification.java | 4 +- .../TaskAggregationSpecificationBuilder.java | 16 +- .../base/config/DepartmentConfiguration.java | 228 ++ .../DepartmentConfigurationRepository.java | 11 + .../DepartmentConfigurationService.java | 76 + ...itialDepartmentConfigurationDefaults.java} | 6 +- .../base/department/DepartmentController.java | 63 +- .../de/eshg/base/mail/MailController.java | 46 +- .../GdprRightToObjectLetterGenerator.java | 22 +- .../base/street/MunicipalityDirectory.java | 7 +- .../base/street/StreetDirectoryService.java | 9 +- .../de/eshg/base/street/csv/CsvMapper.java | 20 +- .../testhelper/BaseDatabaseResetAction.java | 5 +- .../testhelper/BaseTestHelperResetAction.java | 13 +- .../migrations/0042_department_config.xml | 72 + .../main/resources/migrations/changelog.xml | 1 + .../src/main/groovy/eshg.service.gradle | 4 +- .../service/error/GlobalExceptionHandler.java | 6 + .../NormalizeSequenceIdCustomizer.java | 26 + .../serialization/ObjectMapperCustomizer.java | 22 + .../SerializationObjectMapperConfigurer.java | 17 - .../serialization/SerializationService.java | 43 +- backend/compliance-test/gradle.lockfile | 12 + backend/dental/gradle.lockfile | 15 + backend/dental/openApi.json | 62 +- .../de/eshg/dental/api/ExaminationDto.java | 1 + ...ProphylaxisSessionChildExaminationDto.java | 1 + .../domain/model/OralHygieneStatus.java | 4 +- .../de/eshg/dental/domain/model/Tooth.java | 122 +- .../eshg/dental/domain/model/ToothType.java | 12 + .../eshg/dental/mapper/ExaminationMapper.java | 10 + .../mapper/ProphylaxisSessionMapper.java | 1 + .../statistic/DentalChildAttributes.java | 17 + .../statistic/DentalChildDataSource.java | 59 + .../statistic/DmftCalculationHelper.java | 29 + .../statistic/model/OralHygieneStatus.java | 42 + .../CalculateDmftValuesRequest.java | 15 + .../DentalTestHelperController.java | 14 + .../de/eshg/dental/testhelper/DmftValues.java | 10 + backend/docker-compose.yaml | 15 + backend/file-commons/build.gradle | 4 + backend/file-commons/gradle.lockfile | 43 +- .../de/eshg/file/common/FileExtension.java | 3 +- .../java/de/eshg/file/common/FileType.java | 3 +- backend/inspection/gradle.lockfile | 15 + backend/inspection/openApi.json | 2 +- .../common/persistence/MediaFileContent.java | 15 +- .../MediaFileContentSerializer.java | 60 - .../InspectionProcedureConfiguration.java | 21 - backend/lib-auditlog/build.gradle | 1 + backend/lib-auditlog/gradle.lockfile | 2 +- .../lib/auditlog/AuditLogHousekeeping.java | 50 + .../domain/AuditLogEntryRepository.java | 2 + backend/lib-matrix-client/build.gradle | 2 +- .../eshg/lib/procedure/api/TaskListApi.java | 6 +- backend/lib-procedures/gradle.lockfile | 26 +- backend/lib-procedures/openApi.json | 2 +- .../gdpr/AbstractGdprZipEditorProvider.java | 14 + .../gdpr/DefaultGdprZipEditorProvider.java | 1 + .../gdpr/GdprValidationTaskController.java | 6 +- .../lib/procedure/gdpr/SerializationUtil.java | 28 + ...ialMedicalServicePublicSecurityConfig.java | 1 + backend/lib-statistics/gradle.lockfile | 27 +- backend/lib-xlsx-import/gradle.lockfile | 28 +- .../eshg/lib/xlsximport/ImportValidator.java | 3 +- .../de/eshg/lib/xlsximport/XlsxImport.java | 36 +- backend/measles-protection/gradle.lockfile | 15 + backend/measles-protection/openApi.json | 2 +- .../MeaslesGdprZipEditorProvider.java | 5 +- backend/medical-registry/gradle.lockfile | 15 + backend/medical-registry/openApi.json | 2 +- backend/official-medical-service/build.gradle | 2 + .../official-medical-service/gradle.lockfile | 25 +- backend/official-medical-service/openApi.json | 78 +- .../CitizenProcedureService.java | 8 +- .../CitizenPublicController.java | 16 +- .../document/OmsDocumentService.java | 42 +- .../notification/MailClient.java | 5 +- .../notification/NotificationService.java | 43 +- .../notification/NotificationText.java | 31 + .../statistics/AttributeUtil.java | 13 + .../statistics/OmsProcedureAttributes.java | 96 + .../statistics/OmsProcedureDataSource.java | 73 + .../src/main/resources/application.properties | 4 + .../resources/concerns/concerns.test.yaml | 215 ++ .../default/de/new_citizen_procedure.html | 18 + .../default/de/new_document.html | 14 + .../de/new_citizen_procedure.html | 18 + .../ga_frankfurt/de/new_document.html | 14 + backend/opendata/gradle.lockfile | 36 +- .../de/eshg/opendata/OpenDataService.java | 2 +- .../de/eshg/rest/service/error/ErrorCode.java | 2 + .../error/InternalServerErrorException.java | 20 + backend/school-entry/gradle.lockfile | 15 + backend/school-entry/openApi.json | 2 +- backend/statistics/gradle.lockfile | 26 +- backend/statistics/openApi.json | 2 +- .../aggregation/AnalysisService.java | 23 +- .../AbstractChartDiagramCreationService.java | 105 +- .../BarChartDiagramCreationService.java | 160 +- .../ChoroplethMapDiagramCreationService.java | 34 +- .../diagramcreation/DataPointHolder.java | 3 +- .../DiagramCreationService.java | 4 +- .../HistogramChartDiagramCreationService.java | 93 +- .../PieChartDiagramCreationService.java | 44 +- ...PointBasedChartDiagramCreationService.java | 90 +- .../statistics/mapper/AnalysisMapper.java | 107 +- .../src/main/resources/application.properties | 1 + backend/sti-protection/gradle.lockfile | 25 +- backend/sti-protection/openApi.json | 101 +- .../CitizenAppointmentService.java | 4 +- .../de/eshg/stiprotection/CitizenService.java | 5 +- .../StiProtectionProcedureController.java | 25 +- .../StiProtectionProcedureService.java | 181 +- .../api/CreatedByUserTypeDto.java | 14 + ...tStiProtectionProceduresFilterOptions.java | 28 + .../persistence/db/CreatedByUserType.java | 11 + .../stiprotection/persistence/db/Person.java | 7 +- .../db/StiProtectionProcedure.java | 22 +- .../waitingroom/WaitingRoomSpecification.java | 12 +- .../migrations/0055_filter_procedures.xml | 56 + .../main/resources/migrations/changelog.xml | 1 + .../testhelper/DefaultTestHelperService.java | 4 +- .../TestHelperServiceResetAction.java | 2 +- backend/travel-medicine/gradle.lockfile | 25 +- backend/travel-medicine/openApi.json | 2 +- .../TravelMedicineGdprZipEditorProvider.java | 8 +- build.gradle | 17 - buildSrc/src/main/groovy/next-app.gradle | 21 - buildSrc/src/main/groovy/vitest.gradle | 8 +- .../sexuelle-gesundheit/sexarbeit/page.tsx | 23 +- .../sexarbeit/termin-buchen/page.tsx | 12 + .../sexuelle-gesundheit/sti-beratung/page.tsx | 25 +- .../sti-beratung/termin-buchen/page.tsx | 12 + .../api/queries/citizenPublicApi.ts | 36 +- .../appointment/AppointmentForm.tsx | 24 +- .../appointment/AppointmentFormSidePanel.tsx | 4 +- .../appointment/AppointmentStepWrapper.tsx | 9 +- .../appointment/NoAppointmentCard.tsx | 2 +- .../appointment/steps/AffectedPersonForm.tsx | 2 +- .../appointment/steps/AppointmentStep.tsx | 6 +- .../appointment/steps/ConcerFilters.tsx | 97 + .../appointment/steps/ConcernStep.tsx | 94 +- .../appointment/steps/DocumentForm.tsx | 10 +- .../appointment/steps/InformationCard.tsx | 9 +- .../appointment/steps/OverviewSection.tsx | 177 +- .../steps/useConcernFilterValues.ts | 23 + .../locales/de/appointment.json | 13 +- .../locales/en/appointment.json | 19 +- .../shared/file/FileArrayField.tsx | 9 +- .../officialMedicalService/shared/helpers.ts | 42 +- .../api/queries/publicCitizenApi.ts | 21 + .../appointment/AppointmentDataContext.tsx | 45 + .../appointment/AppointmentPickerSection.tsx | 204 ++ .../appointment/AppointmentStepper.tsx | 37 + .../appointment/BookAppointmentPage.tsx | 23 + .../components/appointment/StepButtons.tsx | 25 + .../components/appointment/StepLayout.tsx | 130 + .../components/appointment/TimeSlotStep.tsx | 106 + .../components/shared/StepContext.tsx | 95 + .../stiProtection/locales/de/forms.json | 67 + .../stiProtection/locales/en/forms.json | 67 + .../pages/landingpage/Landingpage.tsx | 39 + .../pages/landingpage/LandingpageContent.tsx | 2 + .../landingpage/LandingpageSidePanel.tsx | 2 + .../shared/components/DetailsField.tsx | 2 +- .../shared/components/FormSheet.tsx | 8 +- citizen-portal/src/lib/i18n/client.ts | 2 +- .../src/lib/shared/components/layout/grid.tsx | 2 + config/vitest.base.ts | 2 +- docker-compose.yaml | 71 + docs/gradle.adoc | 2 +- docs/migration.adoc | 19 + .../examinations/[examinationId]/page.tsx | 5 +- .../examinations/[participantIndex]/page.tsx | 5 +- .../src/app/playground/charts/page.tsx | 100 +- .../app/playground/sideNavigation/page.tsx | 223 +- .../src/lib/baseModule/api/queries/tasks.ts | 7 +- .../components/layout/ChatSettingsSidebar.tsx | 2 +- .../layout/sideNavigation/SideNavigation.tsx | 16 +- .../filterNavigationItemsWithAccess.ts | 48 + ...onItem.tsx => CollapsedNavigationItem.tsx} | 82 +- .../items/ExpandedNavigationItem.tsx | 289 ++ .../sideNavigation/items/NavigationItem.tsx | 320 +-- .../sideNavigation/items/isItemSelected.ts | 4 +- ...lapsed.tsx => CollapsedNavigationList.tsx} | 36 +- .../lists/CollapsedNavigationListContext.ts | 24 + ...xpanded.tsx => ExpandedNavigationList.tsx} | 29 +- .../lists/NavigationItemGroup.tsx | 35 + .../lists/NavigationListCollapsedContext.ts | 14 - .../lists/NavigationListContext.ts | 22 + .../layout/sideNavigation/lists/StyledList.ts | 16 - .../components/layout/sideNavigation/types.ts | 5 - .../sideNavigation/useNavigationItems.ts | 67 - .../useSideNavigationItemProps.ts | 15 + .../sideNavigationItemsResolver.tsx | 128 +- ...gationItems.tsx => sideNavigationItem.tsx} | 32 +- .../chat/shared/sideNavigationItem.tsx | 1 + .../children/details/ChildExaminationForm.tsx | 4 +- .../AdditionalInformationFormSection.tsx | 50 +- .../examinations/ExaminationFormLayout.tsx | 13 +- .../dentalExamination/AddToothButton.tsx | 1 + .../FullDentitionOverview.tsx | 67 +- .../dentalExamination/Legend.tsx | 84 +- .../dentalExamination/Quadrant.tsx | 3 +- .../dentalExamination/QuadrantHeading.tsx | 5 +- .../ReadonlyExaminationResult.tsx | 43 + .../dentalExamination/ReadonlyToothButton.tsx | 83 + .../dentalExamination/RemoveToothButton.tsx | 43 +- .../dentalExamination/ResultInputField.tsx | 67 +- .../dentalExamination/Teeth.tsx | 71 +- .../dentalExamination/ToothForm.tsx | 8 +- .../DentalExaminationStoreProvider.tsx | 7 +- .../dentalExaminationStore/actions.ts | 280 -- .../actions/navigate.ts | 10 +- .../actions/navigateTo.ts | 55 + .../dentalExaminationStore/actions/result.ts | 211 ++ .../dentalExaminationStore/actions/tooth.ts | 161 ++ .../dentalExaminationStore/actions/utils.ts | 24 + .../dentalExaminationStore/constants.ts | 59 +- .../dentalExaminationStore.ts | 67 +- .../dentalExaminationStore/factories.ts | 3 +- .../hooks/useElementFocus.ts | 46 + .../hooks/useKeyboardNavigationHandler.ts | 25 + .../useParticipantExaminationForm.ts | 9 +- .../prophylaxisSessionStore/actions.ts | 7 + .../participantSorting.ts | 1 + ...seSyncOutgoingProphylaxisSessionChanges.ts | 2 +- .../inspection/shared/sideNavigationItem.tsx | 32 +- .../shared/sideNavigationItem.tsx | 25 +- .../shared/sideNavigationItem.tsx | 28 +- .../AppointmentBlockGroupTable.tsx | 6 + .../shared/sideNavigationItem.tsx | 15 +- .../ProcedureFilterSettings.tsx | 321 +-- .../schoolEntry/shared/sideNavigationItem.tsx | 66 +- .../ChartsSamplePreview.tsx | 138 +- .../ConfigureChoroplethChartStep.tsx | 104 +- .../ConfigureHistogramChartStep.tsx | 31 +- .../worldContinentsGeoJSON.ts | 2377 +++++++++++++++++ .../ImportGeoShapeStep.tsx | 2 +- .../shared/charts/ChoroplethMap.tsx | 7 +- .../statistics/shared/sideNavigationItem.tsx | 1 + .../stiProtection/api/queries/procedures.ts | 10 + .../TextTemplatesOverviewTable.tsx | 4 +- .../procedures/details/ProcedureDetails.tsx | 12 +- .../shared/sideNavigationItem.tsx | 18 +- .../shared/sideNavigationItem.tsx | 38 +- .../archiving/shared/sideNavigationItem.tsx | 2 + .../components/chip/ChipWithTooltip.tsx | 1 + .../filterSettings/ActiveFilter.tsx | 14 +- .../components/pagination/IconButton.tsx | 22 +- .../components/pagination/Pagination.tsx | 20 +- .../appointmentPicker/AppointmentCalendar.tsx | 47 +- .../AppointmentPickerField.tsx | 80 +- .../formFields/appointmentPicker/Day.tsx | 32 +- .../appointmentPicker/MonthSelection.tsx | 16 +- .../appointmentPicker/WeekdayHeaders.tsx | 6 +- .../formFields/appointmentPicker/helpers.ts | 109 +- .../formFields/appointmentPicker/labels.ts | 1 + .../src/errorHandling/errorResolvers.ts | 1 + .../dental/src/api/models/ChildExamination.ts | 8 +- packages/dental/src/api/models/Examination.ts | 13 +- .../src/api/models/ExaminationResult.ts | 4 +- .../src/api/models/ExaminationStatus.ts | 70 +- packages/dental/src/config/teeth.ts | 59 + ...gationItems.tsx => sideNavigationItem.tsx} | 17 +- .../src/types/sideNavigation.ts | 29 +- .../toMatchValidationFile.ts | 1 + pnpm-lock.yaml | 31 +- vitest.config.ts | 42 - 279 files changed, 9640 insertions(+), 2675 deletions(-) create mode 100644 backend/base/src/main/java/de/eshg/base/config/DepartmentConfiguration.java create mode 100644 backend/base/src/main/java/de/eshg/base/config/DepartmentConfigurationRepository.java create mode 100644 backend/base/src/main/java/de/eshg/base/config/DepartmentConfigurationService.java rename backend/base/src/main/java/de/eshg/base/{department/DepartmentConfiguration.java => config/InitialDepartmentConfigurationDefaults.java} (95%) create mode 100644 backend/base/src/main/resources/migrations/0042_department_config.xml create mode 100644 backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/NormalizeSequenceIdCustomizer.java create mode 100644 backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/ObjectMapperCustomizer.java delete mode 100644 backend/business-module-persistence-commons/src/main/java/de/eshg/domain/model/serialization/SerializationObjectMapperConfigurer.java create mode 100644 backend/dental/src/main/java/de/eshg/dental/domain/model/ToothType.java create mode 100644 backend/dental/src/main/java/de/eshg/dental/statistic/DmftCalculationHelper.java create mode 100644 backend/dental/src/main/java/de/eshg/dental/statistic/model/OralHygieneStatus.java create mode 100644 backend/dental/src/main/java/de/eshg/dental/testhelper/CalculateDmftValuesRequest.java create mode 100644 backend/dental/src/main/java/de/eshg/dental/testhelper/DmftValues.java delete mode 100644 backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContentSerializer.java create mode 100644 backend/lib-auditlog/src/main/java/de/eshg/lib/auditlog/AuditLogHousekeeping.java create mode 100644 backend/lib-procedures/src/main/java/de/eshg/lib/procedure/gdpr/SerializationUtil.java create mode 100644 backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/AttributeUtil.java create mode 100644 backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/OmsProcedureAttributes.java create mode 100644 backend/official-medical-service/src/main/java/de/eshg/officialmedicalservice/statistics/OmsProcedureDataSource.java create mode 100644 backend/official-medical-service/src/main/resources/concerns/concerns.test.yaml create mode 100644 backend/official-medical-service/src/main/resources/notifications/default/de/new_citizen_procedure.html create mode 100644 backend/official-medical-service/src/main/resources/notifications/default/de/new_document.html create mode 100644 backend/official-medical-service/src/main/resources/notifications/ga_frankfurt/de/new_citizen_procedure.html create mode 100644 backend/official-medical-service/src/main/resources/notifications/ga_frankfurt/de/new_document.html create mode 100644 backend/rest-service-errors/src/main/java/de/eshg/rest/service/error/InternalServerErrorException.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/api/CreatedByUserTypeDto.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/api/GetStiProtectionProceduresFilterOptions.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/persistence/db/CreatedByUserType.java create mode 100644 backend/sti-protection/src/main/resources/migrations/0055_filter_procedures.xml create mode 100644 citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sexarbeit/termin-buchen/page.tsx create mode 100644 citizen-portal/src/app/[lang]/(privatpersonen)/sexuelle-gesundheit/sti-beratung/termin-buchen/page.tsx create mode 100644 citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/ConcerFilters.tsx create mode 100644 citizen-portal/src/lib/businessModules/officialMedicalService/components/appointment/steps/useConcernFilterValues.ts create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentDataContext.tsx create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentPickerSection.tsx create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/components/appointment/AppointmentStepper.tsx create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/components/appointment/BookAppointmentPage.tsx create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/components/appointment/StepButtons.tsx create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/components/appointment/StepLayout.tsx create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/components/appointment/TimeSlotStep.tsx create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/components/shared/StepContext.tsx create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/locales/de/forms.json create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/locales/en/forms.json create mode 100644 citizen-portal/src/lib/businessModules/stiProtection/pages/landingpage/Landingpage.tsx create mode 100644 docker-compose.yaml create mode 100644 docs/migration.adoc create mode 100644 employee-portal/src/lib/baseModule/components/layout/sideNavigation/filterNavigationItemsWithAccess.ts rename employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/{NavigationIconItem.tsx => CollapsedNavigationItem.tsx} (80%) create mode 100644 employee-portal/src/lib/baseModule/components/layout/sideNavigation/items/ExpandedNavigationItem.tsx rename employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/{NavigationListCollapsed.tsx => CollapsedNavigationList.tsx} (57%) create mode 100644 employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/CollapsedNavigationListContext.ts rename employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/{NavigationListExpanded.tsx => ExpandedNavigationList.tsx} (61%) create mode 100644 employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationItemGroup.tsx delete mode 100644 employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListCollapsedContext.ts create mode 100644 employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/NavigationListContext.ts delete mode 100644 employee-portal/src/lib/baseModule/components/layout/sideNavigation/lists/StyledList.ts delete mode 100644 employee-portal/src/lib/baseModule/components/layout/sideNavigation/useNavigationItems.ts create mode 100644 employee-portal/src/lib/baseModule/components/layout/sideNavigation/useSideNavigationItemProps.ts rename employee-portal/src/lib/baseModule/{sideNavigationItems.tsx => sideNavigationItem.tsx} (84%) create mode 100644 employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ReadonlyExaminationResult.tsx create mode 100644 employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExamination/ReadonlyToothButton.tsx delete mode 100644 employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions.ts create mode 100644 employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/navigateTo.ts create mode 100644 employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/result.ts create mode 100644 employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/tooth.ts create mode 100644 employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/actions/utils.ts create mode 100644 employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useElementFocus.ts create mode 100644 employee-portal/src/lib/businessModules/dental/features/prophylaxisSessions/dentalExaminationStore/hooks/useKeyboardNavigationHandler.ts create mode 100644 employee-portal/src/lib/businessModules/statistics/components/evaluations/details/CreateAnalysisSidebar/worldContinentsGeoJSON.ts create mode 100644 packages/dental/src/config/teeth.ts rename packages/dental/src/shared/{useSideNavigationItems.tsx => sideNavigationItem.tsx} (93%) delete mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 52b1a00b3..669357ca4 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 d109fc6bf..afb756c2b 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 e71a9aabb..d427b2fe0 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 d9c9f3272..e8fcb3672 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 78f45a7f5..58b0912c2 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 fef7a70fe..8a05027d9 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 3e3626b3e..273423f3f 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 381c85e44..0a3274dfc 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 2c1e58a44..52a3a10a6 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 01f353e87..182b9ee24 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 42c9ecb0b..6633ccada 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 000000000..bf0794453 --- /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 000000000..1c709d1b2 --- /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 000000000..4cc14f0e4 --- /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 0489e88eb..dafb304e6 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 34ec53950..dd66b9cd2 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 c93654db6..56b996c77 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 b490dd7da..f4e7a28ef 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 86800a899..78d594369 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 c62c454e2..5b398e54c 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 3db7fa107..dac02c87c 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 e5dd45f14..66184a0d9 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 c2bf34fa9..c6b95392a 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 000000000..1b79e06f8 --- /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 7a468ec09..6103e3204 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 161bc7669..01a7596ed 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 b7892dd7b..8f0666f1f 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 000000000..0f3660e14 --- /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 000000000..728bda7f6 --- /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 8f9667807..000000000 --- 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 15eba83be..d874f59ef 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 c77e4a7a5..8c20ab7a3 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 03d1be535..91d7aa2f3 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 ec27e31ac..9aa5d3b09 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 4d1052174..2e34f9b79 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 e20e9b197..8aa6da520 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 159c43177..ad5aac719 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 cd21b86b4..f320baf6b 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 000000000..30a9c6c58 --- /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 f167beb00..abeed2cb3 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 5729ce128..95e6783af 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 778c9a124..4db1b219e 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 a3f3007c1..0e6c56f1c 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 000000000..3454b99e3 --- /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 000000000..ce4ef3da6 --- /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 000000000..6e86c72b1 --- /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 a71a5daec..ddffbf266 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 000000000..269d8b2cb --- /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 296be3362..3e249cf85 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 c79551f36..2dae1119b 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 8d6033aab..5a76389aa 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 20d5bb726..f769632ac 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 9ce91083a..aef950dfc 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 548afc417..2d045886a 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 72e3232b3..9e878ed0f 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 4c3bb50b6..3308daa76 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 3b239422e..000000000 --- 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 d24adc907..ac534f25f 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 5c5ea09a3..87b2ae8f6 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 ee147b940..af07985d9 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 000000000..dea5b7b7d --- /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 bc5cd5d7a..cfbc6b271 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 968be7e19..5ad0c5652 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 978389f6c..7f7ced186 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 3d9e1e60c..a58afd49b 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 08f7c9f2c..0047b36e5 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 829f41147..2ab48bc2b 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 8518d927a..a0292bd87 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 ac962a1c8..84eda37a4 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 000000000..4e877babf --- /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 6a3280513..e82a7095a 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 b0116d899..442d8a6c0 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 5898d672d..4c5ea9723 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 00b159a8b..6431a8ff5 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 7771f6472..d6284baee 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 e190fef8e..64444d70f 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 e30bdb71b..13e320460 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 04f8d098d..d8473b05c 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 ea5441d9c..03cb98a55 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 45f5f1117..edb9e395d 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 a9f36344d..b8b3e7b41 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 6ddcff0cd..41de4df95 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 2d65e96be..1e0e47ed5 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 91b590eb5..c13e15620 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 3eb6ae602..9693caa61 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 1416cb1ef..0bb96db00 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 de5450b8a..5f0cbeab7 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 65a59066f..5ad852063 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 d4cfeee63..cf8eb9451 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 000000000..f17527266 --- /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 000000000..60604b2d4 --- /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 000000000..c228c0e4d --- /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 508fce33b..763b07201 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 000000000..cbd9dbb16 --- /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 000000000..5a75768df --- /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 000000000..3a7ee3412 --- /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 000000000..5a75768df --- /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 000000000..3a7ee3412 --- /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 b37e5539a..7190b0be7 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 ceef399d9..d27d37e17 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 efc8c3f1a..4cfe9b7ee 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 000000000..b1aecfaea --- /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 8a0a7f775..fe62b565c 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 8fe44ab5a..88af09e79 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 34fb44c6f..2c13bedb7 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 b5621b430..90aff376e 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 7ab44dc5e..a9626005f 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 44484e31f..9c1ee39ef 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 573e97e1e..653afb0d1 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 013d516fe..76e725488 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 544d327bb..1701a0755 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 04d277a47..d7f6f3120 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 3cde9cb7d..ad97131c0 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 57fe29525..ecb0370c3 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 27e068255..5267eb038 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 376de85c5..b1a2bb93b 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 4eb8e872d..8a98f143d 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 024a5d5b0..d776476fc 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 8e9a37b4b..0c0b986f6 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 76b661b52..4693ffa3a 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 fe0d2cdac..9134692fe 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 5bb25ba24..7a869635d 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 dd7001ce3..a1c06dbea 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 000000000..4264dac3a --- /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 000000000..e4c910dcc --- /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 000000000..a85e799bb --- /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 443e1b894..1f81a799d 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 c30a0614d..0ca6aacee 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 890632d38..724b5d6c6 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 000000000..f12d01fed --- /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 4be33e14d..4be4982f1 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 957a3cab3..a4b1dca3d 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 4bc4e91fd..08a7b39f1 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 6cc1f3a24..63c67dce3 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 6cd3f5da9..93317af09 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 977ab60c3..1c42f3a11 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 12e749843..2742ddf46 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 0618a2447..712b3ee5e 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 ef5fe126f..52df87378 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 1b70ba695..368c27fa0 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 000000000..06af9d780 --- /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 7ba70484e..f5afaef69 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 000000000..aa2a9b83c --- /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 9115a116f..9cdbe2e4d 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 49bd1c1a7..df1a53d43 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 854826134..dab901e72 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 a5b299465..43744b93d 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 d6a0a5cd7..2185daa6f 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 cafb9a564..b0640fe1f 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 d5c2657fb..1f9bb786d 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 000000000..fe8cbb5b9 --- /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 5423a82ac..11cb272c3 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 2c9c2c08d..e4bc896f7 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 7783b7df0..8ac57db92 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 6bc70b632..5b249153f 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 000000000..b3736c580 --- /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 c5438c9d9..54c5d7aa0 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 51e4aa5f9..9891aab16 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 da5ed3540..b9065d541 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 7434229eb..872895537 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 0bd08875a..670243ae8 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 000000000..960b63ad1 --- /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 000000000..1d0a29b50 --- /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 000000000..b38955925 --- /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 000000000..99327160a --- /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 000000000..c0ce61a1a --- /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 000000000..cad2a5af3 --- /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 000000000..abc695879 --- /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 000000000..47f9ef088 --- /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 000000000..c541e4a58 --- /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 000000000..997c093d7 --- /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 000000000..f46d9ac44 --- /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 5a2094b1d..e9cbed678 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 3eca5e518..3ff099af7 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 4018fb452..2bfe6d85e 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 ac384f5f3..ebd516b85 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 cf132dbd0..b8a88fdba 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 502767c2e..65b7904ec 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 9b8e1f6e3..09cd706e9 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 000000000..c9491d6f1 --- /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 5d082911f..8eb2bd12c 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 000000000..1a18c7590 --- /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 310927e80..28fb07b14 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 a57c61e46..13671116a 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 b7e4cd618..af18fdc26 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 f22d05213..f34eb7907 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 185feeb25..7efc4554b 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 28a66de0b..9a34632b8 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 d46c078e7..769dde4ef 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 000000000..b2e361f31 --- /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 bfd377313..9e947df03 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 000000000..d35f5d789 --- /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 57f148d24..7d24bbc2c 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 dbd3edcae..ed3a1f442 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 0bf0a33aa..4b47be5f6 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 000000000..0d9b7da3c --- /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 9f794330d..c1c1c9035 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 000000000..abc55b8ce --- /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 8646fd233..000000000 --- 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 000000000..c0aadaf4d --- /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 60e302433..000000000 --- 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 c6d4d3b56..9b8b57acb 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 825b05fbf..000000000 --- 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 000000000..04957ee44 --- /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 caa679f18..acf74642d 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 9ae45baa0..0fbddffda 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 aec5a429d..c1e1b9451 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 18ca9b084..61705094b 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 20f7e9706..30c11dd1f 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 b33f68baf..b4e5f39b7 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 fe7b01ad9..24cbd4829 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 5a4955d44..a938ed35f 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 ad524cd84..cde9c7f28 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 af76e6cf4..509d432f6 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 eb290e4e8..c6a028182 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 000000000..8098fbbbc --- /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 000000000..acb19c954 --- /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 950f635ab..931eee16c 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 9583aefb4..07a76724b 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 ec796d5d0..221ff83ac 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 106833dbd..a9d5a044f 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 c837c33cf..b7f2ad2db 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 ac4b8328d..000000000 --- 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 c70bbc6c5..f4a250cf8 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 000000000..9e37715bf --- /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 000000000..57fb151ab --- /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 000000000..affd0f90e --- /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 000000000..eaf4ae3e4 --- /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 8badadf43..eac7ef0cb 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 ea487e7f0..5384d9bf2 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 e9c7a3cf2..27d5c595f 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 000000000..3fe53c96b --- /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 000000000..b24650f49 --- /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 60b4af9c7..3fc3b35f5 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 6e7baef74..5516b4889 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 55bca9040..9f4b88d6f 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 7d46a095b..b1f2990bd 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 e7523f509..8ddfc0c5c 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 6e428a5a6..83b0da570 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 3154295ea..f1c3996ef 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 1635f4679..d3db67e4e 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 8755227c3..e3d5bcca6 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 da5b52375..be80701ec 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 97b441086..85ffda44d 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 bb7457a87..f4880aa3e 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 c9b710403..71d664620 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 9edf43137..df84ada09 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 000000000..351613ab4 --- /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 9ec1f29d4..7485bd076 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 a12eea003..f973458f3 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 893f8bbba..161c22313 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 0b8e0cdc9..d0f7ddafe 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 082df9867..7dd637a87 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 e3edb70d8..057455690 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 75528b616..9cc0d87cb 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 507ee4b1a..ac5de3411 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 226e5b227..c5e099cda 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 a89b55021..bec3c6795 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 420c9f54c..4c0090585 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 21883706c..8b23b3ac8 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 3e4437934..cde71397d 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 345c2d757..5799d7719 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 64c088bea..7710b7c78 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 97128cbb1..d242a9788 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 66c1378b0..0b747b2d8 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 2d114146c..d2013553d 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 daf1b4f78..29088d737 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 31b61add2..e54f0ae57 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 717043c79..40c875f5e 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 1b10ea394..c5b31e1fc 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 11345c2b4..338d9c469 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 cd7fcd933..ccf7a7139 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 862b3c0e2..218e76506 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 000000000..89c33472f --- /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 4027ed7dd..6c9e70346 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 a0cae2c90..a1cee2b78 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 ca2f35c48..12b230874 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 04b3039f5..f4f2c68d7 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 45bde8e4a..000000000 --- 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, - ], - }, - }, -}); -- GitLab