From 74f0c7d04b0768249b5517a19331c9184a9cc002 Mon Sep 17 00:00:00 2001 From: cronn Bot <no-reply@cronn.de> Date: Mon, 21 Oct 2024 13:33:02 +0000 Subject: [PATCH] Update v1.1.0 --- .editorconfig | 5 +- .gitignore | 2 + admin-portal/package.json | 24 +- backend/auditlog-api/gradle.lockfile | 1 + backend/auditlog/build.gradle | 1 + backend/auditlog/gradle.lockfile | 79 +- .../de/eshg/auditlog/AuditLogController.java | 2 +- backend/auth/gradle.lockfile | 1 + .../src/main/resources/application.properties | 1 + backend/base-api/gradle.lockfile | 1 + .../java/de/eshg/base/CountryCodeDto.java | 268 -- .../java/de/eshg/base/address/AddressDto.java | 4 +- .../eshg/base/address/DomesticAddressDto.java | 6 +- .../eshg/base/address/PostboxAddressDto.java | 6 +- .../api/person/AddPersonFileStateRequest.java | 4 +- .../person/AddPersonFileStateResponse.java | 4 +- .../ExternalAddPersonFileStateRequest.java | 4 +- .../person/GetPersonFileStateResponse.java | 4 +- .../person/GetReferencePersonResponse.java | 4 +- .../centralfile/api/person/PersonDetails.java | 4 +- .../api/person/PersonDetailsDto.java | 4 +- .../contact/api/ContactAddressChange.java | 4 +- .../api/DomesticContactAddressChange.java | 4 +- .../api/PostboxContactAddressChange.java | 4 +- .../department/GetDepartmentInfoResponse.java | 4 +- .../de/eshg/base/feature/BaseFeature.java | 5 +- .../base/gdpr/DeleteGdprDownloadsRequest.java | 12 + .../de/eshg/base/gdpr/GdprProcedureApi.java | 17 +- .../java/de/eshg/base/street/StreetApi.java | 4 +- .../base/testhelper/BaseTestHelperApi.java | 4 + .../de/eshg/base/user/api/UserRoleDto.java | 2 + backend/base/build.gradle | 1 + backend/base/gradle.lockfile | 91 +- backend/base/openApi.yaml | 62 +- .../base/address/mapper/AddressMapper.java | 9 +- .../embeddable/EmbeddableAddress.java | 2 +- .../address/persistence/entity/Address.java | 2 +- .../persistence/entity/DelegatingAddress.java | 2 +- .../base/centralfile/mapper/PersonMapper.java | 12 +- .../persistence/entity/BirthDetails.java | 2 +- .../de/eshg/base/contact/ContactMapper.java | 8 +- .../entity/DomesticContactAddress.java | 2 +- .../entity/PostboxContactAddress.java | 2 +- .../department/DepartmentConfiguration.java | 2 +- .../base/department/DepartmentController.java | 10 +- .../base/gdpr/GdprProcedureController.java | 6 + .../eshg/base/gdpr/GdprProcedureService.java | 11 + .../base/gdpr/persistence/GdprProcedure.java | 17 +- .../EmployeeKeycloakProvisioning.java | 4 +- .../base/keycloak/KeycloakTestClient.java | 4 +- .../keycloak/KeycloakTestProvisioning.java | 4 +- .../GdprRightToObjectLetterGenerator.java | 2 +- .../config/BaseInternalSecurityConfig.java | 30 +- .../base/statistics/StatisticsController.java | 5 +- .../de/eshg/base/street/StreetController.java | 4 +- .../de/eshg/base/street/StreetService.java | 6 +- .../testhelper/AbstractContactPopulator.java | 12 +- .../testhelper/BaseTestHelperController.java | 6 + .../testhelper/BaseTestHelperService.java | 11 +- .../HealthDepartmentContactPopulator.java | 43 + .../de/eshg/base/user/UserController.java | 9 +- .../de/eshg/base/user/mapper/UserMapper.java | 15 +- .../java/de/eshg/base/util/MappingUtil.java | 9 - .../application-preview-features.properties | 2 +- backend/build.gradle | 3 +- backend/buildscript-gradle.lockfile | 6 +- .../business-module-commons/gradle.lockfile | 3 +- .../gradle.lockfile | 1 + backend/central-repository/gradle.lockfile | 1 + backend/chat-management/gradle.lockfile | 1 + backend/compliance-test/gradle.lockfile | 22 +- backend/docker-compose.yaml | 2 + backend/file-commons/README_LICENSE.adoc | 5 + backend/file-commons/build.gradle | 12 + .../file-commons/buildscript-gradle.lockfile | 4 + backend/file-commons/gradle.lockfile | 124 + .../eshg/file/common}/CustomMediaTypes.java | 5 +- .../de/eshg/file/common}/FileExtension.java | 5 +- .../java/de/eshg/file/common}/FileType.java | 17 +- .../eshg/file/common}/FileTypeDetector.java | 13 +- .../common}/PdfAConformanceValidator.java | 6 +- backend/inspection/build.gradle | 1 + backend/inspection/gradle.lockfile | 68 +- backend/inspection/openApi.yaml | 4 + .../checklist/MediaFileService.java | 2 +- .../persistence/element/ChecklistElement.java | 3 +- .../common/persistence/MediaFileContent.java | 30 + .../MediaFileContentSerializer.java | 60 + .../InspectionProcedureConfiguration.java | 21 + .../websearch/persistence/WebSearch.java | 2 +- .../websearch/persistence/WebSearchEntry.java | 6 +- .../websearch/persistence/WebSearchQuery.java | 5 +- .../incident/InspectionIncidentService.java | 6 +- .../inspection/InspectionFinalizer.java | 8 +- .../inspection/InspectionService.java | 4 +- .../inspection/InspectionUpdater.java | 13 +- .../inspection/persistence/Inspection.java | 46 +- .../persistence/InspectionTask.java | 19 +- .../inspection/report/persistence/Report.java | 4 +- .../testhelper/FacilityTestDataProvider.java | 22 +- .../de/eshg/inspection/util/FileUtil.java | 2 +- .../src/main/resources/application.properties | 1 - .../migrations/0038_remove_report_fk.xml | 21 + ...39_remove_fk_checklistelement_incident.xml | 23 + .../0040_introduce_procedure_file_type.xml | 11 + ...1_add_medical_registry_procedure_types.xml | 14 + .../main/resources/migrations/changelog.xml | 4 + backend/lib-aggregation/gradle.lockfile | 1 + backend/lib-appointmentblock/gradle.lockfile | 3 +- .../AppointmentBlockGroupsPopulator.java | 52 +- backend/lib-auditlog/gradle.lockfile | 3 +- backend/lib-base-client/gradle.lockfile | 1 + backend/lib-calendar-api/gradle.lockfile | 1 + backend/lib-calendar/gradle.lockfile | 1 + .../gradle.lockfile | 1 + .../gradle.lockfile | 1 + backend/lib-commons/build.gradle | 4 + backend/lib-commons/gradle.lockfile | 3 +- .../java/de/eshg/lib/common}/CountryCode.java | 13 +- .../lib-document-generator/gradle.lockfile | 1 + backend/lib-editor/gradle.lockfile | 1 + .../gradle.lockfile | 1 + .../lib-four-eyes-principle/gradle.lockfile | 1 + .../lib/keycloak/EmployeePermissionRole.java | 7 +- .../eshg/lib/keycloak/EmployeeTestUser.java | 8 +- .../eshg/lib/keycloak/ModuleLeaderGroup.java | 3 +- .../eshg/lib/keycloak/ModuleMemberGroup.java | 3 +- backend/lib-lsd-api/gradle.lockfile | 1 + backend/lib-mutex/gradle.lockfile | 1 + backend/lib-notification-api/gradle.lockfile | 1 + backend/lib-notification/gradle.lockfile | 1 + backend/lib-procedures-api/gradle.lockfile | 1 + .../lib/procedure/model/PersonTypeDto.java | 3 +- .../lib/procedure/model/ProcedureTypeDto.java | 3 + backend/lib-procedures/build.gradle | 1 + backend/lib-procedures/gradle.lockfile | 69 +- backend/lib-procedures/openApi.yaml | 4 + .../eshg/lib/procedure/domain/model/File.java | 6 +- .../lib/procedure/domain/model/FileAware.java | 2 +- .../procedure/domain/model/FileTypeGroup.java | 14 +- .../domain/model/InboxProgressEntry.java | 2 +- .../domain/model/InboxProgressEntryType.java | 2 +- .../domain/model/ManualProgressEntry.java | 2 +- .../domain/model/ManualProgressEntryType.java | 2 +- .../procedure/domain/model/PersonType.java | 3 +- .../domain/model/ProcedureFileType.java | 25 + .../procedure/domain/model/ProcedureType.java | 3 + .../model/ProcessedInboxProgressEntry.java | 2 +- .../domain/model/RelatedFacility.java | 6 + .../procedure/domain/model/RelatedPerson.java | 6 + .../ManualProgressEntryRepository.java | 4 +- .../SerializationObjectMapperConfigurer.java | 17 + .../serialization/SerializationService.java | 43 +- .../de/eshg/lib/procedure/file/EmlParser.java | 17 +- .../lib/procedure/file/FileController.java | 2 +- .../procedure/file/FileExtensionEnricher.java | 10 +- .../eshg/lib/procedure/file/FileFactory.java | 8 +- .../procedure/file/FileStorageService.java | 4 +- .../lib/procedure/file/FileTypeMapper.java | 24 + .../lib/procedure/file/FileUploadService.java | 21 +- .../procedure/file/FileUploadValidator.java | 13 +- .../eshg/lib/procedure/file/ParsedMail.java | 8 +- .../archiving/ArchivingController.java | 2 +- .../housekeeping/archiving/ArchivingJob.java | 6 +- .../lib/procedure/mapping/FileMapper.java | 4 +- .../procedure/mapping/PersonTypeMapper.java | 1 + .../procedure/mapping/ProcedureMapper.java | 6 + .../lib/procedure/util/FileValidator.java | 8 +- backend/lib-relay/gradle.lockfile | 1 + .../service/security/config/BaseUrls.java | 14 + backend/lib-security-config/gradle.lockfile | 3 +- .../AbstractPublicSecurityConfiguration.java | 15 +- .../config/BasePublicSecurityConfig.java | 21 +- .../InspectionPublicSecurityConfig.java | 12 + ...MeaslesProtectionPublicSecurityConfig.java | 6 + .../MedicalRegistryPublicSecurityConfig.java | 3 + .../config/OpenDataPublicSecurityConfig.java | 30 + .../SchoolEntryPublicSecurityConfig.java | 10 + .../StiProtectionPublicSecurityConfig.java | 5 + .../TravelMedicinePublicSecurityConfig.java | 12 + .../gradle.lockfile | 1 + .../lib-service-directory-api/gradle.lockfile | 1 + backend/lib-statistics/gradle.lockfile | 70 +- backend/lib-xlsx-import/README_LICENSE.adoc | 5 + backend/lib-xlsx-import/build.gradle | 25 + .../buildscript-gradle.lockfile | 4 + backend/lib-xlsx-import/gradle.lockfile | 148 + .../eshg/lib/xlsximport}/ColumnAccessor.java | 4 +- .../xlsximport}/FeedbackColumnAccessor.java | 6 +- .../eshg/lib/xlsximport/ImportStatistics.java | 67 + .../de/eshg/lib/xlsximport}/ImportStatus.java | 4 +- .../eshg/lib/xlsximport}/ImportValidator.java | 5 +- .../java/de/eshg/lib/xlsximport/Importer.java | 184 ++ .../de/eshg/lib/xlsximport/RowReader.java} | 38 +- .../de/eshg/lib/xlsximport}/RowValues.java | 18 +- .../de/eshg/lib/xlsximport}/XlsxColumn.java | 4 +- .../eshg/lib/xlsximport}/XlsxNormalizer.java | 13 +- .../xlsximport}/api/ImportStatisticsDto.java | 4 +- .../lib/xlsximport}/model/AddressData.java | 8 +- .../lib/xlsximport}/model/ImportResult.java | 6 +- .../lib/xlsximport/util/FileResponseUtil.java | 58 + .../eshg/lib/xlsximport/util}/XlsxUtil.java | 16 +- .../eshg/lib/xlsximport/ImportResponse.java | 12 + .../MultipartResponseAssertionTraits.java | 71 + .../de/eshg/lib/xlsximport/MultipartUtil.java | 63 + .../lib/xlsximport/XlsxAssertionTraits.java | 95 + .../local-service-directory/gradle.lockfile | 1 + .../lsd/keycloak/KeycloakProvisioning.java | 11 +- .../lsd/keycloak/LsdInitialSetupService.java | 174 + .../eshg/lsd/keycloak/LsdKeycloakClient.java | 54 +- .../LsdInternalKeycloakProperties.java | 7 +- .../lsd/testhelper/LsdTestHelperService.java | 2 +- .../resources/application-local.properties | 2 + .../src/main/resources/application.properties | 4 +- backend/measles-protection/build.gradle | 3 +- backend/measles-protection/gradle.lockfile | 63 +- backend/measles-protection/openApi.yaml | 4 + .../api/AffectedPersonDto.java | 4 +- .../api/draft/AffectedPersonDetailsDto.java | 4 +- .../pdf/coverletter/CoverLetterService.java | 4 +- .../ProtectionProcedurePopulator.java | 6 +- .../0024_introduce_procedure_file_type.xml | 11 + .../0025_medical_registry_procedure_types.xml | 14 + .../main/resources/migrations/changelog.xml | 2 + backend/medical-registry/gradle.lockfile | 68 +- backend/medical-registry/openApi.yaml | 220 ++ .../MedicalRegistryController.java | 78 + .../MedicalRegistryService.java | 184 ++ .../eshg/medicalregistry/api/AddressDto.java | 16 + .../api/CreateProcedureDto.java | 18 + .../api/CreateProcedureRequest.java | 13 + .../api/EmploymentStatusDto.java | 15 + .../api/EmploymentTypeDto.java | 14 + .../api/GetProcedureResponse.java | 21 + .../eshg/medicalregistry/api/PracticeDto.java | 27 + .../medicalregistry/api/ProfessionalDto.java | 72 + .../api/ProfessionalTitleDto.java | 49 + .../business/model/ProcedureDetailsData.java | 8 + .../domain/model/EmploymentStatus.java | 12 + .../domain/model/EmploymentType.java | 11 + .../domain/model/Facility.java | 15 - .../domain/model/MedicalRegistryEntry.java | 45 +- .../model/MedicalRegistryEntryChange.java | 30 + ...edicalRegistrySystemProgressEntryType.java | 18 + .../medicalregistry/domain/model/Person.java | 15 - .../domain/model/Practice.java | 78 + .../domain/model/Professional.java | 159 + .../domain/model/ProfessionalTitle.java | 46 + .../domain/model/TypeOfChange.java | 18 + .../medicalregistry/mapper/AddressMapper.java | 34 + .../mapper/PracticeMapper.java | 31 + .../mapper/ProfessionalMapper.java | 209 ++ .../medicalregistry/util/MapperUtils.java | 17 + backend/opendata/build.gradle | 13 + backend/opendata/gradle.lockfile | 132 +- backend/opendata/openApi.yaml | 559 ++++ .../main/java/de/eshg/opendata/FileType.java | 11 - .../de/eshg/opendata/OpenDataApplication.java | 3 + .../de/eshg/opendata/OpenDataController.java | 146 + .../java/de/eshg/opendata/OpenDataMapper.java | 60 + .../de/eshg/opendata/OpenDataService.java | 288 ++ .../opendata/VersionFilterSpecification.java | 86 + .../opendata/api/GetOpenDocumentsRequest.java | 30 + .../api/GetOpenDocumentsResponse.java | 12 + .../opendata/api/PostOpenDocumentRequest.java | 37 + .../de/eshg/opendata/api/ResourceDto.java | 15 + .../api/UpdateVersionMetaDataRequest.java | 25 + .../java/de/eshg/opendata/api/VersionDto.java | 38 + .../opendata/domain/model/FileContent.java | 28 + .../domain/model/OpenDataFileType.java | 23 + .../eshg/opendata/domain/model/Resource.java | 38 +- .../eshg/opendata/domain/model/Version.java | 95 +- .../domain/repository/ResourceRepository.java | 26 +- .../domain/repository/VersionRepository.java | 14 +- .../src/main/resources/application.properties | 1 + backend/relay-server/gradle.lockfile | 1 + .../resources/matrix/synapse/homeserver.yaml | 20 +- backend/rest-service-commons/gradle.lockfile | 4 +- backend/rest-service-errors/gradle.lockfile | 1 + backend/school-entry/build.gradle | 10 +- backend/school-entry/gradle.lockfile | 66 +- backend/school-entry/openApi.yaml | 35 +- .../de/eshg/schoolentry/ProceduresHelper.java | 2 +- .../SchoolEntryCitizenService.java | 2 +- .../schoolentry/SchoolEntryController.java | 76 +- .../eshg/schoolentry/SchoolEntryService.java | 89 +- .../java/de/eshg/schoolentry/Validator.java | 11 - .../eshg/schoolentry/api/CreatePersonDto.java | 6 +- .../api/GetClosedProceduresResponse.java | 12 + .../schoolentry/api/PersonDetailsDto.java | 4 +- .../schoolentry/api/UpdatePersonRequest.java | 4 +- .../schoolentry/business/model/ChildData.java | 6 +- .../business/model/ChildDetailsData.java | 4 +- .../business/model/CustodianDetailsData.java | 4 +- .../business/model/ImportChildData.java | 7 +- .../business/model/ImportCustodianData.java | 1 + .../business/model/MergeProcedureData.java | 4 +- .../eshg/schoolentry/client/PersonClient.java | 4 +- .../config/SchoolEntryFeature.java | 7 - .../domain/model/SchoolEntryProcedure.java | 13 +- .../SchoolEntryProcedureRepository.java | 4 + .../importer/CitizenListColumn.java | 2 + ...ocessor.java => CitizenListRowReader.java} | 67 +- .../importer/CitizenListRowValueMapper.java | 38 + .../importer/CitizenListRowValues.java | 10 +- .../importer/EqualityComparator.java | 10 - .../schoolentry/importer/ImportService.java | 520 +-- .../importer/PastProcedureListColumn.java | 2 + ...r.java => PastProcedureListRowReader.java} | 51 +- .../PastProcedureListRowValueMapper.java | 30 + .../importer/PastProcedureListRowValues.java | 9 +- .../importer/SchoolEntryImporter.java | 291 ++ .../importer/SchoolEntryRowValues.java | 24 + .../importer/SchoolListColumn.java | 2 + .../importer/SchoolListRowProcessor.java | 112 - .../importer/SchoolListRowReader.java | 67 + .../importer/SchoolListRowValueMapper.java | 51 + .../importer/SchoolListRowValues.java | 10 +- .../schoolentry/mapper/AddressMapper.java | 2 +- .../pdf/invitation/InvitationGenerator.java | 5 +- .../medicalreport/MedicalReportGenerator.java | 5 +- .../SchoolInfoLetterExaminationMapper.java | 17 +- .../SchoolInfoLetterGenerator.java | 4 +- .../statistics/StatisticsValueMappers.java | 2 + .../SchoolEntryProceduresPopulator.java | 118 +- .../SchoolEntryTestHelperController.java | 7 + .../SchoolEntryTestHelperService.java | 4 + .../util/ProcedureTypeAssignmentHelper.java | 19 +- .../de/eshg/schoolentry/util/TaskUtil.java | 45 + .../application-preview-features.properties | 7 - .../0047_introduce_procedure_file_type.xml | 11 + ...8_add_medical_registry_procedure_types.xml | 14 + .../main/resources/migrations/changelog.xml | 2 + .../resources/templates/medicalreport.ftlx | 1 + backend/service-directory/gradle.lockfile | 1 + backend/settings.gradle | 2 + backend/spatz/gradle.lockfile | 1 + backend/statistics/build.gradle | 3 +- backend/statistics/gradle.lockfile | 79 +- backend/statistics/openApi.yaml | 60 +- .../AttributeCodeToNameMapping.java | 15 - .../EvaluationTemplateController.java | 43 +- .../statistics/EvaluationTemplateService.java | 98 +- .../aggregation/DataAggregationService.java | 4 +- .../aggregation/DataSourceValidator.java | 43 +- .../aggregation/InspectionSimulator.java | 32 +- .../aggregation/ReportController.java | 10 +- .../aggregation/ReportExecution.java | 14 + .../aggregation/ReportSeriesService.java | 4 +- .../statistics/aggregation/ReportService.java | 30 +- .../aggregation/StatisticService.java | 100 +- .../AbstractAddEvaluationTemplateRequest.java | 37 + ...aluationTemplateFromEvaluationRequest.java | 25 + .../AddEvaluationTemplateRequest.java | 17 - ...luationTemplateWithDataSourcesRequest.java | 28 + .../report/GetReportDetailPageResponse.java | 3 +- .../statistics/api/report/ReportInfoDto.java | 2 +- .../statistics/api/report/ReportStateDto.java | 3 +- .../datatransfer/AnalysisTemplateData.java | 14 + .../datatransfer/DiagramTemplateData.java | 12 + .../datatransfer/EvaluationTemplateData.java | 12 + .../export/DataExportController.java | 2 +- .../statistics/mapper/EvaluationMapper.java | 28 +- .../mapper/EvaluationTemplateMapper.java | 179 +- .../eshg/statistics/mapper/ReportMapper.java | 8 +- .../entity/AggregationResultState.java | 3 +- .../evaluationtemplate/BaseDataAttribute.java | 53 + .../evaluationtemplate/DataAttribute.java | 38 +- .../entity/evaluationtemplate/DataSource.java | 11 + .../EvaluationTemplate.java | 11 + .../persistence/entity/report/Report.java | 2 +- ..._add_deleting_aggregation_result_state.xml | 14 + .../0029_execution_date_mandatory.xml | 19 + ...ttribute_names_to_evaluation_templates.xml | 56 + .../main/resources/migrations/changelog.xml | 3 + backend/sti-protection/build.gradle | 3 +- backend/sti-protection/gradle.lockfile | 104 +- backend/sti-protection/openApi.yaml | 6 + .../StiProtectionProcedureController.java | 3 + .../StiProtectionProcedureService.java | 6 +- .../ProcedureStatusTransition.java | 15 + .../api/CreateProcedureRequest.java | 4 +- .../de/eshg/stiprotection/api/PersonDto.java | 4 +- .../StiProtectionProcedureOverviewDto.java | 2 + .../aspect/ProtectedProcedureAspect.java | 70 + .../config/DateTimeConstants.java | 18 + .../mapper/StiProtectionProcedureMapper.java | 1 + .../AnonymousIdentificationDocumentData.java | 8 + ...nonymousIdentificationDocumentService.java | 62 + .../pdf/identification/Appointment.java | 15 + .../pdf/identification/Department.java | 17 + .../pdf/identification/DocumentSender.java | 8 + .../stiprotection/persistence/db/Person.java | 8 +- .../testhelper/StiProtectionPopulator.java | 6 +- .../0002_introduce_procedure_file_type.xml | 11 + .../0003_rename_country_code_dto.xml | 11 + ...4_add_medical_registry_procedure_types.xml | 14 + .../main/resources/migrations/changelog.xml | 3 + .../templates/identification/.editorconfig | 3 + .../templates/identification/anon-ident.css | 143 + .../templates/identification/anon_indent.ftlx | 100 + .../templates/identification/footer.ftlx | 12 + .../templates/identification/header.ftlx | 53 + .../logo-gesundheitsamt-ffm.svg | 12 + backend/test-commons/gradle.lockfile | 2 +- backend/test-helper-commons/gradle.lockfile | 1 + backend/travel-medicine/build.gradle | 1 + backend/travel-medicine/gradle.lockfile | 68 +- backend/travel-medicine/openApi.yaml | 36 + .../certificate/CertificateService.java | 5 +- ...althInsuranceCertificatePdfParameters.java | 7 +- .../citizenauth/CitizenAuthController.java | 21 + .../DepartmentInfoProperties.java | 4 +- .../AppointmentDetailsMapper.java | 15 +- .../VaccinationConsultationService.java | 56 +- .../api/GetAppointmentDetailsResponse.java | 1 + ...ationConsultationTravelDetailsRequest.java | 4 +- .../api/PatientDto.java | 4 +- .../api/PersonAddressDto.java | 4 +- .../PostVaccinationConsultationRequest.java | 4 +- .../api/TravelInformationDto.java | 4 +- .../persistence/entity/ProcedureStep.java | 13 + .../entity/VaccinationConsultation.java | 8 +- .../0029_add_bookings_remaining_to_ps.xml | 19 + .../0030_introduce_procedure_file_type.xml | 11 + ...1_add_medical_registry_procedure_types.xml | 14 + .../main/resources/migrations/changelog.xml | 3 + .../de/eshg/frontend/PlaywrightTask.groovy | 12 +- buildSrc/src/main/groovy/node.gradle | 6 +- citizen-portal/.env | 2 + citizen-portal/package.json | 27 +- .../markdown/frankfurt/accessibility.md | 50 + .../public/markdown/frankfurt/imprint.md | 53 + .../public/markdown/frankfurt/privacy.md | 103 + .../meine-termine/details/buchen/page.tsx | 28 + .../[lang]/(static)/barrierefreiheit/page.tsx | 15 + .../app/[lang]/(static)/datenschutz/page.tsx | 10 + .../app/[lang]/(static)/impressum/page.tsx | 10 + .../[lang]/{ => (static)}/kontakt/page.tsx | 0 .../{ => (static)}/nutzungshinweise/page.tsx | 0 .../src/app/[lang]/barrierefreiheit/page.tsx | 147 - .../src/app/[lang]/datenschutz/page.tsx | 264 -- .../src/app/[lang]/impressum/page.tsx | 129 - .../src/app/[lang]/playground/alert/page.tsx | 106 + .../src/app/[lang]/playground/page.tsx | 16 +- citizen-portal/src/env/server.js | 2 + .../components/ContactInformation.tsx | 2 +- .../baseModule/components/MarkdownPage.tsx | 66 + .../lib/baseModule/locales/de/contact.json | 2 +- .../lib/baseModule/locales/en/contact.json | 2 +- .../steps/CitizenAnamnesisStepFour.tsx | 1 + .../api/mutations/citizenAuthApi.ts | 30 +- .../AppointmentContent.tsx | 4 +- .../NoAppointmentsContent.tsx | 10 +- .../shared/components/InfoModal.tsx | 8 +- .../components/shared/contexts/IdContext.tsx | 12 +- .../AppointmentDetailsSidePanel.tsx | 58 +- .../AppointmentPageContent.tsx | 10 - .../rebook/RebookAppointment.tsx | 118 + .../rebook/RebookAppointmentPageContent.tsx | 126 + .../rebook/RebookAppointmentSidePanel.tsx | 79 + .../locales/de/appointmentDetails.json | 1 + .../locales/de/rebookAppointment.json | 24 + .../locales/en/appointmentDetails.json | 1 + .../locales/en/rebookAppointment.json | 24 + .../travelMedicine/shared/routes.ts | 1 + .../layout/TitleAndSheetContentLayout.tsx | 2 + .../src/lib/shared/components/layout/page.tsx | 17 +- docs/e2e-tests.adoc | 118 +- docs/gradle.adoc | 4 +- docs/queries-and-mutations.adoc | 2 +- employee-portal/.env | 2 + employee-portal/package.json | 29 +- .../public/markdown/common/release-notes.md | 147 + .../markdown/frankfurt/accessibility.md | 69 + .../public/markdown/frankfurt/contact.md | 12 + .../public/markdown/frankfurt/privacy.md | 195 ++ .../(static)/[documentType]/page.tsx | 43 + .../{ => (static)}/acknowledgements/page.tsx | 0 .../{ => (static)}/usage-notes/page.tsx | 0 .../app/(baseModule)/accessibility/page.tsx | 21 - .../src/app/(baseModule)/contact/page.tsx | 19 - .../src/app/(baseModule)/privacy/page.tsx | 19 - .../app/(baseModule)/release-notes/page.tsx | 19 - .../src/app/(businessModules)/chat/layout.tsx | 18 - .../src/app/(businessModules)/chat/page.tsx | 25 +- .../[defId]/versions/[versionId]/new/page.tsx | 21 +- .../def/[defId]/versions/[versionId]/page.tsx | 27 +- .../inspection/checklist/def/new/page.tsx | 5 +- .../inspection/checklist/def/page.tsx | 7 +- .../procedures/new/[procedureId]/page.tsx | 48 +- .../versions/[version]/page.tsx | 32 +- .../versions/[version]/page.tsx | 32 +- .../inspection/textblocks/page.tsx | 13 +- .../procedures/[id]/examinations/layout.tsx | 84 +- .../school-entry/procedures/page.tsx | 14 +- .../statistics/geo-shapes/template.tsx | 8 + .../statistics/reports/[id]/page.tsx | 10 +- .../statistics/reports/template.tsx | 8 + .../statistics/statistics/template.tsx | 8 + .../sti-protection/procedures/page.tsx | 2 +- .../procedures/import-data/page.tsx | 10 - employee-portal/src/app/not-found.tsx | 53 +- .../src/app/playground/alert/page.tsx | 107 + .../playground/appointment-picker/page.tsx | 177 + .../playground/chat/chatPlaygroundContent.tsx | 4 +- employee-portal/src/app/playground/page.tsx | 8 + employee-portal/src/env/server.js | 2 + .../src/lib/baseModule/api/mapper/contacts.ts | 3 + .../src/lib/baseModule/api/mutations/gdpr.ts | 26 + .../src/lib/baseModule/api/mutations/users.ts | 24 + .../src/lib/baseModule/api/queries/config.ts | 7 +- .../src/lib/baseModule/api/queries/users.ts | 8 +- .../components/StaticTextDocumentPanel.tsx | 9 +- .../accessibility/Accessibility.tsx | 171 - .../baseModule/components/contact/Contact.tsx | 49 - .../components/contacts/ContactsTable.tsx | 105 +- .../contacts/ContactsTableTitle.tsx | 37 + .../components/contacts/columns.tsx | 18 +- .../contacts/forms/card/AddressCardsField.tsx | 96 - .../contacts/forms/card/AddressLine.tsx | 31 - .../contacts/forms/card/AddressMergeField.tsx | 159 + .../forms/card/InstitutionContactCard.tsx | 32 +- .../contacts/forms/card/PersonContactCard.tsx | 38 +- .../components/contacts/forms/helpers.ts | 58 +- .../merge/MergeInstitutionContactForm.tsx | 191 +- .../forms/merge/MergePersonContactForm.tsx | 251 +- .../contacts/forms/merge/MergeStringField.tsx | 10 +- .../forms/merge/SelectMergeTargetForm.tsx | 133 + .../baseModule/components/contacts/helpers.ts | 16 + .../contacts/history/ContactHistory.tsx | 3 + .../modals/AddInstitutionContactSidebar.tsx | 2 + .../modals/AddPersonContactSidebar.tsx | 2 + .../modals/MergeInstitutionContactSidebar.tsx | 76 + .../modals/MergePersonContactSidebar.tsx | 76 + .../gdpr/procedure/DownloadReportButton.tsx | 49 + .../gdpr/procedure/GDPRProcedureDetails.tsx | 77 +- .../sidebars/EditMatterOfConcernSidebar.tsx | 102 + .../procedure/tiles/ProcedureDetailsTile.tsx | 148 +- .../components/layout/SelfUserSidebar.tsx | 39 +- .../messagesSidebar/MessageInformation.tsx | 49 + .../messagesSidebar/MessagesSidebar.tsx | 3 +- .../MessagesSidebarContent.tsx | 28 +- .../components/markdown/MarkdownPage.tsx | 79 + .../baseModule/components/privacy/Privacy.tsx | 371 --- .../ProcedureMetricsDisplay.tsx | 1 + .../taskMetrics/TaskMetricsDisplay.tsx | 91 +- .../taskMetrics/slowestAndFastestColumns.tsx | 3 + .../components/releaseNotes/ReleaseNotes.tsx | 263 -- .../chat/components/ChatConsentModal.tsx | 16 +- .../components/ChatFeatureUnavailable.tsx | 24 +- .../chat/components/ChatNoAccessAlert.tsx | 9 +- .../messageTeaser/MessageTeaser.tsx | 173 + .../messageTeaser/MessageTeaserProvider.tsx | 80 + .../components/roomList/ReceiptStatus.tsx | 29 +- .../chat/components/roomList/RoomListItem.tsx | 19 +- .../components/secureBackup/SSOAuthModal.tsx | 14 +- .../lib/businessModules/chat/matrix/crypto.ts | 12 +- .../chat/shared/ChatClientProvider.tsx | 20 +- .../chat/shared/ChatProvider.tsx | 5 +- .../chat/shared/hooks/useChatLifecycle.tsx | 9 +- .../chat/shared/hooks/useMatrixClient.tsx | 7 +- .../inspection/api/mutations/inventory.ts | 12 +- .../inspection/api/queries/checklist.ts | 15 +- .../api/queries/checklistDefinition.ts | 20 +- .../inspection/api/queries/department.ts | 5 +- .../inspection/api/queries/facility.ts | 20 +- .../inspection/api/queries/incidents.ts | 19 +- .../inspection/api/queries/inspection.ts | 35 +- .../api/queries/inspectionReport.ts | 64 +- .../inspection/api/queries/objectTypes.ts | 9 +- .../api/queries/packlistDefinition.ts | 11 +- .../inspection/api/queries/users.ts | 18 +- .../EditChecklistDefinition.tsx | 10 +- .../elements/ChecklistDefinitionElement.tsx | 275 +- .../ChecklistDefinitionElementsList.tsx | 139 +- .../elements/NoteAndHelpTextInput.tsx | 28 +- .../inner/ChecklistDefinitionAnswerItem.tsx | 118 +- ...hecklistDefinitionElementCheckboxInner.tsx | 110 +- .../inner/ChecklistDefinitionElementInner.tsx | 2 + .../ChecklistDefinitionElementMultiInner.tsx | 4 + .../header/ChecklistDefinitionHeaderCard.tsx | 6 +- .../helpers/InputWithDeleteButton.tsx | 155 +- .../checklistDefinition/helpers/helpers.ts | 22 + .../ChecklistDefinitionOverviewTable.tsx | 12 +- .../checklistDefinition/overview/columns.tsx | 6 + .../sections/ChecklistDefinitionSection.tsx | 57 +- .../ChecklistDefinitionSectionsList.tsx | 8 +- .../history/ChecklistVersionsSidebar.tsx | 1 + .../facility/search/results/columns.tsx | 2 +- .../InspectionTabNavigationToolbar.tsx | 7 +- .../assignee/InspectionAssigneeSelection.tsx | 10 +- .../basedata/InspectionTabBasedata.tsx | 25 +- .../basedata/InspectionTypeCard.tsx | 17 +- .../execution/ExecutionSidePanel.tsx | 8 +- .../execution/InspectionTabExecution.tsx | 60 +- .../checklist/form/ChecklistFileElement.tsx | 21 +- .../checklist/form/ChecklistLabel.tsx | 12 +- .../inspection/new/AddInspectionTiles.tsx | 7 + .../inspection/new/AdditionalInfoTile.tsx | 9 +- .../planning/InspectionTabPlanning.tsx | 39 +- .../planning/checklist/ChecklistTile.tsx | 9 +- .../InspectionTabReportResult.tsx | 12 +- .../editor/InspectionReportEditor.tsx | 24 +- .../objectType/ObjectTypesTable.tsx | 2 +- .../CreateOrEditPacklistDefinitionSidebar.tsx | 8 +- .../CreatePacklistDefinitionSidebar.tsx | 3 + .../EditPacklistDefinitionSidebar.tsx | 23 +- .../elements/PacklistDefinitionElement.tsx | 2 +- .../header/PacklistDefinitionHeaderCard.tsx | 6 +- .../ChecklistDefinitionRepoOverviewTable.tsx | 1 + .../components/textBlock/TextBlocksTable.tsx | 20 +- .../shared/offline/ServiceWorkerProvider.tsx | 22 +- .../shared/offline/usePrecacheInspections.ts | 246 +- .../AppointmentBlockGroupForm.tsx | 6 +- .../AppointmentBlockGroupsTable.tsx | 6 +- .../ValidateAppointmentBlock.ts | 78 - .../AdditionalInfoSection.tsx | 11 +- .../procedureDetails/AddressDetails.tsx | 8 +- .../procedureDetails/AffectedPerson.tsx | 6 +- .../procedureDetails/ContactDetails.tsx | 5 +- .../procedureDetails/Custodians.tsx | 7 +- .../EditAccessRestrictionSidebar.tsx | 2 +- .../procedures/procedureDetails/Facility.tsx | 11 +- .../procedureDetails/FacilityContact.tsx | 14 +- .../procedureDetails/NewCustodianButton.tsx | 6 +- .../procedureDetails/NewFacilityButton.tsx | 7 +- .../procedureDetails/PersonDetails.tsx | 9 +- .../procedures/procedureDetails/ProofTab.tsx | 27 +- .../UpdateProcedureSection.tsx | 6 +- .../proof/AccessRestrictionCard.tsx | 16 +- .../proof/AppointmentCard.tsx | 22 +- .../procedureDetails/proof/ProofTabEntry.tsx | 2 +- .../proceduresTable/ProcedureSearchBar.tsx | 3 +- .../proceduresTable/ProceduresTable.tsx | 13 +- .../hooks/useTablePageParams.ts | 30 - .../MeaslesProtectionProcedureLayout.tsx | 9 +- .../schoolEntry/api/models/Location.ts | 21 + .../schoolEntry/api/models/Procedure.ts | 26 +- .../api/models/ProcedureDetails.ts | 38 +- .../api/mutations/schoolEntryApi.ts | 23 +- .../AppointmentBlockGroupForm.tsx | 85 +- .../CreateAppointmentBlockGroupForm.tsx | 2 +- .../features/procedures/ProcedureToolbar.tsx | 15 +- .../DevelopmentScreeningForm.tsx | 25 +- .../developmentScreening/Icd10Sidebar.tsx | 24 +- .../importData/ImportDataFields.tsx | 15 +- .../importData/ImportDataSidebar.tsx | 29 +- .../procedures/importData/ImportResult.tsx | 38 +- .../procedureDetails/LabelSelection.tsx | 4 +- .../ProcedureActionsPanel.tsx | 5 +- .../procedureDetails/ProcedureDetails.tsx | 13 +- .../ProcedureDetailsSection.tsx | 60 +- .../UpdateProcedureSidebar.tsx | 418 +-- .../ProcedureFilterSettings.tsx | 36 +- .../proceduresTable/ProcedureTableTitle.tsx | 82 +- .../proceduresTable/ProceduresTable.tsx | 61 +- .../reports/MedicalReportSidebar.tsx | 28 +- .../reports/SchoolInfoLetterSidebar.tsx | 33 +- .../schoolEntry/shared/routes.ts | 3 - .../schoolEntry/shared/sideNavigationItem.tsx | 20 +- .../api/models/reportDetailsViewTypes.ts | 6 +- .../api/models/reportSeriesTypes.ts | 37 + .../api/models/reportsOverviewTypes.ts | 30 +- .../statistics/api/models/statisticReports.ts | 21 +- .../api/mutations/useAddAutoReportSeries.ts | 4 +- .../mutations/useDeactivateReportSeries.ts | 31 + .../api/mutations/useDeleteReport.ts | 8 +- .../api/mutations/useDeleteReportSeries.ts | 24 + .../api/mutations/useUpdateDataBasis.ts | 53 + .../api/queries/useGetReportDetails.ts | 14 +- .../api/queries/useGetReportsOverview.ts | 55 +- .../api/queries/useGetStatisticReports.ts | 101 +- .../components/reports/ReportDetailsTile.tsx | 40 +- .../components/reports/ReportsOverview.tsx | 19 +- .../statistics/components/reports/columns.tsx | 52 +- .../reports/getReportActionItems.tsx | 98 +- .../useDeleteReportWithConfirmation.tsx | 52 - .../reports/useDeleteWithConfirmation.tsx | 81 + .../components/shared/charts/ScatterChart.tsx | 4 +- .../ChooseAttributesStep.tsx | 16 +- .../validateChooseAttributeStep.ts | 4 +- .../components/statistics/ReportStateChip.tsx | 2 + .../components/statistics/StatisticsTable.tsx | 41 +- .../details/DetailsInformationCard.tsx | 17 +- .../statistics/details/StatisticDetails.tsx | 1 + .../UpdateStatisticDataBasisSidebar.tsx | 18 +- .../UpdateStatisticDataBasisStep.tsx | 4 +- .../validateUpdateStatisticDataBasisStep.ts | 23 + .../AutomateReportSidebar.tsx | 6 +- .../AutomateReportStep.tsx | 8 +- .../automateReportFormModel.ts | 37 +- .../details/reports/ReportAutomationTile.tsx | 117 + .../details/reports/StatisticReports.tsx | 155 +- .../UpdateReportSidebar.tsx | 7 +- .../stiProtection/api/mutations/procedures.ts | 56 + .../AppointmentBlockGroupForm.tsx | 8 +- .../CreateAppointmentBlockGroupForm.tsx | 2 +- .../ValidateAppointmentBlock.ts | 78 - .../StiProtectionProceduresSearchBar.tsx | 2 +- .../StiProtectionProceduresTable.tsx | 119 +- .../features/procedures/ProcedureToolbar.tsx | 7 +- .../AddNewProcedureSidebar.tsx | 0 .../addNewProcedure/AppointmentForm.tsx | 12 +- .../addNewProcedure/PersonalDataForm.tsx | 0 .../addNewProcedure/SummaryForm.tsx | 0 .../details/AdditionalDataSection.tsx | 36 +- .../details/CloseAndReopenDialogs.tsx | 144 + .../details/CloseProcedurePanel.tsx | 55 +- .../procedures/details/PersonDetails.tsx | 87 +- .../procedures/details/ProcedureDetails.tsx | 4 +- .../addNewProcedure/AppointmentCalendar.tsx | 258 -- .../AppointmentPickerField.tsx | 178 - .../stiProtection/shared/helpers.ts | 20 + .../AppointmentBlockGroupForm.tsx | 13 +- .../CreateAppointmentBlockGroupForm.tsx | 2 +- .../validateAppointmentBlock.ts | 72 - .../diseases/DiseasesOverviewTable.tsx | 10 +- .../OtherServiceTemplateForm.tsx | 10 +- ...nationConsultationTabNavigationToolbar.tsx | 9 +- .../baseData/InformationStatementsTable.tsx | 10 +- .../baseData/InitialAppointmentTile.tsx | 10 +- .../baseData/PatientPanel.tsx | 10 +- .../baseData/ServicePlanTable.tsx | 10 +- .../baseData/TravelDataTile.tsx | 10 +- .../medicalHistory/MedicalHistoryContent.tsx | 10 +- .../vaccines/VaccinesOverviewTable.tsx | 10 +- .../shared/api/mutations/approvalRequests.ts | 2 +- .../src/lib/shared/components/BaseModal.tsx | 12 +- .../src/lib/shared/components/FileCard.tsx | 3 + .../SidebarStepper/SidebarStepper.tsx | 22 +- .../AppointmentBlockFieldArrayWithDays.tsx | 74 +- .../AppointmentBlockFormWithDays.tsx | 31 +- .../AppointmentBlockGroupFields.tsx | 7 +- .../AppointmentStaffSelection.tsx | 2 +- .../WeekdayCheckboxGroup.tsx | 2 +- .../validateAppointmentBlock.ts | 2 +- .../components/archiveView/ArchiveTable.tsx | 3 + .../archiveView/ArchiveTableTitle.tsx | 33 +- .../archiveView/archiveTableColumns.tsx | 9 + .../shared/components/buttons/ActionsMenu.tsx | 10 +- .../components/buttons/IconTooltipButton.tsx | 82 + .../components/buttons/ToggleButton.tsx | 5 +- .../shared/components/cards/ProcedureCard.tsx | 2 +- .../components/chat/MessageTeaserProvider.tsx | 243 -- .../components/detailsCard/DetailsCard.tsx} | 16 +- .../components/detailsCard}/LabeledValue.tsx | 2 +- .../shared/components/drawer/useSidebar.tsx | 3 +- .../facilitySidebar/FacilityForm.tsx | 2 +- .../lib/shared/components/form/FormSheet.tsx | 7 +- .../shared/components/form/SidebarForm.tsx | 14 +- .../components/formFields/TextareaField.tsx | 4 +- .../components/layout/MainContentLayout.tsx | 4 +- .../lib/shared/components/layout/Toolbar.tsx | 2 +- .../pagination/RowsPerPageSelect.tsx | 9 +- .../shared/components/procedures/constants.ts | 5 + .../lib/shared/components/sidebar/Sidebar.tsx | 8 +- .../components/sidebar/SidebarContent.tsx | 23 +- .../TabNavigationToolbar.tsx | 18 +- .../lib/shared/components/table/DataTable.tsx | 5 +- .../table/RowSelectionTableToolbar.tsx | 58 + .../shared/components/table/TableSheet.tsx | 8 +- .../lib/shared/hooks/useTablePageParams.ts | 61 + .../serviceWorker/common/validatePassword.ts | 10 +- ...olPlugin.ts => CacheableResponsePlugin.ts} | 9 +- employee-portal/src/serviceWorker/sw/index.ts | 8 +- lib-portal/package.json | 18 +- lib-portal/src/api/useHandledMutation.ts | 15 +- lib-portal/src/components/Alert.tsx | 24 +- .../src/components}/Row.tsx | 2 - .../src/components/formFields/InputField.tsx | 6 +- .../appointmentPicker/AppointmentCalendar.tsx | 87 + .../AppointmentListForDate.tsx | 122 + .../AppointmentPickerField.tsx | 161 + .../formFields/appointmentPicker/Day.tsx | 99 + .../appointmentPicker/MonthSelection.tsx | 59 + .../appointmentPicker/WeekdayHeaders.tsx | 36 + .../formFields/appointmentPicker/helpers.ts | 57 + .../formFields/appointmentPicker/labels.ts | 16 + lib-portal/src/errorHandling/AlertContext.tsx | 151 +- lib-portal/src/errorHandling/errorMappers.tsx | 12 +- .../src}/hooks/useUuid.ts | 0 .../contentSecurityPolicyHeaderMiddleware.ts | 8 +- package.json | 20 +- pnpm-lock.yaml | 2934 ++++++++++++----- reverse-proxy/forward_headers.conf | 3 + reverse-proxy/keycloak.conf | 7 +- 787 files changed, 18607 insertions(+), 8771 deletions(-) delete mode 100644 backend/base-api/src/main/java/de/eshg/base/CountryCodeDto.java create mode 100644 backend/base-api/src/main/java/de/eshg/base/gdpr/DeleteGdprDownloadsRequest.java create mode 100644 backend/base/src/main/java/de/eshg/base/testhelper/HealthDepartmentContactPopulator.java create mode 100644 backend/file-commons/README_LICENSE.adoc create mode 100644 backend/file-commons/build.gradle create mode 100644 backend/file-commons/buildscript-gradle.lockfile create mode 100644 backend/file-commons/gradle.lockfile rename backend/{business-module-commons/src/main/java/de/base/rest => file-commons/src/main/java/de/eshg/file/common}/CustomMediaTypes.java (93%) rename backend/{lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model => file-commons/src/main/java/de/eshg/file/common}/FileExtension.java (85%) rename backend/{lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model => file-commons/src/main/java/de/eshg/file/common}/FileType.java (79%) rename backend/{lib-procedures/src/main/java/de/eshg/lib/procedure/file => file-commons/src/main/java/de/eshg/file/common}/FileTypeDetector.java (67%) rename backend/{lib-procedures/src/main/java/de/eshg/lib/procedure/file => file-commons/src/main/java/de/eshg/file/common}/PdfAConformanceValidator.java (92%) create mode 100644 backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContentSerializer.java create mode 100644 backend/inspection/src/main/resources/migrations/0038_remove_report_fk.xml create mode 100644 backend/inspection/src/main/resources/migrations/0039_remove_fk_checklistelement_incident.xml create mode 100644 backend/inspection/src/main/resources/migrations/0040_introduce_procedure_file_type.xml create mode 100644 backend/inspection/src/main/resources/migrations/0041_add_medical_registry_procedure_types.xml rename backend/{base/src/main/java/de/eshg/base/util => lib-commons/src/main/java/de/eshg/lib/common}/CountryCode.java (79%) create mode 100644 backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcedureFileType.java create mode 100644 backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/serialization/SerializationObjectMapperConfigurer.java create mode 100644 backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileTypeMapper.java create mode 100644 backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/OpenDataPublicSecurityConfig.java create mode 100644 backend/lib-xlsx-import/README_LICENSE.adoc create mode 100644 backend/lib-xlsx-import/build.gradle create mode 100644 backend/lib-xlsx-import/buildscript-gradle.lockfile create mode 100644 backend/lib-xlsx-import/gradle.lockfile rename backend/{school-entry/src/main/java/de/eshg/schoolentry/importer => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/ColumnAccessor.java (94%) rename backend/{school-entry/src/main/java/de/eshg/schoolentry/importer => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/FeedbackColumnAccessor.java (89%) create mode 100644 backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportStatistics.java rename backend/{school-entry/src/main/java/de/eshg/schoolentry/importer => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/ImportStatus.java (93%) rename backend/{school-entry/src/main/java/de/eshg/schoolentry/importer => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/ImportValidator.java (97%) create mode 100644 backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/Importer.java rename backend/{school-entry/src/main/java/de/eshg/schoolentry/importer/RowProcessor.java => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/RowReader.java} (91%) rename backend/{school-entry/src/main/java/de/eshg/schoolentry/importer => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/RowValues.java (62%) rename backend/{school-entry/src/main/java/de/eshg/schoolentry/importer => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/XlsxColumn.java (90%) rename backend/{school-entry/src/main/java/de/eshg/schoolentry/importer => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/XlsxNormalizer.java (93%) rename backend/{school-entry/src/main/java/de/eshg/schoolentry => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/api/ImportStatisticsDto.java (87%) rename backend/{school-entry/src/main/java/de/eshg/schoolentry/business => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/model/AddressData.java (54%) rename backend/{school-entry/src/main/java/de/eshg/schoolentry/business => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport}/model/ImportResult.java (53%) create mode 100644 backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/util/FileResponseUtil.java rename backend/{school-entry/src/main/java/de/eshg/schoolentry/importer => lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/util}/XlsxUtil.java (70%) create mode 100644 backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/ImportResponse.java create mode 100644 backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/MultipartResponseAssertionTraits.java create mode 100644 backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/MultipartUtil.java create mode 100644 backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/XlsxAssertionTraits.java create mode 100644 backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/LsdInitialSetupService.java create mode 100644 backend/local-service-directory/src/main/resources/application-local.properties create mode 100644 backend/measles-protection/src/main/resources/migrations/0024_introduce_procedure_file_type.xml create mode 100644 backend/measles-protection/src/main/resources/migrations/0025_medical_registry_procedure_types.xml create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/MedicalRegistryController.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/MedicalRegistryService.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/AddressDto.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/CreateProcedureDto.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/CreateProcedureRequest.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/EmploymentStatusDto.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/EmploymentTypeDto.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/GetProcedureResponse.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/PracticeDto.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/ProfessionalDto.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/ProfessionalTitleDto.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/business/model/ProcedureDetailsData.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/EmploymentStatus.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/EmploymentType.java delete mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Facility.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistryEntryChange.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistrySystemProgressEntryType.java delete mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Person.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Practice.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Professional.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/ProfessionalTitle.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/TypeOfChange.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/AddressMapper.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/PracticeMapper.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/ProfessionalMapper.java create mode 100644 backend/medical-registry/src/main/java/de/eshg/medicalregistry/util/MapperUtils.java create mode 100644 backend/opendata/openApi.yaml delete mode 100644 backend/opendata/src/main/java/de/eshg/opendata/FileType.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/OpenDataController.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/OpenDataMapper.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/OpenDataService.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/VersionFilterSpecification.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/api/GetOpenDocumentsRequest.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/api/GetOpenDocumentsResponse.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/api/PostOpenDocumentRequest.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/api/ResourceDto.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/api/UpdateVersionMetaDataRequest.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/api/VersionDto.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/domain/model/FileContent.java create mode 100644 backend/opendata/src/main/java/de/eshg/opendata/domain/model/OpenDataFileType.java create mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/api/GetClosedProceduresResponse.java rename backend/school-entry/src/main/java/de/eshg/schoolentry/importer/{CitizenListRowProcessor.java => CitizenListRowReader.java} (65%) create mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowValueMapper.java delete mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/importer/EqualityComparator.java rename backend/school-entry/src/main/java/de/eshg/schoolentry/importer/{PastProcedureListRowProcessor.java => PastProcedureListRowReader.java} (62%) create mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowValueMapper.java create mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolEntryImporter.java create mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolEntryRowValues.java delete mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowProcessor.java create mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowReader.java create mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowValueMapper.java create mode 100644 backend/school-entry/src/main/java/de/eshg/schoolentry/util/TaskUtil.java create mode 100644 backend/school-entry/src/main/resources/migrations/0047_introduce_procedure_file_type.xml create mode 100644 backend/school-entry/src/main/resources/migrations/0048_add_medical_registry_procedure_types.xml delete mode 100644 backend/statistics/src/main/java/de/eshg/statistics/AttributeCodeToNameMapping.java create mode 100644 backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AbstractAddEvaluationTemplateRequest.java create mode 100644 backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateFromEvaluationRequest.java delete mode 100644 backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateRequest.java create mode 100644 backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateWithDataSourcesRequest.java create mode 100644 backend/statistics/src/main/java/de/eshg/statistics/datatransfer/AnalysisTemplateData.java create mode 100644 backend/statistics/src/main/java/de/eshg/statistics/datatransfer/DiagramTemplateData.java create mode 100644 backend/statistics/src/main/java/de/eshg/statistics/datatransfer/EvaluationTemplateData.java create mode 100644 backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/BaseDataAttribute.java create mode 100644 backend/statistics/src/main/resources/migrations/0028_add_deleting_aggregation_result_state.xml create mode 100644 backend/statistics/src/main/resources/migrations/0029_execution_date_mandatory.xml create mode 100644 backend/statistics/src/main/resources/migrations/0030_add_attribute_names_to_evaluation_templates.xml create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/annotations/ProcedureStatusTransition.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/aspect/ProtectedProcedureAspect.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/config/DateTimeConstants.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/AnonymousIdentificationDocumentData.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/AnonymousIdentificationDocumentService.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/Appointment.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/Department.java create mode 100644 backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/DocumentSender.java create mode 100644 backend/sti-protection/src/main/resources/migrations/0002_introduce_procedure_file_type.xml create mode 100644 backend/sti-protection/src/main/resources/migrations/0003_rename_country_code_dto.xml create mode 100644 backend/sti-protection/src/main/resources/migrations/0004_add_medical_registry_procedure_types.xml create mode 100644 backend/sti-protection/src/main/resources/templates/identification/.editorconfig create mode 100644 backend/sti-protection/src/main/resources/templates/identification/anon-ident.css create mode 100644 backend/sti-protection/src/main/resources/templates/identification/anon_indent.ftlx create mode 100644 backend/sti-protection/src/main/resources/templates/identification/footer.ftlx create mode 100644 backend/sti-protection/src/main/resources/templates/identification/header.ftlx create mode 100644 backend/sti-protection/src/main/resources/templates/identification/logo-gesundheitsamt-ffm.svg create mode 100644 backend/travel-medicine/src/main/resources/migrations/0029_add_bookings_remaining_to_ps.xml create mode 100644 backend/travel-medicine/src/main/resources/migrations/0030_introduce_procedure_file_type.xml create mode 100644 backend/travel-medicine/src/main/resources/migrations/0031_add_medical_registry_procedure_types.xml create mode 100644 citizen-portal/public/markdown/frankfurt/accessibility.md create mode 100644 citizen-portal/public/markdown/frankfurt/imprint.md create mode 100644 citizen-portal/public/markdown/frankfurt/privacy.md create mode 100644 citizen-portal/src/app/[lang]/(privatpersonen)/impfberatung/meine-termine/details/buchen/page.tsx create mode 100644 citizen-portal/src/app/[lang]/(static)/barrierefreiheit/page.tsx create mode 100644 citizen-portal/src/app/[lang]/(static)/datenschutz/page.tsx create mode 100644 citizen-portal/src/app/[lang]/(static)/impressum/page.tsx rename citizen-portal/src/app/[lang]/{ => (static)}/kontakt/page.tsx (100%) rename citizen-portal/src/app/[lang]/{ => (static)}/nutzungshinweise/page.tsx (100%) delete mode 100644 citizen-portal/src/app/[lang]/barrierefreiheit/page.tsx delete mode 100644 citizen-portal/src/app/[lang]/datenschutz/page.tsx delete mode 100644 citizen-portal/src/app/[lang]/impressum/page.tsx create mode 100644 citizen-portal/src/app/[lang]/playground/alert/page.tsx create mode 100644 citizen-portal/src/lib/baseModule/components/MarkdownPage.tsx create mode 100644 citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointment.tsx create mode 100644 citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentPageContent.tsx create mode 100644 citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentSidePanel.tsx create mode 100644 citizen-portal/src/lib/businessModules/travelMedicine/locales/de/rebookAppointment.json create mode 100644 citizen-portal/src/lib/businessModules/travelMedicine/locales/en/rebookAppointment.json create mode 100644 employee-portal/public/markdown/common/release-notes.md create mode 100644 employee-portal/public/markdown/frankfurt/accessibility.md create mode 100644 employee-portal/public/markdown/frankfurt/contact.md create mode 100644 employee-portal/public/markdown/frankfurt/privacy.md create mode 100644 employee-portal/src/app/(baseModule)/(static)/[documentType]/page.tsx rename employee-portal/src/app/(baseModule)/{ => (static)}/acknowledgements/page.tsx (100%) rename employee-portal/src/app/(baseModule)/{ => (static)}/usage-notes/page.tsx (100%) delete mode 100644 employee-portal/src/app/(baseModule)/accessibility/page.tsx delete mode 100644 employee-portal/src/app/(baseModule)/contact/page.tsx delete mode 100644 employee-portal/src/app/(baseModule)/privacy/page.tsx delete mode 100644 employee-portal/src/app/(baseModule)/release-notes/page.tsx delete mode 100644 employee-portal/src/app/(businessModules)/chat/layout.tsx create mode 100644 employee-portal/src/app/(businessModules)/statistics/geo-shapes/template.tsx create mode 100644 employee-portal/src/app/(businessModules)/statistics/reports/template.tsx create mode 100644 employee-portal/src/app/(businessModules)/statistics/statistics/template.tsx delete mode 100644 employee-portal/src/app/@modal/(businessModules)/school-entry/procedures/import-data/page.tsx create mode 100644 employee-portal/src/app/playground/alert/page.tsx create mode 100644 employee-portal/src/app/playground/appointment-picker/page.tsx delete mode 100644 employee-portal/src/lib/baseModule/components/accessibility/Accessibility.tsx delete mode 100644 employee-portal/src/lib/baseModule/components/contact/Contact.tsx create mode 100644 employee-portal/src/lib/baseModule/components/contacts/ContactsTableTitle.tsx delete mode 100644 employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressCardsField.tsx delete mode 100644 employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressLine.tsx create mode 100644 employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressMergeField.tsx create mode 100644 employee-portal/src/lib/baseModule/components/contacts/forms/merge/SelectMergeTargetForm.tsx create mode 100644 employee-portal/src/lib/baseModule/components/contacts/modals/MergeInstitutionContactSidebar.tsx create mode 100644 employee-portal/src/lib/baseModule/components/contacts/modals/MergePersonContactSidebar.tsx create mode 100644 employee-portal/src/lib/baseModule/components/gdpr/procedure/DownloadReportButton.tsx create mode 100644 employee-portal/src/lib/baseModule/components/gdpr/procedure/sidebars/EditMatterOfConcernSidebar.tsx create mode 100644 employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessageInformation.tsx create mode 100644 employee-portal/src/lib/baseModule/components/markdown/MarkdownPage.tsx delete mode 100644 employee-portal/src/lib/baseModule/components/privacy/Privacy.tsx delete mode 100644 employee-portal/src/lib/baseModule/components/releaseNotes/ReleaseNotes.tsx create mode 100644 employee-portal/src/lib/businessModules/chat/components/messageTeaser/MessageTeaser.tsx create mode 100644 employee-portal/src/lib/businessModules/chat/components/messageTeaser/MessageTeaserProvider.tsx delete mode 100644 employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/ValidateAppointmentBlock.ts delete mode 100644 employee-portal/src/lib/businessModules/measlesProtection/hooks/useTablePageParams.ts create mode 100644 employee-portal/src/lib/businessModules/schoolEntry/api/models/Location.ts create mode 100644 employee-portal/src/lib/businessModules/statistics/api/models/reportSeriesTypes.ts create mode 100644 employee-portal/src/lib/businessModules/statistics/api/mutations/useDeactivateReportSeries.ts create mode 100644 employee-portal/src/lib/businessModules/statistics/api/mutations/useDeleteReportSeries.ts create mode 100644 employee-portal/src/lib/businessModules/statistics/api/mutations/useUpdateDataBasis.ts delete mode 100644 employee-portal/src/lib/businessModules/statistics/components/reports/useDeleteReportWithConfirmation.tsx create mode 100644 employee-portal/src/lib/businessModules/statistics/components/reports/useDeleteWithConfirmation.tsx create mode 100644 employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/validateUpdateStatisticDataBasisStep.ts create mode 100644 employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/ReportAutomationTile.tsx delete mode 100644 employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/ValidateAppointmentBlock.ts rename employee-portal/src/lib/businessModules/stiProtection/{ => features}/procedures/addNewProcedure/AddNewProcedureSidebar.tsx (100%) rename employee-portal/src/lib/businessModules/stiProtection/{ => features}/procedures/addNewProcedure/AppointmentForm.tsx (94%) rename employee-portal/src/lib/businessModules/stiProtection/{ => features}/procedures/addNewProcedure/PersonalDataForm.tsx (100%) rename employee-portal/src/lib/businessModules/stiProtection/{ => features}/procedures/addNewProcedure/SummaryForm.tsx (100%) create mode 100644 employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/CloseAndReopenDialogs.tsx delete mode 100644 employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentCalendar.tsx delete mode 100644 employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentPickerField.tsx delete mode 100644 employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/validateAppointmentBlock.ts rename employee-portal/src/lib/{businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm => shared/components/appointmentBlocks}/validateAppointmentBlock.ts (98%) create mode 100644 employee-portal/src/lib/shared/components/buttons/IconTooltipButton.tsx delete mode 100644 employee-portal/src/lib/shared/components/chat/MessageTeaserProvider.tsx rename employee-portal/src/lib/{businessModules/measlesProtection/components/procedures/procedureDetails/DetailCard.tsx => shared/components/detailsCard/DetailsCard.tsx} (81%) rename employee-portal/src/lib/{businessModules/measlesProtection/components/procedures/procedureDetails => shared/components/detailsCard}/LabeledValue.tsx (98%) create mode 100644 employee-portal/src/lib/shared/components/table/RowSelectionTableToolbar.tsx create mode 100644 employee-portal/src/lib/shared/hooks/useTablePageParams.ts rename employee-portal/src/serviceWorker/sw/{FilterByCacheControlPlugin.ts => CacheableResponsePlugin.ts} (70%) rename {employee-portal/src/lib/shared => lib-portal/src/components}/Row.tsx (96%) create mode 100644 lib-portal/src/components/formFields/appointmentPicker/AppointmentCalendar.tsx create mode 100644 lib-portal/src/components/formFields/appointmentPicker/AppointmentListForDate.tsx create mode 100644 lib-portal/src/components/formFields/appointmentPicker/AppointmentPickerField.tsx create mode 100644 lib-portal/src/components/formFields/appointmentPicker/Day.tsx create mode 100644 lib-portal/src/components/formFields/appointmentPicker/MonthSelection.tsx create mode 100644 lib-portal/src/components/formFields/appointmentPicker/WeekdayHeaders.tsx create mode 100644 lib-portal/src/components/formFields/appointmentPicker/helpers.ts create mode 100644 lib-portal/src/components/formFields/appointmentPicker/labels.ts rename {employee-portal/src/lib/shared => lib-portal/src}/hooks/useUuid.ts (100%) diff --git a/.editorconfig b/.editorconfig index 08dc9d8a0..56f179d51 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,4 +14,7 @@ insert_final_newline = false trim_trailing_whitespace = false [*.java] -ij_java_names_count_to_use_import_on_demand = 999 \ No newline at end of file +ij_java_names_count_to_use_import_on_demand = 999 + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitignore b/.gitignore index fe9bd7cbe..583a654a9 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ bin/ .local .jpb/ + +config/vitest.base.js diff --git a/admin-portal/package.json b/admin-portal/package.json index 732583539..e88c0e18c 100644 --- a/admin-portal/package.json +++ b/admin-portal/package.json @@ -12,29 +12,29 @@ "@mui/icons-material": "5.16.7", "@mui/joy": "5.0.0-beta.48", "@mui/material": "npm:@mui/joy@5.0.0-beta.48", - "@tanstack/react-query": "5.56.2", + "@tanstack/react-query": "5.59.10", "@tanstack/react-table": "8.20.5", "asn1js": "3.0.5", - "i18next": "23.15.1", + "i18next": "23.15.2", "i18next-resources-to-backend": "1.2.1", - "next": "14.2.12", + "next": "14.2.14", "pkijs": "3.2.4", "pvutils": "1.1.3", "react": "18.3.1", "react-dom": "18.3.1", "react-i18next": "15.0.2", - "valibot": "0.42.0", + "valibot": "0.42.1", "zod": "3.23.8" }, "devDependencies": { - "@next/bundle-analyzer": "14.2.12", - "@tanstack/eslint-plugin-query": "5.56.1", - "@types/react": "18.3.7", - "@types/react-dom": "18.3.0", - "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-istanbul": "2.1.1", - "eslint-config-next": "14.2.12", + "@next/bundle-analyzer": "14.2.14", + "@tanstack/eslint-plugin-query": "5.59.7", + "@types/react": "18.3.11", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "4.3.2", + "@vitest/coverage-istanbul": "2.1.2", + "eslint-config-next": "14.2.14", "vite-tsconfig-paths": "5.0.1", - "vitest": "2.1.1" + "vitest": "2.1.2" } } diff --git a/backend/auditlog-api/gradle.lockfile b/backend/auditlog-api/gradle.lockfile index df0329dc5..2653d764a 100644 --- a/backend/auditlog-api/gradle.lockfile +++ b/backend/auditlog-api/gradle.lockfile @@ -14,6 +14,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspat io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/auditlog/build.gradle b/backend/auditlog/build.gradle index 3e29ad5b3..edbe1d05f 100644 --- a/backend/auditlog/build.gradle +++ b/backend/auditlog/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation project(':business-module-persistence-commons') implementation project(':lib-base-client') implementation project(':lib-auditlog') + implementation project(':file-commons') implementation 'commons-io:commons-io:latest.release' implementation 'org.bouncycastle:bcprov-jdk18on:latest.release' diff --git a/backend/auditlog/gradle.lockfile b/backend/auditlog/gradle.lockfile index 4528aff91..b9af58f25 100644 --- a/backend/auditlog/gradle.lockfile +++ b/backend/auditlog/gradle.lockfile @@ -13,11 +13,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -25,7 +25,7 @@ com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=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 @@ -34,11 +34,11 @@ com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath, com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -46,10 +46,10 @@ de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeCla de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-jakarta9:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -63,6 +63,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -73,18 +74,19 @@ jakarta.transaction:jakarta.transaction-api:2.0.1=compileClasspath,productionRun jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.24.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.11.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -92,24 +94,25 @@ org.apache.httpcomponents.core5:httpcore5-h2:5.2.5=productionRuntimeClasspath,ru org.apache.httpcomponents.core5:httpcore5:5.2.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.23.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.23.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-core:2.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.30=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcpkix-jdk18on:1.78.1=testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.bouncycastle:bcprov-jdk18on:1.78.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.78.1=testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.78.1=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 org.glassfish.jaxb:jaxb-runtime:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -120,27 +123,28 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspath +org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -150,7 +154,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -166,7 +170,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -189,7 +193,7 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -197,7 +201,14 @@ org.testcontainers:database-commons:1.19.8=testCompileClasspath,testRuntimeClass org.testcontainers:jdbc:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:junit-jupiter:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testCompileClasspath,testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:parser:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:pdf-model:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:validation-model-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:verapdf-xmp-core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.webjars:swagger-ui:5.17.14=testCompileClasspath,testRuntimeClasspath org.xmlunit:xmlunit-core:2.9.1=testCompileClasspath,testRuntimeClasspath org.yaml:snakeyaml:2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,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 b41055de3..625be0c4b 100644 --- a/backend/auditlog/src/main/java/de/eshg/auditlog/AuditLogController.java +++ b/backend/auditlog/src/main/java/de/eshg/auditlog/AuditLogController.java @@ -5,10 +5,10 @@ package de.eshg.auditlog; -import static de.base.rest.CustomMediaTypes.TEXT_PLAIN_UTF_8; import static de.eshg.auditlog.AuditLogApi.QueryParameter.END_DATE; import static de.eshg.auditlog.AuditLogApi.QueryParameter.START_DATE; import static de.eshg.base.user.api.UserRoleDto.AUDITLOG_DECRYPT_AND_ACCESS; +import static de.eshg.file.common.CustomMediaTypes.TEXT_PLAIN_UTF_8; import de.cronn.commons.lang.StreamUtil; import de.eshg.auditlog.crypto.AsymmetricEncryption; diff --git a/backend/auth/gradle.lockfile b/backend/auth/gradle.lockfile index 9c1c9940a..fcade9b70 100644 --- a/backend/auth/gradle.lockfile +++ b/backend/auth/gradle.lockfile @@ -58,6 +58,7 @@ io.netty:netty-transport-native-unix-common:4.1.113.Final=compileClasspath,produ io.netty:netty-transport:4.1.113.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.projectreactor:reactor-core:3.6.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/auth/src/main/resources/application.properties b/backend/auth/src/main/resources/application.properties index e9fdaf577..acb3c5918 100644 --- a/backend/auth/src/main/resources/application.properties +++ b/backend/auth/src/main/resources/application.properties @@ -1,4 +1,5 @@ server.port=8092 +server.tomcat.max-http-response-header-size=24KB spring.security.oauth2.client.registration.keycloak.client-id=system-eshg-auth-service spring.security.oauth2.client.registration.keycloak.scope=openid diff --git a/backend/base-api/gradle.lockfile b/backend/base-api/gradle.lockfile index afd4540f8..c6eccab78 100644 --- a/backend/base-api/gradle.lockfile +++ b/backend/base-api/gradle.lockfile @@ -14,6 +14,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspat io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath diff --git a/backend/base-api/src/main/java/de/eshg/base/CountryCodeDto.java b/backend/base-api/src/main/java/de/eshg/base/CountryCodeDto.java deleted file mode 100644 index 2173110af..000000000 --- a/backend/base-api/src/main/java/de/eshg/base/CountryCodeDto.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: Apache-2.0 - */ - -package de.eshg.base; - -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.Locale; - -// ISO 3166-1 -@Schema(name = "CountryCode", description = "List of country codes in ISO 3166-1 alpha-2 format.") -public enum CountryCodeDto { - AD, - AE, - AF, - AG, - AI, - AL, - AM, - AO, - AQ, - AR, - AS, - AT, - AU, - AW, - AX, - AZ, - BA, - BB, - BD, - BE, - BF, - BG, - BH, - BI, - BJ, - BL, - BM, - BN, - BO, - BQ, - BR, - BS, - BT, - BV, - BW, - BY, - BZ, - CA, - CC, - CD, - CF, - CG, - CH, - CI, - CK, - CL, - CM, - CN, - CO, - CR, - CU, - CV, - CW, - CX, - CY, - CZ, - DE, - DJ, - DK, - DM, - DO, - DZ, - EC, - EE, - EG, - EH, - ER, - ES, - ET, - FI, - FJ, - FK, - FM, - FO, - FR, - GA, - GB, - GD, - GE, - GF, - GG, - GH, - GI, - GL, - GM, - GN, - GP, - GQ, - GR, - GS, - GT, - GU, - GW, - GY, - HK, - HM, - HN, - HR, - HT, - HU, - ID, - IE, - IL, - IM, - IN, - IO, - IQ, - IR, - IS, - IT, - JE, - JM, - JO, - JP, - KE, - KG, - KH, - KI, - KM, - KN, - KP, - KR, - KW, - KY, - KZ, - LA, - LB, - LC, - LI, - LK, - LR, - LS, - LT, - LU, - LV, - LY, - MA, - MC, - MD, - ME, - MF, - MG, - MH, - MK, - ML, - MM, - MN, - MO, - MP, - MQ, - MR, - MS, - MT, - MU, - MV, - MW, - MX, - MY, - MZ, - NA, - NC, - NE, - NF, - NG, - NI, - NL, - NO, - NP, - NR, - NU, - NZ, - OM, - PA, - PE, - PF, - PG, - PH, - PK, - PL, - PM, - PN, - PR, - PS, - PT, - PW, - PY, - QA, - RE, - RO, - RS, - RU, - RW, - SA, - SB, - SC, - SD, - SE, - SG, - SH, - SI, - SJ, - SK, - SL, - SM, - SN, - SO, - SR, - SS, - ST, - SV, - SX, - SY, - SZ, - TC, - TD, - TF, - TG, - TH, - TJ, - TK, - TL, - TM, - TN, - TO, - TR, - TT, - TV, - TW, - TZ, - UA, - UG, - UM, - US, - UY, - UZ, - VA, - VC, - VE, - VG, - VI, - VN, - VU, - WF, - WS, - YE, - YT, - ZA, - ZM, - ZW; - - public static String getCountryName(CountryCodeDto countryCode) { - Locale locale = new Locale.Builder().setRegion(countryCode.name()).setLanguage("DE").build(); - return locale.getDisplayCountry(locale); - } -} diff --git a/backend/base-api/src/main/java/de/eshg/base/address/AddressDto.java b/backend/base-api/src/main/java/de/eshg/base/address/AddressDto.java index e99e3c7e4..7a536d347 100644 --- a/backend/base-api/src/main/java/de/eshg/base/address/AddressDto.java +++ b/backend/base-api/src/main/java/de/eshg/base/address/AddressDto.java @@ -8,8 +8,8 @@ package de.eshg.base.address; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import de.eshg.base.CountryCodeDto; import de.eshg.base.HasTypeDiscriminator; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; @Schema(name = "Address") @@ -24,7 +24,7 @@ import io.swagger.v3.oas.annotations.media.Schema; public sealed interface AddressDto extends HasTypeDiscriminator permits DomesticAddressDto, PostboxAddressDto { - CountryCodeDto country(); + CountryCode country(); String city(); diff --git a/backend/base-api/src/main/java/de/eshg/base/address/DomesticAddressDto.java b/backend/base-api/src/main/java/de/eshg/base/address/DomesticAddressDto.java index fbcb2f981..954b9c92b 100644 --- a/backend/base-api/src/main/java/de/eshg/base/address/DomesticAddressDto.java +++ b/backend/base-api/src/main/java/de/eshg/base/address/DomesticAddressDto.java @@ -5,14 +5,14 @@ package de.eshg.base.address; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(name = DomesticAddressDto.SCHEMA_NAME, description = "A usual domestic address.") public record DomesticAddressDto( - @NotNull CountryCodeDto country, + @NotNull CountryCode country, @Schema(description = "The city in which the address is located.", example = "Berlin") @NotNull @Size(min = 1, max = 50) @@ -48,7 +48,7 @@ public record DomesticAddressDto( return SCHEMA_NAME; } - public DomesticAddressDto(CountryCodeDto country, String city, String postalCode, String street) { + public DomesticAddressDto(CountryCode country, String city, String postalCode, String street) { this(country, city, postalCode, null, street, null, null); } } diff --git a/backend/base-api/src/main/java/de/eshg/base/address/PostboxAddressDto.java b/backend/base-api/src/main/java/de/eshg/base/address/PostboxAddressDto.java index 75ba9b43d..239a3fbce 100644 --- a/backend/base-api/src/main/java/de/eshg/base/address/PostboxAddressDto.java +++ b/backend/base-api/src/main/java/de/eshg/base/address/PostboxAddressDto.java @@ -5,14 +5,14 @@ package de.eshg.base.address; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Schema(name = PostboxAddressDto.SCHEMA_NAME, description = "An address which is a postbox.") public record PostboxAddressDto( - @NotNull CountryCodeDto country, + @NotNull CountryCode country, @Schema(description = "The city in which the address is located.", example = "Berlin") @NotNull @Size(min = 1, max = 50) @@ -40,7 +40,7 @@ public record PostboxAddressDto( return SCHEMA_NAME; } - public PostboxAddressDto(CountryCodeDto country, String city, String postalCode, String postbox) { + public PostboxAddressDto(CountryCode country, String city, String postalCode, String postbox) { this(country, city, postalCode, null, postbox); } } diff --git a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/AddPersonFileStateRequest.java b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/AddPersonFileStateRequest.java index 474367c9e..0c9e8376b 100644 --- a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/AddPersonFileStateRequest.java +++ b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/AddPersonFileStateRequest.java @@ -6,11 +6,11 @@ package de.eshg.base.centralfile.api.person; import de.eshg.CustomValidations.EmailAddressConstraint; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; import de.eshg.base.centralfile.api.DataOriginDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -34,7 +34,7 @@ public record AddPersonFileStateRequest( @NotNull LocalDate dateOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, List<@EmailAddressConstraint String> emailAddresses, List<@NotNull @Size(min = 1, max = 23) String> phoneNumbers, @Valid AddressDto contactAddress, diff --git a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/AddPersonFileStateResponse.java b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/AddPersonFileStateResponse.java index c47ddfc51..3e8f75104 100644 --- a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/AddPersonFileStateResponse.java +++ b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/AddPersonFileStateResponse.java @@ -6,11 +6,11 @@ package de.eshg.base.centralfile.api.person; import de.eshg.CustomValidations.EmailAddressConstraint; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; import de.eshg.base.centralfile.api.DataOriginDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -32,7 +32,7 @@ public record AddPersonFileStateResponse( @NotNull LocalDate dateOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, @NotNull List<@EmailAddressConstraint String> emailAddresses, @NotNull List<@NotNull @Size(min = 1, max = 23) String> phoneNumbers, @Schema( diff --git a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/ExternalAddPersonFileStateRequest.java b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/ExternalAddPersonFileStateRequest.java index b7647ca4b..9e454fc6f 100644 --- a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/ExternalAddPersonFileStateRequest.java +++ b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/ExternalAddPersonFileStateRequest.java @@ -6,10 +6,10 @@ package de.eshg.base.centralfile.api.person; import de.eshg.CustomValidations.EmailAddressConstraint; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -27,7 +27,7 @@ public record ExternalAddPersonFileStateRequest( @NotNull LocalDate dateOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, List<@EmailAddressConstraint String> emailAddresses, List<@NotNull @Size(min = 1, max = 23) String> phoneNumbers, @Valid AddressDto contactAddress, diff --git a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/GetPersonFileStateResponse.java b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/GetPersonFileStateResponse.java index aaa3e3a04..48b9623aa 100644 --- a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/GetPersonFileStateResponse.java +++ b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/GetPersonFileStateResponse.java @@ -6,11 +6,11 @@ package de.eshg.base.centralfile.api.person; import de.eshg.CustomValidations.EmailAddressConstraint; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; import de.eshg.base.centralfile.api.DataOriginDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -31,7 +31,7 @@ public record GetPersonFileStateResponse( @NotNull LocalDate dateOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, @NotNull List<@EmailAddressConstraint String> emailAddresses, @NotNull List<@NotNull @Size(min = 1, max = 23) String> phoneNumbers, @Schema( diff --git a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/GetReferencePersonResponse.java b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/GetReferencePersonResponse.java index 2eb3283e1..2f29dd5f9 100644 --- a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/GetReferencePersonResponse.java +++ b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/GetReferencePersonResponse.java @@ -6,11 +6,11 @@ package de.eshg.base.centralfile.api.person; import de.eshg.CustomValidations.EmailAddressConstraint; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; import de.eshg.base.centralfile.api.DataOriginDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -36,7 +36,7 @@ public record GetReferencePersonResponse( @NotNull LocalDate dateOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, @NotNull List<@EmailAddressConstraint String> emailAddresses, @NotNull List<@NotNull @Size(min = 1, max = 23) String> phoneNumbers, @Valid AddressDto contactAddress, diff --git a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/PersonDetails.java b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/PersonDetails.java index a7ff3cab5..152837248 100644 --- a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/PersonDetails.java +++ b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/PersonDetails.java @@ -5,10 +5,10 @@ package de.eshg.base.centralfile.api.person; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; @@ -37,7 +37,7 @@ public interface PersonDetails { @Schema(description = "The place of birth (without country) of the Person.", example = "Berlin") String placeOfBirth(); - CountryCodeDto countryOfBirth(); + CountryCode countryOfBirth(); @ArraySchema( arraySchema = diff --git a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/PersonDetailsDto.java b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/PersonDetailsDto.java index 193724c24..0e27d0ec2 100644 --- a/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/PersonDetailsDto.java +++ b/backend/base-api/src/main/java/de/eshg/base/centralfile/api/person/PersonDetailsDto.java @@ -6,10 +6,10 @@ package de.eshg.base.centralfile.api.person; import de.eshg.CustomValidations.EmailAddressConstraint; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -27,7 +27,7 @@ public record PersonDetailsDto( @NotNull LocalDate dateOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, List<@EmailAddressConstraint String> emailAddresses, List<@NotNull @Size(min = 1, max = 23) String> phoneNumbers, @Valid AddressDto contactAddress, diff --git a/backend/base-api/src/main/java/de/eshg/base/contact/api/ContactAddressChange.java b/backend/base-api/src/main/java/de/eshg/base/contact/api/ContactAddressChange.java index 8ee17dc8e..9f15960da 100644 --- a/backend/base-api/src/main/java/de/eshg/base/contact/api/ContactAddressChange.java +++ b/backend/base-api/src/main/java/de/eshg/base/contact/api/ContactAddressChange.java @@ -8,8 +8,8 @@ package de.eshg.base.contact.api; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import de.eshg.base.CountryCodeDto; import de.eshg.base.history.HistoryChange; +import de.eshg.lib.common.CountryCode; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type") @JsonSubTypes({ @@ -21,7 +21,7 @@ import de.eshg.base.history.HistoryChange; public sealed interface ContactAddressChange extends AbstractContactChange permits DomesticContactAddressChange, PostboxContactAddressChange { - HistoryChange<CountryCodeDto> country(); + HistoryChange<CountryCode> country(); HistoryChange<String> city(); diff --git a/backend/base-api/src/main/java/de/eshg/base/contact/api/DomesticContactAddressChange.java b/backend/base-api/src/main/java/de/eshg/base/contact/api/DomesticContactAddressChange.java index 0fb95dcda..ede778279 100644 --- a/backend/base-api/src/main/java/de/eshg/base/contact/api/DomesticContactAddressChange.java +++ b/backend/base-api/src/main/java/de/eshg/base/contact/api/DomesticContactAddressChange.java @@ -5,13 +5,13 @@ package de.eshg.base.contact.api; -import de.eshg.base.CountryCodeDto; import de.eshg.base.history.HistoryChange; +import de.eshg.lib.common.CountryCode; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; public record DomesticContactAddressChange( - @NotNull @Valid HistoryChange<CountryCodeDto> country, + @NotNull @Valid HistoryChange<CountryCode> country, @NotNull @Valid HistoryChange<String> city, @NotNull @Valid HistoryChange<String> postalCode, @NotNull @Valid HistoryChange<String> differentName, diff --git a/backend/base-api/src/main/java/de/eshg/base/contact/api/PostboxContactAddressChange.java b/backend/base-api/src/main/java/de/eshg/base/contact/api/PostboxContactAddressChange.java index ea0f00095..94c1e0b3d 100644 --- a/backend/base-api/src/main/java/de/eshg/base/contact/api/PostboxContactAddressChange.java +++ b/backend/base-api/src/main/java/de/eshg/base/contact/api/PostboxContactAddressChange.java @@ -5,13 +5,13 @@ package de.eshg.base.contact.api; -import de.eshg.base.CountryCodeDto; import de.eshg.base.history.HistoryChange; +import de.eshg.lib.common.CountryCode; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; public record PostboxContactAddressChange( - @NotNull @Valid HistoryChange<CountryCodeDto> country, + @NotNull @Valid HistoryChange<CountryCode> country, @NotNull @Valid HistoryChange<String> city, @NotNull @Valid HistoryChange<String> postalCode, @NotNull @Valid HistoryChange<String> differentName, diff --git a/backend/base-api/src/main/java/de/eshg/base/department/GetDepartmentInfoResponse.java b/backend/base-api/src/main/java/de/eshg/base/department/GetDepartmentInfoResponse.java index da5007d8e..16c1873fd 100644 --- a/backend/base-api/src/main/java/de/eshg/base/department/GetDepartmentInfoResponse.java +++ b/backend/base-api/src/main/java/de/eshg/base/department/GetDepartmentInfoResponse.java @@ -5,7 +5,7 @@ package de.eshg.base.department; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -43,7 +43,7 @@ public record GetDepartmentInfoResponse( description = "The ISO country code of the country where the department is located.", example = "DE") @NotNull - CountryCodeDto country, + CountryCode country, @Schema( description = "The primary contact telephone number for the department", example = "+491234567890") diff --git a/backend/base-api/src/main/java/de/eshg/base/feature/BaseFeature.java b/backend/base-api/src/main/java/de/eshg/base/feature/BaseFeature.java index a85b28a29..d6a00c52e 100644 --- a/backend/base-api/src/main/java/de/eshg/base/feature/BaseFeature.java +++ b/backend/base-api/src/main/java/de/eshg/base/feature/BaseFeature.java @@ -12,8 +12,7 @@ public enum BaseFeature { INBOX, STI_PROTECTION, VERIFICATION_OF_EXTERNAL_DATA, - ACCOUNT_ACTIVE_SESSIONS, OPEN_DATA, - LOGIN_PROTOCOL, - GDPR + GDPR, + CONTACT_MERGE } diff --git a/backend/base-api/src/main/java/de/eshg/base/gdpr/DeleteGdprDownloadsRequest.java b/backend/base-api/src/main/java/de/eshg/base/gdpr/DeleteGdprDownloadsRequest.java new file mode 100644 index 000000000..4881d84cb --- /dev/null +++ b/backend/base-api/src/main/java/de/eshg/base/gdpr/DeleteGdprDownloadsRequest.java @@ -0,0 +1,12 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.base.gdpr; + +import jakarta.validation.constraints.NotEmpty; +import java.util.Set; +import java.util.UUID; + +public record DeleteGdprDownloadsRequest(@NotEmpty Set<UUID> downloadIds) {} diff --git a/backend/base-api/src/main/java/de/eshg/base/gdpr/GdprProcedureApi.java b/backend/base-api/src/main/java/de/eshg/base/gdpr/GdprProcedureApi.java index 09267672a..34995c1a3 100644 --- a/backend/base-api/src/main/java/de/eshg/base/gdpr/GdprProcedureApi.java +++ b/backend/base-api/src/main/java/de/eshg/base/gdpr/GdprProcedureApi.java @@ -22,10 +22,7 @@ import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.service.annotation.GetExchange; -import org.springframework.web.service.annotation.HttpExchange; -import org.springframework.web.service.annotation.PostExchange; -import org.springframework.web.service.annotation.PutExchange; +import org.springframework.web.service.annotation.*; @HttpExchange(url = GdprProcedureApi.BASE_URL) public interface GdprProcedureApi { @@ -92,7 +89,9 @@ public interface GdprProcedureApi { @PostExchange("/{id}/downloads") @ApiResponse(responseCode = "200") - @Operation(summary = "Add a download of GDPR-related document or data for this GDPR procedure.") + @Operation( + summary = + "Add one or multiple downloads of GDPR-related document or data for this GDPR procedure.") void addDownloads( @PathVariable("id") UUID id, @RequestBody @Valid AddGdprDownloadsRequest request); @@ -102,4 +101,12 @@ public interface GdprProcedureApi { summary = "Get list of download ids of GDPR-related documents or data of this GDPR procedure.") GetGdprDownloadsResponse getDownloads(@PathVariable("id") UUID id); + + @DeleteExchange("/{id}/downloads") + @ApiResponse(responseCode = "200") + @Operation( + summary = + "Delete one or multiple downloads of GDPR-related document or data of this GDPR procedure.") + void deleteDownloads( + @PathVariable("id") UUID id, @RequestBody @Valid DeleteGdprDownloadsRequest request); } diff --git a/backend/base-api/src/main/java/de/eshg/base/street/StreetApi.java b/backend/base-api/src/main/java/de/eshg/base/street/StreetApi.java index efab006fb..c5c3d8bff 100644 --- a/backend/base-api/src/main/java/de/eshg/base/street/StreetApi.java +++ b/backend/base-api/src/main/java/de/eshg/base/street/StreetApi.java @@ -5,7 +5,7 @@ package de.eshg.base.street; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import de.eshg.rest.service.security.config.BaseUrls; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -47,7 +47,7 @@ public interface StreetApi { description = "The country code in ISO 3166-1 alpha-2 format of the address for which the search shall be done") @RequestParam(name = "country") - CountryCodeDto country); + CountryCode country); @GetExchange(AUTOCOMPLETE) @ApiResponse(responseCode = "200") diff --git a/backend/base-api/src/main/java/de/eshg/base/testhelper/BaseTestHelperApi.java b/backend/base-api/src/main/java/de/eshg/base/testhelper/BaseTestHelperApi.java index 6d9e433bf..bf59d34f3 100644 --- a/backend/base-api/src/main/java/de/eshg/base/testhelper/BaseTestHelperApi.java +++ b/backend/base-api/src/main/java/de/eshg/base/testhelper/BaseTestHelperApi.java @@ -53,6 +53,10 @@ public interface BaseTestHelperApi extends TestHelperApi, LoginProvider { @PostExchange("/population/contacts/schools") SearchContactsResponse populateSchoolContacts(@Valid @RequestBody PopulationRequest request); + @PostExchange("/population/contacts/health-departments") + SearchContactsResponse populateHealthDepartmentContacts( + @Valid @RequestBody PopulationRequest request); + @PostExchange("/keycloak/reset") void resetKeycloak(); diff --git a/backend/base-api/src/main/java/de/eshg/base/user/api/UserRoleDto.java b/backend/base-api/src/main/java/de/eshg/base/user/api/UserRoleDto.java index 49deae453..3a34b29d0 100644 --- a/backend/base-api/src/main/java/de/eshg/base/user/api/UserRoleDto.java +++ b/backend/base-api/src/main/java/de/eshg/base/user/api/UserRoleDto.java @@ -75,4 +75,6 @@ public enum UserRoleDto { STI_PROTECTION_LEADER, MEDICAL_REGISTRY_LEADER, MEDICAL_REGISTRY_ADMIN, + OPEN_DATA_ADMIN, + OPEN_DATA_LEADER, } diff --git a/backend/base/build.gradle b/backend/base/build.gradle index 1ad4f13ae..b5e5e1e6c 100644 --- a/backend/base/build.gradle +++ b/backend/base/build.gradle @@ -16,6 +16,7 @@ dependencies { implementation project(':util-commons') implementation project(':lib-service-directory-api') implementation project(':lib-mutex') + implementation project(':file-commons') implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' diff --git a/backend/base/gradle.lockfile b/backend/base/gradle.lockfile index 32a476855..b351705dc 100644 --- a/backend/base/gradle.lockfile +++ b/backend/base/gradle.lockfile @@ -17,11 +17,11 @@ com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClass com.fasterxml.woodstox:woodstox-core:6.6.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.java-json-tools:btf:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.java-json-tools:jackson-coreutils:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.java-json-tools:json-patch:1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -35,7 +35,7 @@ com.google.guava:guava:33.3.1-jre=compileClasspath,productionRuntimeClasspath,ru com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 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.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 @@ -52,11 +52,11 @@ com.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,compileClasspath com.sun.istack:istack-commons-tools:4.1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.sun.xml.bind.external:relaxng-datatype:4.0.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.sun.xml.bind.external:rngom:4.0.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.tngtech.archunit:archunit-junit5-api:1.3.0=testCompileClasspath,testRuntimeClasspath -com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath -com.tngtech.archunit:archunit-junit5-engine:1.3.0=testRuntimeClasspath -com.tngtech.archunit:archunit-junit5:1.3.0=testCompileClasspath,testRuntimeClasspath -com.tngtech.archunit:archunit:1.3.0=testCompileClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,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 commons-beanutils:commons-beanutils:1.9.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -68,10 +68,10 @@ de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeCla de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.rototor.pdfbox:graphics2d:3.0.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.github.java-diff-utils:java-diff-utils:4.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.github.openhtmltopdf:openhtmltopdf-core:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -106,19 +106,19 @@ jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClass jakarta.ws.rs:jakarta.ws.rs-api:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath -net.java.dev.stax-utils:stax-utils:20070216=testRuntimeClasspath +net.java.dev.jna:jna:5.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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-compress:1.24.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.11.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.cxf:cxf-core:4.0.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -140,6 +140,7 @@ 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:2.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -166,12 +167,12 @@ org.apache.xmlgraphics:batik-xml:1.17=productionRuntimeClasspath,runtimeClasspat org.apache.xmlgraphics:xmlgraphics-commons:2.9=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,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.78.1=testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.78.1=testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.78.1=testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.78.1=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 @@ -186,8 +187,8 @@ org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,compileClasspath,produ org.glassfish.jaxb:jaxb-xjc:4.0.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.glassfish.jaxb:xsom:4.0.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -211,16 +212,16 @@ org.jetbrains.kotlin:kotlin-stdlib-common:1.9.25=testCompileClasspath,testRuntim org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.25=testCompileClasspath,testRuntimeClasspath org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.25=testCompileClasspath,testRuntimeClasspath org.jetbrains.kotlin:kotlin-stdlib:1.9.25=testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jsoup:jsoup:1.16.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.keycloak:keycloak-admin-client:25.0.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.keycloak:keycloak-common:25.0.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.keycloak:keycloak-core:25.0.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -229,16 +230,16 @@ org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath, org.mnode.ical4j:ical4j:4.0.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspath -org.mozilla:rhino:1.7.13=testRuntimeClasspath +org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.reactivestreams:reactive-streams:1.0.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -248,7 +249,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -266,7 +267,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -290,25 +291,25 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.testcontainers:database-commons:1.19.8=testRuntimeClasspath org.testcontainers:jdbc:1.19.8=testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.threeten:threeten-extra:1.8.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.thymeleaf:thymeleaf-spring6:3.1.2.RELEASE=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.thymeleaf:thymeleaf:3.1.2.RELEASE=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.unbescape:unbescape:1.1.6.RELEASE=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.verapdf:core-jakarta:1.26.1=testRuntimeClasspath -org.verapdf:feature-reporting-jakarta:1.26.1=testRuntimeClasspath -org.verapdf:metadata-fixer-jakarta:1.26.1=testRuntimeClasspath -org.verapdf:parser:1.26.1=testRuntimeClasspath -org.verapdf:pdf-model:1.26.1=testRuntimeClasspath -org.verapdf:validation-model-jakarta:1.26.1=testRuntimeClasspath -org.verapdf:verapdf-xmp-core-jakarta:1.26.1=testRuntimeClasspath +org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:parser:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:pdf-model:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:validation-model-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:verapdf-xmp-core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.webjars:swagger-ui:5.17.14=testCompileClasspath,testRuntimeClasspath org.xmlunit:xmlunit-core:2.9.1=testCompileClasspath,testRuntimeClasspath org.yaml:snakeyaml:2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -324,4 +325,4 @@ org.zalando:logbook-spring-boot-starter:3.9.0=productionRuntimeClasspath,runtime org.zalando:logbook-spring:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath xml-apis:xml-apis-ext:1.3.04=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath xml-apis:xml-apis:1.4.01=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -empty=developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath +empty=developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath diff --git a/backend/base/openApi.yaml b/backend/base/openApi.yaml index bc8e5b06e..6bad3756f 100644 --- a/backend/base/openApi.yaml +++ b/backend/base/openApi.yaml @@ -1371,6 +1371,28 @@ paths: tags: - GdprProcedure /gdpr-procedures/{id}/downloads: + delete: + operationId: deleteDownloads + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DeleteGdprDownloadsRequest" + required: true + responses: + "200": + description: OK + summary: Delete one or multiple downloads of GDPR-related document or data of + this GDPR procedure. + tags: + - GdprProcedure get: operationId: getDownloads parameters: @@ -1409,7 +1431,8 @@ paths: responses: "200": description: OK - summary: Add a download of GDPR-related document or data for this GDPR procedure. + summary: Add one or multiple downloads of GDPR-related document or data for + this GDPR procedure. tags: - GdprProcedure /gdpr-procedures/{id}/fileStateIds: @@ -2992,6 +3015,24 @@ paths: description: OK tags: - TestHelper + /test-helper/population/contacts/health-departments: + post: + operationId: populateHealthDepartmentContacts + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PopulationRequest" + required: true + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/SearchContactsResponse" + description: OK + tags: + - TestHelper /test-helper/population/contacts/schools: post: operationId: populateSchoolContacts @@ -4174,10 +4215,9 @@ components: - INBOX - STI_PROTECTION - VERIFICATION_OF_EXTERNAL_DATA - - ACCOUNT_ACTIVE_SESSIONS - OPEN_DATA - - LOGIN_PROTOCOL - GDPR + - CONTACT_MERGE BlockingEventsOfCalendar: type: object properties: @@ -4700,6 +4740,17 @@ components: minItems: 1 required: - fileStateIds + DeleteGdprDownloadsRequest: + type: object + properties: + downloadIds: + type: array + items: + type: string + format: uuid + uniqueItems: true + required: + - downloadIds DetailedEvent: type: object properties: @@ -7668,6 +7719,9 @@ components: - TM_VACCINATION_CONSULTATION - MEASLES_PROTECTION - STI_PROTECTION + - MEDICAL_REGISTRY_ENTRY + - MEDICAL_REGISTRY_CITIZEN_DRAFT + - MEDICAL_REGISTRY_EMPLOYEE_DRAFT ProcedureWithDuration: type: object properties: @@ -8603,6 +8657,8 @@ components: - STI_PROTECTION_LEADER - MEDICAL_REGISTRY_LEADER - MEDICAL_REGISTRY_ADMIN + - OPEN_DATA_ADMIN + - OPEN_DATA_LEADER VCardAddress: type: object properties: diff --git a/backend/base/src/main/java/de/eshg/base/address/mapper/AddressMapper.java b/backend/base/src/main/java/de/eshg/base/address/mapper/AddressMapper.java index 36a478922..d9930cc4e 100644 --- a/backend/base/src/main/java/de/eshg/base/address/mapper/AddressMapper.java +++ b/backend/base/src/main/java/de/eshg/base/address/mapper/AddressMapper.java @@ -5,9 +5,6 @@ package de.eshg.base.address.mapper; -import static de.eshg.base.util.MappingUtil.mapCountryCodeToApi; -import static de.eshg.base.util.MappingUtil.mapCountryCodeToDm; - import de.eshg.base.address.AddressDto; import de.eshg.base.address.DomesticAddressDto; import de.eshg.base.address.PostboxAddressDto; @@ -27,7 +24,7 @@ public final class AddressMapper { public static PostboxAddressDto mapPostboxAddressToApi(PostboxAddress address) { return new PostboxAddressDto( - mapCountryCodeToApi(address.getCountry()), + address.getCountry(), address.getCity(), address.getPostalCode(), address.getDifferentName(), @@ -36,7 +33,7 @@ public final class AddressMapper { public static DomesticAddressDto mapDomesticAddressToApi(DomesticAddress address) { return new DomesticAddressDto( - mapCountryCodeToApi(address.getCountry()), + address.getCountry(), address.getCity(), address.getPostalCode(), address.getDifferentName(), @@ -64,7 +61,7 @@ public final class AddressMapper { private static void setAddressDmFields(Address address, AddressDto addressDto) { address.setPostalCode(addressDto.postalCode()); address.setCity(addressDto.city()); - address.setCountry(mapCountryCodeToDm(addressDto.country())); + address.setCountry(addressDto.country()); address.setDifferentName(addressDto.differentName()); } diff --git a/backend/base/src/main/java/de/eshg/base/address/persistence/embeddable/EmbeddableAddress.java b/backend/base/src/main/java/de/eshg/base/address/persistence/embeddable/EmbeddableAddress.java index 0d6751f0d..fa2ff9cbf 100644 --- a/backend/base/src/main/java/de/eshg/base/address/persistence/embeddable/EmbeddableAddress.java +++ b/backend/base/src/main/java/de/eshg/base/address/persistence/embeddable/EmbeddableAddress.java @@ -5,7 +5,7 @@ package de.eshg.base.address.persistence.embeddable; -import de.eshg.base.util.CountryCode; +import de.eshg.lib.common.CountryCode; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; import jakarta.persistence.Column; diff --git a/backend/base/src/main/java/de/eshg/base/address/persistence/entity/Address.java b/backend/base/src/main/java/de/eshg/base/address/persistence/entity/Address.java index 5fca7a236..e89f5dbbf 100644 --- a/backend/base/src/main/java/de/eshg/base/address/persistence/entity/Address.java +++ b/backend/base/src/main/java/de/eshg/base/address/persistence/entity/Address.java @@ -5,7 +5,7 @@ package de.eshg.base.address.persistence.entity; -import de.eshg.base.util.CountryCode; +import de.eshg.lib.common.CountryCode; public interface Address { diff --git a/backend/base/src/main/java/de/eshg/base/address/persistence/entity/DelegatingAddress.java b/backend/base/src/main/java/de/eshg/base/address/persistence/entity/DelegatingAddress.java index c897659bd..6bf8be8ed 100644 --- a/backend/base/src/main/java/de/eshg/base/address/persistence/entity/DelegatingAddress.java +++ b/backend/base/src/main/java/de/eshg/base/address/persistence/entity/DelegatingAddress.java @@ -6,7 +6,7 @@ package de.eshg.base.address.persistence.entity; import de.eshg.base.address.persistence.embeddable.EmbeddableAddress; -import de.eshg.base.util.CountryCode; +import de.eshg.lib.common.CountryCode; public interface DelegatingAddress<E extends EmbeddableAddress> extends Address { diff --git a/backend/base/src/main/java/de/eshg/base/centralfile/mapper/PersonMapper.java b/backend/base/src/main/java/de/eshg/base/centralfile/mapper/PersonMapper.java index 2b5caa0b5..c843c0d63 100644 --- a/backend/base/src/main/java/de/eshg/base/centralfile/mapper/PersonMapper.java +++ b/backend/base/src/main/java/de/eshg/base/centralfile/mapper/PersonMapper.java @@ -7,8 +7,6 @@ package de.eshg.base.centralfile.mapper; import static de.eshg.base.address.mapper.AddressMapper.mapAddressToApi; import static de.eshg.base.util.MappingUtil.extractStrings; -import static de.eshg.base.util.MappingUtil.mapCountryCodeToApi; -import static de.eshg.base.util.MappingUtil.mapCountryCodeToDm; import static de.eshg.base.util.MappingUtil.mapDataOriginToApi; import static de.eshg.base.util.MappingUtil.mapDataOriginToDm; import static de.eshg.base.util.MappingUtil.mapGenderToApi; @@ -56,7 +54,7 @@ public class PersonMapper { person.getBirthDetails().dateOfBirth(), person.getBirthDetails().nameAtBirth(), person.getBirthDetails().placeOfBirth(), - mapCountryCodeToApi(person.getBirthDetails().countryOfBirth()), + person.getBirthDetails().countryOfBirth(), extractStrings(person.getEmailAddresses(), PersonEmailAddress::getEmailAddress), extractStrings(person.getPhoneNumbers(), PersonPhoneNumber::getPhoneNumber), person.getReferenceVersion(), @@ -75,7 +73,7 @@ public class PersonMapper { person.getBirthDetails().dateOfBirth(), person.getBirthDetails().nameAtBirth(), person.getBirthDetails().placeOfBirth(), - mapCountryCodeToApi(person.getBirthDetails().countryOfBirth()), + person.getBirthDetails().countryOfBirth(), extractStrings(person.getEmailAddresses(), PersonEmailAddress::getEmailAddress), extractStrings(person.getPhoneNumbers(), PersonPhoneNumber::getPhoneNumber), mapAddressToApi(person.getContactAddress()), @@ -94,7 +92,7 @@ public class PersonMapper { person.getBirthDetails().dateOfBirth(), person.getBirthDetails().nameAtBirth(), person.getBirthDetails().placeOfBirth(), - mapCountryCodeToApi(person.getBirthDetails().countryOfBirth()), + person.getBirthDetails().countryOfBirth(), extractStrings(person.getEmailAddresses(), PersonEmailAddress::getEmailAddress), extractStrings(person.getPhoneNumbers(), PersonPhoneNumber::getPhoneNumber), mapAddressToApi(person.getContactAddress()), @@ -114,7 +112,7 @@ public class PersonMapper { person.getBirthDetails().dateOfBirth(), person.getBirthDetails().nameAtBirth(), person.getBirthDetails().placeOfBirth(), - mapCountryCodeToApi(person.getBirthDetails().countryOfBirth()), + person.getBirthDetails().countryOfBirth(), extractStrings(person.getEmailAddresses(), PersonEmailAddress::getEmailAddress), extractStrings(person.getPhoneNumbers(), PersonPhoneNumber::getPhoneNumber), person.getReferenceVersion(), @@ -209,7 +207,7 @@ public class PersonMapper { personDetails.dateOfBirth(), personDetails.nameAtBirth(), personDetails.placeOfBirth(), - mapCountryCodeToDm(personDetails.countryOfBirth())); + personDetails.countryOfBirth()); } public static List<PersonEmailAddress> mapEmailAddressesToDm(List<String> emailAddresses) { diff --git a/backend/base/src/main/java/de/eshg/base/centralfile/persistence/entity/BirthDetails.java b/backend/base/src/main/java/de/eshg/base/centralfile/persistence/entity/BirthDetails.java index ceac3c3ea..7a6324543 100644 --- a/backend/base/src/main/java/de/eshg/base/centralfile/persistence/entity/BirthDetails.java +++ b/backend/base/src/main/java/de/eshg/base/centralfile/persistence/entity/BirthDetails.java @@ -5,7 +5,7 @@ package de.eshg.base.centralfile.persistence.entity; -import de.eshg.base.util.CountryCode; +import de.eshg.lib.common.CountryCode; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; import jakarta.persistence.Column; diff --git a/backend/base/src/main/java/de/eshg/base/contact/ContactMapper.java b/backend/base/src/main/java/de/eshg/base/contact/ContactMapper.java index a50767e01..ba7ed06a9 100644 --- a/backend/base/src/main/java/de/eshg/base/contact/ContactMapper.java +++ b/backend/base/src/main/java/de/eshg/base/contact/ContactMapper.java @@ -365,9 +365,7 @@ public class ContactMapper { private static DomesticContactAddressChange mapDomesticContactAddressChangeToApi( Set<String> fields, DomesticContactAddress entity) { return new DomesticContactAddressChange( - newChange( - fields.contains(DomesticContactAddress_.COUNTRY), - mapCountryCodeToApi(entity.getCountry())), + newChange(fields.contains(DomesticContactAddress_.COUNTRY), entity.getCountry()), newChange(fields.contains(DomesticContactAddress_.CITY), entity.getCity()), newChange(fields.contains(DomesticContactAddress_.POSTAL_CODE), entity.getPostalCode()), newChange( @@ -382,9 +380,7 @@ public class ContactMapper { private static PostboxContactAddressChange mapPostboxContactAddressChangeToApi( Set<String> fields, PostboxContactAddress entity) { return new PostboxContactAddressChange( - newChange( - fields.contains(PostboxContactAddress_.COUNTRY), - mapCountryCodeToApi(entity.getCountry())), + newChange(fields.contains(PostboxContactAddress_.COUNTRY), entity.getCountry()), newChange(fields.contains(PostboxContactAddress_.CITY), entity.getCity()), newChange(fields.contains(PostboxContactAddress_.POSTAL_CODE), entity.getPostalCode()), newChange( diff --git a/backend/base/src/main/java/de/eshg/base/contact/persistence/entity/DomesticContactAddress.java b/backend/base/src/main/java/de/eshg/base/contact/persistence/entity/DomesticContactAddress.java index 8774d4113..1e0fdc596 100644 --- a/backend/base/src/main/java/de/eshg/base/contact/persistence/entity/DomesticContactAddress.java +++ b/backend/base/src/main/java/de/eshg/base/contact/persistence/entity/DomesticContactAddress.java @@ -6,7 +6,7 @@ package de.eshg.base.contact.persistence.entity; import de.eshg.base.address.persistence.entity.DomesticAddress; -import de.eshg.base.util.CountryCode; +import de.eshg.lib.common.CountryCode; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; import jakarta.persistence.Column; diff --git a/backend/base/src/main/java/de/eshg/base/contact/persistence/entity/PostboxContactAddress.java b/backend/base/src/main/java/de/eshg/base/contact/persistence/entity/PostboxContactAddress.java index f4ea7f422..7b5d671db 100644 --- a/backend/base/src/main/java/de/eshg/base/contact/persistence/entity/PostboxContactAddress.java +++ b/backend/base/src/main/java/de/eshg/base/contact/persistence/entity/PostboxContactAddress.java @@ -6,7 +6,7 @@ package de.eshg.base.contact.persistence.entity; import de.eshg.base.address.persistence.entity.PostboxAddress; -import de.eshg.base.util.CountryCode; +import de.eshg.lib.common.CountryCode; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; import jakarta.persistence.Column; diff --git a/backend/base/src/main/java/de/eshg/base/department/DepartmentConfiguration.java b/backend/base/src/main/java/de/eshg/base/department/DepartmentConfiguration.java index 894da3011..d4f086d7e 100644 --- a/backend/base/src/main/java/de/eshg/base/department/DepartmentConfiguration.java +++ b/backend/base/src/main/java/de/eshg/base/department/DepartmentConfiguration.java @@ -5,7 +5,7 @@ package de.eshg.base.department; -import de.eshg.base.util.CountryCode; +import de.eshg.lib.common.CountryCode; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import org.springframework.boot.context.properties.ConfigurationProperties; 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 5105a7dd3..cbf73c020 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,7 @@ package de.eshg.base.department; -import de.base.rest.CustomMediaTypes; -import de.eshg.base.CountryCodeDto; -import de.eshg.base.util.CountryCode; +import de.eshg.file.common.CustomMediaTypes; import io.swagger.v3.oas.annotations.tags.Tag; import java.io.IOException; import org.springframework.core.io.Resource; @@ -65,7 +63,7 @@ public class DepartmentController implements DepartmentApi { departmentConfig.houseNumber(), departmentConfig.postalCode(), departmentConfig.city(), - mapCountryCodeToApi(departmentConfig.country()), + departmentConfig.country(), departmentConfig.phoneNumber(), departmentConfig.homepage(), departmentConfig.email(), @@ -75,8 +73,4 @@ public class DepartmentController implements DepartmentApi { private static LocationDto mapLocationToApi(DepartmentConfiguration departmentConfig) { return new LocationDto(departmentConfig.latitude(), departmentConfig.longitude()); } - - private static CountryCodeDto mapCountryCodeToApi(CountryCode country) { - return CountryCodeDto.valueOf(country.name()); - } } diff --git a/backend/base/src/main/java/de/eshg/base/gdpr/GdprProcedureController.java b/backend/base/src/main/java/de/eshg/base/gdpr/GdprProcedureController.java index d37230708..9d1d97446 100644 --- a/backend/base/src/main/java/de/eshg/base/gdpr/GdprProcedureController.java +++ b/backend/base/src/main/java/de/eshg/base/gdpr/GdprProcedureController.java @@ -403,6 +403,12 @@ public class GdprProcedureController implements GdprProcedureApi { return new GetGdprDownloadsResponse(mapDownloadToApi(procedure.getDownloads())); } + @Override + @Transactional + public void deleteDownloads(UUID id, DeleteGdprDownloadsRequest request) { + service.deleteGdprDownloads(id, request.downloadIds()); + } + private static Set<UUID> mapDownloadToApi(Collection<GdprDownload> downloads) { return downloads.stream().map(GdprDownload::getDownloadId).collect(Collectors.toSet()); } diff --git a/backend/base/src/main/java/de/eshg/base/gdpr/GdprProcedureService.java b/backend/base/src/main/java/de/eshg/base/gdpr/GdprProcedureService.java index 76d7efb21..5aae366d4 100644 --- a/backend/base/src/main/java/de/eshg/base/gdpr/GdprProcedureService.java +++ b/backend/base/src/main/java/de/eshg/base/gdpr/GdprProcedureService.java @@ -108,4 +108,15 @@ public class GdprProcedureService { log.info("Added downloadIds={} to GdprProcedure(id={})", downloadIdsToAdd, id); } + + public void deleteGdprDownloads(UUID id, @NotNull Set<UUID> downloadIdsToDelete) { + log.info("Deleting downloadIds={} of GdprProcedure(id={})", downloadIdsToDelete, id); + GdprProcedure procedure = getGdprProcedureForUpdate(id); + + for (UUID uuid : downloadIdsToDelete) { + procedure.deleteDownload(uuid); + } + + log.info("Deleted downloadIds={} of GdprProcedure(id={})", downloadIdsToDelete, id); + } } diff --git a/backend/base/src/main/java/de/eshg/base/gdpr/persistence/GdprProcedure.java b/backend/base/src/main/java/de/eshg/base/gdpr/persistence/GdprProcedure.java index 00c03bf53..98e518f9f 100644 --- a/backend/base/src/main/java/de/eshg/base/gdpr/persistence/GdprProcedure.java +++ b/backend/base/src/main/java/de/eshg/base/gdpr/persistence/GdprProcedure.java @@ -10,10 +10,7 @@ import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; import jakarta.persistence.*; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.UUID; +import java.util.*; import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLEnumJdbcType; import org.springframework.data.annotation.CreatedDate; @@ -146,4 +143,16 @@ public class GdprProcedure extends BaseEntityWithExternalId { downloads.add(download); download.setGdprProcedure(this); } + + public void deleteDownload(UUID downloadIdToDelete) { + Iterator<GdprDownload> iterator = downloads.iterator(); + while (iterator.hasNext()) { + GdprDownload download = iterator.next(); + if (download.getDownloadId().equals(downloadIdToDelete)) { + download.setGdprProcedure(null); + iterator.remove(); + break; + } + } + } } diff --git a/backend/base/src/main/java/de/eshg/base/keycloak/EmployeeKeycloakProvisioning.java b/backend/base/src/main/java/de/eshg/base/keycloak/EmployeeKeycloakProvisioning.java index e599acfef..3282585aa 100644 --- a/backend/base/src/main/java/de/eshg/base/keycloak/EmployeeKeycloakProvisioning.java +++ b/backend/base/src/main/java/de/eshg/base/keycloak/EmployeeKeycloakProvisioning.java @@ -146,6 +146,7 @@ public class EmployeeKeycloakProvisioning extends KeycloakProvisioning<EmployeeK List<GroupRepresentation> inspection_landesamt = KeycloakMapper.map(ModuleMemberGroup.INSPECTION_LANDESAMT); List<GroupRepresentation> statistics = KeycloakMapper.map(ModuleMemberGroup.STATISTICS); + List<GroupRepresentation> openData = KeycloakMapper.map(ModuleMemberGroup.OPEN_DATA); keycloakClient.createOrUpdateGroups( CollectionUtils.listUnion( @@ -156,7 +157,8 @@ public class EmployeeKeycloakProvisioning extends KeycloakProvisioning<EmployeeK modules, inspection_la_checklists, inspection_landesamt, - statistics))); + statistics, + openData))); } private void createOrUpdateRoles() { diff --git a/backend/base/src/main/java/de/eshg/base/keycloak/KeycloakTestClient.java b/backend/base/src/main/java/de/eshg/base/keycloak/KeycloakTestClient.java index 4154ba5ea..bcc994957 100644 --- a/backend/base/src/main/java/de/eshg/base/keycloak/KeycloakTestClient.java +++ b/backend/base/src/main/java/de/eshg/base/keycloak/KeycloakTestClient.java @@ -239,7 +239,9 @@ public class KeycloakTestClient { (representation, configured) -> { Map<String, List<String>> attributes = UserMapper.mapAttributesToDm( - configured.phoneNumber(), configured.externalChatUsername()); + new LinkedHashMap<>(), + configured.phoneNumber(), + configured.externalChatUsername()); representation.setAttributes(attributes); representation.setEnabled(true); }); diff --git a/backend/base/src/main/java/de/eshg/base/keycloak/KeycloakTestProvisioning.java b/backend/base/src/main/java/de/eshg/base/keycloak/KeycloakTestProvisioning.java index 6b7510174..6807c35e5 100644 --- a/backend/base/src/main/java/de/eshg/base/keycloak/KeycloakTestProvisioning.java +++ b/backend/base/src/main/java/de/eshg/base/keycloak/KeycloakTestProvisioning.java @@ -15,6 +15,7 @@ import de.eshg.lib.keycloak.PermissionRole; import de.eshg.testhelper.environment.EnvironmentConfig; import jakarta.annotation.PostConstruct; import java.time.Duration; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -111,7 +112,8 @@ public abstract class KeycloakTestProvisioning { } Map<String, List<String>> attributes = - UserMapper.mapAttributesToDm(user.phoneNumber(), user.externalChatUsername()); + UserMapper.mapAttributesToDm( + new LinkedHashMap<>(), user.phoneNumber(), user.externalChatUsername()); userRepresentation.setAttributes(!attributes.isEmpty() ? attributes : null); } } 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 e9388ed0c..8828c6d71 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 @@ -5,7 +5,6 @@ package de.eshg.base.pdf.gdpr; -import de.base.rest.CustomMediaTypes; import de.eshg.base.address.persistence.entity.Address; import de.eshg.base.address.persistence.entity.DomesticAddress; import de.eshg.base.address.persistence.entity.PostboxAddress; @@ -22,6 +21,7 @@ import de.eshg.base.pdf.data.FieldData; import de.eshg.base.pdf.data.FieldRow; import de.eshg.base.pdf.data.FieldSet; import de.eshg.base.util.Salutation; +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; diff --git a/backend/base/src/main/java/de/eshg/base/spring/config/BaseInternalSecurityConfig.java b/backend/base/src/main/java/de/eshg/base/spring/config/BaseInternalSecurityConfig.java index 23de3b0bb..194c12fbc 100644 --- a/backend/base/src/main/java/de/eshg/base/spring/config/BaseInternalSecurityConfig.java +++ b/backend/base/src/main/java/de/eshg/base/spring/config/BaseInternalSecurityConfig.java @@ -41,6 +41,7 @@ public class BaseInternalSecurityConfig { facilities(auth); persons(auth); mail(auth); + gdpr(auth); auth.requestMatchers(POST, InventoryApi.BASE_URL + "/*" + InventoryApi.BOOKING + "/**") .hasRole(EmployeePermissionRole.BASE_INVENTORY_USE.getKeycloakName()); @@ -63,6 +64,19 @@ public class BaseInternalSecurityConfig { }; } + private void gdpr( + AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry + auth) { + auth.requestMatchers(GET, BaseUrls.Base.GDPR_PROCEDURE_API + "/**") + .hasRole(EmployeePermissionRole.BASE_GDPR_PROCEDURE_READ.getKeycloakName()); + auth.requestMatchers(POST, BaseUrls.Base.GDPR_PROCEDURE_API + "/**") + .hasRole(EmployeePermissionRole.BASE_GDPR_PROCEDURE_WRITE.getKeycloakName()); + auth.requestMatchers(PUT, BaseUrls.Base.GDPR_PROCEDURE_API + "/**") + .hasRole(EmployeePermissionRole.BASE_GDPR_PROCEDURE_WRITE.getKeycloakName()); + auth.requestMatchers(DELETE, BaseUrls.Base.GDPR_PROCEDURE_API + "/**") + .hasRole(EmployeePermissionRole.BASE_GDPR_PROCEDURE_WRITE.getKeycloakName()); + } + private static void users( AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry auth) { @@ -102,7 +116,9 @@ public class BaseInternalSecurityConfig { auth.requestMatchers( POST, FacilityApi.BASE_URL + FacilityApi.FILE_STATES_URL + BaseUrls.Base.BULK_GET_URL_END) - .hasRole(EmployeePermissionRole.BASE_FACILITIES_READ.getKeycloakName()); + .hasAnyRole( + EmployeePermissionRole.BASE_FACILITIES_READ.getKeycloakName(), + EmployeePermissionRole.PROCEDURE_ARCHIVE.getKeycloakName()); auth.requestMatchers( POST, FacilityApi.BASE_URL + FacilityApi.FILE_STATES_URL + ARCHIVE_DELETION) .hasRole(EmployeePermissionRole.BASE_FACILITIES_DELETE.getKeycloakName()); @@ -117,7 +133,10 @@ public class BaseInternalSecurityConfig { auth.requestMatchers(POST, FacilityApi.BASE_URL + "/**") .hasRole(EmployeePermissionRole.BASE_FACILITIES_WRITE.getKeycloakName()); auth.requestMatchers(GET, FacilityApi.BASE_URL + "/**") - .hasRole(EmployeePermissionRole.BASE_FACILITIES_READ.getKeycloakName()); + .hasAnyRole( + EmployeePermissionRole.BASE_FACILITIES_READ.getKeycloakName(), + EmployeePermissionRole.PROCEDURE_ARCHIVE.getKeycloakName(), + EmployeePermissionRole.PROCEDURE_ARCHIVE_ADMIN.getKeycloakName()); } private static void persons( @@ -125,7 +144,9 @@ public class BaseInternalSecurityConfig { auth) { auth.requestMatchers( POST, PersonApi.BASE_URL + PersonApi.FILE_STATES_URL + BaseUrls.Base.BULK_GET_URL_END) - .hasRole(EmployeePermissionRole.BASE_PERSONS_READ.getKeycloakName()); + .hasAnyRole( + EmployeePermissionRole.BASE_PERSONS_READ.getKeycloakName(), + EmployeePermissionRole.PROCEDURE_ARCHIVE.getKeycloakName()); auth.requestMatchers(POST, PersonApi.BASE_URL + PersonApi.FILE_STATES_URL + ARCHIVE_DELETION) .hasRole(EmployeePermissionRole.BASE_PERSONS_DELETE.getKeycloakName()); @@ -139,7 +160,8 @@ public class BaseInternalSecurityConfig { auth.requestMatchers(GET, BaseUrls.Base.PERSON_API + PersonApi.FILE_STATES_URL + "/*") .hasAnyRole( EmployeePermissionRole.BASE_PERSONS_READ.getKeycloakName(), - CitizenPermissionRole.ACCESS_CODE_USER.getKeycloakName()); + CitizenPermissionRole.ACCESS_CODE_USER.getKeycloakName(), + EmployeePermissionRole.PROCEDURE_ARCHIVE.getKeycloakName()); auth.requestMatchers(POST, PersonApi.BASE_URL + "/**") .hasRole(EmployeePermissionRole.BASE_PERSONS_WRITE.getKeycloakName()); diff --git a/backend/base/src/main/java/de/eshg/base/statistics/StatisticsController.java b/backend/base/src/main/java/de/eshg/base/statistics/StatisticsController.java index e18e115db..0b0daa273 100644 --- a/backend/base/src/main/java/de/eshg/base/statistics/StatisticsController.java +++ b/backend/base/src/main/java/de/eshg/base/statistics/StatisticsController.java @@ -19,9 +19,8 @@ import de.eshg.base.statistics.options.GenderOptions; import de.eshg.base.street.DistrictDto; import de.eshg.base.street.SearchStreetResponse; import de.eshg.base.street.StreetController; -import de.eshg.base.util.CountryCode; import de.eshg.base.util.Gender; -import de.eshg.base.util.MappingUtil; +import de.eshg.lib.common.CountryCode; import de.eshg.lib.statistics.api.DataRow; import de.eshg.lib.statistics.api.SubjectType; import de.eshg.lib.statistics.api.ValueType; @@ -251,7 +250,7 @@ public class StatisticsController implements BaseStatisticsApi { domesticAddress.getStreet(), domesticAddress.getHouseNumber(), domesticAddress.getPostalCode(), - MappingUtil.mapCountryCodeToApi(domesticAddress.getCountry())); + domesticAddress.getCountry()); Set<DistrictDto> districts = searchStreetResponse.cityDistricts(); if (districts.size() == 1) { return districts.iterator().next(); diff --git a/backend/base/src/main/java/de/eshg/base/street/StreetController.java b/backend/base/src/main/java/de/eshg/base/street/StreetController.java index b9ee26b63..d96d08a4a 100644 --- a/backend/base/src/main/java/de/eshg/base/street/StreetController.java +++ b/backend/base/src/main/java/de/eshg/base/street/StreetController.java @@ -5,7 +5,7 @@ package de.eshg.base.street; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import de.eshg.rest.service.error.BadRequestException; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.Set; @@ -23,7 +23,7 @@ public class StreetController implements StreetApi { @Override public SearchStreetResponse searchStreet( - String streetName, String houseNumber, String postalCode, CountryCodeDto country) { + String streetName, String houseNumber, String postalCode, CountryCode country) { return StreetMapper.mapToSearchStreetResponse( streetService.getData( streetName, StreetMapper.mapToHouseNumber(houseNumber), postalCode, country)); diff --git a/backend/base/src/main/java/de/eshg/base/street/StreetService.java b/backend/base/src/main/java/de/eshg/base/street/StreetService.java index e3c1212b5..7ccd8b6b9 100644 --- a/backend/base/src/main/java/de/eshg/base/street/StreetService.java +++ b/backend/base/src/main/java/de/eshg/base/street/StreetService.java @@ -5,7 +5,7 @@ package de.eshg.base.street; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Function; @@ -25,8 +25,8 @@ public class StreetService { } public Set<AdministrativeData> getData( - String streetName, HouseNumber houseNumber, String postalCode, CountryCodeDto country) { - if (country != CountryCodeDto.DE) { + String streetName, HouseNumber houseNumber, String postalCode, CountryCode country) { + if (country != CountryCode.DE) { return Set.of(); } diff --git a/backend/base/src/main/java/de/eshg/base/testhelper/AbstractContactPopulator.java b/backend/base/src/main/java/de/eshg/base/testhelper/AbstractContactPopulator.java index e56decc97..c06bb4387 100644 --- a/backend/base/src/main/java/de/eshg/base/testhelper/AbstractContactPopulator.java +++ b/backend/base/src/main/java/de/eshg/base/testhelper/AbstractContactPopulator.java @@ -7,7 +7,6 @@ package de.eshg.base.testhelper; import static de.eshg.base.util.ClassNameUtil.getClassNameAsPropertyKey; -import de.eshg.base.CountryCodeDto; import de.eshg.base.address.AddressDto; import de.eshg.base.address.DomesticAddressDto; import de.eshg.base.address.PostboxAddressDto; @@ -17,6 +16,7 @@ import de.eshg.base.contact.api.ContactDto; import de.eshg.base.contact.api.InstitutionContactCategoryDto; import de.eshg.base.contact.persistence.entity.Contact; import de.eshg.base.contact.persistence.repository.ContactRepository; +import de.eshg.lib.common.CountryCode; import de.eshg.testhelper.environment.EnvironmentConfig; import de.eshg.testhelper.population.BasePopulator; import java.time.Clock; @@ -49,7 +49,11 @@ public abstract class AbstractContactPopulator extends BasePopulator<ContactDto> InstitutionContactCategoryDto category = categorySupplier.get(); List<String> phoneNumbers = optional(faker, randomListOfPhoneNumbers(faker, 7)); List<String> emailAddresses = optional(faker, randomListOfEmails(faker, 7)); - AddressDto contactAddress = createAddress(faker); + AddressDto contactAddress = + switch (category) { + case SCHOOL, HEALTH_DEPARTMENT -> createDomesticAddress(faker); + case null, default -> createAddress(faker); + }; AddressDto differentBillingAddress = optional(faker, createAddress(faker)); return contactController.addContact( new AddInstitutionContactRequest( @@ -69,7 +73,7 @@ public abstract class AbstractContactPopulator extends BasePopulator<ContactDto> } private DomesticAddressDto createDomesticAddress(Faker faker) { - CountryCodeDto country = randomElement(faker, CountryCodeDto.values()); + CountryCode country = randomElement(faker, CountryCode.values()); String city = faker.address().city(); String postalCode = faker.address().postcode(); String differentName = optional(faker, faker.fullMetalAlchemist().character()); @@ -81,7 +85,7 @@ public abstract class AbstractContactPopulator extends BasePopulator<ContactDto> } private PostboxAddressDto createPostboxAddress(Faker faker) { - CountryCodeDto country = randomElement(faker, CountryCodeDto.values()); + CountryCode country = randomElement(faker, CountryCode.values()); String city = faker.dungeonsAndDragons().cities(); String postalCode = faker.address().postcode(); String differentName = optional(faker, faker.fullMetalAlchemist().character()); diff --git a/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperController.java b/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperController.java index 5f6b12254..96379e173 100644 --- a/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperController.java +++ b/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperController.java @@ -99,6 +99,12 @@ public class BaseTestHelperController extends TestHelperController return baseTestHelperService.populateSchoolContacts(request.numberOfEntitiesToPopulate()); } + @Override + public SearchContactsResponse populateHealthDepartmentContacts(PopulationRequest request) { + return baseTestHelperService.populateHealthDepartmentsContacts( + request.numberOfEntitiesToPopulate()); + } + @Override public void resetKeycloak() { baseTestHelperService.resetKeycloak(); diff --git a/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperService.java b/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperService.java index aeeea2d43..c52df8519 100644 --- a/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperService.java +++ b/backend/base/src/main/java/de/eshg/base/testhelper/BaseTestHelperService.java @@ -68,6 +68,7 @@ public class BaseTestHelperService extends DefaultTestHelperService { private final EmployeeKeycloakTestClient employeeKeycloakTestClient; private final CitizenKeycloakTestClient citizenKeycloakTestClient; private final CitizenKeycloakTestProvisioning citizenKeycloakTestProvisioning; + private final HealthDepartmentContactPopulator healthDepartmentContactPopulator; private final CalendarService calendarService; @@ -97,7 +98,8 @@ public class BaseTestHelperService extends DefaultTestHelperService { InventoryPopulator inventoryPopulator, ContactPopulator contactPopulator, SchoolContactPopulator schoolContactPopulator, - EnvironmentConfig environmentConfig) { + EnvironmentConfig environmentConfig, + HealthDepartmentContactPopulator healthDepartmentContactPopulator) { super( databaseResetHelper, testRequestInterceptor, @@ -116,6 +118,7 @@ public class BaseTestHelperService extends DefaultTestHelperService { this.schoolContactPopulator = schoolContactPopulator; this.accessCodeGenerator = accessCodeGenerator; this.citizenKeycloakTestProvisioning = citizenKeycloakTestProvisioning; + this.healthDepartmentContactPopulator = healthDepartmentContactPopulator; } public void resetKeycloak() { @@ -299,4 +302,10 @@ public class BaseTestHelperService extends DefaultTestHelperService { schoolContactPopulator.populate(numberOfEntitiesToPopulate); return new SearchContactsResponse(result.entities(), result.totalNumberOfElements()); } + + public SearchContactsResponse populateHealthDepartmentsContacts(int numberOfEntitiesToPopulate) { + ListWithTotalNumber<ContactDto> result = + healthDepartmentContactPopulator.populate(numberOfEntitiesToPopulate); + return new SearchContactsResponse(result.entities(), result.totalNumberOfElements()); + } } diff --git a/backend/base/src/main/java/de/eshg/base/testhelper/HealthDepartmentContactPopulator.java b/backend/base/src/main/java/de/eshg/base/testhelper/HealthDepartmentContactPopulator.java new file mode 100644 index 000000000..ba665bb4c --- /dev/null +++ b/backend/base/src/main/java/de/eshg/base/testhelper/HealthDepartmentContactPopulator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.base.testhelper; + +import de.eshg.base.contact.ContactController; +import de.eshg.base.contact.api.ContactDto; +import de.eshg.base.contact.api.InstitutionContactCategoryDto; +import de.eshg.base.contact.persistence.entity.InstitutionContactCategory; +import de.eshg.base.contact.persistence.repository.ContactRepository; +import de.eshg.testhelper.ConditionalOnTestHelperEnabled; +import de.eshg.testhelper.environment.EnvironmentConfig; +import de.eshg.testhelper.population.BasePopulator; +import java.time.Clock; +import net.datafaker.Faker; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnTestHelperEnabled +public class HealthDepartmentContactPopulator extends AbstractContactPopulator { + protected HealthDepartmentContactPopulator( + Clock clock, + Environment environment, + ContactController contactController, + ContactRepository contactRepository, + EnvironmentConfig environmentConfig) { + super(clock, environment, contactController, contactRepository, environmentConfig); + } + + @Override + protected ContactDto populate( + int index, Faker faker, BasePopulator<ContactDto>.UniqueValueProvider uniqueValueProvider) { + return createInstitutionContact(faker, () -> InstitutionContactCategoryDto.HEALTH_DEPARTMENT); + } + + @Override + protected long countExistingEntities() { + return this.contactRepository.countByCategory(InstitutionContactCategory.HEALTH_DEPARTMENT); + } +} diff --git a/backend/base/src/main/java/de/eshg/base/user/UserController.java b/backend/base/src/main/java/de/eshg/base/user/UserController.java index eedb0825e..c131fac63 100644 --- a/backend/base/src/main/java/de/eshg/base/user/UserController.java +++ b/backend/base/src/main/java/de/eshg/base/user/UserController.java @@ -9,7 +9,6 @@ import de.eshg.base.calendar.CalendarEventService; import de.eshg.base.calendar.CalendarService; import de.eshg.base.calendar.api.GetEventsOfCalendarResponse; import de.eshg.base.calendar.api.UserCalendar; -import de.eshg.base.feature.BaseFeature; import de.eshg.base.feature.BaseFeatureToggle; import de.eshg.base.keycloak.EmployeeKeycloakClient; import de.eshg.base.keycloak.EmployeeUserAttribute; @@ -133,9 +132,11 @@ public class UserController implements UserApi { userService.updateUser( EmployeeKeycloakClient.getSelfUserId(), user -> { + Map<String, List<String>> currentAttributes = + Objects.requireNonNullElseGet(user.getAttributes(), LinkedHashMap::new); Map<String, List<String>> attributes = UserMapper.mapAttributesToDm( - request.phoneNumber(), request.externalChatUsername()); + currentAttributes, request.phoneNumber(), request.externalChatUsername()); user.setAttributes(attributes); }); return UserMapper.mapUserToApi(updated); @@ -201,7 +202,6 @@ public class UserController implements UserApi { @Override public GetActiveSessionsResponse getSelfActiveSessions() { - featureToggle.assertNewFeatureIsEnabled(BaseFeature.ACCOUNT_ACTIVE_SESSIONS); String sessionId = CurrentUserHelper.getCurrentUserSessionIdGracefully().orElse(""); return new GetActiveSessionsResponse( userService.getSelfActiveSessions().sessions().stream() @@ -224,14 +224,11 @@ public class UserController implements UserApi { @Override public void invalidateActiveSessions(InvalidateSessionsRequest request) { - featureToggle.assertNewFeatureIsEnabled(BaseFeature.ACCOUNT_ACTIVE_SESSIONS); userService.invalidateSessions(request.sessions()); } @Override public GetEventsResponse getSelfEvents(UserEventFilterParameters parameters) { - featureToggle.assertNewFeatureIsEnabled(BaseFeature.LOGIN_PROTOCOL); - int offset = parameters.offset(); int limit = parameters.limit(); Set<KeycloakEventType> eventTypes = getEventTypeOrFallback(parameters.type()); diff --git a/backend/base/src/main/java/de/eshg/base/user/mapper/UserMapper.java b/backend/base/src/main/java/de/eshg/base/user/mapper/UserMapper.java index a1f27038f..b43654397 100644 --- a/backend/base/src/main/java/de/eshg/base/user/mapper/UserMapper.java +++ b/backend/base/src/main/java/de/eshg/base/user/mapper/UserMapper.java @@ -30,7 +30,7 @@ import de.eshg.lib.keycloak.EmployeePermissionRole; import de.eshg.lib.keycloak.KeycloakRole; import java.time.Instant; import java.util.Arrays; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -52,7 +52,7 @@ public class UserMapper { representation.setLastName(user.lastName()); representation.setGroups(user.groups()); representation.setAttributes( - mapAttributesToDm(user.phoneNumber(), user.externalChatUsername())); + mapAttributesToDm(new LinkedHashMap<>(), user.phoneNumber(), user.externalChatUsername())); return representation; } @@ -175,6 +175,8 @@ public class UserMapper { case STI_PROTECTION_LEADER -> EmployeePermissionRole.STI_PROTECTION_LEADER; case MEDICAL_REGISTRY_LEADER -> EmployeePermissionRole.MEDICAL_REGISTRY_LEADER; case MEDICAL_REGISTRY_ADMIN -> EmployeePermissionRole.MEDICAL_REGISTRY_ADMIN; + case OPEN_DATA_ADMIN -> EmployeePermissionRole.OPEN_DATA_ADMIN; + case OPEN_DATA_LEADER -> EmployeePermissionRole.OPEN_DATA_LEADER; }; } @@ -248,6 +250,8 @@ public class UserMapper { case STI_PROTECTION_LEADER -> UserRoleDto.STI_PROTECTION_LEADER; case MEDICAL_REGISTRY_LEADER -> UserRoleDto.MEDICAL_REGISTRY_LEADER; case MEDICAL_REGISTRY_ADMIN -> UserRoleDto.MEDICAL_REGISTRY_ADMIN; + case OPEN_DATA_ADMIN -> UserRoleDto.OPEN_DATA_ADMIN; + case OPEN_DATA_LEADER -> UserRoleDto.OPEN_DATA_LEADER; }; } @@ -259,14 +263,17 @@ public class UserMapper { } public static Map<String, List<String>> mapAttributesToDm( - String phoneNumber, String externalChatUsername) { - Map<String, List<String>> attributes = new HashMap<>(); + Map<String, List<String>> attributes, String phoneNumber, String externalChatUsername) { if (phoneNumber != null) { attributes.put(EmployeeUserAttribute.PHONE_NUMBER.getKey(), List.of(phoneNumber)); + } else { + attributes.remove(EmployeeUserAttribute.PHONE_NUMBER.getKey()); } if (externalChatUsername != null) { attributes.put( EmployeeUserAttribute.EXTERNAL_CHAT_USERNAME.getKey(), List.of(externalChatUsername)); + } else { + attributes.remove(EmployeeUserAttribute.EXTERNAL_CHAT_USERNAME.getKey()); } return attributes; } diff --git a/backend/base/src/main/java/de/eshg/base/util/MappingUtil.java b/backend/base/src/main/java/de/eshg/base/util/MappingUtil.java index 49d58c56d..a2a382ca8 100644 --- a/backend/base/src/main/java/de/eshg/base/util/MappingUtil.java +++ b/backend/base/src/main/java/de/eshg/base/util/MappingUtil.java @@ -5,7 +5,6 @@ package de.eshg.base.util; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.SortDirection; @@ -66,14 +65,6 @@ public class MappingUtil { }; } - public static CountryCode mapCountryCodeToDm(CountryCodeDto countryCode) { - return countryCode == null ? null : CountryCode.valueOf(countryCode.name()); - } - - public static CountryCodeDto mapCountryCodeToApi(CountryCode countryCode) { - return countryCode == null ? null : CountryCodeDto.valueOf(countryCode.name()); - } - public static DataOrigin mapDataOriginToDm(DataOriginDto dataOrigin) { return switch (dataOrigin) { case MANUAL -> DataOrigin.MANUAL; diff --git a/backend/base/src/main/resources/application-preview-features.properties b/backend/base/src/main/resources/application-preview-features.properties index 2ca838691..271508a87 100644 --- a/backend/base/src/main/resources/application-preview-features.properties +++ b/backend/base/src/main/resources/application-preview-features.properties @@ -1 +1 @@ -de.eshg.base.feature-toggle.enabled-new-features=TASK_METRICS, STI_PROTECTION, CHAT_USERNAME, ACCOUNT_ACTIVE_SESSIONS, LOGIN_PROTOCOL, INBOX +de.eshg.base.feature-toggle.enabled-new-features=TASK_METRICS, STI_PROTECTION, CHAT_USERNAME, INBOX, CONTACT_MERGE diff --git a/backend/build.gradle b/backend/build.gradle index 2140f4def..28ad66d7a 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -302,7 +302,8 @@ def projectsToSequentialize = [ project(':auditlog'), project(':chat-management'), project(':sti-protection'), - project(':medical-registry') + project(':medical-registry'), + project(':opendata') ] // We do not need to sequentialize when we only execute schema tests diff --git a/backend/buildscript-gradle.lockfile b/backend/buildscript-gradle.lockfile index 65811731f..e1e9a9308 100644 --- a/backend/buildscript-gradle.lockfile +++ b/backend/buildscript-gradle.lockfile @@ -2,8 +2,8 @@ # Manual edits can break the build and are not advised. # This file is expected to be part of source control. aopalliance:aopalliance:1.0=classpath -com.avast.gradle.docker-compose:com.avast.gradle.docker-compose.gradle.plugin:0.17.8=classpath -com.avast.gradle:gradle-docker-compose-plugin:0.17.8=classpath +com.avast.gradle.docker-compose:com.avast.gradle.docker-compose.gradle.plugin:0.17.9=classpath +com.avast.gradle:gradle-docker-compose-plugin:0.17.9=classpath com.diffplug.durian:durian-collect:1.2.0=classpath com.diffplug.durian:durian-core:1.2.0=classpath com.diffplug.durian:durian-io:1.2.0=classpath @@ -99,6 +99,6 @@ org.springframework:spring-jcl:6.1.13=classpath org.tomlj:tomlj:1.0.0=classpath org.tukaani:xz:1.9=classpath org.webjars:d3js:4.10.2=classpath -org.yaml:snakeyaml:2.2=classpath +org.yaml:snakeyaml:2.3=classpath rpost.grantt:rpost.grantt.gradle.plugin:0.3=classpath empty= diff --git a/backend/business-module-commons/gradle.lockfile b/backend/business-module-commons/gradle.lockfile index 8d8c4b3d0..4ee39e4ea 100644 --- a/backend/business-module-commons/gradle.lockfile +++ b/backend/business-module-commons/gradle.lockfile @@ -53,6 +53,7 @@ io.prometheus:prometheus-metrics-model:1.2.1=compileClasspath,productionRuntimeC io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.2.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath @@ -95,7 +96,7 @@ org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath -org.jetbrains:annotations:26.0.0=compileClasspath +org.jetbrains:annotations:26.0.1=compileClasspath org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath diff --git a/backend/business-module-persistence-commons/gradle.lockfile b/backend/business-module-persistence-commons/gradle.lockfile index 87cb14b89..aa748a54b 100644 --- a/backend/business-module-persistence-commons/gradle.lockfile +++ b/backend/business-module-persistence-commons/gradle.lockfile @@ -60,6 +60,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath diff --git a/backend/central-repository/gradle.lockfile b/backend/central-repository/gradle.lockfile index 9cff6781a..1f0f271a0 100644 --- a/backend/central-repository/gradle.lockfile +++ b/backend/central-repository/gradle.lockfile @@ -60,6 +60,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/chat-management/gradle.lockfile b/backend/chat-management/gradle.lockfile index 657cda385..fd00a2f33 100644 --- a/backend/chat-management/gradle.lockfile +++ b/backend/chat-management/gradle.lockfile @@ -61,6 +61,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/compliance-test/gradle.lockfile b/backend/compliance-test/gradle.lockfile index c74b3a8d6..360ec7a16 100644 --- a/backend/compliance-test/gradle.lockfile +++ b/backend/compliance-test/gradle.lockfile @@ -31,7 +31,7 @@ com.github.java-json-tools:json-patch:1.13=testRuntimeClasspath com.github.java-json-tools:msg-simple:1.2=testRuntimeClasspath com.github.mangstadt:vinnie:2.0.2=testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=testCompileClasspath,testRuntimeClasspath -com.github.virtuald:curvesapi:1.08=testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=testCompileClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.2=testCompileClasspath,testRuntimeClasspath @@ -96,11 +96,11 @@ com.tngtech.archunit:archunit-junit5:1.3.0=testRuntimeClasspath 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=testCompileClasspath,testRuntimeClasspath -com.zaxxer:SparseBitSet:1.3=testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=testCompileClasspath,testRuntimeClasspath commons-beanutils:commons-beanutils:1.9.4=testRuntimeClasspath -commons-codec:commons-codec:1.16.1=testRuntimeClasspath +commons-codec:commons-codec:1.16.1=testCompileClasspath,testRuntimeClasspath commons-collections:commons-collections:3.2.2=testRuntimeClasspath -commons-io:commons-io:2.17.0=testRuntimeClasspath +commons-io:commons-io:2.16.1=testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.3=testRuntimeClasspath de.cronn:commons-lang:1.2=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=testRuntimeClasspath @@ -184,14 +184,14 @@ 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.antlr:antlr4-runtime:4.13.0=testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-collections4:4.4=testRuntimeClasspath -org.apache.commons:commons-compress:1.26.2=testRuntimeClasspath +org.apache.commons:commons-collections4:4.4=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.26.2=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 org.apache.commons:commons-lang3:3.14.0=testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-math3:3.6.1=testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.12.0=testRuntimeClasspath org.apache.cxf:cxf-core:4.0.5=testCompileClasspath,testRuntimeClasspath org.apache.cxf:cxf-rt-frontend-jaxrs:4.0.5=testCompileClasspath,testRuntimeClasspath @@ -214,9 +214,9 @@ org.apache.pdfbox:pdfbox-io:3.0.3=testRuntimeClasspath org.apache.pdfbox:pdfbox-tools:2.0.31=testRuntimeClasspath org.apache.pdfbox:pdfbox:3.0.3=testRuntimeClasspath org.apache.pdfbox:xmpbox:3.0.3=testRuntimeClasspath -org.apache.poi:poi-ooxml-lite:5.3.0=testRuntimeClasspath -org.apache.poi:poi-ooxml:5.3.0=testRuntimeClasspath -org.apache.poi:poi:5.3.0=testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.3.0=testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.3.0=testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi:5.3.0=testCompileClasspath,testRuntimeClasspath org.apache.tika:tika-bom:2.9.2=testRuntimeClasspath org.apache.tika:tika-core:2.9.2=testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:2.9.2=testRuntimeClasspath @@ -226,7 +226,7 @@ org.apache.tomcat.embed:tomcat-embed-el:10.1.30=testCompileClasspath,testRuntime org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.30=testRuntimeClasspath org.apache.ws.xmlschema:xmlschema-core:2.3.1=testCompileClasspath,testRuntimeClasspath -org.apache.xmlbeans:xmlbeans:5.2.1=testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.2.1=testCompileClasspath,testRuntimeClasspath org.apache.xmlgraphics:batik-anim:1.17=testRuntimeClasspath org.apache.xmlgraphics:batik-awt-util:1.18=testRuntimeClasspath org.apache.xmlgraphics:batik-bridge:1.17=testRuntimeClasspath diff --git a/backend/docker-compose.yaml b/backend/docker-compose.yaml index 30cf7f4b0..ca86d27f0 100644 --- a/backend/docker-compose.yaml +++ b/backend/docker-compose.yaml @@ -106,6 +106,7 @@ services: volumes: - ../reverse-proxy/nginx.conf:/etc/nginx/nginx.conf:ro - ../reverse-proxy/keycloak.conf:/etc/nginx/conf.d/default.conf:ro + - ../reverse-proxy/forward_headers.conf:/etc/nginx/forward_headers.conf:ro keycloak: image: ga-lotse/keycloak @@ -118,6 +119,7 @@ services: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin KC_DB: dev-file + KC_PROXY_HEADERS: xforwarded KC_HOSTNAME: http://localhost:4003 depends_on: - maildev diff --git a/backend/file-commons/README_LICENSE.adoc b/backend/file-commons/README_LICENSE.adoc new file mode 100644 index 000000000..87f2419aa --- /dev/null +++ b/backend/file-commons/README_LICENSE.adoc @@ -0,0 +1,5 @@ +== Licensing + +All files within this directory, including those in all subdirectories, are licensed under the Apache License 2.0. + +For the complete license text, please refer to the `LICENSE-APACHE-2.0.txt` file located in the project root. diff --git a/backend/file-commons/build.gradle b/backend/file-commons/build.gradle new file mode 100644 index 000000000..6d2878342 --- /dev/null +++ b/backend/file-commons/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "eshg.java-lib" +} + +dependencies { + implementation project(':rest-service-errors') + implementation project(':test-commons') + implementation 'org.springframework:spring-web' + implementation 'org.apache.tika:tika-core:latest.release' + implementation 'org.verapdf:validation-model-jakarta:latest.release' + implementation 'de.cronn:reflection-util:latest.release' +} diff --git a/backend/file-commons/buildscript-gradle.lockfile b/backend/file-commons/buildscript-gradle.lockfile new file mode 100644 index 000000000..0d156738b --- /dev/null +++ b/backend/file-commons/buildscript-gradle.lockfile @@ -0,0 +1,4 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +empty=classpath diff --git a/backend/file-commons/gradle.lockfile b/backend/file-commons/gradle.lockfile new file mode 100644 index 000000000..2731ce7a9 --- /dev/null +++ b/backend/file-commons/gradle.lockfile @@ -0,0 +1,124 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.5.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +ch.qos.logback:logback-core:1.5.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=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 +com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.jayway.jsonpath:json-path:2.9.0=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=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.15.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +de.cronn:reflection-util:2.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath +jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.19=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.13.0=productionRuntimeClasspath,runtimeClasspath,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=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.24.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.commons:commons-lang3:3.14.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-api:2.23.1=testCompileClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-to-slf4j:2.23.1=testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-core:2.9.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=compileClasspath,testCompileClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.78.1=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=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.glassfish.jaxb:jaxb-runtime:4.0.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.glassfish.jaxb:txw2:4.0.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt +org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt +org.jacoco:org.jacoco.core:0.8.11=jacocoAnt +org.jacoco:org.jacoco.report:0.8.11=jacocoAnt +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath +org.junit:junit-bom:5.10.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspath +org.mozilla:rhino:1.7.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.ow2.asm:asm-commons:9.6=jacocoAnt +org.ow2.asm:asm-tree:9.6=jacocoAnt +org.ow2.asm:asm:9.6=jacocoAnt,testCompileClasspath,testRuntimeClasspath +org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath +org.slf4j:jul-to-slf4j:2.0.16=testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-autoconfigure:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-logging:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.data:spring-data-commons:3.3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.springframework:spring-aop:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-beans:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-context:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-core:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-expression:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-webmvc:6.1.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:core-jakarta:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.verapdf:feature-reporting-jakarta:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.verapdf:metadata-fixer-jakarta:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.verapdf:parser:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.verapdf:pdf-model:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.verapdf:validation-model-jakarta:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.verapdf:verapdf-xmp-core-jakarta:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.xmlunit:xmlunit-core:2.9.1=testCompileClasspath,testRuntimeClasspath +org.yaml:snakeyaml:2.2=testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath diff --git a/backend/business-module-commons/src/main/java/de/base/rest/CustomMediaTypes.java b/backend/file-commons/src/main/java/de/eshg/file/common/CustomMediaTypes.java similarity index 93% rename from backend/business-module-commons/src/main/java/de/base/rest/CustomMediaTypes.java rename to backend/file-commons/src/main/java/de/eshg/file/common/CustomMediaTypes.java index 541b9f1ba..b3a9ff65e 100644 --- a/backend/business-module-commons/src/main/java/de/base/rest/CustomMediaTypes.java +++ b/backend/file-commons/src/main/java/de/eshg/file/common/CustomMediaTypes.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package de.base.rest; +package de.eshg.file.common; import java.util.List; import org.springframework.http.MediaType; @@ -46,4 +46,7 @@ public class CustomMediaTypes { public static final String MEDIA_TYPE_MP3_VALUE = "audio/mpeg"; public static final MediaType MEDIA_TYPE_MP3 = MediaType.parseMediaType(MEDIA_TYPE_MP3_VALUE); + + public static final String CSV_VALUE = "text/csv"; + public static final MediaType CSV = MediaType.valueOf(CSV_VALUE); } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileExtension.java b/backend/file-commons/src/main/java/de/eshg/file/common/FileExtension.java similarity index 85% rename from backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileExtension.java rename to backend/file-commons/src/main/java/de/eshg/file/common/FileExtension.java index f7cfb3b5f..707d6041f 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileExtension.java +++ b/backend/file-commons/src/main/java/de/eshg/file/common/FileExtension.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.lib.procedure.domain.model; +package de.eshg.file.common; public enum FileExtension { JPG("jpg"), @@ -12,7 +12,8 @@ public enum FileExtension { JFIF("jfif"), PNG("png"), PDF("pdf"), - EML("eml"); + EML("eml"), + CSV("csv"); private final String value; diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileType.java b/backend/file-commons/src/main/java/de/eshg/file/common/FileType.java similarity index 79% rename from backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileType.java rename to backend/file-commons/src/main/java/de/eshg/file/common/FileType.java index edefda715..9ba308f34 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileType.java +++ b/backend/file-commons/src/main/java/de/eshg/file/common/FileType.java @@ -3,23 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.lib.procedure.domain.model; +package de.eshg.file.common; -import static de.eshg.lib.procedure.domain.model.FileExtension.JFIF; -import static de.eshg.lib.procedure.domain.model.FileExtension.JPE; -import static de.eshg.lib.procedure.domain.model.FileExtension.JPG; - -import de.base.rest.CustomMediaTypes; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.http.MediaType; public enum FileType { - JPEG(MediaType.IMAGE_JPEG, JPG, FileExtension.JPEG, JPE, JFIF), + JPEG( + MediaType.IMAGE_JPEG, + FileExtension.JPG, + FileExtension.JPEG, + FileExtension.JPE, + FileExtension.JFIF), PNG(MediaType.IMAGE_PNG, FileExtension.PNG), PDF(MediaType.APPLICATION_PDF, FileExtension.PDF), - EML(CustomMediaTypes.EML, FileExtension.EML); + EML(CustomMediaTypes.EML, FileExtension.EML), + CSV(CustomMediaTypes.CSV, FileExtension.CSV); private final MediaType mediaType; private final FileExtension defaultFileExtension; diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileTypeDetector.java b/backend/file-commons/src/main/java/de/eshg/file/common/FileTypeDetector.java similarity index 67% rename from backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileTypeDetector.java rename to backend/file-commons/src/main/java/de/eshg/file/common/FileTypeDetector.java index 20faf5c38..e716717a3 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileTypeDetector.java +++ b/backend/file-commons/src/main/java/de/eshg/file/common/FileTypeDetector.java @@ -3,29 +3,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.lib.procedure.file; +package de.eshg.file.common; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.rest.service.error.BadRequestException; import java.io.IOException; import java.io.InputStream; import org.apache.tika.Tika; +import org.springframework.web.multipart.MultipartFile; public class FileTypeDetector { private FileTypeDetector() {} - static String detect(byte[] fileContent) { + public static String detect(byte[] fileContent) { return new Tika().detect(fileContent); } + public static FileType getSupportedFileTypeOrThrow(MultipartFile file) throws IOException { + String contentType = new Tika().detect(file.getBytes(), file.getOriginalFilename()); + return getFileType(contentType); + } + public static FileType getSupportedFileTypeOrThrow(InputStream fileInputStream) throws IOException { String contentType = new Tika().detect(fileInputStream); return getFileType(contentType); } - static FileType getSupportedFileTypeOrThrow(byte[] fileContent) { + public static FileType getSupportedFileTypeOrThrow(byte[] fileContent) { String contentType = new Tika().detect(fileContent); return getFileType(contentType); } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/PdfAConformanceValidator.java b/backend/file-commons/src/main/java/de/eshg/file/common/PdfAConformanceValidator.java similarity index 92% rename from backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/PdfAConformanceValidator.java rename to backend/file-commons/src/main/java/de/eshg/file/common/PdfAConformanceValidator.java index 190cf1bf8..09529169e 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/PdfAConformanceValidator.java +++ b/backend/file-commons/src/main/java/de/eshg/file/common/PdfAConformanceValidator.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.lib.procedure.file; +package de.eshg.file.common; import de.eshg.rest.service.error.BadRequestException; import de.eshg.rest.service.error.ErrorCode; @@ -19,7 +19,7 @@ import org.verapdf.pdfa.PDFAValidator; import org.verapdf.pdfa.VeraPDFFoundry; import org.verapdf.pdfa.results.ValidationResult; -class PdfAConformanceValidator { +public class PdfAConformanceValidator { private PdfAConformanceValidator() {} @@ -27,7 +27,7 @@ class PdfAConformanceValidator { VeraGreenfieldFoundryProvider.initialise(); } - static void validate(byte[] fileContent) { + public static void validate(byte[] fileContent) { try (VeraPDFFoundry foundry = Foundries.defaultInstance(); PDFAParser parser = foundry.createParser(new ByteArrayInputStream(fileContent)); PDFAValidator validator = foundry.createValidator(parser.getFlavour(), false)) { diff --git a/backend/inspection/build.gradle b/backend/inspection/build.gradle index ba11fe17a..f65de3853 100644 --- a/backend/inspection/build.gradle +++ b/backend/inspection/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation project(':lib-document-generator') implementation project(':lib-editor') implementation project(':business-module-persistence-commons') + implementation project(':file-commons') implementation 'de.topobyte:osm4j-core:latest.release' implementation 'de.topobyte:osm4j-pbf:latest.release' diff --git a/backend/inspection/gradle.lockfile b/backend/inspection/gradle.lockfile index 3e5ba1f1b..f76636f3a 100644 --- a/backend/inspection/gradle.lockfile +++ b/backend/inspection/gradle.lockfile @@ -16,11 +16,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -29,7 +29,7 @@ com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,te com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.protobuf:protobuf-javalite:3.23.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=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 @@ -73,23 +73,24 @@ com.slimjars.trove4j:trove4j-prime-finder:1.0.1=compileClasspath,productionRunti com.slimjars.trove4j:trove4j-primitive-hash:1.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.slimjars.trove4j:trove4j-primitive-iterator:1.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.16.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.rototor.pdfbox:graphics2d:3.0.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.topobyte:adt-multicollections:0.0.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.topobyte:osm4j-core:1.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -111,6 +112,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -122,11 +124,11 @@ jakarta.transaction:jakarta.transaction-api:2.0.1=annotationProcessor,compileCla jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.14.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.javacrumbs.shedlock:shedlock-core:5.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -135,10 +137,10 @@ net.jpountz.lz4:lz4:1.3.0=compileClasspath,productionRuntimeClasspath,runtimeCla net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.26.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.12.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -181,7 +183,7 @@ org.apache.xmlgraphics:batik-xml:1.17=productionRuntimeClasspath,runtimeClasspat org.apache.xmlgraphics:xmlgraphics-commons:2.9=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcmail-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -194,8 +196,8 @@ org.freemarker:freemarker:2.3.33=productionRuntimeClasspath,runtimeClasspath,tes org.glassfish.jaxb:jaxb-core:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -207,15 +209,15 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath @@ -223,12 +225,12 @@ org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspa org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -238,7 +240,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -254,7 +256,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -277,14 +279,14 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.testcontainers:database-commons:1.19.8=testRuntimeClasspath org.testcontainers:jdbc:1.19.8=testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath diff --git a/backend/inspection/openApi.yaml b/backend/inspection/openApi.yaml index bcd17721d..f961bbf08 100644 --- a/backend/inspection/openApi.yaml +++ b/backend/inspection/openApi.yaml @@ -7092,6 +7092,7 @@ components: enum: - PATIENT - PARENT + - PROFESSIONAL Population: type: object properties: @@ -7239,6 +7240,9 @@ components: - TM_VACCINATION_CONSULTATION - MEASLES_PROTECTION - STI_PROTECTION + - MEDICAL_REGISTRY_ENTRY + - MEDICAL_REGISTRY_CITIZEN_DRAFT + - MEDICAL_REGISTRY_EMPLOYEE_DRAFT ProcedureWithDuration: type: object properties: diff --git a/backend/inspection/src/main/java/de/eshg/inspection/checklist/MediaFileService.java b/backend/inspection/src/main/java/de/eshg/inspection/checklist/MediaFileService.java index 90cd8cc35..1d5ad0af2 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/checklist/MediaFileService.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/checklist/MediaFileService.java @@ -5,7 +5,7 @@ package de.eshg.inspection.checklist; -import de.base.rest.CustomMediaTypes; +import de.eshg.file.common.CustomMediaTypes; import de.eshg.inspection.checklist.api.UploadMediaFileRequestDto; import de.eshg.inspection.checklist.api.update.UpdateChecklistDto; import de.eshg.inspection.checklist.api.update.UpdateChecklistResponse; diff --git a/backend/inspection/src/main/java/de/eshg/inspection/checklist/persistence/element/ChecklistElement.java b/backend/inspection/src/main/java/de/eshg/inspection/checklist/persistence/element/ChecklistElement.java index 2cd837f8a..790367fa4 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/checklist/persistence/element/ChecklistElement.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/checklist/persistence/element/ChecklistElement.java @@ -10,6 +10,7 @@ import de.eshg.inspection.checklist.persistence.ChecklistSection; import de.eshg.inspection.checklistdefinition.api.ChecklistElementType; import de.eshg.inspection.checklistdefinition.persistence.section.element.ChecklistDefinitionElement; import de.eshg.inspection.incident.persistence.InspectionIncident; +import de.eshg.inspection.incident.persistence.InspectionIncident_; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; import jakarta.persistence.DiscriminatorColumn; @@ -51,7 +52,7 @@ public abstract class ChecklistElement extends GloballyUniqueEntityBase { @DataSensitivity(SensitivityLevel.SENSITIVE) private ChecklistDefinitionElement checklistDefinitionElement; - @OneToOne + @OneToOne(mappedBy = InspectionIncident_.CHECKLIST_ELEMENT) @DataSensitivity(SensitivityLevel.SENSITIVE) private InspectionIncident inspectionIncident; 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 6cd76eec8..ad1cb381c 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 @@ -5,18 +5,28 @@ package de.eshg.inspection.common.persistence; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import de.eshg.domain.model.BaseEntity; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Lob; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; import jakarta.validation.constraints.NotNull; +import java.io.IOException; import java.sql.Blob; +import java.sql.SQLException; import java.sql.Types; +import java.util.ArrayList; +import java.util.List; import org.hibernate.annotations.JdbcTypeCode; @Entity @DataSensitivity(SensitivityLevel.PROTECTED) +@JsonIgnoreProperties("mediaFiles") public class MediaFileContent extends BaseEntity { @Lob @@ -24,6 +34,14 @@ public class MediaFileContent extends BaseEntity { @NotNull private Blob file; + @OneToMany( + mappedBy = MediaFile_.FILE_CONTENT, + fetch = FetchType.LAZY, + orphanRemoval = true, + cascade = CascadeType.PERSIST) + @OrderBy + private final List<MediaFile> mediaFiles = new ArrayList<>(); + public Blob getFile() { return file; } @@ -31,4 +49,16 @@ public class MediaFileContent extends BaseEntity { public void setFile(Blob file) { this.file = file; } + + public List<MediaFile> getMediaFiles() { + return mediaFiles; + } + + public byte[] getAllBytes() { + try { + return file.getBinaryStream().readAllBytes(); + } catch (IOException | SQLException e) { + throw new RuntimeException(e); + } + } } 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 new file mode 100644 index 000000000..869a64c32 --- /dev/null +++ b/backend/inspection/src/main/java/de/eshg/inspection/common/persistence/MediaFileContentSerializer.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 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 ca8eac376..5762e7566 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,12 +5,18 @@ package de.eshg.inspection.config; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import de.eshg.inspection.common.persistence.MediaFileContentSerializer; import de.eshg.inspection.inspection.InspectionMapper; import de.eshg.inspection.inspection.persistence.Inspection; import de.eshg.inspection.inspection.persistence.InspectionTask; import de.eshg.lib.keycloak.ModuleLeaderRole; import de.eshg.lib.keycloak.ModuleMemberGroup; +import de.eshg.lib.procedure.domain.serialization.SerializationObjectMapperConfigurer; import de.eshg.lib.procedure.procedures.SummaryProvider; +import java.util.function.BiConsumer; +import java.util.function.UnaryOperator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -46,4 +52,19 @@ public class InspectionProcedureConfiguration { } }; } + + @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/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearch.java b/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearch.java index 7f662e910..56edf46a8 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearch.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearch.java @@ -66,7 +66,7 @@ public class WebSearch extends GloballyUniqueEntityBase { @OneToMany( mappedBy = WebSearchEntry_.WEB_SEARCH, - cascade = CascadeType.ALL, + cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.MERGE}, fetch = FetchType.LAZY, orphanRemoval = true) @OrderBy( diff --git a/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearchEntry.java b/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearchEntry.java index eaea8499b..d8449a70b 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearchEntry.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearchEntry.java @@ -34,8 +34,10 @@ import org.hibernate.dialect.PostgreSQLEnumJdbcType; @Table(indexes = @Index(columnList = "websearch_id")) public class WebSearchEntry extends BaseEntityWithExternalId { - @ManyToOne(optional = false, cascade = CascadeType.ALL) - @JoinColumn(name = "websearch_id", referencedColumnName = "id", nullable = false) + @ManyToOne( + optional = false, + cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.MERGE}) + @JoinColumn(name = "websearch_id", nullable = false) @NotNull @DataSensitivity(SensitivityLevel.PUBLIC) private WebSearch webSearch; diff --git a/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearchQuery.java b/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearchQuery.java index 16f0bbf66..966467fd1 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearchQuery.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/facility/websearch/persistence/WebSearchQuery.java @@ -8,6 +8,7 @@ package de.eshg.inspection.facility.websearch.persistence; import de.eshg.domain.model.BaseEntity; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; @@ -26,7 +27,9 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Table(indexes = @Index(columnList = "websearch_id")) @EntityListeners(AuditingEntityListener.class) public class WebSearchQuery extends BaseEntity { - @ManyToOne + @ManyToOne( + optional = false, + cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.MERGE}) @JoinColumn(name = "websearch_id", nullable = false) @NotNull @DataSensitivity(SensitivityLevel.PUBLIC) diff --git a/backend/inspection/src/main/java/de/eshg/inspection/incident/InspectionIncidentService.java b/backend/inspection/src/main/java/de/eshg/inspection/incident/InspectionIncidentService.java index 718ac45d7..6eb7bc441 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/incident/InspectionIncidentService.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/incident/InspectionIncidentService.java @@ -60,7 +60,7 @@ public class InspectionIncidentService { incident.setInspection(inspection); incident.setManualPosition(newPosition); - InspectionUpdater.advanceToExecutingPhase(inspection); + inspectionUpdater.advanceToExecutingPhase(inspection); inspectionUpdater.updateModified(inspection); return InspectionIncidentMapper.mapToDto(incident); @@ -84,7 +84,7 @@ public class InspectionIncidentService { inspectionIncident.setDescription(description); } - InspectionUpdater.advanceToExecutingPhase(inspection); + inspectionUpdater.advanceToExecutingPhase(inspection); inspectionUpdater.updateModified(inspection); return InspectionIncidentMapper.mapToDto(inspectionIncident); @@ -99,7 +99,7 @@ public class InspectionIncidentService { inspection.getIncidents().remove(incident); adjustPosition(inspection.getIncidents(), incident.getManualPosition()); - InspectionUpdater.advanceToExecutingPhase(inspection); + inspectionUpdater.advanceToExecutingPhase(inspection); inspectionUpdater.updateModified(inspection); } diff --git a/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionFinalizer.java b/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionFinalizer.java index 91a6bc86e..b33221639 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionFinalizer.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionFinalizer.java @@ -30,9 +30,9 @@ import de.eshg.inspection.report.persistence.Report; import de.eshg.inspection.util.FileUtil; import de.eshg.lib.auditlog.AuditLogger; import de.eshg.lib.procedure.domain.factory.SystemProgressEntryFactory; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.Pdf; import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.lib.procedure.domain.model.ProcedureStatus; import de.eshg.lib.procedure.domain.model.ProcedureType; import de.eshg.lib.procedure.domain.model.SystemProgressEntry; @@ -139,7 +139,7 @@ public class InspectionFinalizer { addProgressEntryForFinalization(inspection); inspection.setPhase(InspectionPhase.CREATING_REPORT_AND_INVOICE); inspection.getExecutionTaskOrThrow().setTaskStatus(TaskStatus.CLOSED); - inspection.createReportTask(); + inspection.createReportTask(clock); inspectionValidator.generateSignatureHash(signature, signatureFile, inspection.getPhase()); inspectionValidator.generateChecklistHashes(inspection.getChecklists(), inspection.getPhase()); inspectionUpdater.updateModified(inspection); @@ -200,7 +200,9 @@ public class InspectionFinalizer { pdfMetaData.setCreatedDate(reportDate.toInstant()); pdfMetaData.setDescription(reportData.inspection().title()); String filename = reportData.reportInfo().filename(); - Pdf pdf = FileFactory.createPdfWithMetaData(filename, FileType.PDF, bytes, pdfMetaData, false); + Pdf pdf = + FileFactory.createPdfWithMetaData( + filename, ProcedureFileType.PDF, bytes, pdfMetaData, false); report.setReportFile(pdf); } diff --git a/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionService.java b/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionService.java index e0d57d817..8ca08e463 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionService.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionService.java @@ -146,7 +146,7 @@ public class InspectionService { inspection.updateProcedureStatus(ProcedureStatus.IN_PROGRESS, clock, auditLogger); // create planning task (without appointment) - InspectionTask planningTask = InspectionTask.newPlanningTask(request.assigneeId()); + InspectionTask planningTask = InspectionTask.newPlanningTask(request.assigneeId(), clock); inspection.addTask(planningTask); addManualProgressEntry(request.progressEntryText(), inspection); @@ -346,7 +346,7 @@ public class InspectionService { .orElseThrow( () -> new NotFoundException("This checklist is not part of this inspection")); - InspectionUpdater.advanceToExecutingPhase(inspection); + inspectionUpdater.advanceToExecutingPhase(inspection); ChecklistDto checklistDto = checklistService.updateChecklist(checklist, updateChecklist); return new UpdateChecklistResponse(checklistDto); diff --git a/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionUpdater.java b/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionUpdater.java index 09c3bd973..85d899c0c 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionUpdater.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/inspection/InspectionUpdater.java @@ -334,7 +334,7 @@ public class InspectionUpdater { // get or create planning task InspectionTask planningTask = planningTaskOpt.orElseGet( - () -> inspection.createPlanningTask(CurrentUserHelper.getCurrentUserId())); + () -> inspection.createPlanningTask(CurrentUserHelper.getCurrentUserId(), clock)); planningTask.updateDueAt(computePlanningDueDate(inspection, appointmentStart)); } @@ -782,7 +782,7 @@ public class InspectionUpdater { // Create planning task if it doesn't exist yet. if (inspection.getPlanningTask().isEmpty()) { - inspection.createPlanningTask(assigneeId); + inspection.createPlanningTask(assigneeId, clock); } if (inspection.getCalendarEventId() != null) { @@ -842,7 +842,7 @@ public class InspectionUpdater { } } - private static void advanceToReadyForExecutionIfPossible(Inspection inspection) { + private void advanceToReadyForExecutionIfPossible(Inspection inspection) { if (inspection.getPhase().equals(InspectionPhase.PLANNING) && inspection.getPlannedAppointment() != null && !inspection.getChecklists().isEmpty() @@ -857,20 +857,21 @@ public class InspectionUpdater { // copy plannedAppointment to executionAppointment inspection.setExecutionAppointment(inspection.getPlannedAppointment().getClone()); // create execution task - InspectionTask executionTask = inspection.createExecutionTask(); + InspectionTask executionTask = inspection.createExecutionTask(clock); executionTask.updateDueAt(inspection.getExecutionAppointment().getAppointmentStart()); // set next phase inspection.setPhase(InspectionPhase.READY_FOR_EXECUTION); } } - public static void advanceToExecutingPhase(Inspection inspection) { + public void advanceToExecutingPhase(Inspection inspection) { if (inspection.getPhase() == InspectionPhase.READY_FOR_EXECUTION) { inspection.setPhase(InspectionPhase.EXECUTING); InspectionTask planningTask = inspection .getPlanningTask() - .orElseGet(() -> inspection.createPlanningTask(CurrentUserHelper.getCurrentUserId())); + .orElseGet( + () -> inspection.createPlanningTask(CurrentUserHelper.getCurrentUserId(), clock)); planningTask.setTaskStatus(TaskStatus.CLOSED); InspectionAppointment plannedAppointment = inspection.getPlannedAppointment(); if (plannedAppointment != null) { diff --git a/backend/inspection/src/main/java/de/eshg/inspection/inspection/persistence/Inspection.java b/backend/inspection/src/main/java/de/eshg/inspection/inspection/persistence/Inspection.java index f7e4d8cfa..acf869236 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/inspection/persistence/Inspection.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/inspection/persistence/Inspection.java @@ -27,6 +27,7 @@ import de.eshg.rest.service.error.ErrorCode; import de.eshg.rest.service.security.CurrentUserHelper; import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; +import java.time.Clock; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -78,7 +79,7 @@ public class Inspection @OneToMany( mappedBy = InspectionInventory_.INSPECTION, - cascade = CascadeType.ALL, + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) @OrderBy @DataSensitivity(SensitivityLevel.SENSITIVE) @@ -86,7 +87,7 @@ public class Inspection @OneToMany( mappedBy = InspectionResource_.INSPECTION, - cascade = CascadeType.ALL, + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) @OrderBy @DataSensitivity(SensitivityLevel.SENSITIVE) @@ -94,7 +95,7 @@ public class Inspection @OneToMany( mappedBy = InspectionIncident_.INSPECTION, - cascade = CascadeType.PERSIST, + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) @OrderBy @DataSensitivity(SensitivityLevel.SENSITIVE) @@ -123,15 +124,21 @@ public class Inspection @DataSensitivity(SensitivityLevel.SENSITIVE) private String notes; - @OneToOne(cascade = CascadeType.PERSIST) + @OneToOne( + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, + orphanRemoval = true) @DataSensitivity(SensitivityLevel.SENSITIVE) private InspectionAppointment plannedAppointment; - @OneToOne(cascade = CascadeType.PERSIST) + @OneToOne( + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, + orphanRemoval = true) @DataSensitivity(SensitivityLevel.SENSITIVE) private InspectionAppointment executionAppointment; - @OneToOne(cascade = CascadeType.PERSIST, orphanRemoval = true) + @OneToOne( + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, + orphanRemoval = true) @DataSensitivity(SensitivityLevel.SENSITIVE) private InspectionTravelTime travelTime; @@ -139,11 +146,16 @@ public class Inspection @DataSensitivity(SensitivityLevel.SENSITIVE) private UUID calendarEventId; - @OneToOne(cascade = CascadeType.PERSIST, orphanRemoval = true) + @OneToOne( + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, + orphanRemoval = true) @DataSensitivity(SensitivityLevel.SENSITIVE) private InspectionAnnouncement announcement; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST, orphanRemoval = true) + @OneToOne( + fetch = FetchType.LAZY, + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, + orphanRemoval = true) @DataSensitivity(SensitivityLevel.SENSITIVE) private Report report; @@ -333,37 +345,39 @@ public class Inspection this.calendarEventId = calendarEventId; } - public InspectionTask createPlanningTask(UUID assigneeId) { + public InspectionTask createPlanningTask(UUID assigneeId, Clock clock) { if (getPlanningTask().isPresent()) { throw new IllegalStateException("Task with type PLANNING already created"); } - InspectionTask task = InspectionTask.newPlanningTask(assigneeId); + InspectionTask task = InspectionTask.newPlanningTask(assigneeId, clock); addTask(task); return task; } - public InspectionTask createExecutionTask() { + public InspectionTask createExecutionTask(Clock clock) { if (getExecutionTask().isPresent()) { throw new IllegalStateException("Task with type EXECUTION already created"); } InspectionTask task = InspectionTask.newExecutionTask( getPlanningTask() - .orElseGet(() -> createPlanningTask(CurrentUserHelper.getCurrentUserId())) - .getAssigneeId()); + .orElseGet(() -> createPlanningTask(CurrentUserHelper.getCurrentUserId(), clock)) + .getAssigneeId(), + clock); addTask(task); return task; } - public InspectionTask createReportTask() { + public InspectionTask createReportTask(Clock clock) { if (getReportTask().isPresent()) { throw new IllegalStateException("Task with type REPORT already created"); } InspectionTask task = InspectionTask.newReportTask( getPlanningTask() - .orElseGet(() -> createPlanningTask(CurrentUserHelper.getCurrentUserId())) - .getAssigneeId()); + .orElseGet(() -> createPlanningTask(CurrentUserHelper.getCurrentUserId(), clock)) + .getAssigneeId(), + clock); addTask(task); return task; } diff --git a/backend/inspection/src/main/java/de/eshg/inspection/inspection/persistence/InspectionTask.java b/backend/inspection/src/main/java/de/eshg/inspection/inspection/persistence/InspectionTask.java index 4e4923da9..c448da154 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/inspection/persistence/InspectionTask.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/inspection/persistence/InspectionTask.java @@ -14,7 +14,7 @@ import de.eshg.rest.service.security.CurrentUserHelper; import jakarta.persistence.Entity; import jakarta.persistence.Index; import jakarta.persistence.Table; -import java.time.Instant; +import java.time.Clock; import java.util.UUID; @Entity @@ -22,24 +22,25 @@ import java.util.UUID; @DataSensitivity(SensitivityLevel.SENSITIVE) public class InspectionTask extends Task<Inspection> { - public static InspectionTask newPlanningTask(UUID assigneeId) { - return createInspectionTask(TaskType.INSPECTION_PLANNING, assigneeId); + public static InspectionTask newPlanningTask(UUID assigneeId, Clock clock) { + return createInspectionTask(TaskType.INSPECTION_PLANNING, assigneeId, clock); } - public static InspectionTask newExecutionTask(UUID assigneeId) { - return createInspectionTask(TaskType.INSPECTION_EXECUTION, assigneeId); + public static InspectionTask newExecutionTask(UUID assigneeId, Clock clock) { + return createInspectionTask(TaskType.INSPECTION_EXECUTION, assigneeId, clock); } - public static InspectionTask newReportTask(UUID assigneeId) { - return createInspectionTask(TaskType.INSPECTION_REPORT, assigneeId); + public static InspectionTask newReportTask(UUID assigneeId, Clock clock) { + return createInspectionTask(TaskType.INSPECTION_REPORT, assigneeId, clock); } - private static InspectionTask createInspectionTask(TaskType taskType, UUID assigneeId) { + private static InspectionTask createInspectionTask( + TaskType taskType, UUID assigneeId, Clock clock) { InspectionTask task = new InspectionTask(); task.setTaskType(taskType); task.setTaskStatus(TaskStatus.OPEN); UUID currentUserId = CurrentUserHelper.getCurrentUserId(); - task.assign(assigneeId, currentUserId, Instant.now()); + task.assign(assigneeId, currentUserId, clock.instant()); return task; } } diff --git a/backend/inspection/src/main/java/de/eshg/inspection/report/persistence/Report.java b/backend/inspection/src/main/java/de/eshg/inspection/report/persistence/Report.java index a6d61972a..c38dc4901 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/report/persistence/Report.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/report/persistence/Report.java @@ -8,6 +8,7 @@ package de.eshg.inspection.report.persistence; import de.eshg.domain.model.GloballyUniqueEntityBase; import de.eshg.inspection.inspection.InspectionValidator; import de.eshg.inspection.inspection.persistence.Inspection; +import de.eshg.inspection.inspection.persistence.Inspection_; import de.eshg.inspection.report.persistence.element.ReportElement; import de.eshg.inspection.report.persistence.element.ReportElement_; import de.eshg.lib.common.DataSensitivity; @@ -19,7 +20,6 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; -import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; @@ -50,7 +50,7 @@ public class Report extends GloballyUniqueEntityBase { private Instant createdAt; @NotNull - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @OneToOne(mappedBy = Inspection_.REPORT, cascade = CascadeType.PERSIST) @DataSensitivity(SensitivityLevel.SENSITIVE) private Inspection inspection; diff --git a/backend/inspection/src/main/java/de/eshg/inspection/testhelper/FacilityTestDataProvider.java b/backend/inspection/src/main/java/de/eshg/inspection/testhelper/FacilityTestDataProvider.java index 573a0f977..76a8f5516 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/testhelper/FacilityTestDataProvider.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/testhelper/FacilityTestDataProvider.java @@ -5,7 +5,6 @@ package de.eshg.inspection.testhelper; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; @@ -24,6 +23,7 @@ import de.eshg.inspection.inspection.api.InspectionType; import de.eshg.inspection.inspection.api.StartInspectionRequest; import de.eshg.inspection.objecttype.persistence.ObjectType; import de.eshg.inspection.objecttype.persistence.ObjectTypeRepository; +import de.eshg.lib.common.CountryCode; import java.util.Collections; import java.util.List; import org.springframework.stereotype.Component; @@ -43,7 +43,7 @@ public class FacilityTestDataProvider { private static final List<DomesticAddressDto> domesticAddressList = List.of( new DomesticAddressDto( - CountryCodeDto.DE, + CountryCode.DE, "Frankfurt am Main", "65933", "Indira Nails", @@ -51,9 +51,9 @@ public class FacilityTestDataProvider { "27", null), new DomesticAddressDto( - CountryCodeDto.DE, "Frankfurt am Main", "60311", "Lalesen", "Hasengasse", "3", null), + CountryCode.DE, "Frankfurt am Main", "60311", "Lalesen", "Hasengasse", "3", null), new DomesticAddressDto( - CountryCodeDto.DE, + CountryCode.DE, "Frankfurt am Main", "60431", "Mena's Studio", @@ -61,17 +61,11 @@ public class FacilityTestDataProvider { "22", null), new DomesticAddressDto( - CountryCodeDto.DE, - "Frankfurt am Main", - "60313", - "My Nails", - "Alte Gasse", - "51", - null), + CountryCode.DE, "Frankfurt am Main", "60313", "My Nails", "Alte Gasse", "51", null), new DomesticAddressDto( - CountryCodeDto.DE, "Frankfurt am Main", "60329", "Nails", "Taunusstraße", "18", null), + CountryCode.DE, "Frankfurt am Main", "60329", "Nails", "Taunusstraße", "18", null), new DomesticAddressDto( - CountryCodeDto.DE, + CountryCode.DE, "Frankfurt am Main", "60320", "Oh, my Nails!", @@ -205,7 +199,7 @@ public class FacilityTestDataProvider { private static PostboxAddressDto createPostboxAddress(int index) { return new PostboxAddressDto( - CountryCodeDto.US, "Frankfurt am Main", "60320", getNameOfFacility(index), "12" + index); + CountryCode.US, "Frankfurt am Main", "60320", getNameOfFacility(index), "12" + index); } private static AddFacilityFileStateRequest createAddFacilityFileStateRequest( diff --git a/backend/inspection/src/main/java/de/eshg/inspection/util/FileUtil.java b/backend/inspection/src/main/java/de/eshg/inspection/util/FileUtil.java index 5ce49eaaa..18060730c 100644 --- a/backend/inspection/src/main/java/de/eshg/inspection/util/FileUtil.java +++ b/backend/inspection/src/main/java/de/eshg/inspection/util/FileUtil.java @@ -8,7 +8,7 @@ package de.eshg.inspection.util; import static de.eshg.lib.procedure.util.FileValidator.validate; import static de.eshg.lib.procedure.util.FileValidator.validateAudioFile; -import de.base.rest.CustomMediaTypes; +import de.eshg.file.common.CustomMediaTypes; import de.eshg.inspection.common.persistence.MediaFile; import de.eshg.inspection.common.persistence.MediaFileContent; import de.eshg.rest.service.error.BadRequestException; diff --git a/backend/inspection/src/main/resources/application.properties b/backend/inspection/src/main/resources/application.properties index c648f6cf3..1014e0559 100644 --- a/backend/inspection/src/main/resources/application.properties +++ b/backend/inspection/src/main/resources/application.properties @@ -43,7 +43,6 @@ eshg.inspection.scheduling.job.websearch.cron=0 0 1 * * * de.eshg.inspection.hash-algorithm=BLAKE2B_512 # test data population -eshg.population.enabled=false eshg.population.text_block=20 eshg.population.checklist_definition=5 eshg.population.inspection=6 diff --git a/backend/inspection/src/main/resources/migrations/0038_remove_report_fk.xml b/backend/inspection/src/main/resources/migrations/0038_remove_report_fk.xml new file mode 100644 index 000000000..8e33b29a8 --- /dev/null +++ b/backend/inspection/src/main/resources/migrations/0038_remove_report_fk.xml @@ -0,0 +1,21 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 SCOOP Software GmbH, cronn GmbH + SPDX-License-Identifier: AGPL-3.0-only +--> + +<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="1728475894148-1"> + <dropForeignKeyConstraint baseTableName="report" + constraintName="fk_report_inspection"/> + </changeSet> + <changeSet author="GA-Lotse" id="1728475894148-2"> + <dropUniqueConstraint constraintName="report_inspection_id_key" + tableName="report"/> + </changeSet> + <changeSet author="GA-Lotse" id="1728475894148-3"> + <dropColumn columnName="inspection_id" tableName="report"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/inspection/src/main/resources/migrations/0039_remove_fk_checklistelement_incident.xml b/backend/inspection/src/main/resources/migrations/0039_remove_fk_checklistelement_incident.xml new file mode 100644 index 000000000..81e3d31b1 --- /dev/null +++ b/backend/inspection/src/main/resources/migrations/0039_remove_fk_checklistelement_incident.xml @@ -0,0 +1,23 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 SCOOP Software GmbH, cronn GmbH + SPDX-License-Identifier: AGPL-3.0-only +--> + +<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="1728482035845-1"> + <dropForeignKeyConstraint baseTableName="checklist_element" + constraintName="fk_checklist_element_inspection_incident"/> + </changeSet> + <changeSet author="GA-Lotse" id="1728482035845-2"> + <dropUniqueConstraint + constraintName="checklist_element_inspection_incident_id_key" + tableName="checklist_element"/> + </changeSet> + <changeSet author="GA-Lotse" id="1728482035845-3"> + <dropColumn columnName="inspection_incident_id" + tableName="checklist_element"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/inspection/src/main/resources/migrations/0040_introduce_procedure_file_type.xml b/backend/inspection/src/main/resources/migrations/0040_introduce_procedure_file_type.xml new file mode 100644 index 000000000..1ee443742 --- /dev/null +++ b/backend/inspection/src/main/resources/migrations/0040_introduce_procedure_file_type.xml @@ -0,0 +1,11 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 SCOOP Software GmbH, 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="1728922179310-1"> + <ext:renamePostgresEnumType oldName="fileType" newName="procedureFileType"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/inspection/src/main/resources/migrations/0041_add_medical_registry_procedure_types.xml b/backend/inspection/src/main/resources/migrations/0041_add_medical_registry_procedure_types.xml new file mode 100644 index 000000000..fc2ba4314 --- /dev/null +++ b/backend/inspection/src/main/resources/migrations/0041_add_medical_registry_procedure_types.xml @@ -0,0 +1,14 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 SCOOP Software GmbH, 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="1728647075086-1"> + <ext:addPostgresEnumValues enumTypeName="persontype" valuesToAdd="PROFESSIONAL"/> + </changeSet> + <changeSet author="GA-Lotse" id="1728647075086-2"> + <ext:addPostgresEnumValues enumTypeName="proceduretype" valuesToAdd="MEDICAL_REGISTRY_CITIZEN_DRAFT, MEDICAL_REGISTRY_EMPLOYEE_DRAFT, MEDICAL_REGISTRY_ENTRY"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/inspection/src/main/resources/migrations/changelog.xml b/backend/inspection/src/main/resources/migrations/changelog.xml index ede013484..1fa70a35e 100644 --- a/backend/inspection/src/main/resources/migrations/changelog.xml +++ b/backend/inspection/src/main/resources/migrations/changelog.xml @@ -45,4 +45,8 @@ <include file="migrations/0035_refactor_web_search_entry_tags.xml"/> <include file="migrations/0036_inspection_notes.xml"/> <include file="migrations/0037_cld_drafts.xml"/> + <include file="migrations/0038_remove_report_fk.xml"/> + <include file="migrations/0039_remove_fk_checklistelement_incident.xml"/> + <include file="migrations/0040_introduce_procedure_file_type.xml"/> + <include file="migrations/0041_add_medical_registry_procedure_types.xml"/> </databaseChangeLog> diff --git a/backend/lib-aggregation/gradle.lockfile b/backend/lib-aggregation/gradle.lockfile index 39ea523a3..aea6485a0 100644 --- a/backend/lib-aggregation/gradle.lockfile +++ b/backend/lib-aggregation/gradle.lockfile @@ -53,6 +53,7 @@ io.prometheus:prometheus-metrics-model:1.2.1=productionRuntimeClasspath,runtimeC io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath diff --git a/backend/lib-appointmentblock/gradle.lockfile b/backend/lib-appointmentblock/gradle.lockfile index fb84ec89a..8f707ad8f 100644 --- a/backend/lib-appointmentblock/gradle.lockfile +++ b/backend/lib-appointmentblock/gradle.lockfile @@ -60,6 +60,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -118,7 +119,7 @@ org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:26.0.0=compileClasspath +org.jetbrains:annotations:26.0.1=compileClasspath org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-appointmentblock/src/main/java/de/eshg/lib/appointmentblock/testhelper/AppointmentBlockGroupsPopulator.java b/backend/lib-appointmentblock/src/main/java/de/eshg/lib/appointmentblock/testhelper/AppointmentBlockGroupsPopulator.java index 837d9126e..9c2cae6bb 100644 --- a/backend/lib-appointmentblock/src/main/java/de/eshg/lib/appointmentblock/testhelper/AppointmentBlockGroupsPopulator.java +++ b/backend/lib-appointmentblock/src/main/java/de/eshg/lib/appointmentblock/testhelper/AppointmentBlockGroupsPopulator.java @@ -10,6 +10,9 @@ import static de.eshg.lib.appointmentblock.AppointmentBlockService.TECHNICAL_GRO import static de.eshg.lib.appointmentblock.AppointmentBlockService.TECHNICAL_GROUP_MFAS; import static de.eshg.lib.appointmentblock.AppointmentBlockService.TECHNICAL_GROUP_PHYSICIANS; +import de.eshg.base.contact.ContactApi; +import de.eshg.base.contact.api.*; +import de.eshg.base.testhelper.BaseTestHelperApi; import de.eshg.base.user.UserApi; import de.eshg.base.user.api.UserFilterParameters; import de.eshg.lib.appointmentblock.AppointmentBlockController; @@ -23,10 +26,12 @@ import de.eshg.lib.appointmentblock.persistence.entity.AppointmentBlockGroup; import de.eshg.lib.appointmentblock.spring.AppointmentBlockProperties; import de.eshg.lib.keycloak.TechnicalGroup; import de.eshg.testhelper.ConditionalOnTestHelperEnabled; +import de.eshg.testhelper.api.PopulationRequest; import de.eshg.testhelper.environment.EnvironmentConfig; import de.eshg.testhelper.population.BasePopulator; import de.eshg.testhelper.population.ListWithTotalNumber; import de.eshg.testhelper.population.PopulateWithAccessTokenHelper; +import de.eshg.testhelper.population.RequestContextFaker; import java.time.*; import java.util.List; import java.util.Optional; @@ -51,6 +56,8 @@ public class AppointmentBlockGroupsPopulator private final Optional<TechnicalGroup> groupMfas; private final Optional<TechnicalGroup> groupConsultants; private final UserApi userApi; + private final ContactApi contactApi; + private final BaseTestHelperApi baseTestHelperApi; public AppointmentBlockGroupsPopulator( Clock clock, @@ -65,20 +72,25 @@ public class AppointmentBlockGroupsPopulator @SuppressWarnings("unused") // Used as dependency CreateAppointmentTypeTask createAppointmentTypeTask, UserApi userApi, - EnvironmentConfig environmentConfig) { + EnvironmentConfig environmentConfig, + ContactApi contactApi, + BaseTestHelperApi baseTestHelperApi) { super( clock, environment, getClassNameAsPropertyKey(AppointmentBlockGroup.class), environmentConfig); this.populateWithAccessTokenHelper = populateWithAccessTokenHelper; - this.appointmentBlockController = appointmentBlockController; + this.appointmentBlockController = + RequestContextFaker.withFakedRequestContextsIfNecessary(appointmentBlockController); this.appointmentBlockGroupRepository = appointmentBlockGroupRepository; this.appointmentBlockProperties = appointmentBlockProperties; this.groupPhysicians = groupPhysicians; this.groupMfas = groupMfas; this.groupConsultants = groupConsultants; this.userApi = userApi; + this.contactApi = contactApi; + this.baseTestHelperApi = baseTestHelperApi; } @Override @@ -116,6 +128,14 @@ public class AppointmentBlockGroupsPopulator List<UUID> mfaIds = getRandomUserIdAsList(faker, groupMfas); List<UUID> consultantIds = getRandomUserIdAsList(faker, groupConsultants); + UUID locationId = + switch (appointmentBlockProperties.getLocationSelectionMode()) { + case NONE -> null; + case HEALTH_DEPARTMENT -> + getIdOfFirstContactForCategory(InstitutionContactCategoryDto.HEALTH_DEPARTMENT); + case SCHOOL -> getIdOfFirstContactForCategory(InstitutionContactCategoryDto.SCHOOL); + }; + CreateDailyAppointmentBlockGroupRequest request = new CreateDailyAppointmentBlockGroupRequest( AppointmentTypeMapper.toInterfaceType(type), @@ -123,7 +143,8 @@ public class AppointmentBlockGroupsPopulator List.of(new CreateDailyAppointmentBlockDto(start, end, List.of(dayOfWeek))), physicianIds, mfaIds, - consultantIds); + consultantIds, + locationId); return appointmentBlockController.createDailyAppointmentBlocksForGroup(request); } @@ -139,4 +160,29 @@ public class AppointmentBlockGroupsPopulator protected long countExistingEntities() { return this.appointmentBlockGroupRepository.count(); } + + private UUID getIdOfFirstContactForCategory(InstitutionContactCategoryDto category) { + return contactApi + .getContacts( + new ContactFilterParameters( + null, null, ContactTypeDto.INSTITUTION, category, null, null, null, null)) + .elements() + .stream() + .findFirst() + .map(ContactDto::id) + .orElseGet(() -> populateOneContactOfCategoryAndGetId(category)); + } + + private UUID populateOneContactOfCategoryAndGetId(InstitutionContactCategoryDto category) { + SearchContactsResponse response = + switch (category) { + case SCHOOL -> baseTestHelperApi.populateSchoolContacts(new PopulationRequest(1)); + case HEALTH_DEPARTMENT -> + baseTestHelperApi.populateHealthDepartmentContacts(new PopulationRequest(1)); + case null, default -> + throw new IllegalStateException( + "Expected only to be used with SCHOOL or HEALTH_DEPARTMENT. Got: " + category); + }; + return response.elements().getFirst().id(); + } } diff --git a/backend/lib-auditlog/gradle.lockfile b/backend/lib-auditlog/gradle.lockfile index 5c2856dbe..bb6ed0842 100644 --- a/backend/lib-auditlog/gradle.lockfile +++ b/backend/lib-auditlog/gradle.lockfile @@ -56,6 +56,7 @@ io.prometheus:prometheus-metrics-model:1.2.1=testRuntimeClasspath io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.2.1=testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -176,4 +177,4 @@ org.zalando:logbook-servlet:3.9.0=productionRuntimeClasspath,runtimeClasspath,te org.zalando:logbook-spring-boot-autoconfigure:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.zalando:logbook-spring-boot-starter:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.zalando:logbook-spring:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath +empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath diff --git a/backend/lib-base-client/gradle.lockfile b/backend/lib-base-client/gradle.lockfile index ec7236eaa..8444c6ce4 100644 --- a/backend/lib-base-client/gradle.lockfile +++ b/backend/lib-base-client/gradle.lockfile @@ -26,6 +26,7 @@ de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,test io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-calendar-api/gradle.lockfile b/backend/lib-calendar-api/gradle.lockfile index 1eced9418..6b3a09ebe 100644 --- a/backend/lib-calendar-api/gradle.lockfile +++ b/backend/lib-calendar-api/gradle.lockfile @@ -10,6 +10,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspat io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-calendar/gradle.lockfile b/backend/lib-calendar/gradle.lockfile index 87aed7b4d..4ec2a8c91 100644 --- a/backend/lib-calendar/gradle.lockfile +++ b/backend/lib-calendar/gradle.lockfile @@ -17,6 +17,7 @@ de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeCla io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-central-repository-api/gradle.lockfile b/backend/lib-central-repository-api/gradle.lockfile index b504a894b..357904f40 100644 --- a/backend/lib-central-repository-api/gradle.lockfile +++ b/backend/lib-central-repository-api/gradle.lockfile @@ -14,6 +14,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspat io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-central-repository-client/gradle.lockfile b/backend/lib-central-repository-client/gradle.lockfile index 393076240..56ed9293f 100644 --- a/backend/lib-central-repository-client/gradle.lockfile +++ b/backend/lib-central-repository-client/gradle.lockfile @@ -17,6 +17,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspat io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-commons/build.gradle b/backend/lib-commons/build.gradle index 16abd18c2..8418315c6 100644 --- a/backend/lib-commons/build.gradle +++ b/backend/lib-commons/build.gradle @@ -1,3 +1,7 @@ plugins { id "eshg.java-lib" } + +dependencies { + implementation("io.swagger.core.v3:swagger-annotations:latest.release") +} diff --git a/backend/lib-commons/gradle.lockfile b/backend/lib-commons/gradle.lockfile index 0dccf5855..2b37c9fe2 100644 --- a/backend/lib-commons/gradle.lockfile +++ b/backend/lib-commons/gradle.lockfile @@ -7,6 +7,7 @@ com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.13.4=testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=testCompileClasspath,testRuntimeClasspath @@ -58,4 +59,4 @@ org.springframework:spring-jcl:6.1.13=testCompileClasspath,testRuntimeClasspath org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath org.xmlunit:xmlunit-core:2.9.1=testCompileClasspath,testRuntimeClasspath org.yaml:snakeyaml:2.2=testCompileClasspath,testRuntimeClasspath -empty=annotationProcessor,compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath +empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath diff --git a/backend/base/src/main/java/de/eshg/base/util/CountryCode.java b/backend/lib-commons/src/main/java/de/eshg/lib/common/CountryCode.java similarity index 79% rename from backend/base/src/main/java/de/eshg/base/util/CountryCode.java rename to backend/lib-commons/src/main/java/de/eshg/lib/common/CountryCode.java index 8b5441f58..3006b9c0b 100644 --- a/backend/base/src/main/java/de/eshg/base/util/CountryCode.java +++ b/backend/lib-commons/src/main/java/de/eshg/lib/common/CountryCode.java @@ -3,9 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.base.util; +package de.eshg.lib.common; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Locale; // ISO 3166-1 +@Schema(name = "CountryCode", description = "List of country codes in ISO 3166-1 alpha-2 format.") public enum CountryCode { AD, AE, @@ -255,5 +259,10 @@ public enum CountryCode { YT, ZA, ZM, - ZW, + ZW; + + public static String getCountryName(CountryCode countryCode) { + Locale locale = new Locale.Builder().setRegion(countryCode.name()).setLanguage("DE").build(); + return locale.getDisplayCountry(locale); + } } diff --git a/backend/lib-document-generator/gradle.lockfile b/backend/lib-document-generator/gradle.lockfile index 74b0acaf4..ff371130e 100644 --- a/backend/lib-document-generator/gradle.lockfile +++ b/backend/lib-document-generator/gradle.lockfile @@ -29,6 +29,7 @@ io.github.openhtmltopdf:openhtmltopdf-svg-support:1.1.22=compileClasspath,produc io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath diff --git a/backend/lib-editor/gradle.lockfile b/backend/lib-editor/gradle.lockfile index 90d724253..b31d50e08 100644 --- a/backend/lib-editor/gradle.lockfile +++ b/backend/lib-editor/gradle.lockfile @@ -53,6 +53,7 @@ io.prometheus:prometheus-metrics-model:1.2.1=productionRuntimeClasspath,runtimeC io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-four-eyes-principle-api/gradle.lockfile b/backend/lib-four-eyes-principle-api/gradle.lockfile index df0329dc5..2653d764a 100644 --- a/backend/lib-four-eyes-principle-api/gradle.lockfile +++ b/backend/lib-four-eyes-principle-api/gradle.lockfile @@ -14,6 +14,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspat io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-four-eyes-principle/gradle.lockfile b/backend/lib-four-eyes-principle/gradle.lockfile index 5012517da..1afdf959b 100644 --- a/backend/lib-four-eyes-principle/gradle.lockfile +++ b/backend/lib-four-eyes-principle/gradle.lockfile @@ -64,6 +64,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/EmployeePermissionRole.java b/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/EmployeePermissionRole.java index 7a5cd6456..3bce32246 100644 --- a/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/EmployeePermissionRole.java +++ b/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/EmployeePermissionRole.java @@ -36,6 +36,8 @@ public enum EmployeePermissionRole implements PermissionRole { LEADER_KEYCLOAK_NAME, LEADER_DESCRIPTION.formatted("Medizinalkartei"), Module.MEDICAL_REGISTRY), + OPEN_DATA_LEADER( + LEADER_KEYCLOAK_NAME, LEADER_DESCRIPTION.formatted("Open Data"), Module.OPEN_DATA), BASE_PERSONS_READ( READ_PERMISSION_TEMPLATE.formatted("Personen (Zentralkartei)"), @@ -280,6 +282,8 @@ public enum EmployeePermissionRole implements PermissionRole { BASE_FACILITIES_READ, BASE_FACILITIES_WRITE), + OPEN_DATA_ADMIN(ADMIN_KEYCLOAK_NAME.formatted("Open Data"), Module.OPEN_DATA), + CHAT_MANAGEMENT_WRITE( READ_AND_WRITE_PERMISSION_TEMPLATE.formatted("Chat - Management"), Module.CHAT_MANAGEMENT); @@ -332,7 +336,8 @@ public enum EmployeePermissionRole implements PermissionRole { ARCHIVE("Archiv"), AUDIT_LOG_SERVICE("Audit-Log-Service"), STI_PROTECTION("HIV-STI-Service"), - MEDICAL_REGISTRY("Medizinalkartei"); + MEDICAL_REGISTRY("Medizinalkartei"), + OPEN_DATA("Open Data"); private final String displayName; diff --git a/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/EmployeeTestUser.java b/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/EmployeeTestUser.java index ba768d8ae..13861e36b 100644 --- a/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/EmployeeTestUser.java +++ b/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/EmployeeTestUser.java @@ -184,7 +184,13 @@ public enum EmployeeTestUser implements KeycloakUser { "Jürgen", "Klinsmann", List.of(ModuleMemberGroup.MEDICAL_REGISTRY, ModuleLeaderGroup.MEDICAL_REGISTRY)), - ; + OPEN_DATA_DUMMY( + "open_data_dummy_user", + "+49 555 123 470", + "password", + "Daniel", + "Schultze", + List.of(ModuleMemberGroup.OPEN_DATA)); private final String username; private final String email; diff --git a/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/ModuleLeaderGroup.java b/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/ModuleLeaderGroup.java index cba81a858..b27707bec 100644 --- a/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/ModuleLeaderGroup.java +++ b/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/ModuleLeaderGroup.java @@ -46,7 +46,8 @@ public enum ModuleLeaderGroup implements KeycloakGroup { MEDICAL_REGISTRY( "Medizinalkartei", ModuleMemberGroup.MEDICAL_REGISTRY, - EmployeePermissionRole.MEDICAL_REGISTRY_LEADER); + EmployeePermissionRole.MEDICAL_REGISTRY_LEADER), + OPEN_DATA("Open Data", ModuleMemberGroup.OPEN_DATA, EmployeePermissionRole.OPEN_DATA_LEADER); private final String keycloakNameWithoutPrefix; private final List<EmployeePermissionRole> roles; diff --git a/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/ModuleMemberGroup.java b/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/ModuleMemberGroup.java index 62000e03f..57de29806 100644 --- a/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/ModuleMemberGroup.java +++ b/backend/lib-keycloak/src/main/java/de/eshg/lib/keycloak/ModuleMemberGroup.java @@ -53,7 +53,8 @@ public enum ModuleMemberGroup implements KeycloakGroup { MEDICAL_REGISTRY( "Medizinalkartei", getStandardRoles(), - List.of(EmployeePermissionRole.MEDICAL_REGISTRY_ADMIN)); + List.of(EmployeePermissionRole.MEDICAL_REGISTRY_ADMIN)), + OPEN_DATA("Open Data", EmployeePermissionRole.OPEN_DATA_ADMIN); private final String keycloakNameWithoutPrefix; private final List<EmployeePermissionRole> roles; diff --git a/backend/lib-lsd-api/gradle.lockfile b/backend/lib-lsd-api/gradle.lockfile index dfa6f8c8c..0453e1b09 100644 --- a/backend/lib-lsd-api/gradle.lockfile +++ b/backend/lib-lsd-api/gradle.lockfile @@ -55,6 +55,7 @@ io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClassp io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.quarkus:quarkus-junit4-mock:3.15.1=testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.mail:jakarta.mail-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath diff --git a/backend/lib-mutex/gradle.lockfile b/backend/lib-mutex/gradle.lockfile index a8cea4da4..bab676264 100644 --- a/backend/lib-mutex/gradle.lockfile +++ b/backend/lib-mutex/gradle.lockfile @@ -60,6 +60,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-notification-api/gradle.lockfile b/backend/lib-notification-api/gradle.lockfile index 78dc9a608..9297ba485 100644 --- a/backend/lib-notification-api/gradle.lockfile +++ b/backend/lib-notification-api/gradle.lockfile @@ -14,6 +14,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspat io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-notification/gradle.lockfile b/backend/lib-notification/gradle.lockfile index ed493a1bd..54cd7b739 100644 --- a/backend/lib-notification/gradle.lockfile +++ b/backend/lib-notification/gradle.lockfile @@ -61,6 +61,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-procedures-api/gradle.lockfile b/backend/lib-procedures-api/gradle.lockfile index 8a642f75c..f6f39e059 100644 --- a/backend/lib-procedures-api/gradle.lockfile +++ b/backend/lib-procedures-api/gradle.lockfile @@ -15,6 +15,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspat io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/model/PersonTypeDto.java b/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/model/PersonTypeDto.java index b95b74120..1f5d7715c 100644 --- a/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/model/PersonTypeDto.java +++ b/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/model/PersonTypeDto.java @@ -10,5 +10,6 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(name = "PersonType") public enum PersonTypeDto { PATIENT, - PARENT + PARENT, + PROFESSIONAL } diff --git a/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/model/ProcedureTypeDto.java b/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/model/ProcedureTypeDto.java index f5d67b10f..0b1569972 100644 --- a/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/model/ProcedureTypeDto.java +++ b/backend/lib-procedures-api/src/main/java/de/eshg/lib/procedure/model/ProcedureTypeDto.java @@ -18,4 +18,7 @@ public enum ProcedureTypeDto { TM_VACCINATION_CONSULTATION, MEASLES_PROTECTION, STI_PROTECTION, + MEDICAL_REGISTRY_ENTRY, + MEDICAL_REGISTRY_CITIZEN_DRAFT, + MEDICAL_REGISTRY_EMPLOYEE_DRAFT, } diff --git a/backend/lib-procedures/build.gradle b/backend/lib-procedures/build.gradle index 1bc60af57..0914d09fe 100644 --- a/backend/lib-procedures/build.gradle +++ b/backend/lib-procedures/build.gradle @@ -17,6 +17,7 @@ dependencies { implementation project(':business-module-commons') implementation project(':business-module-persistence-commons') implementation project(':rest-oauth-client-commons') + implementation project(':file-commons') implementation 'jakarta.persistence:jakarta.persistence-api' implementation 'org.springframework:spring-context' diff --git a/backend/lib-procedures/gradle.lockfile b/backend/lib-procedures/gradle.lockfile index 93f54f5a4..b07b4042e 100644 --- a/backend/lib-procedures/gradle.lockfile +++ b/backend/lib-procedures/gradle.lockfile @@ -16,11 +16,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -28,7 +28,7 @@ com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.h2database:h2:2.2.224=testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -42,24 +42,24 @@ com.sun.istack:istack-commons-tools:4.1.2=xjc com.sun.xml.bind.external:relaxng-datatype:4.0.5=xjc com.sun.xml.bind.external:rngom:4.0.5=xjc com.sun.xml.dtd-parser:dtd-parser:1.5.1=xjc -com.tngtech.archunit:archunit-junit5-api:1.3.0=testRuntimeClasspath -com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=testRuntimeClasspath -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-codec:commons-codec:1.16.1=testCompileClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.16.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.15.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-jakarta9:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -73,6 +73,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,xjc @@ -83,18 +84,18 @@ jakarta.transaction:jakarta.transaction-api:2.0.1=annotationProcessor,compileCla jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,xjc javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.14.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.14.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.26.1=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.26.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.12.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -117,7 +118,7 @@ org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=compileClasspath,producti org.apache.tomcat:tomcat-annotations-api:10.1.30=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcmail-jdk18on:1.78.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.78.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -132,8 +133,8 @@ org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,compileClasspath,produ org.glassfish.jaxb:jaxb-xjc:4.0.5=xjc org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,xjc org.glassfish.jaxb:xsom:4.0.5=xjc -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -146,15 +147,15 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath @@ -162,12 +163,12 @@ org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspa org.mozilla:rhino:1.7.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.postgresql:postgresql:42.7.4=testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -177,7 +178,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -193,7 +194,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -216,7 +217,7 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -224,7 +225,7 @@ org.testcontainers:database-commons:1.19.8=testCompileClasspath,testRuntimeClass org.testcontainers:jdbc:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:junit-jupiter:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testCompileClasspath,testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:core-jakarta:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:feature-reporting-jakarta:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:metadata-fixer-jakarta:1.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-procedures/openApi.yaml b/backend/lib-procedures/openApi.yaml index d7056ad38..f272ccca6 100644 --- a/backend/lib-procedures/openApi.yaml +++ b/backend/lib-procedures/openApi.yaml @@ -3283,6 +3283,7 @@ components: enum: - PATIENT - PARENT + - PROFESSIONAL Population: type: object properties: @@ -3430,6 +3431,9 @@ components: - TM_VACCINATION_CONSULTATION - MEASLES_PROTECTION - STI_PROTECTION + - MEDICAL_REGISTRY_ENTRY + - MEDICAL_REGISTRY_CITIZEN_DRAFT + - MEDICAL_REGISTRY_EMPLOYEE_DRAFT ProcedureWithDuration: type: object properties: diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/File.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/File.java index 7d188ae23..bcf72b3df 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/File.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/File.java @@ -63,7 +63,7 @@ public abstract class File extends BaseEntityWithExternalId implements LockableE @DataSensitivity(SensitivityLevel.PUBLIC) @JdbcType(PostgreSQLEnumJdbcType.class) @Column(nullable = false) - private FileType fileType; + private ProcedureFileType fileType; @DataSensitivity(SensitivityLevel.PUBLIC) private int fileSizeBytes; @@ -127,11 +127,11 @@ public abstract class File extends BaseEntityWithExternalId implements LockableE this.fileName = fileName; } - public FileType getFileType() { + public ProcedureFileType getFileType() { return fileType; } - public void setFileType(FileType fileType) { + public void setFileType(ProcedureFileType fileType) { this.fileType = fileType; } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileAware.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileAware.java index 60ff1d2bf..1515086fa 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileAware.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileAware.java @@ -25,5 +25,5 @@ public interface FileAware { void setMessageText(String messageText); - boolean supportsUpload(FileType fileType); + boolean supportsUpload(ProcedureFileType fileType); } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileTypeGroup.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileTypeGroup.java index 2d496a54b..50ebbbdab 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileTypeGroup.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/FileTypeGroup.java @@ -5,10 +5,10 @@ package de.eshg.lib.procedure.domain.model; -import static de.eshg.lib.procedure.domain.model.FileType.EML; -import static de.eshg.lib.procedure.domain.model.FileType.JPEG; -import static de.eshg.lib.procedure.domain.model.FileType.PDF; -import static de.eshg.lib.procedure.domain.model.FileType.PNG; +import static de.eshg.lib.procedure.domain.model.ProcedureFileType.EML; +import static de.eshg.lib.procedure.domain.model.ProcedureFileType.JPEG; +import static de.eshg.lib.procedure.domain.model.ProcedureFileType.PDF; +import static de.eshg.lib.procedure.domain.model.ProcedureFileType.PNG; import java.util.Arrays; import java.util.EnumSet; @@ -19,13 +19,13 @@ public enum FileTypeGroup { EMAIL(EML), IMAGE(JPEG, PNG); - private final EnumSet<FileType> fileTypes; + private final EnumSet<ProcedureFileType> fileTypes; - FileTypeGroup(FileType... fileTypes) { + FileTypeGroup(ProcedureFileType... fileTypes) { this.fileTypes = EnumSet.copyOf(Arrays.asList(fileTypes)); } - public Set<FileType> getFileTypes() { + public Set<ProcedureFileType> getFileTypes() { return fileTypes; } } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/InboxProgressEntry.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/InboxProgressEntry.java index 1d8afd133..127da4687 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/InboxProgressEntry.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/InboxProgressEntry.java @@ -75,7 +75,7 @@ public class InboxProgressEntry extends BaseEntityWithExternalId implements File } @Override - public boolean supportsUpload(FileType fileType) { + public boolean supportsUpload(ProcedureFileType fileType) { return getInboxProgressEntryType().supports(fileType); } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/InboxProgressEntryType.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/InboxProgressEntryType.java index 9f9202f3a..d8ae85335 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/InboxProgressEntryType.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/InboxProgressEntryType.java @@ -16,7 +16,7 @@ public enum InboxProgressEntryType { this.fileTypeGroup = fileTypeGroup; } - boolean supports(FileType fileType) { + boolean supports(ProcedureFileType fileType) { if (fileTypeGroup == null) { return false; } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ManualProgressEntry.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ManualProgressEntry.java index 9c618760f..ca5843af7 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ManualProgressEntry.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ManualProgressEntry.java @@ -95,7 +95,7 @@ public class ManualProgressEntry extends ProgressEntry implements FileAware, Loc } @Override - public boolean supportsUpload(FileType fileType) { + public boolean supportsUpload(ProcedureFileType fileType) { return getManualProgressEntryType().supports(fileType); } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ManualProgressEntryType.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ManualProgressEntryType.java index 63598ef2a..d7e62dcce 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ManualProgressEntryType.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ManualProgressEntryType.java @@ -27,7 +27,7 @@ public enum ManualProgressEntryType { } } - boolean supports(FileType fileType) { + boolean supports(ProcedureFileType fileType) { if (fileTypeGroup == null) { return false; } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/PersonType.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/PersonType.java index 9453dccae..64b4c1dc7 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/PersonType.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/PersonType.java @@ -7,5 +7,6 @@ package de.eshg.lib.procedure.domain.model; public enum PersonType { PATIENT, - PARENT + PARENT, + PROFESSIONAL } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcedureFileType.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcedureFileType.java new file mode 100644 index 000000000..6c2228750 --- /dev/null +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcedureFileType.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.procedure.domain.model; + +import de.eshg.file.common.FileType; + +public enum ProcedureFileType { + JPEG(FileType.JPEG), + PNG(FileType.PNG), + PDF(FileType.PDF), + EML(FileType.EML); + + private final FileType fileType; + + ProcedureFileType(FileType fileType) { + this.fileType = fileType; + } + + public FileType getCommonFileType() { + return fileType; + } +} diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcedureType.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcedureType.java index a5687accd..f0383541a 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcedureType.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcedureType.java @@ -15,4 +15,7 @@ public enum ProcedureType { TM_VACCINATION_CONSULTATION, MEASLES_PROTECTION, STI_PROTECTION, + MEDICAL_REGISTRY_ENTRY, + MEDICAL_REGISTRY_CITIZEN_DRAFT, + MEDICAL_REGISTRY_EMPLOYEE_DRAFT, } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcessedInboxProgressEntry.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcessedInboxProgressEntry.java index 4241ee5d2..b25f53de5 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcessedInboxProgressEntry.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/ProcessedInboxProgressEntry.java @@ -75,7 +75,7 @@ public class ProcessedInboxProgressEntry extends ProgressEntry implements FileAw } @Override - public boolean supportsUpload(FileType fileType) { + public boolean supportsUpload(ProcedureFileType fileType) { return false; } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/RelatedFacility.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/RelatedFacility.java index 172de65ae..1458a5111 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/RelatedFacility.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/RelatedFacility.java @@ -34,6 +34,12 @@ public abstract class RelatedFacility<ProcedureT extends Procedure<ProcedureT, ? @Column(nullable = false) private FacilityType facilityType; + public RelatedFacility() {} + + protected RelatedFacility(FacilityType facilityType) { + this.facilityType = facilityType; + } + public ProcedureT getProcedure() { return procedure; } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/RelatedPerson.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/RelatedPerson.java index 826d28918..8987cee69 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/RelatedPerson.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/model/RelatedPerson.java @@ -34,6 +34,12 @@ public abstract class RelatedPerson<ProcedureT extends Procedure<ProcedureT, ?, @DataSensitivity(SensitivityLevel.PUBLIC) private PersonType personType; + public RelatedPerson() {} + + protected RelatedPerson(PersonType personType) { + this.personType = personType; + } + public ProcedureT getProcedure() { return procedure; } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/repository/ManualProgressEntryRepository.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/repository/ManualProgressEntryRepository.java index 4ed04b1e6..68e27d23e 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/repository/ManualProgressEntryRepository.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/repository/ManualProgressEntryRepository.java @@ -5,9 +5,9 @@ package de.eshg.lib.procedure.domain.repository; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.KeyDocumentType; import de.eshg.lib.procedure.domain.model.ManualProgressEntry; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -49,7 +49,7 @@ public interface ManualProgressEntryRepository @Param("keyDocumentType") KeyDocumentType keyDocumentType); boolean existsByProcedureIdAndKeyDocumentTypeAndFileFileTypeNot( - Long procedureId, KeyDocumentType keyDocumentType, FileType fileType); + Long procedureId, KeyDocumentType keyDocumentType, ProcedureFileType fileType); Optional<ManualProgressEntry> findByExternalId(UUID externalId); } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/serialization/SerializationObjectMapperConfigurer.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/serialization/SerializationObjectMapperConfigurer.java new file mode 100644 index 000000000..20398c8ee --- /dev/null +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/serialization/SerializationObjectMapperConfigurer.java @@ -0,0 +1,17 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.procedure.domain.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/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/serialization/SerializationService.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/serialization/SerializationService.java index 334ca3d8f..2c8e678e6 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/serialization/SerializationService.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/domain/serialization/SerializationService.java @@ -14,6 +14,7 @@ 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; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -22,7 +23,9 @@ import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module.Feature; import de.eshg.domain.model.EntityWithExternalId; 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; @@ -33,16 +36,21 @@ import org.springframework.stereotype.Component; public class SerializationService { private final ObjectMapper jsonObjectMapper; + private final Optional<SerializationObjectMapperConfigurer> serializationObjectMapperConfigurer; - public SerializationService(ObjectMapper objectMapper) { + public SerializationService( + ObjectMapper objectMapper, + Optional<SerializationObjectMapperConfigurer> serializationObjectMapperConfigurer) { jsonObjectMapper = objectMapper .copy() .registerModule(new Hibernate6Module().enable(Feature.FORCE_LAZY_LOADING)) + .registerModule(new SimpleModule().addSerializer(Blob.class, new SqlBlobSerializer())) .enable(SerializationFeature.INDENT_OUTPUT) .setVisibility(PropertyAccessor.ALL, Visibility.NONE) .setVisibility(PropertyAccessor.FIELD, Visibility.ANY) .addMixIn(GenericEntity.class, GenericEntityMixin.class); + this.serializationObjectMapperConfigurer = serializationObjectMapperConfigurer; } public String toJson(GenericEntity<?> entity) { @@ -72,8 +80,12 @@ public class SerializationService { zipFileWrapper::addEntry, zipFileWrapper::getCollisionFreeFileName); ObjectMapper objectMapper = createObjectMapperWithSerializer(fileContentSerializer); + serializationObjectMapperConfigurer.ifPresent( + p -> + p.configure( + objectMapper, zipFileWrapper::addEntry, zipFileWrapper::getCollisionFreeFileName)); - JsonNode jsonNode = objectMapper.valueToTree(entity); + JsonNode jsonNode = toJsonNode(entity, objectMapper); String jsonNodeAsCsv = jsonNodeToCsv(entity.getClass().getSimpleName(), jsonNode); zipFileWrapper.addEntry(dataFileBaseName + ".csv", jsonNodeAsCsv.getBytes()); @@ -88,7 +100,7 @@ public class SerializationService { return switch (node) { case ArrayNode arrayNode -> jsonArrayToCsv(baseKey, arrayNode); case ObjectNode objectNode -> jsonObjectToCsv(baseKey, objectNode); - default -> formatAsCsvLine(baseKey, node.asText()); + default -> formatAsCsvLine(baseKey, node); }; } @@ -105,8 +117,29 @@ public class SerializationService { .collect(Collectors.joining(System.lineSeparator())); } - private String formatAsCsvLine(String key, String value) { - return key + "," + value; + private String formatAsCsvLine(String key, JsonNode node) { + return key + "," + formatAsCsvValue(node); + } + + private String formatAsCsvValue(JsonNode node) { + if (node.isTextual()) { + return node.asText().replace("\n", "\\n"); + } else { + return node.asText(); + } + } + + private static JsonNode toJsonNode(EntityWithExternalId entity, ObjectMapper objectMapper) { + try { + // Workaround for https://github.com/FasterXML/jackson-databind/issues/2140 + // Cannot simply do: return objectMapper.valueToTree(entity); + // Must instead write once to String and then to JsonNode. + return objectMapper.readTree(objectMapper.writeValueAsString(entity)); + } catch (JsonProcessingException e) { + throw new UncheckedIOException( + "Error during serializing object of type " + entity.getClass().getTypeName() + " as json", + e); + } } @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/EmlParser.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/EmlParser.java index 11eead180..2bf6528f2 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/EmlParser.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/EmlParser.java @@ -5,19 +5,20 @@ package de.eshg.lib.procedure.file; -import static de.eshg.lib.procedure.domain.model.FileType.EML; -import static de.eshg.lib.procedure.domain.model.FileType.JPEG; -import static de.eshg.lib.procedure.domain.model.FileType.PDF; -import static de.eshg.lib.procedure.domain.model.FileType.PNG; +import static de.eshg.file.common.FileType.JPEG; +import static de.eshg.file.common.FileType.PDF; +import static de.eshg.file.common.FileType.PNG; import static jakarta.mail.Part.ATTACHMENT; import static org.springframework.http.MediaType.TEXT_HTML_VALUE; import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; +import de.eshg.file.common.FileType; +import de.eshg.file.common.FileTypeDetector; import de.eshg.lib.procedure.domain.model.File; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.ImageMetaData; import de.eshg.lib.procedure.domain.model.MailMetaData; import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.rest.service.error.BadRequestException; import jakarta.mail.Address; import jakarta.mail.BodyPart; @@ -55,7 +56,7 @@ class EmlParser { Message message = new FixedMessageIdMimeMessage(session, inputStream); ParsedMail parsedMail = new ParsedMail(); - parsedMail.setFileType(EML); + parsedMail.setFileType(ProcedureFileType.EML); parsedMail.setSubject(message.getSubject()); parsedMail.setMessageText(extractMessageText(message)); parsedMail.setMetaData(extractMetaData(message)); @@ -166,7 +167,7 @@ class EmlParser { File file = createFile( bodyPart.getFileName(), - parseFileType(bodyPart), + FileTypeMapper.mapToProcedureFileType(parseFileType(bodyPart)), parseFileContent(bodyPart), deletable); files.add(file); @@ -202,7 +203,7 @@ class EmlParser { } private static File createFile( - String fileName, FileType fileType, byte[] fileContent, boolean deletable) + String fileName, ProcedureFileType fileType, byte[] fileContent, boolean deletable) throws IOException { return switch (fileType) { case JPEG, PNG -> { diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileController.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileController.java index 69cfebce0..b8efd8df3 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileController.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileController.java @@ -89,7 +89,7 @@ public class FileController implements FileApi { .filename(file.getFileName(), StandardCharsets.UTF_8) .build(); return ResponseEntity.ok() - .contentType(file.getFileType().getMediaType()) + .contentType(file.getFileType().getCommonFileType().getMediaType()) .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) .body(fileContent.getContent()); } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileExtensionEnricher.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileExtensionEnricher.java index e8f57b760..f5002d84b 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileExtensionEnricher.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileExtensionEnricher.java @@ -5,7 +5,7 @@ package de.eshg.lib.procedure.file; -import de.eshg.lib.procedure.domain.model.FileType; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -16,19 +16,19 @@ class FileExtensionEnricher { private static final Pattern FILE_EXTENSION_REGEX_PATTERN = Pattern.compile("^.+\\.([A-Za-z0-9]+)$"); - static String enrich(String fileName, FileType fileType) { + static String enrich(String fileName, ProcedureFileType fileType) { if (shouldEnrich(fileName, fileType)) { - return fileName + "." + fileType.getDefaultFileExtension().getValue(); + return fileName + "." + fileType.getCommonFileType().getDefaultFileExtension().getValue(); } return fileName; } - private static boolean shouldEnrich(String fileName, FileType fileType) { + private static boolean shouldEnrich(String fileName, ProcedureFileType fileType) { Matcher fileExtensionMatcher = getFileExtensionRegexPattern().matcher(fileName); return !hasFileExtension(fileExtensionMatcher) - || !fileType.hasValidFileExtension(fileExtensionMatcher.group(1)); + || !fileType.getCommonFileType().hasValidFileExtension(fileExtensionMatcher.group(1)); } private static boolean hasFileExtension(Matcher fileExtensionMatcher) { diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileFactory.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileFactory.java index 000d37e33..c2f70f64b 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileFactory.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileFactory.java @@ -7,12 +7,12 @@ package de.eshg.lib.procedure.file; import de.eshg.lib.procedure.domain.model.File; import de.eshg.lib.procedure.domain.model.FileContent; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.Image; import de.eshg.lib.procedure.domain.model.ImageMetaData; import de.eshg.lib.procedure.domain.model.Mail; import de.eshg.lib.procedure.domain.model.Pdf; import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; public class FileFactory { @@ -20,7 +20,7 @@ public class FileFactory { public static Image createImageWithMetaData( String fileName, - FileType fileType, + ProcedureFileType fileType, byte[] content, ImageMetaData imageMetaData, boolean deletable) { @@ -32,7 +32,7 @@ public class FileFactory { public static Pdf createPdfWithMetaData( String fileName, - FileType fileType, + ProcedureFileType fileType, byte[] content, PdfMetaData pdfMetaData, boolean deletable) { @@ -61,7 +61,7 @@ public class FileFactory { } private static void setFileProperties( - File file, String fileName, FileType fileType, byte[] content, boolean deletable) { + File file, String fileName, ProcedureFileType fileType, byte[] content, boolean deletable) { FileContent fileContent = new FileContent(); fileContent.setContent(content); diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileStorageService.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileStorageService.java index eb874e21e..dbb36e7c2 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileStorageService.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileStorageService.java @@ -12,7 +12,6 @@ import de.eshg.lib.procedure.domain.model.FileAware; import de.eshg.lib.procedure.domain.model.FileContent; import de.eshg.lib.procedure.domain.model.FileDeletionApprovalRequest; import de.eshg.lib.procedure.domain.model.FileDeletionApprovalRequestNotification; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.Image; import de.eshg.lib.procedure.domain.model.ImageMetaData; import de.eshg.lib.procedure.domain.model.Mail; @@ -21,6 +20,7 @@ import de.eshg.lib.procedure.domain.model.MetaData; import de.eshg.lib.procedure.domain.model.Pdf; import de.eshg.lib.procedure.domain.model.PdfMetaData; import de.eshg.lib.procedure.domain.model.Procedure; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.lib.procedure.domain.model.ProcedureStatus; import de.eshg.lib.procedure.domain.repository.FileRepository; import de.eshg.lib.procedure.domain.repository.ProcedureRepository; @@ -118,7 +118,7 @@ public class FileStorageService { } private void auditLogMetaDataModification( - Set<String> updatedFields, FileType fileType, UUID externalId) { + Set<String> updatedFields, ProcedureFileType fileType, UUID externalId) { auditLogger.log( "Dokumentenmanagement", "Änderungen Metadaten", diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileTypeMapper.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileTypeMapper.java new file mode 100644 index 000000000..e97e5231a --- /dev/null +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileTypeMapper.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.procedure.file; + +import de.eshg.file.common.FileType; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; +import de.eshg.rest.service.error.BadRequestException; + +public final class FileTypeMapper { + private FileTypeMapper() {} + + public static ProcedureFileType mapToProcedureFileType(FileType fileType) { + return switch (fileType) { + case JPEG -> ProcedureFileType.JPEG; + case PNG -> ProcedureFileType.PNG; + case PDF -> ProcedureFileType.PDF; + case EML -> ProcedureFileType.EML; + default -> throw new BadRequestException("File type is not supported"); + }; + } +} diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileUploadService.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileUploadService.java index a0ddbb0a2..0e00079a7 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileUploadService.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileUploadService.java @@ -8,6 +8,8 @@ package de.eshg.lib.procedure.file; import static de.eshg.lib.procedure.file.FileFactory.createImageWithMetaData; import static de.eshg.lib.procedure.file.FileFactory.createPdfWithMetaData; +import de.eshg.file.common.FileTypeDetector; +import de.eshg.file.common.PdfAConformanceValidator; import de.eshg.lib.procedure.domain.model.*; import de.eshg.lib.procedure.model.FileMetaDataDto; import java.io.IOException; @@ -29,7 +31,9 @@ public class FileUploadService { public void handleFile(FileAware fileAware, MultipartFile file, FileMetaDataDto fileMetaData) throws IOException { - FileType fileType = FileTypeDetector.getSupportedFileTypeOrThrow(file.getBytes()); + ProcedureFileType fileType = + FileTypeMapper.mapToProcedureFileType( + FileTypeDetector.getSupportedFileTypeOrThrow(file.getBytes())); fileUploadValidator.validateFileAwareSupportsFileUpload(fileAware, fileType); switch (fileType) { @@ -40,7 +44,10 @@ public class FileUploadService { } private void handleImage( - FileAware fileAware, FileType fileType, MultipartFile file, FileMetaDataDto fileMetaData) + FileAware fileAware, + ProcedureFileType fileType, + MultipartFile file, + FileMetaDataDto fileMetaData) throws IOException { byte[] fileContent = file.getBytes(); String fileName = FileExtensionEnricher.enrich(file.getOriginalFilename(), fileType); @@ -57,7 +64,10 @@ public class FileUploadService { } private void handlePdf( - FileAware fileAware, FileType fileType, MultipartFile file, FileMetaDataDto fileMetaData) + FileAware fileAware, + ProcedureFileType fileType, + MultipartFile file, + FileMetaDataDto fileMetaData) throws IOException { byte[] fileContent = file.getBytes(); String fileName = FileExtensionEnricher.enrich(file.getOriginalFilename(), fileType); @@ -76,7 +86,10 @@ public class FileUploadService { } private void handleMailEml( - FileAware fileAware, FileType fileType, MultipartFile file, FileMetaDataDto fileMetaData) + FileAware fileAware, + ProcedureFileType fileType, + MultipartFile file, + FileMetaDataDto fileMetaData) throws IOException { byte[] fileContent = file.getBytes(); String fileName = FileExtensionEnricher.enrich(file.getOriginalFilename(), fileType); diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileUploadValidator.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileUploadValidator.java index 999362def..cf6cbf5af 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileUploadValidator.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/FileUploadValidator.java @@ -5,12 +5,12 @@ package de.eshg.lib.procedure.file; -import static de.eshg.lib.procedure.domain.model.FileType.EML; +import static de.eshg.lib.procedure.domain.model.ProcedureFileType.EML; import de.eshg.lib.procedure.domain.model.FileAware; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.KeyDocumentType; import de.eshg.lib.procedure.domain.model.ManualProgressEntry; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.lib.procedure.domain.repository.ManualProgressEntryRepository; import de.eshg.rest.service.error.BadRequestException; import java.util.Objects; @@ -23,7 +23,7 @@ public class FileUploadValidator { this.manualProgressEntryRepository = manualProgressEntryRepository; } - void validateFileAwareSupportsFileUpload(FileAware fileAware, FileType fileType) { + void validateFileAwareSupportsFileUpload(FileAware fileAware, ProcedureFileType fileType) { validateProgressEntryTypeSupportsFileType(fileAware, fileType); validateFileAwareSubjectAndMessageTextIsNull(fileAware, fileType); @@ -32,7 +32,8 @@ public class FileUploadValidator { } } - private void validateProgressEntryTypeSupportsFileType(FileAware fileAware, FileType fileType) { + private void validateProgressEntryTypeSupportsFileType( + FileAware fileAware, ProcedureFileType fileType) { if (!fileAware.supportsUpload(fileType)) { throw new BadRequestException( "File upload not supported for file type `%s`.".formatted(fileType)); @@ -40,7 +41,7 @@ public class FileUploadValidator { } private void validateFileAwareSubjectAndMessageTextIsNull( - FileAware fileAware, FileType fileType) { + FileAware fileAware, ProcedureFileType fileType) { if (EML.equals(fileType) && hasFileAwareSubjectOrMessageText(fileAware)) { throw new BadRequestException( "Subject and message text are parsed from eml and should not be given"); @@ -52,7 +53,7 @@ public class FileUploadValidator { } private void validateKeyDocumentsUniformFileTypes( - ManualProgressEntry manualProgressEntry, FileType fileType) { + ManualProgressEntry manualProgressEntry, ProcedureFileType fileType) { KeyDocumentType keyDocumentType = manualProgressEntry.getKeyDocumentType(); if (keyDocumentType == null) { diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/ParsedMail.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/ParsedMail.java index 052d338fd..d31455a95 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/ParsedMail.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/file/ParsedMail.java @@ -6,14 +6,14 @@ package de.eshg.lib.procedure.file; import de.eshg.lib.procedure.domain.model.File; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.MailMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import java.util.ArrayList; import java.util.List; class ParsedMail { private String fileName; - private FileType fileType; + private ProcedureFileType fileType; private byte[] content; private String subject; private String messageText; @@ -30,11 +30,11 @@ class ParsedMail { this.fileName = fileName; } - FileType getFileType() { + ProcedureFileType getFileType() { return fileType; } - void setFileType(FileType fileType) { + void setFileType(ProcedureFileType fileType) { this.fileType = fileType; } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/housekeeping/archiving/ArchivingController.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/housekeeping/archiving/ArchivingController.java index 75fc8a86e..b33f32030 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/housekeeping/archiving/ArchivingController.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/housekeeping/archiving/ArchivingController.java @@ -22,11 +22,11 @@ import static org.springframework.data.domain.PageRequest.ofSize; import static org.springframework.data.jpa.domain.Specification.where; import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; -import de.base.rest.CustomMediaTypes; import de.cronn.commons.lang.StreamUtil; import de.eshg.base.util.CollectionUtils; import de.eshg.domain.model.EntityWithExternalId; import de.eshg.domain.model.SequencedBaseEntityWithExternalId_; +import de.eshg.file.common.CustomMediaTypes; import de.eshg.lib.procedure.api.ArchivingApi; import de.eshg.lib.procedure.domain.model.ArchivingRelevance; import de.eshg.lib.procedure.domain.model.Procedure; diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/housekeeping/archiving/ArchivingJob.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/housekeeping/archiving/ArchivingJob.java index 9021b15f0..fd4cc2635 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/housekeeping/archiving/ArchivingJob.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/housekeeping/archiving/ArchivingJob.java @@ -24,21 +24,17 @@ import java.util.Set; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service -@ConditionalOnProperty( - name = "de.eshg.lib.procedure.housekeeping.archiving-job-enabled", - havingValue = "true") public class ArchivingJob<ProcedureT extends Procedure<ProcedureT, ?, ?, ?>> { private static final Logger logger = LoggerFactory.getLogger(ArchivingJob.class); private final ArchivingProperties archivingProperties; private final ProcedureRepository<ProcedureT> procedureRepository; private final ArchivableProceduresSpecification<ProcedureT> archivableProceduresSpecification; - private ArchivingJobService<ProcedureT> archivingJobService; + private final ArchivingJobService<ProcedureT> archivingJobService; private final Clock clock; public ArchivingJob( diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/FileMapper.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/FileMapper.java index 3da54e9a6..ba53ea0da 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/FileMapper.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/FileMapper.java @@ -7,7 +7,6 @@ package de.eshg.lib.procedure.mapping; import de.eshg.domain.model.audit.DefaultRevisionEntity; import de.eshg.lib.procedure.domain.model.File; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.Image; import de.eshg.lib.procedure.domain.model.ImageMetaData; import de.eshg.lib.procedure.domain.model.Mail; @@ -15,6 +14,7 @@ import de.eshg.lib.procedure.domain.model.MailMetaData; import de.eshg.lib.procedure.domain.model.MetaData; import de.eshg.lib.procedure.domain.model.Pdf; import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.lib.procedure.model.AbstractFileDto; import de.eshg.lib.procedure.model.ConcreteFileDto; import de.eshg.lib.procedure.model.FileTypeDto; @@ -181,7 +181,7 @@ public final class FileMapper { return metaDataDto; } - private static FileTypeDto toInterfaceType(FileType fileType) { + private static FileTypeDto toInterfaceType(ProcedureFileType fileType) { return switch (fileType) { case JPEG -> FileTypeDto.JPEG; case PNG -> FileTypeDto.PNG; diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/PersonTypeMapper.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/PersonTypeMapper.java index c359224d2..66a8e4074 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/PersonTypeMapper.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/PersonTypeMapper.java @@ -16,6 +16,7 @@ public final class PersonTypeMapper { return switch (personType) { case PATIENT -> PersonTypeDto.PATIENT; case PARENT -> PersonTypeDto.PARENT; + case PROFESSIONAL -> PersonTypeDto.PROFESSIONAL; }; } } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/ProcedureMapper.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/ProcedureMapper.java index e43092286..8eb3ee7c9 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/ProcedureMapper.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/mapping/ProcedureMapper.java @@ -62,6 +62,9 @@ public final class ProcedureMapper { case TM_VACCINATION_CONSULTATION -> ProcedureTypeDto.TM_VACCINATION_CONSULTATION; case MEASLES_PROTECTION -> ProcedureTypeDto.MEASLES_PROTECTION; case STI_PROTECTION -> ProcedureTypeDto.STI_PROTECTION; + case MEDICAL_REGISTRY_ENTRY -> ProcedureTypeDto.MEDICAL_REGISTRY_ENTRY; + case MEDICAL_REGISTRY_CITIZEN_DRAFT -> ProcedureTypeDto.MEDICAL_REGISTRY_CITIZEN_DRAFT; + case MEDICAL_REGISTRY_EMPLOYEE_DRAFT -> ProcedureTypeDto.MEDICAL_REGISTRY_EMPLOYEE_DRAFT; }; } @@ -85,6 +88,9 @@ public final class ProcedureMapper { case TM_VACCINATION_CONSULTATION -> ProcedureType.TM_VACCINATION_CONSULTATION; case MEASLES_PROTECTION -> ProcedureType.MEASLES_PROTECTION; case STI_PROTECTION -> ProcedureType.STI_PROTECTION; + case MEDICAL_REGISTRY_CITIZEN_DRAFT -> ProcedureType.MEDICAL_REGISTRY_CITIZEN_DRAFT; + case MEDICAL_REGISTRY_EMPLOYEE_DRAFT -> ProcedureType.MEDICAL_REGISTRY_EMPLOYEE_DRAFT; + case MEDICAL_REGISTRY_ENTRY -> ProcedureType.MEDICAL_REGISTRY_ENTRY; }; } diff --git a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/util/FileValidator.java b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/util/FileValidator.java index 38a77c82a..d748066fb 100644 --- a/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/util/FileValidator.java +++ b/backend/lib-procedures/src/main/java/de/eshg/lib/procedure/util/FileValidator.java @@ -5,11 +5,11 @@ package de.eshg.lib.procedure.util; -import static de.base.rest.CustomMediaTypes.MEDIA_TYPE_WAV; -import static de.base.rest.CustomMediaTypes.WAV_MEDIA_TYPE_VALUES_LIST; +import static de.eshg.file.common.CustomMediaTypes.MEDIA_TYPE_WAV; +import static de.eshg.file.common.CustomMediaTypes.WAV_MEDIA_TYPE_VALUES_LIST; -import de.eshg.lib.procedure.domain.model.FileType; -import de.eshg.lib.procedure.file.FileTypeDetector; +import de.eshg.file.common.FileType; +import de.eshg.file.common.FileTypeDetector; import de.eshg.rest.service.error.BadRequestException; import java.io.IOException; import java.util.regex.Matcher; diff --git a/backend/lib-relay/gradle.lockfile b/backend/lib-relay/gradle.lockfile index d01a63e7c..21ec2e28c 100644 --- a/backend/lib-relay/gradle.lockfile +++ b/backend/lib-relay/gradle.lockfile @@ -34,6 +34,7 @@ de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testFixturesCompi io.micrometer:micrometer-commons:1.13.4=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath diff --git a/backend/lib-security-config-urls/src/main/java/de/eshg/rest/service/security/config/BaseUrls.java b/backend/lib-security-config-urls/src/main/java/de/eshg/rest/service/security/config/BaseUrls.java index e0e9642b1..c556483c6 100644 --- a/backend/lib-security-config-urls/src/main/java/de/eshg/rest/service/security/config/BaseUrls.java +++ b/backend/lib-security-config-urls/src/main/java/de/eshg/rest/service/security/config/BaseUrls.java @@ -134,6 +134,14 @@ public final class BaseUrls { public static final class StiProtection { public static final String PROCEDURE_CONTROLLER = "/sti-procedures"; + + private StiProtection() {} + } + + public static final class MedicalRegistry { + public static final String MEDICAL_REGISTRY_CONTROLLER = "/medical-registry-entries"; + + private MedicalRegistry() {} } public static final class Statistics { @@ -196,4 +204,10 @@ public final class BaseUrls { private EditorLibrary() {} } + + public static final class OpenData { + public static final String OPEN_DATA_CONTROLLER = "/open-documents"; + + private OpenData() {} + } } diff --git a/backend/lib-security-config/gradle.lockfile b/backend/lib-security-config/gradle.lockfile index 1670ff8c0..b0f5a959f 100644 --- a/backend/lib-security-config/gradle.lockfile +++ b/backend/lib-security-config/gradle.lockfile @@ -34,6 +34,7 @@ de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClassp io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=testCompileClasspath,testRuntimeClasspath @@ -63,7 +64,7 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jetbrains:annotations:17.0.0=testRuntimeClasspath -org.jetbrains:annotations:26.0.0=compileClasspath +org.jetbrains:annotations:26.0.1=compileClasspath org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/AbstractPublicSecurityConfiguration.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/AbstractPublicSecurityConfiguration.java index a5d1fed49..33bb9b44e 100644 --- a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/AbstractPublicSecurityConfiguration.java +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/AbstractPublicSecurityConfiguration.java @@ -78,15 +78,19 @@ public abstract class AbstractPublicSecurityConfiguration { protected void grantAccessToLibAppointmentBlockUrls( PermissionRole permissionRole, boolean allowUpdateAppointmentType) { + requestMatchers(HttpMethod.GET, BaseUrls.LibAppointmentBlock.APPOINTMENT_BLOCK_API + "/**") + .hasAnyRole(permissionRole, EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers(BaseUrls.LibAppointmentBlock.APPOINTMENT_BLOCK_API + "/**") .hasRole(permissionRole); if (allowUpdateAppointmentType) { + requestMatchers(HttpMethod.GET, BaseUrls.LibAppointmentBlock.APPOINTMENT_TYPE_API + "/**") + .hasAnyRole(permissionRole, EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers(BaseUrls.LibAppointmentBlock.APPOINTMENT_TYPE_API + "/**") .hasRole(permissionRole); } else { requestMatchers(HttpMethod.GET, BaseUrls.LibAppointmentBlock.APPOINTMENT_TYPE_API + "/**") - .hasRole(permissionRole); + .hasAnyRole(permissionRole, EmployeePermissionRole.PROCEDURE_ARCHIVE); } } @@ -99,11 +103,10 @@ public abstract class AbstractPublicSecurityConfiguration { .hasRole(moduleLeaderRole.getEmployeePermissionRole()); requestMatchers( - GET, - ProcedureLibrary.PROCEDURES_API + "/**", - ProcedureLibrary.INBOX_PROCEDURES_API + "/**", - ProcedureLibrary.TASKS_API + "/**", - ProcedureLibrary.FILES_API + "/**") + GET, ProcedureLibrary.PROCEDURES_API + "/**", ProcedureLibrary.FILES_API + "/**") + .hasAnyRole(procedureAccessRole, EmployeePermissionRole.PROCEDURE_ARCHIVE); + requestMatchers( + GET, ProcedureLibrary.INBOX_PROCEDURES_API + "/**", ProcedureLibrary.TASKS_API + "/**") .hasRole(procedureAccessRole); requestMatchers(GET, ProcedureLibrary.PROCEDURE_METRICS_API + "/**") .hasRole(EmployeePermissionRole.BASE_PROCEDURE_METRICS_READ); diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/BasePublicSecurityConfig.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/BasePublicSecurityConfig.java index 625dfe421..c7f7fb898 100644 --- a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/BasePublicSecurityConfig.java +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/BasePublicSecurityConfig.java @@ -57,14 +57,18 @@ public final class BasePublicSecurityConfig extends AbstractPublicSecurityConfig private void persons() { requestMatchers(GET, BaseUrls.Base.PERSON_API) - .hasRole(EmployeePermissionRole.BASE_PERSONS_READ); + .hasAnyRole( + EmployeePermissionRole.BASE_PERSONS_READ, EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers(GET, BaseUrls.Base.PERSON_API + "/centralfilestates/*/diff") .hasRole(EmployeePermissionRole.BASE_PERSONS_READ); } private void facilities() { requestMatchers(GET, BaseUrls.Base.FACILITY_API) - .hasRole(EmployeePermissionRole.BASE_FACILITIES_READ); + .hasAnyRole( + EmployeePermissionRole.BASE_FACILITIES_READ, + EmployeePermissionRole.PROCEDURE_ARCHIVE, + EmployeePermissionRole.PROCEDURE_ARCHIVE_ADMIN); } private void gdpr() { @@ -86,9 +90,12 @@ public final class BasePublicSecurityConfig extends AbstractPublicSecurityConfig .hasRole(EmployeePermissionRole.BASE_CONTACTS_WRITE); requestMatchers(GET, BaseUrls.Base.CONTACT_API + "/{id}") .hasAnyRole( - EmployeePermissionRole.BASE_CONTACTS_READ, CitizenPermissionRole.ACCESS_CODE_USER); + EmployeePermissionRole.BASE_CONTACTS_READ, + CitizenPermissionRole.ACCESS_CODE_USER, + EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers(GET, BaseUrls.Base.CONTACT_API + "/**") - .hasAnyRole(EmployeePermissionRole.BASE_CONTACTS_READ); + .hasAnyRole( + EmployeePermissionRole.BASE_CONTACTS_READ, EmployeePermissionRole.PROCEDURE_ARCHIVE); } private void inventory() { @@ -101,7 +108,8 @@ public final class BasePublicSecurityConfig extends AbstractPublicSecurityConfig requestMatchers(PUT, BaseUrls.Base.INVENTORY_API + "/**") .hasRole(EmployeePermissionRole.BASE_INVENTORY_ADMINISTRATE); requestMatchers(GET, BaseUrls.Base.INVENTORY_API, BaseUrls.Base.INVENTORY_API + "/*") - .hasRole(EmployeePermissionRole.BASE_INVENTORY_READ); + .hasAnyRole( + EmployeePermissionRole.BASE_INVENTORY_READ, EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers( GET, BaseUrls.Base.INVENTORY_API + "/*" + BaseUrls.Base.INVENTORY_BOOKING_URL + "/**") .hasRole(EmployeePermissionRole.BASE_INVENTORY_READ); @@ -113,7 +121,8 @@ public final class BasePublicSecurityConfig extends AbstractPublicSecurityConfig requestMatchers(PATCH, BaseUrls.Base.RESOURCES_API + "/**") .hasRole(EmployeePermissionRole.BASE_RESOURCES_WRITE); requestMatchers(GET, BaseUrls.Base.RESOURCES_API + "/**") - .hasRole(EmployeePermissionRole.BASE_RESOURCES_READ); + .hasAnyRole( + EmployeePermissionRole.BASE_RESOURCES_READ, EmployeePermissionRole.PROCEDURE_ARCHIVE); } private void calendarsAndEvents() { diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/InspectionPublicSecurityConfig.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/InspectionPublicSecurityConfig.java index 5b9f5a851..f2c7f52c5 100644 --- a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/InspectionPublicSecurityConfig.java +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/InspectionPublicSecurityConfig.java @@ -62,6 +62,14 @@ public final class InspectionPublicSecurityConfig extends AbstractPublicSecurity } private void procedure() { + requestMatchers( + GET, + BaseUrls.Inspection.INSPECTION_CONTROLLER + "/{id}/**", + BaseUrls.Inspection.CHECKLIST_CONTROLLER + "/**", + BaseUrls.Inspection.PACKLIST_CONTROLLER + "/**") + .hasAnyRole( + EmployeePermissionRole.INSPECTION_PROCEDURE_EDIT, + EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers( BaseUrls.Inspection.INSPECTION_CONTROLLER + "/**", BaseUrls.Inspection.FACILITY_CONTROLLER + "/**", @@ -72,6 +80,10 @@ public final class InspectionPublicSecurityConfig extends AbstractPublicSecurity } private void editor() { + requestMatchers(GET, BaseUrls.EditorLibrary.EDITOR_API + "/**") + .hasAnyRole( + EmployeePermissionRole.INSPECTION_PROCEDURE_EDIT, + EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers(BaseUrls.EditorLibrary.EDITOR_API + "/**") .hasRole(EmployeePermissionRole.INSPECTION_PROCEDURE_EDIT); requestMatchers(BaseUrls.EditorLibrary.TEXTBLOCK_API + "/**") diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/MeaslesProtectionPublicSecurityConfig.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/MeaslesProtectionPublicSecurityConfig.java index c2506af0d..9c7d0b9cf 100644 --- a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/MeaslesProtectionPublicSecurityConfig.java +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/MeaslesProtectionPublicSecurityConfig.java @@ -5,6 +5,7 @@ package de.eshg.rest.service.security.config; +import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.PUT; import de.eshg.lib.keycloak.EmployeePermissionRole; @@ -29,6 +30,11 @@ public final class MeaslesProtectionPublicSecurityConfig requestMatchers(PUT, BaseUrls.MeaslesProtection.PROCEDURE_CONTROLLER + "/*/reopen") .hasRole(EmployeePermissionRole.MEASLES_PROTECTION_LEADER); + requestMatchers(GET, BaseUrls.MeaslesProtection.PROCEDURE_CONTROLLER + "/{id}/**") + .hasAnyRole( + EmployeePermissionRole.MEASLES_PROTECTION_LEADER, + EmployeePermissionRole.MEASLES_PROTECTION_ADMIN, + EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers( BaseUrls.MeaslesProtection.PROCEDURE_CONTROLLER + "/**", BaseUrls.EVENT_METADATA_API + "/**") diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/MedicalRegistryPublicSecurityConfig.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/MedicalRegistryPublicSecurityConfig.java index 4d1bcffaa..077f9a312 100644 --- a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/MedicalRegistryPublicSecurityConfig.java +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/MedicalRegistryPublicSecurityConfig.java @@ -16,5 +16,8 @@ public final class MedicalRegistryPublicSecurityConfig extends AbstractPublicSec super("medical-registry"); grantAccessToLibProceduresUrls( EmployeePermissionRole.MEDICAL_REGISTRY_ADMIN, ModuleLeaderRole.MEDICAL_REGISTRY_LEADER); + + requestMatchers(BaseUrls.MedicalRegistry.MEDICAL_REGISTRY_CONTROLLER + "/**") + .hasRole(EmployeePermissionRole.MEDICAL_REGISTRY_ADMIN); } } diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/OpenDataPublicSecurityConfig.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/OpenDataPublicSecurityConfig.java new file mode 100644 index 000000000..da7d7abf7 --- /dev/null +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/OpenDataPublicSecurityConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.rest.service.security.config; + +import de.eshg.lib.keycloak.EmployeePermissionRole; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; + +@Component +public final class OpenDataPublicSecurityConfig extends AbstractPublicSecurityConfiguration { + OpenDataPublicSecurityConfig() { + super("opendata"); + + requestMatchers(HttpMethod.GET, BaseUrls.OpenData.OPEN_DATA_CONTROLLER) + .hasRole(EmployeePermissionRole.OPEN_DATA_ADMIN); + requestMatchers(HttpMethod.GET, BaseUrls.OpenData.OPEN_DATA_CONTROLLER + "/search/**") + .hasRole(EmployeePermissionRole.OPEN_DATA_ADMIN); + requestMatchers(HttpMethod.GET, BaseUrls.OpenData.OPEN_DATA_CONTROLLER + "/*/download") + .hasRole(EmployeePermissionRole.OPEN_DATA_ADMIN); + requestMatchers(HttpMethod.PUT, BaseUrls.OpenData.OPEN_DATA_CONTROLLER + "/**") + .hasRole(EmployeePermissionRole.OPEN_DATA_ADMIN); + requestMatchers(HttpMethod.DELETE, BaseUrls.OpenData.OPEN_DATA_CONTROLLER + "/**") + .hasRole(EmployeePermissionRole.OPEN_DATA_ADMIN); + requestMatchers(HttpMethod.POST, BaseUrls.OpenData.OPEN_DATA_CONTROLLER) + .hasRole(EmployeePermissionRole.OPEN_DATA_ADMIN); + } +} diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/SchoolEntryPublicSecurityConfig.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/SchoolEntryPublicSecurityConfig.java index 1370aa0f6..41fbf53f8 100644 --- a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/SchoolEntryPublicSecurityConfig.java +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/SchoolEntryPublicSecurityConfig.java @@ -5,6 +5,8 @@ package de.eshg.rest.service.security.config; +import static org.springframework.http.HttpMethod.GET; + import de.eshg.lib.keycloak.CitizenPermissionRole; import de.eshg.lib.keycloak.EmployeePermissionRole; import de.eshg.lib.keycloak.ModuleLeaderRole; @@ -33,6 +35,14 @@ public final class SchoolEntryPublicSecurityConfig extends AbstractPublicSecurit BaseUrls.SchoolEntry.CONFIG_CONTROLLER + "/**") .hasRole(EmployeePermissionRole.STANDARD_EMPLOYEE); + requestMatchers( + GET, + BaseUrls.SchoolEntry.SCHOOL_ENTRY_CONTROLLER + "/*", + BaseUrls.SchoolEntry.SCHOOL_ENTRY_CONTROLLER + "/{procedureId}/**", + BaseUrls.SchoolEntry.VALUE_EVALUATOR_CONTROLLER + "/**", + BaseUrls.SchoolEntry.COUNTRY_CODES_CONTROLLER + "/**") + .hasAnyRole( + EmployeePermissionRole.SCHOOL_ENTRY_ADMIN, EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers( BaseUrls.SchoolEntry.SCHOOL_ENTRY_CONTROLLER + "/**", BaseUrls.SchoolEntry.VALUE_EVALUATOR_CONTROLLER + "/**", diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/StiProtectionPublicSecurityConfig.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/StiProtectionPublicSecurityConfig.java index 60df92025..db8bd04be 100644 --- a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/StiProtectionPublicSecurityConfig.java +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/StiProtectionPublicSecurityConfig.java @@ -5,6 +5,8 @@ package de.eshg.rest.service.security.config; +import static org.springframework.http.HttpMethod.GET; + import de.eshg.lib.keycloak.EmployeePermissionRole; import de.eshg.lib.keycloak.ModuleLeaderRole; import org.springframework.stereotype.Component; @@ -18,6 +20,9 @@ public final class StiProtectionPublicSecurityConfig extends AbstractPublicSecur grantAccessToLibProceduresUrls( EmployeePermissionRole.STI_PROTECTION_USER, ModuleLeaderRole.STI_PROTECTION_LEADER); + requestMatchers(GET, BaseUrls.StiProtection.PROCEDURE_CONTROLLER + "/{id}/**") + .hasAnyRole( + EmployeePermissionRole.STI_PROTECTION_USER, EmployeePermissionRole.PROCEDURE_ARCHIVE); requestMatchers( BaseUrls.StiProtection.PROCEDURE_CONTROLLER + "/**", BaseUrls.EVENT_METADATA_API + "/**") diff --git a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/TravelMedicinePublicSecurityConfig.java b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/TravelMedicinePublicSecurityConfig.java index 1c2e40cdf..847813314 100644 --- a/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/TravelMedicinePublicSecurityConfig.java +++ b/backend/lib-security-config/src/main/java/de/eshg/rest/service/security/config/TravelMedicinePublicSecurityConfig.java @@ -5,6 +5,8 @@ package de.eshg.rest.service.security.config; +import static org.springframework.http.HttpMethod.GET; + import de.eshg.lib.keycloak.CitizenPermissionRole; import de.eshg.lib.keycloak.EmployeePermissionRole; import de.eshg.lib.keycloak.ModuleLeaderRole; @@ -27,6 +29,16 @@ public final class TravelMedicinePublicSecurityConfig extends AbstractPublicSecu requestMatchers(BaseUrls.TravelMedicine.CITIZEN_AUTH_CONTROLLER + "/**") .hasRole(CitizenPermissionRole.ACCESS_CODE_USER); + requestMatchers( + GET, + BaseUrls.TravelMedicine.INFORMATION_STATEMENT_TEMPLATE_CONTROLLER + "/**", + BaseUrls.TravelMedicine.VACCINE_CONTROLLER + "/**", + BaseUrls.TravelMedicine.DISEASE_CONTROLLER + "/**", + BaseUrls.TravelMedicine.OTHER_SERVICE_TEMPLATE_CONTROLLER + "/**", + BaseUrls.TravelMedicine.VACCINATION_CONSULTATION_CONTROLLER + "/{procedureId}/**", + BaseUrls.TravelMedicine.PROCEDURE_STEP_CONTROLLER + "/**") + .hasAnyRole( + EmployeePermissionRole.PROCEDURE_ARCHIVE, EmployeePermissionRole.TRAVEL_MEDICINE_ADMIN); requestMatchers( BaseUrls.TravelMedicine.INFORMATION_STATEMENT_TEMPLATE_CONTROLLER + "/**", BaseUrls.TravelMedicine.MEDICAL_HISTORY_TEMPLATE_CONTROLLER + "/**", diff --git a/backend/lib-service-directory-admin-api/gradle.lockfile b/backend/lib-service-directory-admin-api/gradle.lockfile index e9ece9941..f5349095e 100644 --- a/backend/lib-service-directory-admin-api/gradle.lockfile +++ b/backend/lib-service-directory-admin-api/gradle.lockfile @@ -34,6 +34,7 @@ de.cronn:validation-file-assertions:0.8.0=testFixturesCompileClasspath,testFixtu io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath diff --git a/backend/lib-service-directory-api/gradle.lockfile b/backend/lib-service-directory-api/gradle.lockfile index 8ce36c12a..64c3685a3 100644 --- a/backend/lib-service-directory-api/gradle.lockfile +++ b/backend/lib-service-directory-api/gradle.lockfile @@ -17,6 +17,7 @@ de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeCla io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/lib-statistics/gradle.lockfile b/backend/lib-statistics/gradle.lockfile index 2b8b78e9e..22098461c 100644 --- a/backend/lib-statistics/gradle.lockfile +++ b/backend/lib-statistics/gradle.lockfile @@ -16,11 +16,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -28,7 +28,7 @@ com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=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 @@ -37,23 +37,24 @@ com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath, com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.16.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-jakarta9:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -67,6 +68,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -77,18 +79,18 @@ jakarta.transaction:jakarta.transaction-api:2.0.1=compileClasspath,productionRun jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.14.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.26.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.12.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -111,7 +113,7 @@ org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=compileClasspath,producti org.apache.tomcat:tomcat-annotations-api:10.1.30=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcmail-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -123,8 +125,8 @@ org.eclipse.angus:jakarta.mail:2.0.3=productionRuntimeClasspath,runtimeClasspath org.glassfish.jaxb:jaxb-core:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-runtime:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -135,15 +137,15 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath @@ -151,12 +153,12 @@ org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspa org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.postgresql:postgresql:42.7.4=testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -166,7 +168,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -182,7 +184,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -205,7 +207,7 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -213,7 +215,7 @@ org.testcontainers:database-commons:1.19.8=testCompileClasspath,testRuntimeClass org.testcontainers:jdbc:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:junit-jupiter:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testCompileClasspath,testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath diff --git a/backend/lib-xlsx-import/README_LICENSE.adoc b/backend/lib-xlsx-import/README_LICENSE.adoc new file mode 100644 index 000000000..87f2419aa --- /dev/null +++ b/backend/lib-xlsx-import/README_LICENSE.adoc @@ -0,0 +1,5 @@ +== Licensing + +All files within this directory, including those in all subdirectories, are licensed under the Apache License 2.0. + +For the complete license text, please refer to the `LICENSE-APACHE-2.0.txt` file located in the project root. diff --git a/backend/lib-xlsx-import/build.gradle b/backend/lib-xlsx-import/build.gradle new file mode 100644 index 000000000..273145aca --- /dev/null +++ b/backend/lib-xlsx-import/build.gradle @@ -0,0 +1,25 @@ +plugins { + id "eshg.java-lib" + id 'java-test-fixtures' +} + +dependencies { + api project(':base-api') + + implementation project(':file-commons') + implementation project(':rest-service-errors') + + api 'org.apache.poi:poi:latest.release' + api 'org.apache.poi:poi-ooxml:latest.release' + + implementation 'de.cronn:commons-lang:latest.release' + implementation 'org.slf4j:slf4j-api' + + testFixturesApi project(':test-commons') + testFixturesImplementation project(':file-commons') + testFixturesImplementation 'org.assertj:assertj-core' + testFixturesImplementation 'jakarta.mail:jakarta.mail-api' + testFixturesImplementation 'org.eclipse.angus:jakarta.mail:latest.release' + + testImplementation 'org.springframework.boot:spring-boot-starter-web' +} diff --git a/backend/lib-xlsx-import/buildscript-gradle.lockfile b/backend/lib-xlsx-import/buildscript-gradle.lockfile new file mode 100644 index 000000000..0d156738b --- /dev/null +++ b/backend/lib-xlsx-import/buildscript-gradle.lockfile @@ -0,0 +1,4 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +empty=classpath diff --git a/backend/lib-xlsx-import/gradle.lockfile b/backend/lib-xlsx-import/gradle.lockfile new file mode 100644 index 000000000..af9446375 --- /dev/null +++ b/backend/lib-xlsx-import/gradle.lockfile @@ -0,0 +1,148 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.5.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +ch.qos.logback:logback-core:1.5.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.17.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.17.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.2=testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.github.virtuald:curvesapi:1.08=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,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 +com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testRuntimeClasspath +com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath +com.zaxxer:SparseBitSet:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.16.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +commons-io:commons-io:2.16.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-observation:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-core-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-models-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath +jakarta.mail:jakarta.mail-api:2.1.3=testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.19=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.13.0=productionRuntimeClasspath,runtimeClasspath,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=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.26.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.commons:commons-lang3:3.14.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.logging.log4j:log4j-api:2.23.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-to-slf4j:2.23.1=testCompileClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml-lite:5.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi-ooxml:5.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.poi:poi:5.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tika:tika-core:2.9.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-core:10.1.30=testCompileClasspath,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-el:10.1.30=testCompileClasspath,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=testCompileClasspath,testRuntimeClasspath +org.apache.xmlbeans:xmlbeans:5.2.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testFixturesCompileClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,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 +org.eclipse.angus:jakarta.mail:2.0.3=testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:jaxb-core:4.0.5=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:jaxb-runtime:4.0.5=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:txw2:4.0.5=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt +org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt +org.jacoco:org.jacoco.core:0.8.11=jacocoAnt +org.jacoco:org.jacoco.report:0.8.11=jacocoAnt +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspath +org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-commons:9.6=jacocoAnt +org.ow2.asm:asm-tree:9.6=jacocoAnt +org.ow2.asm:asm:9.6=jacocoAnt,testCompileClasspath,testRuntimeClasspath +org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath +org.slf4j:jul-to-slf4j:2.0.16=testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springdoc:springdoc-openapi-starter-common:2.6.0=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-autoconfigure:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-json:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-logging:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-tomcat:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-web:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework.data:spring-data-commons:3.3.4=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework:spring-aop:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework:spring-beans:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework:spring-context:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework:spring-core:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.springframework:spring-webmvc:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.verapdf:parser:1.26.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.verapdf:pdf-model:1.26.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.verapdf:validation-model-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.verapdf:verapdf-xmp-core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +org.xmlunit:xmlunit-core:2.9.1=testCompileClasspath,testRuntimeClasspath +org.yaml:snakeyaml:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath +empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesAnnotationProcessor diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ColumnAccessor.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ColumnAccessor.java similarity index 94% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ColumnAccessor.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ColumnAccessor.java index eac3c7419..4dfd11c08 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ColumnAccessor.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ColumnAccessor.java @@ -1,9 +1,9 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.importer; +package de.eshg.lib.xlsximport; import java.util.List; import java.util.stream.IntStream; diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/FeedbackColumnAccessor.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/FeedbackColumnAccessor.java similarity index 89% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/FeedbackColumnAccessor.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/FeedbackColumnAccessor.java index 9c01a2ec3..9b214a9c4 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/FeedbackColumnAccessor.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/FeedbackColumnAccessor.java @@ -1,9 +1,9 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.importer; +package de.eshg.lib.xlsximport; import java.util.List; import org.apache.poi.ss.usermodel.Cell; @@ -16,7 +16,7 @@ public class FeedbackColumnAccessor { private final int procedureIdColumn; private final int referenceIdColumn; - FeedbackColumnAccessor(List<? extends XlsxColumn> actualColumns) { + public FeedbackColumnAccessor(List<? extends XlsxColumn> actualColumns) { List<String> headers = actualColumns.stream().map(XlsxColumn::getHeader).toList(); this.statusColumn = headers.indexOf(XlsxColumn.STATUS_COLUMN_HEADER); this.procedureIdColumn = headers.indexOf(XlsxColumn.PROCEDURE_COLUMN_HEADER); diff --git a/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportStatistics.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportStatistics.java new file mode 100644 index 000000000..63a52eec6 --- /dev/null +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportStatistics.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.xlsximport; + +import de.eshg.lib.xlsximport.api.ImportStatisticsDto; + +public class ImportStatistics { + private int created = 0; + private int merged = 0; + private int mergeFailed = 0; + private int duplicated = 0; + private int failed = 0; + private int previouslyImported = 0; + + public void countCreated() { + created++; + } + + public void countMerged() { + merged++; + } + + public void countMergeFailed() { + mergeFailed++; + } + + public void countDuplicated() { + duplicated++; + } + + public void countFailed() { + failed++; + } + + public void countPreviouslyImported() { + previouslyImported++; + } + + public void correctMergeToFailed(int count) { + if (merged < count) { + throw new IllegalStateException("Count correction failed."); + } + merged -= count; + mergeFailed += count; + } + + public void correctCreatedToFailed(int count) { + if (created < count) { + throw new IllegalStateException("Count correction failed."); + } + created -= count; + failed += count; + } + + public ImportStatisticsDto mapToDto() { + return new ImportStatisticsDto( + created + merged + mergeFailed + duplicated + failed + previouslyImported, + created, + merged, + mergeFailed, + duplicated, + failed); + } +} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportStatus.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportStatus.java similarity index 93% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportStatus.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportStatus.java index 22cc0fab7..9e215b143 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportStatus.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportStatus.java @@ -1,9 +1,9 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.importer; +package de.eshg.lib.xlsximport; import de.cronn.commons.lang.StreamUtil; import java.util.Arrays; diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportValidator.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportValidator.java similarity index 97% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportValidator.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportValidator.java index d1fae698e..c1e45d7fa 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportValidator.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/ImportValidator.java @@ -1,10 +1,11 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.importer; +package de.eshg.lib.xlsximport; +import de.eshg.lib.xlsximport.util.XlsxUtil; import de.eshg.rest.service.error.BadRequestException; import de.eshg.rest.service.error.ErrorCode; import java.util.*; diff --git a/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/Importer.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/Importer.java new file mode 100644 index 000000000..33383c56a --- /dev/null +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/Importer.java @@ -0,0 +1,184 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.xlsximport; + +import static de.eshg.lib.xlsximport.ImportStatus.EXCEPTION; +import static de.eshg.lib.xlsximport.ImportStatus.MERGE_FAILED; +import static de.eshg.lib.xlsximport.util.XlsxUtil.writeValue; + +import de.cronn.commons.lang.StreamUtil; +import de.eshg.lib.xlsximport.model.ImportResult; +import de.eshg.lib.xlsximport.util.XlsxUtil; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Consumer; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFFont; +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.ByteArrayResource; + +public abstract class Importer<T extends RowValues, C extends XlsxColumn> { + + private static final Logger log = LoggerFactory.getLogger(Importer.class); + + protected final XSSFSheet sheet; + protected final RowReader<T, C> rowReader; + protected final ValidRows<T> validRows = new ValidRows<>(new ArrayList<>(), new ArrayList<>()); + protected final ImportStatistics stats = new ImportStatistics(); + private final FeedbackColumnAccessor col; + private final XSSFCellStyle defaultCellStyle; + private final XSSFCellStyle importedSuccessfullyCellStyle; + private final XSSFCellStyle importFailedCellStyle; + private final XSSFCellStyle importWarningCellStyle; + + protected Importer( + XSSFSheet sheet, RowReader<T, C> rowReader, FeedbackColumnAccessor feedbackColumnAccessor) { + this.sheet = sheet; + this.rowReader = rowReader; + this.col = feedbackColumnAccessor; + this.defaultCellStyle = createDefaultCellStyle(); + + this.importedSuccessfullyCellStyle = + createCellStyle( + font -> { + font.setBold(true); + font.setColor(XlsxUtil.newColor(76, 175, 80)); + }); + + this.importFailedCellStyle = + createCellStyle( + font -> { + font.setBold(true); + font.setColor(XlsxUtil.newColor(176, 0, 0)); + }); + + importWarningCellStyle = + createCellStyle( + font -> { + font.setBold(true); + font.setColor(XlsxUtil.newColor(228, 114, 0)); + }); + } + + private XSSFCellStyle createCellStyle(Consumer<XSSFFont> fontCustomizer) { + XSSFCellStyle cellStyle = createDefaultCellStyle(); + XSSFFont font = cellStyle.getFont(); + fontCustomizer.accept(font); + return cellStyle; + } + + private XSSFCellStyle createDefaultCellStyle() { + XSSFWorkbook workbook = sheet.getWorkbook(); + XSSFCellStyle cellStyle = workbook.createCellStyle(); + cellStyle.setFont(XlsxUtil.createDefaultFont(workbook)); + return cellStyle; + } + + protected record ValidRows<T>(List<T> importableRows, List<T> mergeableRows) {} + + public ImportResult process() throws IOException { + readRowsAndEvaluateActions(); + + createProceduresAndWriteResults(); + mergeProceduresAndWriteResults(); + + return mapImportResult(); + } + + protected abstract void readRowsAndEvaluateActions(); + + protected abstract void createProceduresAndWriteResults(); + + protected abstract void mergeProceduresAndWriteResults(); + + private ImportResult mapImportResult() throws IOException { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + sheet.getWorkbook().write(outputStream); + ByteArrayResource resource = new ByteArrayResource(outputStream.toByteArray()); + + return new ImportResult(stats.mapToDto(), resource); + } + } + + protected Map<Row, T> readRows() { + Map<Row, T> rowValues = new LinkedHashMap<>(); + for (Row row : sheet) { + if (row.getRowNum() == 0) { + // skip the header + continue; + } + deleteReferenceId(row); + try { + rowValues.put(row, rowReader.readRow(row)); + } catch (Exception e) { + log.error("Error in reading row %d".formatted(row.getRowNum()), e); + writeStatus(row, EXCEPTION); + stats.countFailed(); + } + } + return rowValues; + } + + private void deleteReferenceId(Row row) { + if (col.hasReferenceIdColum()) { + writeValue(col.getReferenceId(row), "", defaultCellStyle); + } + } + + protected void writeStatusAndProcedureId(Row row, ImportStatus status, UUID procedureId) { + writeStatus(row, status); + writeValue(col.getProcedureId(row), procedureId.toString(), defaultCellStyle); + } + + private void deleteProcedureId(Row row) { + writeValue(col.getProcedureId(row), "", defaultCellStyle); + } + + protected void writeStatusAndReferenceId(Row row, ImportStatus status, UUID referenceId) { + writeStatus(row, status); + writeValue(col.getReferenceId(row), referenceId.toString(), defaultCellStyle); + } + + protected void writeStatus(Row row, ImportStatus importStatus) { + writeValue(col.getStatus(row), importStatus.getDescription(), getCellStyle(importStatus)); + } + + private XSSFCellStyle getCellStyle(ImportStatus importStatus) { + return switch (importStatus) { + case IMPORTED_SUCCESSFULLY, MERGED_SUCCESSFULLY -> importedSuccessfullyCellStyle; + case ERROR_INPUT_DATA, INVALID_PROCEDURE_ID, EXCEPTION, MERGE_FAILED -> importFailedCellStyle; + case IMPORTED_PREVIOUSLY, DUPLICATE_WITHIN_LIST, DUPLICATE_IN_ASSET -> importWarningCellStyle; + }; + } + + protected void writeMergedFailedStatusInSheet(List<T> mergeableRows, List<UUID> failedIds) { + for (UUID uuid : failedIds) { + Row row = + mergeableRows.stream() + .filter(values -> Objects.equals(uuid, values.getProcedureId())) + .collect(StreamUtil.toSingleElement()) + .getRow(); + deleteProcedureId(row); + writeStatusAndReferenceId(row, MERGE_FAILED, uuid); + } + } + + protected void writeFailedStatusInSheet(List<T> importableRows) { + for (T rowValues : importableRows) { + writeStatus(rowValues.getRow(), EXCEPTION); + } + } +} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/RowProcessor.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/RowReader.java similarity index 91% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/RowProcessor.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/RowReader.java index 1ad2b0f60..a811b0352 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/RowProcessor.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/RowReader.java @@ -1,14 +1,15 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.importer; +package de.eshg.lib.xlsximport; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; -import de.eshg.schoolentry.business.model.AddressData; +import de.eshg.lib.common.CountryCode; +import de.eshg.lib.xlsximport.model.AddressData; +import de.eshg.lib.xlsximport.util.XlsxUtil; import java.time.LocalDate; import java.util.List; import java.util.NoSuchElementException; @@ -19,8 +20,7 @@ import java.util.stream.Stream; import org.apache.poi.ss.usermodel.*; import org.springframework.util.StringUtils; -public abstract class RowProcessor<T extends RowValues, C extends XlsxColumn> - implements EqualityComparator<T>, RowValueMapper<T> { +public abstract class RowReader<T extends RowValues, C extends XlsxColumn> { private static final DataFormatter DATA_FORMATTER = new DataFormatter(); @@ -30,7 +30,7 @@ public abstract class RowProcessor<T extends RowValues, C extends XlsxColumn> private final CreationHelper factory; private final ClientAnchor anchor; - protected RowProcessor(Sheet sheet, List<C> actualColumns) { + protected RowReader(Sheet sheet, List<C> actualColumns) { Workbook workbook = sheet.getWorkbook(); this.actualColumns = actualColumns; @@ -40,21 +40,17 @@ public abstract class RowProcessor<T extends RowValues, C extends XlsxColumn> anchor = factory.createClientAnchor(); } - public List<C> getActualColumns() { - return actualColumns; - } - - public T processRow(Row row) { + public T readRow(Row row) { ColumnAccessor<C> col = new ColumnAccessor<>(row, actualColumns); - T result = process(col); + T result = read(col); result.setRow(row); return result; } - protected abstract T process(ColumnAccessor<C> col); + protected abstract T read(ColumnAccessor<C> col); - protected ImportStatus processStatus( + protected ImportStatus readStatus( ColumnAccessor<C> col, C column, BiConsumer<Cell, String> errorHandler) { Cell cell = col.get(column); String status = cellAsString(cell, true, false, errorHandler); @@ -70,7 +66,7 @@ public abstract class RowProcessor<T extends RowValues, C extends XlsxColumn> } } - protected UUID processProcedureId( + protected UUID readProcedureId( ColumnAccessor<C> col, C column, BiConsumer<Cell, String> errorHandler) { Cell cell = col.get(column); String uuid = cellAsString(cell, true, false, errorHandler); @@ -226,7 +222,7 @@ public abstract class RowProcessor<T extends RowValues, C extends XlsxColumn> }; } - protected CountryCodeDto cellAsCountryCode( + protected CountryCode cellAsCountryCode( ColumnAccessor<C> col, C column, BiConsumer<Cell, String> errorHandler) { Cell cell = col.get(column); String countryCode = cellAsString(cell, true, false, errorHandler); @@ -234,7 +230,7 @@ public abstract class RowProcessor<T extends RowValues, C extends XlsxColumn> return null; } try { - return CountryCodeDto.valueOf(countryCode); + return CountryCode.valueOf(countryCode); } catch (IllegalArgumentException exception) { errorHandler.accept( cell, @@ -300,7 +296,7 @@ public abstract class RowProcessor<T extends RowValues, C extends XlsxColumn> .anyMatch(org.apache.commons.lang3.StringUtils::isNotBlank); } - protected AddressData processAddressData( + protected AddressData readAddressData( ColumnAccessor<C> col, AddressColumns<C> addressColumns, BiConsumer<Cell, String> errorHandler, @@ -315,11 +311,11 @@ public abstract class RowProcessor<T extends RowValues, C extends XlsxColumn> String addressAddition = cellAsString(col, addressColumns.addressAddition(), true, true, errorHandler); return new AddressData( - CountryCodeDto.DE, city, postalCode, street, houseNumber, addressAddition); + CountryCode.DE, city, postalCode, street, houseNumber, addressAddition); } return null; } - protected record AddressColumns<C extends XlsxColumn>( + public record AddressColumns<C extends XlsxColumn>( C street, C houseNumber, C postalCode, C city, C addressAddition) {} } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/RowValues.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/RowValues.java similarity index 62% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/RowValues.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/RowValues.java index 7be60983e..98340d113 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/RowValues.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/RowValues.java @@ -1,21 +1,19 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.importer; +package de.eshg.lib.xlsximport; -import de.eshg.schoolentry.business.model.ImportChildData; import java.util.UUID; import org.apache.poi.ss.usermodel.Row; -public abstract sealed class RowValues - permits CitizenListRowValues, SchoolListRowValues, PastProcedureListRowValues { +public class RowValues { + private Row row; private ImportStatus status; private UUID procedureId; private boolean valid = true; - private ImportChildData child; public Row getRow() { return row; @@ -48,12 +46,4 @@ public abstract sealed class RowValues public void foundInvalidData() { this.valid = false; } - - public ImportChildData getChild() { - return child; - } - - public void setChild(ImportChildData child) { - this.child = child; - } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/XlsxColumn.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxColumn.java similarity index 90% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/XlsxColumn.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxColumn.java index f2ec167bd..d000d65b1 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/XlsxColumn.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxColumn.java @@ -1,9 +1,9 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.importer; +package de.eshg.lib.xlsximport; import java.util.EnumSet; diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/XlsxNormalizer.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxNormalizer.java similarity index 93% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/XlsxNormalizer.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxNormalizer.java index a4b6850c5..d52fe059b 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/XlsxNormalizer.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/XlsxNormalizer.java @@ -1,10 +1,11 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.importer; +package de.eshg.lib.xlsximport; +import de.eshg.lib.xlsximport.util.XlsxUtil; import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; @@ -42,13 +43,7 @@ public class XlsxNormalizer implements AutoCloseable { } private XSSFFont createHeaderFont() { - return createHeaderFont(normalizedWorkbook); - } - - static XSSFFont createHeaderFont(XSSFWorkbook workbook) { - XSSFFont headerFont = XlsxUtil.createDefaultFont(workbook); - headerFont.setBold(true); - return headerFont; + return XlsxUtil.createHeaderFont(normalizedWorkbook); } public XSSFSheet normalize(Sheet sheet) { diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/api/ImportStatisticsDto.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/api/ImportStatisticsDto.java similarity index 87% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/api/ImportStatisticsDto.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/api/ImportStatisticsDto.java index 4f825f486..3e2ac4538 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/api/ImportStatisticsDto.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/api/ImportStatisticsDto.java @@ -1,9 +1,9 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.api; +package de.eshg.lib.xlsximport.api; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/AddressData.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/model/AddressData.java similarity index 54% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/AddressData.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/model/AddressData.java index e9116399d..c6dd239ea 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/AddressData.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/model/AddressData.java @@ -1,14 +1,14 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.business.model; +package de.eshg.lib.xlsximport.model; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; public record AddressData( - CountryCodeDto country, + CountryCode country, String city, String postalCode, String street, diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportResult.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/model/ImportResult.java similarity index 53% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportResult.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/model/ImportResult.java index f87937da0..343a890cf 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportResult.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/model/ImportResult.java @@ -1,11 +1,11 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.business.model; +package de.eshg.lib.xlsximport.model; -import de.eshg.schoolentry.api.ImportStatisticsDto; +import de.eshg.lib.xlsximport.api.ImportStatisticsDto; import org.springframework.core.io.Resource; public record ImportResult(ImportStatisticsDto statistics, Resource file) {} diff --git a/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/util/FileResponseUtil.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/util/FileResponseUtil.java new file mode 100644 index 000000000..131cf2ace --- /dev/null +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/util/FileResponseUtil.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.xlsximport.util; + +import de.eshg.file.common.CustomMediaTypes; +import de.eshg.lib.xlsximport.model.ImportResult; +import org.springframework.core.io.Resource; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +public class FileResponseUtil { + + private FileResponseUtil() {} + + public static ResponseEntity<MultiValueMap<String, Object>> mapImportResultToMultipartResponse( + ImportResult result, String filename) { + MultiValueMap<String, Object> multipart = new LinkedMultiValueMap<>(); + + HttpHeaders statisticsHeaders = new HttpHeaders(); + statisticsHeaders.setContentType(MediaType.APPLICATION_JSON); + multipart.add("statistics", new HttpEntity<>(result.statistics(), statisticsHeaders)); + + HttpHeaders fileHeaders = new HttpHeaders(); + fileHeaders.setContentType(CustomMediaTypes.APPLICATION_XLSX); + fileHeaders.setContentDisposition(fileFormData(filename)); + multipart.add("file", new HttpEntity<>(result.file(), fileHeaders)); + + return ResponseEntity.ok().contentType(MediaType.MULTIPART_FORM_DATA).body(multipart); + } + + public static ResponseEntity<Resource> getTemplateFileResponse(Resource templateFile) { + return ResponseEntity.ok() + .header( + HttpHeaders.CONTENT_DISPOSITION, fileAttachment(templateFile.getFilename()).toString()) + .header(HttpHeaders.CONTENT_TYPE, CustomMediaTypes.APPLICATION_XLSX_VALUE) + .body(templateFile); + } + + private static ContentDisposition fileFormData(String filename) { + return file(filename, ContentDisposition.formData()); + } + + private static ContentDisposition fileAttachment(String filename) { + return file(filename, ContentDisposition.attachment()); + } + + private static ContentDisposition file(String filename, ContentDisposition.Builder builder) { + return builder.name("file").filename(filename).build(); + } +} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/XlsxUtil.java b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/util/XlsxUtil.java similarity index 70% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/XlsxUtil.java rename to backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/util/XlsxUtil.java index 92c877c60..35d9be7bf 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/XlsxUtil.java +++ b/backend/lib-xlsx-import/src/main/java/de/eshg/lib/xlsximport/util/XlsxUtil.java @@ -1,9 +1,9 @@ /* * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ -package de.eshg.schoolentry.importer; +package de.eshg.lib.xlsximport.util; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.xssf.usermodel.XSSFCellStyle; @@ -19,21 +19,27 @@ public final class XlsxUtil { private XlsxUtil() {} - static XSSFColor newColor(int red, int green, int blue) { + public static XSSFColor newColor(int red, int green, int blue) { return new XSSFColor(new byte[] {(byte) red, (byte) green, (byte) blue}); } - static XSSFFont createDefaultFont(XSSFWorkbook workbook) { + public static XSSFFont createDefaultFont(XSSFWorkbook workbook) { XSSFFont font = workbook.createFont(); font.setFontName(DEFAULT_FONT); font.setFontHeight(DEFAULT_FONT_SIZE); return font; } + public static XSSFFont createHeaderFont(XSSFWorkbook workbook) { + XSSFFont headerFont = createDefaultFont(workbook); + headerFont.setBold(true); + return headerFont; + } + public static XSSFCellStyle createHeaderCellStyle(XSSFSheet sheet) { XSSFWorkbook workbook = sheet.getWorkbook(); XSSFCellStyle cellStyle = workbook.createCellStyle(); - cellStyle.setFont(XlsxNormalizer.createHeaderFont(workbook)); + cellStyle.setFont(createHeaderFont(workbook)); return cellStyle; } diff --git a/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/ImportResponse.java b/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/ImportResponse.java new file mode 100644 index 000000000..cad79ae81 --- /dev/null +++ b/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/ImportResponse.java @@ -0,0 +1,12 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.xlsximport; + +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; + +public record ImportResponse( + ResponseEntity<byte[]> responseEntity, String statistics, Resource file) {} diff --git a/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/MultipartResponseAssertionTraits.java b/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/MultipartResponseAssertionTraits.java new file mode 100644 index 000000000..124866e5a --- /dev/null +++ b/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/MultipartResponseAssertionTraits.java @@ -0,0 +1,71 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.xlsximport; + +import de.cronn.assertions.validationfile.normalization.SimpleRegexReplacement; +import de.cronn.assertions.validationfile.normalization.ValidationNormalizer; +import de.cronn.assertions.validationfile.replacements.Replacer; +import de.eshg.base.spring.ResponseEntityValidationFileAssertionTraits; +import jakarta.activation.DataSource; +import jakarta.mail.BodyPart; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; +import java.util.Collections; +import java.util.Objects; +import java.util.stream.Collectors; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +public interface MultipartResponseAssertionTraits + extends ResponseEntityValidationFileAssertionTraits { + + default ValidationNormalizer contentLengthNormalizer() { + return new SimpleRegexReplacement("Content-Length: \\[\\d+\\]", "Content-Length: [[MASKED]]") + .and(new SimpleRegexReplacement("Content-Length: \\d+", "Content-Length: [MASKED]")); + } + + default void assertMultipartHeadersWithFile( + ResponseEntity<byte[]> response, ValidationNormalizer validationNormalizer) throws Exception { + + String multipartHeaders = renderMultipartHeaders(response); + + assertWithFileWithSuffix( + multipartHeaders, + validationNormalizer.and(multipartBoundaryNormalizer(response)), + "headers"); + } + + private static ValidationNormalizer multipartBoundaryNormalizer(ResponseEntity<?> response) { + MediaType contentType = Objects.requireNonNull(response.getHeaders().getContentType()); + return new Replacer( + "boundary=" + contentType.getParameter("boundary"), "boundary=[masked_boundary]"); + } + + private String renderMultipartHeaders(ResponseEntity<byte[]> response) throws Exception { + MediaType contentType = response.getHeaders().getContentType(); + DataSource datasource = new ByteArrayDataSource(response.getBody(), contentType.toString()); + MimeMultipart multipart = new MimeMultipart(datasource); + + StringBuilder sb = new StringBuilder(); + sb.append(renderHeaders(response)); + sb.append("\n"); + + for (int i = 0; i < multipart.getCount(); i++) { + BodyPart part = multipart.getBodyPart(i); + String headers = + Collections.list(part.getAllHeaders()).stream() + .map(header -> header.getName() + ": " + header.getValue()) + .sorted() + .collect(Collectors.joining("\n")); + + sb.append("\n"); + sb.append(headers); + sb.append("\n"); + } + + return sb.toString(); + } +} diff --git a/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/MultipartUtil.java b/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/MultipartUtil.java new file mode 100644 index 000000000..b8c03bc56 --- /dev/null +++ b/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/MultipartUtil.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.xlsximport; + +import static de.eshg.file.common.CustomMediaTypes.APPLICATION_XLSX_VALUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; + +import jakarta.mail.BodyPart; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; +import java.nio.file.Path; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +public class MultipartUtil { + + private MultipartUtil() {} + + public static MultiValueMap<String, Object> toMultipartFormDataRequest(Path filePath) { + MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); + body.add("file", new FileSystemResource(filePath)); + return body; + } + + public static ImportResponse parseImportResponseFromBody(ResponseEntity<byte[]> response) + throws Exception { + ByteArrayDataSource datasource = + new ByteArrayDataSource(response.getBody(), MULTIPART_FORM_DATA_VALUE); + MimeMultipart multipart = new MimeMultipart(datasource); + + assertThat(multipart.getCount()).isEqualTo(2); + return new ImportResponse(response, getStatistics(multipart), getFile(multipart)); + } + + private static String getStatistics(MimeMultipart multipart) throws Exception { + BodyPart part = multipart.getBodyPart(0); + assertThat(part.getContentType()).isEqualTo(APPLICATION_JSON_VALUE); + + return new String(part.getInputStream().readAllBytes()); + } + + private static Resource getFile(MimeMultipart multipart) throws Exception { + BodyPart part = multipart.getBodyPart(1); + assertThat(part.getContentType()).isEqualTo(APPLICATION_XLSX_VALUE); + String fileName = part.getFileName(); + + return new ByteArrayResource(part.getInputStream().readAllBytes()) { + @Override + public String getFilename() { + return fileName; + } + }; + } +} diff --git a/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/XlsxAssertionTraits.java b/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/XlsxAssertionTraits.java new file mode 100644 index 000000000..77b627743 --- /dev/null +++ b/backend/lib-xlsx-import/src/testFixtures/java/de/eshg/lib/xlsximport/XlsxAssertionTraits.java @@ -0,0 +1,95 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lib.xlsximport; + +import de.cronn.assertions.validationfile.junit5.JUnit5ValidationFileAssertions; +import de.cronn.assertions.validationfile.normalization.ValidationNormalizer; +import de.eshg.normalization.UuidNormalizer; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.core.io.Resource; + +public interface XlsxAssertionTraits extends JUnit5ValidationFileAssertions { + + Path getTempFile(String suffix); + + default void assertStatisticsWithFile(ImportResponse response) { + assertWithFileWithSuffix(response.statistics(), "statistics"); + } + + default void assertStatisticsWithFileWithSuffix(ImportResponse response, String suffix) { + assertWithFileWithSuffix(response.statistics(), suffix + "_statistics"); + } + + default void assertXlsxWithFile(ImportResponse response) throws Exception { + assertXlsxWithFile(response.file(), new UuidNormalizer(), "xlsx"); + } + + default void assertXlsxWithFileWithSuffix(ImportResponse response, String suffix) + throws Exception { + assertXlsxWithFile(response.file(), new UuidNormalizer(), suffix); + } + + default void assertXlsxWithFile( + Resource xlsx, ValidationNormalizer validationNormalizer, String suffix) throws Exception { + byte[] content = xlsx.getContentAsByteArray(); + Files.write(getTempFile(".xlsx"), content); + try (InputStream inputStream = new ByteArrayInputStream(content); + XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) { + StringBuilder sb = new StringBuilder(); + int sheetCounter = 1; + int rowCounter = 1; + for (Sheet sheet : workbook) { + append(sb, "-- sheet %d --".formatted(sheetCounter++)); + for (Row row : sheet) { + append(sb, "-- row %d --".formatted(rowCounter++)); + for (Cell cell : row) { + append(sb, getCellProperties(cell)); + } + } + } + assertWithFileWithSuffix(sb.toString(), validationNormalizer, suffix); + } + } + + private static void append(StringBuilder sb, String value) { + sb.append(value); + sb.append(System.lineSeparator()); + } + + private static String getCellProperties(Cell cell) { + String cellType = cell.getCellType().toString(); + String cellValue = getCellValue(cell); + String cellComment = getCellComment(cell); + + return "%s;%s;%s".formatted(cellType, cellValue, cellComment); + } + + private static String getCellValue(Cell cell) { + if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) { + return cell.getLocalDateTimeCellValue().toLocalDate().toString(); + } else if (cell.getCellType() == CellType.NUMERIC) { + return String.valueOf(cell.getNumericCellValue()); + } else { + return cell.getStringCellValue(); + } + } + + private static String getCellComment(Cell cell) { + if (cell.getCellComment() != null) { + return cell.getCellComment().getString().getString(); + } + return ""; + } +} diff --git a/backend/local-service-directory/gradle.lockfile b/backend/local-service-directory/gradle.lockfile index d131a8cf1..f41d90e0f 100644 --- a/backend/local-service-directory/gradle.lockfile +++ b/backend/local-service-directory/gradle.lockfile @@ -79,6 +79,7 @@ io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath, io.setl:rdf-urdna:1.1=compileClasspath io.smallrye.common:smallrye-common-annotation:2.2.0=compileClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/KeycloakProvisioning.java b/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/KeycloakProvisioning.java index 3fd4b699f..90d1cef42 100644 --- a/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/KeycloakProvisioning.java +++ b/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/KeycloakProvisioning.java @@ -129,11 +129,14 @@ public class KeycloakProvisioning { @PostConstruct void provisionRealm() { - boolean created = createOrUpdateRealm(); + configureRealm(); + + boolean isNew = + keycloakClient.getRealm().clients().findByClientId(keycloakClient.getClientId()).isEmpty(); configureUserProfile(); - if (created) { + if (isNew) { addEshgClientScope(); addClient(); @@ -168,8 +171,8 @@ public class KeycloakProvisioning { } } - public boolean createOrUpdateRealm() { - return keycloakClient.createOrUpdateRealm(this::applyRealmAttributes); + public void configureRealm() { + keycloakClient.configureRealm(this::applyRealmAttributes); } private void applyRealmAttributes(RealmRepresentation realmRepresentation) { diff --git a/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/LsdInitialSetupService.java b/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/LsdInitialSetupService.java new file mode 100644 index 000000000..0a14c5e4b --- /dev/null +++ b/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/LsdInitialSetupService.java @@ -0,0 +1,174 @@ +/* + * Copyright 2024 SCOOP Software GmbH, cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.lsd.keycloak; + +import static de.eshg.lsd.keycloak.LsdInitialSetupService.BEAN_NAME; + +import de.cronn.commons.lang.StreamUtil; +import de.eshg.lsd.keycloak.properties.LsdInternalKeycloakProperties; +import de.eshg.lsd.register.api.LsdClientKeycloakProperties; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.Optional; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RealmsResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component(BEAN_NAME) +public class LsdInitialSetupService { + public static final String BEAN_NAME = "lsdInitialSetupService"; + private static final String REALM_MANAGEMENT_CLIENT_ID = "realm-management"; + + private static final Logger log = LoggerFactory.getLogger(LsdInitialSetupService.class); + + private final LsdInternalKeycloakProperties internalProperties; + private final LsdClientKeycloakProperties clientProperties; + + public LsdInitialSetupService( + LsdInternalKeycloakProperties internalProperties, + LsdClientKeycloakProperties clientProperties) { + this.internalProperties = internalProperties; + this.clientProperties = clientProperties; + Keycloak keycloak = tryCreateKeycloakAdminCliClient(); + if (keycloak != null) { + try (keycloak) { + setupRealm(keycloak); + setupClient(keycloak); + } + } + } + + private void setupRealm(Keycloak keycloak) { + RealmsResource realmsResource = keycloak.realms(); + + if (realmsResource.findAll().stream() + .anyMatch(realm -> clientProperties.realm().equals(realm.getRealm()))) { + log.info("Found existing realm '{}'", clientProperties.realm()); + return; + } + + RealmRepresentation realmRepresentation = new RealmRepresentation(); + realmRepresentation.setDisplayName(internalProperties.realmDisplayName()); + realmRepresentation.setRealm(clientProperties.realm()); + realmRepresentation.setEnabled(true); + + realmsResource.create(realmRepresentation); + log.info("Created new realm '{}'", realmRepresentation.getRealm()); + keycloak.tokenManager().refreshToken(); + } + + private void setupClient(Keycloak keycloak) { + RealmResource realmResource = keycloak.realm(clientProperties.realm()); + + ClientsResource clientsResource = realmResource.clients(); + + ClientRepresentation lsdAdminClient = + getClientByClientId(clientsResource, internalProperties.adminClient().clientId()) + .map(ClientResource::toRepresentation) + .orElseGet(() -> tryCreateClient(clientsResource)); + + ClientResource realmManagementClient = + getClientByClientIdOrThrow(clientsResource, REALM_MANAGEMENT_CLIENT_ID); + List<RoleRepresentation> realmManagementRoles = realmManagementClient.roles().list(); + + ClientResource clientResource = clientsResource.get(lsdAdminClient.getId()); + UserResource serverAccountUser = + realmResource.users().get(clientResource.getServiceAccountUser().getId()); + + serverAccountUser + .roles() + .clientLevel(realmManagementClient.toRepresentation().getId()) + .add(realmManagementRoles); + + log.info( + "Added realm management roles to service account for client '{}'", + lsdAdminClient.getClientId()); + } + + private static Optional<ClientResource> getClientByClientId( + ClientsResource clients, String clientId) { + return clients.findByClientId(clientId).stream() + .filter(client -> clientId.equals(client.getClientId())) + .map(client -> clients.get(client.getId())) + .collect(StreamUtil.toSingleOptionalElement()); + } + + private static ClientResource getClientByClientIdOrThrow( + ClientsResource clients, String clientId) { + return getClientByClientId(clients, clientId) + .orElseThrow(() -> new IllegalStateException("Could not find client '" + clientId + "'")); + } + + private ClientRepresentation tryCreateClient(ClientsResource resource) { + ClientRepresentation client = getAdminClientRepresentation(); + + try (Response response = resource.create(client)) { + Response.StatusType statusInfo = response.getStatusInfo(); + if (statusInfo.getFamily() != Response.Status.Family.SUCCESSFUL) { + throw new IllegalStateException( + statusInfo.getStatusCode() + ": " + statusInfo.getReasonPhrase()); + } + } + + log.info("Created client '{}' for realm '{}'", client.getClientId(), clientProperties.realm()); + return getClientByClientIdOrThrow(resource, client.getClientId()).toRepresentation(); + } + + private Keycloak tryCreateKeycloakAdminCliClient() { + try { + Keycloak keycloak = + KeycloakBuilder.builder() + .serverUrl(internalProperties.url()) + .grantType(OAuth2Constants.PASSWORD) + .realm("master") + .clientId("admin-cli") + .username(internalProperties.adminUser().user()) + .password(internalProperties.adminUser().password()) + .build(); + + log.info( + "Connected to Keycloak on '{}'. Keycloak version: {}", + internalProperties.url(), + keycloak.serverInfo().getInfo().getSystemInfo().getVersion()); + + return keycloak; + } catch (Exception e) { + if (e.getCause() instanceof NotAuthorizedException) { + log.info("Could not login with admin user for initial setup"); + return null; + } + + throw e; + } + } + + private ClientRepresentation getAdminClientRepresentation() { + ClientRepresentation client = new ClientRepresentation(); + client.setClientId(internalProperties.adminClient().clientId()); + client.setSecret(internalProperties.adminClient().clientSecret()); + client.setEnabled(true); + client.setAuthorizationServicesEnabled(false); + client.setServiceAccountsEnabled(true); + client.setPublicClient(false); + client.setStandardFlowEnabled(false); + + client.setName("GA-Lotse Local Service Directory Client"); + client.setDescription("Used by the local service directory to manage actors in the directory."); + return client; + } +} diff --git a/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/LsdKeycloakClient.java b/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/LsdKeycloakClient.java index 8d3c2405f..26c742b93 100644 --- a/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/LsdKeycloakClient.java +++ b/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/LsdKeycloakClient.java @@ -32,6 +32,7 @@ import org.keycloak.admin.client.resource.*; import org.keycloak.representations.idm.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.DependsOn; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @@ -40,6 +41,7 @@ import org.springframework.stereotype.Component; * certificates for known users. */ @Component +@DependsOn(LsdInitialSetupService.BEAN_NAME) public class LsdKeycloakClient { private static final Logger log = LoggerFactory.getLogger(LsdKeycloakClient.class); private static final Duration TIMEOUT = Duration.ofSeconds(20); @@ -56,15 +58,13 @@ public class LsdKeycloakClient { String keycloakUrl = lsdInternalKeycloakProperties.url(); try { - // TODO(ISSUE-5445): Needs to be converted to client_credentials authorization keycloak = KeycloakBuilder.builder() .serverUrl(keycloakUrl) - .grantType(OAuth2Constants.PASSWORD) - .realm("master") - .clientId("admin-cli") - .username(lsdInternalKeycloakProperties.admin().user()) - .password(lsdInternalKeycloakProperties.admin().password()) + .grantType(OAuth2Constants.CLIENT_CREDENTIALS) + .realm(lsdClientKeycloakProperties.realm()) + .clientId(lsdInternalKeycloakProperties.adminClient().clientId()) + .clientSecret(lsdInternalKeycloakProperties.adminClient().clientSecret()) .resteasyClient(createClientWithTimeout()) .build(); @@ -132,42 +132,30 @@ public class LsdKeycloakClient { getRealm().roles().create(roleRepresentation); } - public boolean createOrUpdateRealm(Consumer<RealmRepresentation> realmUpdate) { // NOSONAR + public void configureRealm(Consumer<RealmRepresentation> realmUpdate) { // NOSONAR String realmName = getRealmName(); RealmRepresentation realmRepresentation = keycloak.realms().findAll().stream() .filter(realm -> realm.getRealm().equals(realmName)) .collect(StreamUtil.toSingleOptionalElement()) - .orElse(null); + .orElseThrow( + () -> new IllegalStateException("Could not find realm '" + realmName + "'")); - if (realmRepresentation == null) { - realmRepresentation = new RealmRepresentation(); - realmRepresentation.setRealm(realmName); - realmRepresentation.setEnabled(true); - realmUpdate.accept(realmRepresentation); - keycloak.realms().create(realmRepresentation); + String previousRealmAsJson = toJson(realmRepresentation); + realmUpdate.accept(realmRepresentation); + String updatedRealmAsJson = toJson(realmRepresentation); - log.info("Created Keycloak realm '{}'", realmName); - return true; + if (Objects.equals(previousRealmAsJson, updatedRealmAsJson)) { + log.debug("Keycloak realm '{}' already exists. No change necessary.", realmName); } else { - String previousRealmAsJson = toJson(realmRepresentation); - realmUpdate.accept(realmRepresentation); - String updatedRealmAsJson = toJson(realmRepresentation); - - if (Objects.equals(previousRealmAsJson, updatedRealmAsJson)) { - log.debug("Keycloak realm '{}' already exists. No change necessary.", realmName); - } else { - String realmConfigDiff = - Differ.calculateMultilineDiff(previousRealmAsJson, updatedRealmAsJson); - log.info( - "Keycloak realm '{}' already exists but update is required:\n{}", - realmName, - realmConfigDiff); - getRealm().update(realmRepresentation); - } - - return false; + String realmConfigDiff = + Differ.calculateMultilineDiff(previousRealmAsJson, updatedRealmAsJson); + log.info( + "Keycloak realm '{}' already exists but update is required:\n{}", + realmName, + realmConfigDiff); + getRealm().update(realmRepresentation); } } diff --git a/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/properties/LsdInternalKeycloakProperties.java b/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/properties/LsdInternalKeycloakProperties.java index 8ec119f65..8edf1db45 100644 --- a/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/properties/LsdInternalKeycloakProperties.java +++ b/backend/local-service-directory/src/main/java/de/eshg/lsd/keycloak/properties/LsdInternalKeycloakProperties.java @@ -14,7 +14,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; public record LsdInternalKeycloakProperties( String url, String realmDisplayName, - User admin, + AdminUser adminUser, + AdminClient adminClient, Duration eventExpiration, Duration sessionTimeout, boolean lenientPasswordPolicy) { @@ -25,5 +26,7 @@ public record LsdInternalKeycloakProperties( log.info("Local Service Directory Keycloak internal URL: {}", url); } - public record User(String user, String password) {} + public record AdminUser(String user, String password) {} + + public record AdminClient(String clientId, String clientSecret) {} } diff --git a/backend/local-service-directory/src/main/java/de/eshg/lsd/testhelper/LsdTestHelperService.java b/backend/local-service-directory/src/main/java/de/eshg/lsd/testhelper/LsdTestHelperService.java index a14f4bba1..d78bcd447 100644 --- a/backend/local-service-directory/src/main/java/de/eshg/lsd/testhelper/LsdTestHelperService.java +++ b/backend/local-service-directory/src/main/java/de/eshg/lsd/testhelper/LsdTestHelperService.java @@ -70,7 +70,7 @@ public class LsdTestHelperService implements TestHelperService { cachedAccessTokens.clear(); serviceDirectoryTestHelperClient.reset(); - keycloakProvisioning.createOrUpdateRealm(); + keycloakProvisioning.configureRealm(); keycloakProvisioning.deleteUsers(); keycloakProvisioning.addDummyUser(); diff --git a/backend/local-service-directory/src/main/resources/application-local.properties b/backend/local-service-directory/src/main/resources/application-local.properties new file mode 100644 index 000000000..c74744128 --- /dev/null +++ b/backend/local-service-directory/src/main/resources/application-local.properties @@ -0,0 +1,2 @@ +eshg.lsd-keycloak.internal.admin-user.password=admin +eshg.lsd-keycloak.internal.admin-client.client-secret=admin diff --git a/backend/local-service-directory/src/main/resources/application.properties b/backend/local-service-directory/src/main/resources/application.properties index 49446d016..4c58ba810 100644 --- a/backend/local-service-directory/src/main/resources/application.properties +++ b/backend/local-service-directory/src/main/resources/application.properties @@ -5,8 +5,8 @@ eshg.lsd-keycloak.client.realm=eshg-lsd eshg.lsd-keycloak.internal.realm-display-name=ESHG Local Service Directory eshg.lsd-keycloak.client.url=${eshg.keycloak.url} eshg.lsd-keycloak.internal.url=${eshg.keycloak.internal.url} -eshg.lsd-keycloak.internal.admin.user=admin -eshg.lsd-keycloak.internal.admin.password=admin +eshg.lsd-keycloak.internal.admin-user.user=admin +eshg.lsd-keycloak.internal.admin-client.client-id=system-local-service-directory eshg.lsd-keycloak.client.client-id=eshg-actor # TODO: throw out everything into a test-helper properties that is not for production eshg.lsd-keycloak.internal.event-expiration=P90D diff --git a/backend/measles-protection/build.gradle b/backend/measles-protection/build.gradle index 8cec5a07d..97b31c1ba 100644 --- a/backend/measles-protection/build.gradle +++ b/backend/measles-protection/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation project(':lib-calendar') implementation project(":lib-document-generator") implementation project(':business-module-persistence-commons') + implementation project(':file-commons') implementation 'org.apache.poi:poi:latest.release' implementation 'org.apache.poi:poi-ooxml:latest.release' @@ -37,4 +38,4 @@ tasks.named("test").configure { dependencyTrack { projectId = project.findProperty('dependency-track-project-id-measles-protection') ?: "unspecified" -} \ No newline at end of file +} diff --git a/backend/measles-protection/gradle.lockfile b/backend/measles-protection/gradle.lockfile index f4c3576cd..21acf53e6 100644 --- a/backend/measles-protection/gradle.lockfile +++ b/backend/measles-protection/gradle.lockfile @@ -16,11 +16,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.virtuald:curvesapi:1.08=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -29,7 +29,7 @@ com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=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 @@ -38,11 +38,11 @@ com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath, com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,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=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -53,10 +53,10 @@ de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeCla de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.rototor.pdfbox:graphics2d:3.0.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.github.openhtmltopdf:openhtmltopdf-core:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.github.openhtmltopdf:openhtmltopdf-pdfbox:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -75,6 +75,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -85,16 +86,16 @@ jakarta.transaction:jakarta.transaction-api:2.0.1=annotationProcessor,compileCla jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.14.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.26.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -145,7 +146,7 @@ org.apache.xmlgraphics:batik-xml:1.17=productionRuntimeClasspath,runtimeClasspat org.apache.xmlgraphics:xmlgraphics-commons:2.9=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcmail-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -158,8 +159,8 @@ org.freemarker:freemarker:2.3.33=productionRuntimeClasspath,runtimeClasspath,tes org.glassfish.jaxb:jaxb-core:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -171,15 +172,15 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath @@ -187,12 +188,12 @@ org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspa org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -202,7 +203,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -218,7 +219,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -241,14 +242,14 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.testcontainers:database-commons:1.19.8=testRuntimeClasspath org.testcontainers:jdbc:1.19.8=testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath diff --git a/backend/measles-protection/openApi.yaml b/backend/measles-protection/openApi.yaml index 726bed20c..b0a2fe33b 100644 --- a/backend/measles-protection/openApi.yaml +++ b/backend/measles-protection/openApi.yaml @@ -5212,6 +5212,7 @@ components: enum: - PATIENT - PARENT + - PROFESSIONAL Population: type: object properties: @@ -5384,6 +5385,9 @@ components: - TM_VACCINATION_CONSULTATION - MEASLES_PROTECTION - STI_PROTECTION + - MEDICAL_REGISTRY_ENTRY + - MEDICAL_REGISTRY_CITIZEN_DRAFT + - MEDICAL_REGISTRY_EMPLOYEE_DRAFT ProcedureWithDuration: type: object properties: diff --git a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/api/AffectedPersonDto.java b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/api/AffectedPersonDto.java index 3f26dbd41..d62907eb3 100644 --- a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/api/AffectedPersonDto.java +++ b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/api/AffectedPersonDto.java @@ -5,10 +5,10 @@ package de.eshg.measlesprotection.api; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; @@ -32,7 +32,7 @@ public record AffectedPersonDto( LocalDate dateOfBirth, List<@NotBlank String> phoneNumbers, List<@Email String> emailAddresses, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, GenderDto gender, String nameAtBirth, String placeOfBirth, diff --git a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/api/draft/AffectedPersonDetailsDto.java b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/api/draft/AffectedPersonDetailsDto.java index db1313ba9..f437893f9 100644 --- a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/api/draft/AffectedPersonDetailsDto.java +++ b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/api/draft/AffectedPersonDetailsDto.java @@ -5,10 +5,10 @@ package de.eshg.measlesprotection.api.draft; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; @@ -30,7 +30,7 @@ public record AffectedPersonDetailsDto( LocalDate dateOfBirth, List<@NotBlank String> phoneNumbers, List<@Email String> emailAddresses, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, GenderDto gender, String nameAtBirth, String placeOfBirth, diff --git a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/pdf/coverletter/CoverLetterService.java b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/pdf/coverletter/CoverLetterService.java index 39ad4ccf3..0a11771d8 100644 --- a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/pdf/coverletter/CoverLetterService.java +++ b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/pdf/coverletter/CoverLetterService.java @@ -6,9 +6,9 @@ package de.eshg.measlesprotection.pdf.coverletter; import de.eshg.lib.document.generator.DocumentGenerator; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.Pdf; import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.lib.procedure.file.FileFactory; import java.io.ByteArrayOutputStream; import java.time.Clock; @@ -39,7 +39,7 @@ public class CoverLetterService { ZonedDateTime now = ZonedDateTime.now(clock); String name = name(data); return FileFactory.createPdfWithMetaData( - filename(name, now), FileType.PDF, bytes, pdfMetaData(now, name), false); + filename(name, now), ProcedureFileType.PDF, bytes, pdfMetaData(now, name), false); } private static String name(CoverLetterData data) { diff --git a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/testhelper/ProtectionProcedurePopulator.java b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/testhelper/ProtectionProcedurePopulator.java index 01dcebe11..ff256a72a 100644 --- a/backend/measles-protection/src/main/java/de/eshg/measlesprotection/testhelper/ProtectionProcedurePopulator.java +++ b/backend/measles-protection/src/main/java/de/eshg/measlesprotection/testhelper/ProtectionProcedurePopulator.java @@ -7,7 +7,6 @@ package de.eshg.measlesprotection.testhelper; import static de.eshg.base.util.ClassNameUtil.getClassNameAsPropertyKey; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; @@ -15,6 +14,7 @@ import de.eshg.base.address.DomesticAddressDto; import de.eshg.base.centralfile.api.DataOriginDto; import de.eshg.base.centralfile.api.facility.AddFacilityFileStateRequest; import de.eshg.base.centralfile.api.facility.FacilityContactPersonDto; +import de.eshg.lib.common.CountryCode; import de.eshg.measlesprotection.DraftProtectionProcedureController; import de.eshg.measlesprotection.api.MPFacilityTypeDto; import de.eshg.measlesprotection.api.ReportDataDto; @@ -102,7 +102,7 @@ public class ProtectionProcedurePopulator extends BasePopulator<OpenProcedureRes dateOfBirth(faker, childAge(faker)), List.of(faker.phoneNumber().phoneNumber()), List.of(faker.internet().emailAddress()), - CountryCodeDto.DE, + CountryCode.DE, GenderDto.NOT_SPECIFIED, lastName, address.city(), @@ -191,7 +191,7 @@ public class ProtectionProcedurePopulator extends BasePopulator<OpenProcedureRes private static DomesticAddressDto domesticAddress(Address address) { return new DomesticAddressDto( - CountryCodeDto.valueOf(address.countryCode()), + CountryCode.valueOf(address.countryCode()), address.city(), address.zipCode(), null, diff --git a/backend/measles-protection/src/main/resources/migrations/0024_introduce_procedure_file_type.xml b/backend/measles-protection/src/main/resources/migrations/0024_introduce_procedure_file_type.xml new file mode 100644 index 000000000..90a5df65b --- /dev/null +++ b/backend/measles-protection/src/main/resources/migrations/0024_introduce_procedure_file_type.xml @@ -0,0 +1,11 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1728921018785-1"> + <ext:renamePostgresEnumType oldName="fileType" newName="procedureFileType"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/measles-protection/src/main/resources/migrations/0025_medical_registry_procedure_types.xml b/backend/measles-protection/src/main/resources/migrations/0025_medical_registry_procedure_types.xml new file mode 100644 index 000000000..21b419fae --- /dev/null +++ b/backend/measles-protection/src/main/resources/migrations/0025_medical_registry_procedure_types.xml @@ -0,0 +1,14 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1728647075086-1"> + <ext:addPostgresEnumValues enumTypeName="persontype" valuesToAdd="PROFESSIONAL"/> + </changeSet> + <changeSet author="GA-Lotse" id="1728647075086-2"> + <ext:addPostgresEnumValues enumTypeName="proceduretype" valuesToAdd="MEDICAL_REGISTRY_CITIZEN_DRAFT, MEDICAL_REGISTRY_EMPLOYEE_DRAFT, MEDICAL_REGISTRY_ENTRY"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/measles-protection/src/main/resources/migrations/changelog.xml b/backend/measles-protection/src/main/resources/migrations/changelog.xml index c33b1043b..d930f3bcc 100644 --- a/backend/measles-protection/src/main/resources/migrations/changelog.xml +++ b/backend/measles-protection/src/main/resources/migrations/changelog.xml @@ -31,5 +31,7 @@ <include file="migrations/0021_add_procedure_exported_at.xml"/> <include file="migrations/0022_add_consultant_to_appointment_blocks.xml"/> <include file="migrations/0023_procedure_sequences.xml"/> + <include file="migrations/0024_introduce_procedure_file_type.xml"/> + <include file="migrations/0025_medical_registry_procedure_types.xml"/> </databaseChangeLog> diff --git a/backend/medical-registry/gradle.lockfile b/backend/medical-registry/gradle.lockfile index ffc6f5123..8c8378183 100644 --- a/backend/medical-registry/gradle.lockfile +++ b/backend/medical-registry/gradle.lockfile @@ -16,11 +16,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -28,7 +28,7 @@ com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=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 @@ -37,23 +37,24 @@ com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath, com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.16.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-jakarta9:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -67,6 +68,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -77,19 +79,19 @@ jakarta.transaction:jakarta.transaction-api:2.0.1=annotationProcessor,compileCla jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.14.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.26.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.12.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -112,7 +114,7 @@ org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=compileClasspath,producti org.apache.tomcat:tomcat-annotations-api:10.1.30=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcmail-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -124,8 +126,8 @@ org.eclipse.angus:jakarta.mail:2.0.3=productionRuntimeClasspath,runtimeClasspath org.glassfish.jaxb:jaxb-core:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -137,15 +139,15 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath @@ -153,12 +155,12 @@ org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspa org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -168,7 +170,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -184,7 +186,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -207,14 +209,14 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.testcontainers:database-commons:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:jdbc:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testCompileClasspath,testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath diff --git a/backend/medical-registry/openApi.yaml b/backend/medical-registry/openApi.yaml index d6bc1ae86..eae3d1475 100644 --- a/backend/medical-registry/openApi.yaml +++ b/backend/medical-registry/openApi.yaml @@ -550,6 +550,26 @@ paths: summary: Update status of inbox procedure tags: - InboxProcedure + /medical-registry-entries/{procedureId}: + get: + operationId: getProcedure + parameters: + - in: path + name: procedureId + required: true + schema: + type: string + format: uuid + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/GetMedicalRegistryProcedureResponse" + description: OK + summary: Get medical registry procedure by id. + tags: + - MedicalRegistry /procedure-metrics: get: operationId: getProcedureMetrics @@ -2182,6 +2202,17 @@ components: - country - postalCode - street + EmploymentStatus: + type: string + enum: + - SELF_EMPLOYED + - FREELANCE + - EMPLOYEE + EmploymentType: + type: string + enum: + - FULL_TIME + - PART_TIME ExportArchivingRelevantProceduresRequest: type: object properties: @@ -2483,6 +2514,32 @@ components: type: array items: $ref: "#/components/schemas/ManualProgressEntryHistory" + GetMedicalRegistryProcedureResponse: + type: object + properties: + consentToPrivacyPolicy: + type: boolean + employeesEmployed: + type: boolean + id: + type: string + format: uuid + practice: + $ref: "#/components/schemas/Practice" + professional: + $ref: "#/components/schemas/Professional" + requestForWrittenConfirmation: + type: boolean + version: + type: integer + format: int64 + required: + - consentToPrivacyPolicy + - employeesEmployed + - id + - professional + - requestForWrittenConfirmation + - version GetMetaDataHistoryResponse: type: object properties: @@ -2983,6 +3040,22 @@ components: - EMAIL - IMAGE - DOCUMENT + MedicalRegistryAddress: + type: object + properties: + city: + type: string + houseNumber: + type: string + postalCode: + type: string + street: + type: string + required: + - city + - houseNumber + - postalCode + - street MetaData: type: object discriminator: @@ -3167,6 +3240,7 @@ components: enum: - PATIENT - PARENT + - PROFESSIONAL Population: type: object properties: @@ -3221,6 +3295,38 @@ components: - country - postalCode - postbox + Practice: + type: object + properties: + address: + $ref: "#/components/schemas/MedicalRegistryAddress" + emailAddress: + type: string + maxLength: 254 + minLength: 6 + establishmentNumber: + type: string + healthInsuranceAuthorization: + type: boolean + institutionIdentifier: + type: string + name: + type: string + maxLength: 300 + minLength: 1 + openingHours: + type: string + phoneNumber: + type: string + maxLength: 23 + minLength: 1 + website: + type: string + maxLength: 254 + minLength: 6 + required: + - healthInsuranceAuthorization + - name Procedure: type: object properties: @@ -3314,6 +3420,9 @@ components: - TM_VACCINATION_CONSULTATION - MEASLES_PROTECTION - STI_PROTECTION + - MEDICAL_REGISTRY_ENTRY + - MEDICAL_REGISTRY_CITIZEN_DRAFT + - MEDICAL_REGISTRY_EMPLOYEE_DRAFT ProcedureWithDuration: type: object properties: @@ -3359,6 +3468,117 @@ components: - inboxProgressEntryType - modifiedAt - progressEntryId + Professional: + type: object + properties: + address: + $ref: "#/components/schemas/MedicalRegistryAddress" + approbationGrantedOn: + type: string + format: date + approbationIssuingAuthority: + type: string + dateOfBirth: + type: string + format: date + emailAddress: + type: string + maxLength: 254 + minLength: 6 + employmentStatus: + $ref: "#/components/schemas/EmploymentStatus" + employmentType: + $ref: "#/components/schemas/EmploymentType" + fieldOfExpertise: + type: string + firstName: + type: string + maxLength: 80 + minLength: 1 + furtherTraining: + type: string + gender: + $ref: "#/components/schemas/Gender" + lastName: + type: string + maxLength: 120 + minLength: 1 + lifetimeDoctorNumber: + type: string + nameAtBirth: + type: string + maxLength: 40 + minLength: 1 + nationality: + $ref: "#/components/schemas/CountryCode" + phoneNumber: + type: string + maxLength: 23 + minLength: 1 + placeOfBirth: + type: string + maxLength: 50 + minLength: 1 + professionalTitle: + $ref: "#/components/schemas/ProfessionalTitle" + qualifications: + type: string + specialistTitle: + type: string + title: + type: string + maxLength: 119 + minLength: 1 + required: + - approbationGrantedOn + - approbationIssuingAuthority + - dateOfBirth + - employmentStatus + - employmentType + - firstName + - lastName + - nationality + - professionalTitle + ProfessionalTitle: + type: string + enum: + - DOCTORS + - DENTISTS + - PSYCHOLOGICAL_PSYCHOTHERAPISTS + - NURSING_ASSISTANTS + - GERIATRIC_NURSES + - DIETICIANS + - DISINFECTORS + - OCCUPATIONAL_THERAPISTS + - HEALTH_SUPERVISORS + - HEALTHCARE_AND_PEDIATRIC_NURSES + - HEALTHCARE_AND_NURSING_ASSISTANTS + - HEALTHCARE_AND_NURSING_ASSISTANTS_HELPER + - MIDWIVES_MATERNITY_NURSES + - ALTERNATIVE_PRACTITIONERS + - NON_MEDICAL_PRACTITIONER_FOR_CHIROPRACTIC + - ALTERNATIVE_PRACTITIONER_FOR_SPEECH_THERAPY + - NON_MEDICAL_PRACTITIONER_FOR_PHYSIOTHERAPY + - NON_MEDICAL_PRACTITIONERS_FOR_PSYCHOTHERAPY + - CHILD_AND_YOUTH_PSYCHOTHERAPISTS + - SPEECH_THERAPISTS + - MASSEURS_AND_MEDICAL_BATH_ATTENDANTS + - MEDICAL_DOCUMENTALISTS + - MEDICAL_TECHNICAL_LABORATORY_ASSISTANTS + - MEDICAL_TECHNICAL_RADIOLOGY_ASSISTANTS + - MEDICAL_TECHNICAL_ASSISTANTS_FOR_FUNCTIONAL_DIAGNOSTICS + - EMERGENCY_PARAMEDICS + - ORTHOPTISTS + - CARE_ASSISTANTS + - NURSING_SERVICES + - NURSING_SERVICE_MANAGERS + - PHARMACEUTICAL_TECHNICAL_ASSISTANTS + - PHYSIOTHERAPISTS + - PODIATRISTS + - RADIOLOGY_ASSISTANTS + - SPORTS_THERAPISTS + - PHARMACISTS + - VETERINARIANS ProgressEntry: type: object discriminator: diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/MedicalRegistryController.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/MedicalRegistryController.java new file mode 100644 index 000000000..48e7b614b --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/MedicalRegistryController.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry; + +import de.cronn.commons.lang.StreamUtil; +import de.eshg.base.centralfile.api.person.GetPersonFileStateResponse; +import de.eshg.medicalregistry.api.*; +import de.eshg.medicalregistry.domain.model.MedicalRegistryEntry; +import de.eshg.medicalregistry.domain.model.Practice; +import de.eshg.medicalregistry.domain.model.Professional; +import de.eshg.medicalregistry.mapper.PracticeMapper; +import de.eshg.medicalregistry.mapper.ProfessionalMapper; +import de.eshg.rest.service.security.config.BaseUrls; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.UUID; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(MedicalRegistryController.BASE_URL) +@Tag(name = "MedicalRegistry") +public class MedicalRegistryController { + + public static final String BASE_URL = BaseUrls.MedicalRegistry.MEDICAL_REGISTRY_CONTROLLER; + + private final MedicalRegistryService medicalRegistryService; + + public MedicalRegistryController(MedicalRegistryService medicalRegistryService) { + this.medicalRegistryService = medicalRegistryService; + } + + @PostMapping + @Transactional + @Hidden // TODO currently for testing purposes only + public UUID createProcedure(@Valid @RequestBody CreateProcedureRequest procedure) { + MedicalRegistryEntry persistedProcedure = medicalRegistryService.createProcedure(procedure); + return persistedProcedure.getExternalId(); + } + + @GetMapping("/{procedureId}") + @Transactional(readOnly = true) + @Operation(summary = "Get medical registry procedure by id.") + public GetProcedureResponse getProcedure(@PathVariable("procedureId") UUID procedureId) { + MedicalRegistryEntry medicalRegistryEntry = + medicalRegistryService.findProcedureByExternalId(procedureId); + + Professional professional = + medicalRegistryEntry.getRelatedPersons().stream().collect(StreamUtil.toSingleElement()); + GetPersonFileStateResponse professionalDetails = + medicalRegistryService.findProfessionalDetails(professional.getCentralFileStateId()); + + PracticeDto practiceDto = + medicalRegistryEntry.getRelatedFacilities().stream() + .collect(StreamUtil.toSingleOptionalElement()) + .map(this::mapToDto) + .orElse(null); + + return new GetProcedureResponse( + medicalRegistryEntry.getExternalId(), + medicalRegistryEntry.getVersion(), + ProfessionalMapper.mapToDto(professional, professionalDetails), + practiceDto, + medicalRegistryEntry.isEmployeesEmployed(), + medicalRegistryEntry.isConsentToPrivacyPolicy(), + medicalRegistryEntry.isRequestForWrittenConfirmation()); + } + + private PracticeDto mapToDto(Practice p) { + return PracticeMapper.mapToDto( + p, medicalRegistryService.findPracticeDetails(p.getCentralFileStateId())); + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/MedicalRegistryService.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/MedicalRegistryService.java new file mode 100644 index 000000000..33390200d --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/MedicalRegistryService.java @@ -0,0 +1,184 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry; + +import static de.eshg.lib.procedure.mapping.FacilityTypeMapper.*; +import static de.eshg.medicalregistry.mapper.ProfessionalMapper.*; + +import de.eshg.base.address.DomesticAddressDto; +import de.eshg.base.centralfile.FacilityApi; +import de.eshg.base.centralfile.PersonApi; +import de.eshg.base.centralfile.api.DataOriginDto; +import de.eshg.base.centralfile.api.facility.AddFacilityFileStateRequest; +import de.eshg.base.centralfile.api.facility.AddFacilityFileStateResponse; +import de.eshg.base.centralfile.api.facility.GetFacilityFileStateResponse; +import de.eshg.base.centralfile.api.person.AddPersonFileStateRequest; +import de.eshg.base.centralfile.api.person.AddPersonFileStateResponse; +import de.eshg.base.centralfile.api.person.GetPersonFileStateResponse; +import de.eshg.lib.auditlog.AuditLogger; +import de.eshg.lib.common.CountryCode; +import de.eshg.lib.procedure.domain.model.ProcedureStatus; +import de.eshg.lib.procedure.domain.model.ProcedureType; +import de.eshg.medicalregistry.api.*; +import de.eshg.medicalregistry.domain.model.MedicalRegistryEntry; +import de.eshg.medicalregistry.domain.model.Practice; +import de.eshg.medicalregistry.domain.model.Professional; +import de.eshg.medicalregistry.domain.registry.MedicalRegistryEntryRepository; +import de.eshg.rest.service.error.NotFoundException; +import java.time.Clock; +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; +import org.springframework.stereotype.Service; + +@Service +public class MedicalRegistryService { + + private final MedicalRegistryEntryRepository medicalRegistryEntryRepository; + private final PersonApi personApi; + private final FacilityApi facilityApi; + private final AuditLogger auditLogger; + private final Clock clock; + + public MedicalRegistryService( + MedicalRegistryEntryRepository medicalRegistryEntryRepository, + PersonApi personApi, + FacilityApi facilityApi, + AuditLogger auditLogger, + Clock clock) { + this.medicalRegistryEntryRepository = medicalRegistryEntryRepository; + this.personApi = personApi; + this.facilityApi = facilityApi; + this.auditLogger = auditLogger; + this.clock = clock; + } + + public MedicalRegistryEntry findProcedureByExternalId(UUID procedureId) { + return medicalRegistryEntryRepository + .findByExternalId(procedureId) + .orElseThrow(notFoundException(procedureId)); + } + + private static Supplier<NotFoundException> notFoundException(UUID procedureId) { + return () -> + new NotFoundException( + "%s with UUID %s not found" + .formatted(MedicalRegistryEntry.class.getSimpleName(), procedureId)); + } + + public GetPersonFileStateResponse findProfessionalDetails(UUID externalId) { + return personApi.getPersonFileState(externalId); + } + + public GetFacilityFileStateResponse findPracticeDetails(UUID externalId) { + return facilityApi.getFacilityFileState(externalId); + } + + public MedicalRegistryEntry createProcedure(CreateProcedureRequest request) { + CreateProcedureDto procedure = request.procedure(); + ProfessionalDto professional = procedure.professional(); + PracticeDto practice = procedure.practice(); + + MedicalRegistryEntry medicalRegistryEntry = new MedicalRegistryEntry(); + medicalRegistryEntry.setConsentToPrivacyPolicy(procedure.consentToPrivacyPolicy()); + medicalRegistryEntry.setEmployeesEmployed(procedure.employeesEmployed()); + medicalRegistryEntry.setRequestForWrittenConfirmation( + procedure.requestForWrittenConfirmation()); + + UUID personId = createPersonInCentralFile(professional); + + Professional professionalEntity = new Professional(); + professionalEntity.setCentralFileStateId(personId); + professionalEntity.setProfessionalTitle(mapToDomain(professional.professionalTitle())); + professionalEntity.setFieldOfExpertise(professional.fieldOfExpertise()); + professionalEntity.setSpecialistTitle(professional.specialistTitle()); + professionalEntity.setFurtherTraining(professional.furtherTraining()); + professionalEntity.setQualifications(professional.qualifications()); + professionalEntity.setLifetimeDoctorNumber(professional.lifetimeDoctorNumber()); + professionalEntity.setApprobationGrantedOn(professional.approbationGrantedOn()); + professionalEntity.setApprobationIssuingAuthority(professional.approbationIssuingAuthority()); + professionalEntity.setEmploymentType(mapToDomain(professional.employmentType())); + professionalEntity.setEmploymentStatus(mapToDomain(professional.employmentStatus())); + professionalEntity.setNationality(professional.nationality()); + medicalRegistryEntry.addRelatedPerson(professionalEntity); + + if (practice != null) { + UUID facilityExternalId = createFacilityInCentralFile(practice); + + Practice practiceEntity = new Practice(); + practiceEntity.setCentralFileStateId(facilityExternalId); + practiceEntity.setWebsite(practice.website()); + practiceEntity.setInstitutionIdentifier(practice.institutionIdentifier()); + practiceEntity.setEstablishmentNumber(practice.establishmentNumber()); + practiceEntity.setHealthInsuranceAuthorization(practice.healthInsuranceAuthorization()); + practiceEntity.setOpeningHours(practice.openingHours()); + medicalRegistryEntry.addRelatedFacility(practiceEntity); + } + + medicalRegistryEntry.setProcedureType(ProcedureType.MEDICAL_REGISTRY_CITIZEN_DRAFT); + medicalRegistryEntry.updateProcedureStatus(ProcedureStatus.DRAFT, clock, auditLogger); + return medicalRegistryEntryRepository.save(medicalRegistryEntry); + } + + private UUID createPersonInCentralFile(ProfessionalDto professional) { + AddPersonFileStateResponse addPersonResponse = + personApi.addPersonFileState( + new AddPersonFileStateRequest( + null, + professional.title(), + null, + professional.gender(), + professional.firstName(), + professional.lastName(), + professional.dateOfBirth(), + professional.nameAtBirth(), + professional.placeOfBirth(), + null, + toList(professional.emailAddress()), + toList(professional.phoneNumber()), + mapAddress(professional.address()), + null, + DataOriginDto.MANUAL)); + + return addPersonResponse.id(); + } + + private UUID createFacilityInCentralFile(PracticeDto practice) { + AddFacilityFileStateResponse addFacilityResponse = + facilityApi.addFacilityFileState( + new AddFacilityFileStateRequest( + null, + practice.name(), + toList(practice.emailAddress()), + toList(practice.phoneNumber()), + null, + mapAddress(practice.address()), + null, + DataOriginDto.MANUAL, + null)); + + return addFacilityResponse.id(); + } + + private static DomesticAddressDto mapAddress(AddressDto address) { + if (address == null) { + return null; + } + + return new DomesticAddressDto( + CountryCode.DE, + address.city(), + address.postalCode(), + null, + address.street(), + address.houseNumber(), + null); + } + + private static List<String> toList(String value) { + return value == null ? List.of() : List.of(value); + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/AddressDto.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/AddressDto.java new file mode 100644 index 000000000..6a408d83c --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/AddressDto.java @@ -0,0 +1,16 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(name = "MedicalRegistryAddress") +public record AddressDto( + @NotNull String street, + @NotNull String houseNumber, + @NotNull String postalCode, + @NotNull String city) {} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/CreateProcedureDto.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/CreateProcedureDto.java new file mode 100644 index 000000000..fcf935f3d --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/CreateProcedureDto.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +@Schema(name = "CreateMedicalRegistryProcedure") +public record CreateProcedureDto( + @NotNull @Valid ProfessionalDto professional, + @Valid PracticeDto practice, + @NotNull boolean employeesEmployed, + @NotNull boolean consentToPrivacyPolicy, + @NotNull boolean requestForWrittenConfirmation) {} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/CreateProcedureRequest.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/CreateProcedureRequest.java new file mode 100644 index 000000000..d34826ada --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/CreateProcedureRequest.java @@ -0,0 +1,13 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +@Schema(name = "CreateMedicalRegistryProcedureRequest") +public record CreateProcedureRequest(@Valid @NotNull CreateProcedureDto procedure) {} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/EmploymentStatusDto.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/EmploymentStatusDto.java new file mode 100644 index 000000000..d6b39494c --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/EmploymentStatusDto.java @@ -0,0 +1,15 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.api; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "EmploymentStatus") +public enum EmploymentStatusDto { + SELF_EMPLOYED, + FREELANCE, + EMPLOYEE, +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/EmploymentTypeDto.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/EmploymentTypeDto.java new file mode 100644 index 000000000..a73ef83b3 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/EmploymentTypeDto.java @@ -0,0 +1,14 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.api; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "EmploymentType") +public enum EmploymentTypeDto { + FULL_TIME, + PART_TIME, +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/GetProcedureResponse.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/GetProcedureResponse.java new file mode 100644 index 000000000..2b723bf5d --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/GetProcedureResponse.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +@Schema(name = "GetMedicalRegistryProcedureResponse") +public record GetProcedureResponse( + @NotNull UUID id, + @NotNull long version, + @NotNull @Valid ProfessionalDto professional, + @Valid PracticeDto practice, + @NotNull boolean employeesEmployed, + @NotNull boolean consentToPrivacyPolicy, + @NotNull boolean requestForWrittenConfirmation) {} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/PracticeDto.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/PracticeDto.java new file mode 100644 index 000000000..0e72ef082 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/PracticeDto.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Schema(name = "Practice") +public record PracticeDto( + @NotNull @Size(min = 1, max = 300) String name, + @Size(min = 6, max = 254) String emailAddress, + @Size(min = 1, max = 23) String phoneNumber, + @Valid AddressDto address, + @Size(min = 6, max = 254) String website, + String institutionIdentifier, + String establishmentNumber, + @NotNull boolean healthInsuranceAuthorization, + String openingHours) { + public PracticeDto(String name, boolean healthInsuranceAuthorization) { + this(name, null, null, null, null, null, null, healthInsuranceAuthorization, null); + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/ProfessionalDto.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/ProfessionalDto.java new file mode 100644 index 000000000..04b21e7a5 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/ProfessionalDto.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.api; + +import de.eshg.base.GenderDto; +import de.eshg.lib.common.CountryCode; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDate; + +@Schema(name = "Professional") +public record ProfessionalDto( + @Size(min = 1, max = 119) String title, + GenderDto gender, + @NotNull @Size(min = 1, max = 80) String firstName, + @NotNull @Size(min = 1, max = 120) String lastName, + @NotNull LocalDate dateOfBirth, + @Size(min = 1, max = 40) String nameAtBirth, + @Size(min = 1, max = 50) String placeOfBirth, + @Size(min = 6, max = 254) String emailAddress, + @Size(min = 1, max = 23) String phoneNumber, + @Valid AddressDto address, + @NotNull ProfessionalTitleDto professionalTitle, + String fieldOfExpertise, + String specialistTitle, + String furtherTraining, + String qualifications, + @NotNull LocalDate approbationGrantedOn, + @NotNull String approbationIssuingAuthority, + String lifetimeDoctorNumber, + @NotNull EmploymentTypeDto employmentType, + @NotNull EmploymentStatusDto employmentStatus, + @NotNull CountryCode nationality) { + public ProfessionalDto( + @NotNull @Size(min = 1, max = 80) String firstName, + @NotNull @Size(min = 1, max = 120) String lastName, + @NotNull LocalDate dateOfBirth, + @NotNull ProfessionalTitleDto professionalTitle, + @NotNull LocalDate approbationGrantedOn, + @NotNull String approbationIssuingAuthority, + @NotNull EmploymentTypeDto employmentType, + @NotNull EmploymentStatusDto employmentStatus, + @NotNull CountryCode nationality) { + this( + null, + null, + firstName, + lastName, + dateOfBirth, + null, + null, + null, + null, + null, + professionalTitle, + null, + null, + null, + null, + approbationGrantedOn, + approbationIssuingAuthority, + null, + employmentType, + employmentStatus, + nationality); + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/ProfessionalTitleDto.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/ProfessionalTitleDto.java new file mode 100644 index 000000000..7d8e87274 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/api/ProfessionalTitleDto.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.api; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "ProfessionalTitle") +public enum ProfessionalTitleDto { + DOCTORS, + DENTISTS, + PSYCHOLOGICAL_PSYCHOTHERAPISTS, + NURSING_ASSISTANTS, + GERIATRIC_NURSES, + DIETICIANS, + DISINFECTORS, + OCCUPATIONAL_THERAPISTS, + HEALTH_SUPERVISORS, + HEALTHCARE_AND_PEDIATRIC_NURSES, + HEALTHCARE_AND_NURSING_ASSISTANTS, + HEALTHCARE_AND_NURSING_ASSISTANTS_HELPER, + MIDWIVES_MATERNITY_NURSES, + ALTERNATIVE_PRACTITIONERS, + NON_MEDICAL_PRACTITIONER_FOR_CHIROPRACTIC, + ALTERNATIVE_PRACTITIONER_FOR_SPEECH_THERAPY, + NON_MEDICAL_PRACTITIONER_FOR_PHYSIOTHERAPY, + NON_MEDICAL_PRACTITIONERS_FOR_PSYCHOTHERAPY, + CHILD_AND_YOUTH_PSYCHOTHERAPISTS, + SPEECH_THERAPISTS, + MASSEURS_AND_MEDICAL_BATH_ATTENDANTS, + MEDICAL_DOCUMENTALISTS, + MEDICAL_TECHNICAL_LABORATORY_ASSISTANTS, + MEDICAL_TECHNICAL_RADIOLOGY_ASSISTANTS, + MEDICAL_TECHNICAL_ASSISTANTS_FOR_FUNCTIONAL_DIAGNOSTICS, + EMERGENCY_PARAMEDICS, + ORTHOPTISTS, + CARE_ASSISTANTS, + NURSING_SERVICES, + NURSING_SERVICE_MANAGERS, + PHARMACEUTICAL_TECHNICAL_ASSISTANTS, + PHYSIOTHERAPISTS, + PODIATRISTS, + RADIOLOGY_ASSISTANTS, + SPORTS_THERAPISTS, + PHARMACISTS, + VETERINARIANS, +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/business/model/ProcedureDetailsData.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/business/model/ProcedureDetailsData.java new file mode 100644 index 000000000..3e6d1a012 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/business/model/ProcedureDetailsData.java @@ -0,0 +1,8 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.business.model; + +public class ProcedureDetailsData {} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/EmploymentStatus.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/EmploymentStatus.java new file mode 100644 index 000000000..1ddebf305 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/EmploymentStatus.java @@ -0,0 +1,12 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.domain.model; + +public enum EmploymentStatus { + SELF_EMPLOYED, + FREELANCE, + EMPLOYEE +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/EmploymentType.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/EmploymentType.java new file mode 100644 index 000000000..da05806dc --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/EmploymentType.java @@ -0,0 +1,11 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.domain.model; + +public enum EmploymentType { + FULL_TIME, + PART_TIME, +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Facility.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Facility.java deleted file mode 100644 index b053d657c..000000000 --- a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Facility.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: Apache-2.0 - */ - -package de.eshg.medicalregistry.domain.model; - -import de.eshg.lib.procedure.domain.model.RelatedFacility; -import jakarta.persistence.Entity; -import jakarta.persistence.Index; -import jakarta.persistence.Table; - -@Entity -@Table(indexes = @Index(columnList = "procedure_id")) -public class Facility extends RelatedFacility<MedicalRegistryEntry> {} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistryEntry.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistryEntry.java index 92cd923e3..cc2e86156 100644 --- a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistryEntry.java +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistryEntry.java @@ -5,9 +5,52 @@ package de.eshg.medicalregistry.domain.model; +import de.eshg.lib.common.DataSensitivity; +import de.eshg.lib.common.SensitivityLevel; import de.eshg.lib.procedure.domain.model.Procedure; +import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; @Entity +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) public class MedicalRegistryEntry - extends Procedure<MedicalRegistryEntry, MedicalRegistryTask, Person, Facility> {} + extends Procedure<MedicalRegistryEntry, MedicalRegistryTask, Professional, Practice> { + + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + @Column(nullable = false) + private boolean employeesEmployed; + + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + @Column(nullable = false) + private boolean consentToPrivacyPolicy; + + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + @Column(nullable = false) + private boolean requestForWrittenConfirmation; + + public boolean isEmployeesEmployed() { + return employeesEmployed; + } + + public void setEmployeesEmployed(boolean employeesEmployed) { + this.employeesEmployed = employeesEmployed; + } + + public boolean isConsentToPrivacyPolicy() { + return consentToPrivacyPolicy; + } + + public void setConsentToPrivacyPolicy(boolean consentToPrivacyPolicy) { + this.consentToPrivacyPolicy = consentToPrivacyPolicy; + } + + public boolean isRequestForWrittenConfirmation() { + return requestForWrittenConfirmation; + } + + public void setRequestForWrittenConfirmation(boolean requestForWrittenConfirmation) { + this.requestForWrittenConfirmation = requestForWrittenConfirmation; + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistryEntryChange.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistryEntryChange.java new file mode 100644 index 000000000..415faceb0 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistryEntryChange.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.domain.model; + +import de.eshg.lib.common.DataSensitivity; +import de.eshg.lib.common.SensitivityLevel; +import jakarta.persistence.Entity; +import jakarta.validation.constraints.NotNull; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +@Entity +public class MedicalRegistryEntryChange extends MedicalRegistryEntry { + + @NotNull + @JdbcType(PostgreSQLEnumJdbcType.class) + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + private TypeOfChange typeOfChange; + + public TypeOfChange getTypeOfChange() { + return typeOfChange; + } + + public void setTypeOfChange(TypeOfChange typeOfChange) { + this.typeOfChange = typeOfChange; + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistrySystemProgressEntryType.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistrySystemProgressEntryType.java new file mode 100644 index 000000000..9a6df160d --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/MedicalRegistrySystemProgressEntryType.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.domain.model; + +public enum MedicalRegistrySystemProgressEntryType { + NEW_REGISTRATION, + SECOND_PRACTICE, + RE_REGISTRATION, + CHANGE_OF_REGISTRATION, + CHANGE_OF_NAME, + RELOCATION, + DEREGISTRATION, + OTHER, + DOCUMENT_UPLOAD +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Person.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Person.java deleted file mode 100644 index 90717efd2..000000000 --- a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Person.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: Apache-2.0 - */ - -package de.eshg.medicalregistry.domain.model; - -import de.eshg.lib.procedure.domain.model.RelatedPerson; -import jakarta.persistence.Entity; -import jakarta.persistence.Index; -import jakarta.persistence.Table; - -@Entity -@Table(indexes = @Index(columnList = "procedure_id")) -public class Person extends RelatedPerson<MedicalRegistryEntry> {} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Practice.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Practice.java new file mode 100644 index 000000000..6e7108e07 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Practice.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.domain.model; + +import de.eshg.lib.common.DataSensitivity; +import de.eshg.lib.common.SensitivityLevel; +import de.eshg.lib.procedure.domain.model.FacilityType; +import de.eshg.lib.procedure.domain.model.RelatedFacility; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; + +@Entity +@Table(indexes = @Index(columnList = "procedure_id")) +public class Practice extends RelatedFacility<MedicalRegistryEntry> { + + @DataSensitivity(SensitivityLevel.PUBLIC) + private String website; + + @DataSensitivity(SensitivityLevel.PROTECTED) + private String institutionIdentifier; + + @DataSensitivity(SensitivityLevel.PROTECTED) + private String establishmentNumber; + + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + private boolean healthInsuranceAuthorization; + + @DataSensitivity(SensitivityLevel.PUBLIC) + private String openingHours; + + public Practice() { + super(FacilityType.MEDICAL_PRACTICE); + } + + public String getWebsite() { + return website; + } + + public void setWebsite(String website) { + this.website = website; + } + + public String getInstitutionIdentifier() { + return institutionIdentifier; + } + + public void setInstitutionIdentifier(String institutionIdentifier) { + this.institutionIdentifier = institutionIdentifier; + } + + public String getEstablishmentNumber() { + return establishmentNumber; + } + + public void setEstablishmentNumber(String establishmentNumber) { + this.establishmentNumber = establishmentNumber; + } + + public boolean isHealthInsuranceAuthorization() { + return healthInsuranceAuthorization; + } + + public void setHealthInsuranceAuthorization(boolean healthInsuranceAuthorization) { + this.healthInsuranceAuthorization = healthInsuranceAuthorization; + } + + public String getOpeningHours() { + return openingHours; + } + + public void setOpeningHours(String openingHours) { + this.openingHours = openingHours; + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Professional.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Professional.java new file mode 100644 index 000000000..f8cdae4ea --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/Professional.java @@ -0,0 +1,159 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.domain.model; + +import de.eshg.lib.common.CountryCode; +import de.eshg.lib.common.DataSensitivity; +import de.eshg.lib.common.SensitivityLevel; +import de.eshg.lib.procedure.domain.model.PersonType; +import de.eshg.lib.procedure.domain.model.RelatedPerson; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.time.LocalDate; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +@Entity +@Table(indexes = @Index(columnList = "procedure_id")) +public class Professional extends RelatedPerson<MedicalRegistryEntry> { + + @JdbcType(PostgreSQLEnumJdbcType.class) + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + @Column(nullable = false) + private ProfessionalTitle professionalTitle; + + @JdbcType(PostgreSQLEnumJdbcType.class) + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + @Column(nullable = false) + private CountryCode nationality; + + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + private String fieldOfExpertise; + + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + private String specialistTitle; + + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + private String furtherTraining; + + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + private String qualifications; + + @DataSensitivity(SensitivityLevel.PROTECTED) + private String lifetimeDoctorNumber; + + @DataSensitivity(SensitivityLevel.PROTECTED) + @Column(nullable = false) + private LocalDate approbationGrantedOn; + + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + @Column(nullable = false) + private String approbationIssuingAuthority; + + @JdbcType(PostgreSQLEnumJdbcType.class) + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + @Column(nullable = false) + private EmploymentType employmentType; + + @JdbcType(PostgreSQLEnumJdbcType.class) + @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) + @Column(nullable = false) + private EmploymentStatus employmentStatus; + + public Professional() { + super(PersonType.PROFESSIONAL); + } + + public ProfessionalTitle getProfessionalTitle() { + return professionalTitle; + } + + public void setProfessionalTitle(ProfessionalTitle professionalTitle) { + this.professionalTitle = professionalTitle; + } + + public CountryCode getNationality() { + return nationality; + } + + public void setNationality(CountryCode nationality) { + this.nationality = nationality; + } + + public String getFieldOfExpertise() { + return fieldOfExpertise; + } + + public void setFieldOfExpertise(String fieldOfExpertise) { + this.fieldOfExpertise = fieldOfExpertise; + } + + public String getSpecialistTitle() { + return specialistTitle; + } + + public void setSpecialistTitle(String specialistTitle) { + this.specialistTitle = specialistTitle; + } + + public String getFurtherTraining() { + return furtherTraining; + } + + public void setFurtherTraining(String furtherTraining) { + this.furtherTraining = furtherTraining; + } + + public String getQualifications() { + return qualifications; + } + + public void setQualifications(String qualifications) { + this.qualifications = qualifications; + } + + public String getLifetimeDoctorNumber() { + return lifetimeDoctorNumber; + } + + public void setLifetimeDoctorNumber(String lifetimeDoctorNumber) { + this.lifetimeDoctorNumber = lifetimeDoctorNumber; + } + + public LocalDate getApprobationGrantedOn() { + return approbationGrantedOn; + } + + public void setApprobationGrantedOn(LocalDate approbationGrantedOn) { + this.approbationGrantedOn = approbationGrantedOn; + } + + public String getApprobationIssuingAuthority() { + return approbationIssuingAuthority; + } + + public void setApprobationIssuingAuthority(String approbationIssuingAuthority) { + this.approbationIssuingAuthority = approbationIssuingAuthority; + } + + public EmploymentType getEmploymentType() { + return employmentType; + } + + public void setEmploymentType(EmploymentType employmentTitle) { + this.employmentType = employmentTitle; + } + + public EmploymentStatus getEmploymentStatus() { + return employmentStatus; + } + + public void setEmploymentStatus(EmploymentStatus employmentStatus) { + this.employmentStatus = employmentStatus; + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/ProfessionalTitle.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/ProfessionalTitle.java new file mode 100644 index 000000000..3f649e18b --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/ProfessionalTitle.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.domain.model; + +public enum ProfessionalTitle { + DOCTORS, + DENTISTS, + PSYCHOLOGICAL_PSYCHOTHERAPISTS, + NURSING_ASSISTANTS, + GERIATRIC_NURSES, + DIETICIANS, + DISINFECTORS, + OCCUPATIONAL_THERAPISTS, + HEALTH_SUPERVISORS, + HEALTHCARE_AND_PEDIATRIC_NURSES, + HEALTHCARE_AND_NURSING_ASSISTANTS, + HEALTHCARE_AND_NURSING_ASSISTANTS_HELPER, + MIDWIVES_MATERNITY_NURSES, + ALTERNATIVE_PRACTITIONERS, + NON_MEDICAL_PRACTITIONER_FOR_CHIROPRACTIC, + ALTERNATIVE_PRACTITIONER_FOR_SPEECH_THERAPY, + NON_MEDICAL_PRACTITIONER_FOR_PHYSIOTHERAPY, + NON_MEDICAL_PRACTITIONERS_FOR_PSYCHOTHERAPY, + CHILD_AND_YOUTH_PSYCHOTHERAPISTS, + SPEECH_THERAPISTS, + MASSEURS_AND_MEDICAL_BATH_ATTENDANTS, + MEDICAL_DOCUMENTALISTS, + MEDICAL_TECHNICAL_LABORATORY_ASSISTANTS, + MEDICAL_TECHNICAL_RADIOLOGY_ASSISTANTS, + MEDICAL_TECHNICAL_ASSISTANTS_FOR_FUNCTIONAL_DIAGNOSTICS, + EMERGENCY_PARAMEDICS, + ORTHOPTISTS, + CARE_ASSISTANTS, + NURSING_SERVICES, + NURSING_SERVICE_MANAGERS, + PHARMACEUTICAL_TECHNICAL_ASSISTANTS, + PHYSIOTHERAPISTS, + PODIATRISTS, + RADIOLOGY_ASSISTANTS, + SPORTS_THERAPISTS, + PHARMACISTS, + VETERINARIANS, +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/TypeOfChange.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/TypeOfChange.java new file mode 100644 index 000000000..851b8eb33 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/domain/model/TypeOfChange.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.domain.model; + +public enum TypeOfChange { + TypeOfChange, + NEW_REGISTRATION, + SECOND_PRACTICE, + RE_REGISTRATION, + CHANGE_OF_REGISTRATION, + CHANGE_OF_NAME, + RELOCATION, + DEREGISTRATION, + OTHER +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/AddressMapper.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/AddressMapper.java new file mode 100644 index 000000000..b6858f0a7 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/AddressMapper.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.mapper; + +import de.eshg.base.address.DomesticAddressDto; +import de.eshg.medicalregistry.api.AddressDto; + +public final class AddressMapper { + private AddressMapper() {} + + public static AddressDto mapToDto(de.eshg.base.address.AddressDto addressDto) { + if (addressDto == null) { + return null; + } + + if (addressDto instanceof DomesticAddressDto address) { + return mapToDto(address); + } else { + throw new IllegalArgumentException("Unexpected instance of Address"); + } + } + + private static AddressDto mapToDto(DomesticAddressDto addressDto) { + if (addressDto == null) { + return null; + } + + return new AddressDto( + addressDto.street(), addressDto.houseNumber(), addressDto.postalCode(), addressDto.city()); + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/PracticeMapper.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/PracticeMapper.java new file mode 100644 index 000000000..184262149 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/PracticeMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.mapper; + +import static de.eshg.medicalregistry.util.MapperUtils.*; + +import de.eshg.base.centralfile.api.facility.GetFacilityFileStateResponse; +import de.eshg.medicalregistry.api.PracticeDto; +import de.eshg.medicalregistry.domain.model.Practice; + +public final class PracticeMapper { + private PracticeMapper() {} + + public static PracticeDto mapToDto( + Practice practice, GetFacilityFileStateResponse practiceDetails) { + + return new PracticeDto( + practiceDetails.name(), + singleElementOrNull(practiceDetails.emailAddresses()), + singleElementOrNull(practiceDetails.phoneNumbers()), + AddressMapper.mapToDto(practiceDetails.contactAddress()), + practice.getWebsite(), + practice.getInstitutionIdentifier(), + practice.getEstablishmentNumber(), + practice.isHealthInsuranceAuthorization(), + practice.getOpeningHours()); + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/ProfessionalMapper.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/ProfessionalMapper.java new file mode 100644 index 000000000..137efd8ff --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/mapper/ProfessionalMapper.java @@ -0,0 +1,209 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.mapper; + +import static de.eshg.medicalregistry.util.MapperUtils.singleElementOrNull; + +import de.eshg.base.centralfile.api.person.GetPersonFileStateResponse; +import de.eshg.medicalregistry.api.*; +import de.eshg.medicalregistry.domain.model.EmploymentStatus; +import de.eshg.medicalregistry.domain.model.EmploymentType; +import de.eshg.medicalregistry.domain.model.Professional; +import de.eshg.medicalregistry.domain.model.ProfessionalTitle; + +public final class ProfessionalMapper { + private ProfessionalMapper() {} + + public static ProfessionalDto mapToDto( + Professional professional, GetPersonFileStateResponse professionalDetails) { + if (professionalDetails == null) { + return null; + } + + return new ProfessionalDto( + professionalDetails.title(), + professionalDetails.gender(), + professionalDetails.firstName(), + professionalDetails.lastName(), + professionalDetails.dateOfBirth(), + professionalDetails.nameAtBirth(), + professionalDetails.placeOfBirth(), + singleElementOrNull(professionalDetails.emailAddresses()), + singleElementOrNull(professionalDetails.phoneNumbers()), + AddressMapper.mapToDto(professionalDetails.contactAddress()), + mapToDto(professional.getProfessionalTitle()), + professional.getFieldOfExpertise(), + professional.getSpecialistTitle(), + professional.getFurtherTraining(), + professional.getQualifications(), + professional.getApprobationGrantedOn(), + professional.getApprobationIssuingAuthority(), + professional.getLifetimeDoctorNumber(), + mapToDto(professional.getEmploymentType()), + mapToDto(professional.getEmploymentStatus()), + professional.getNationality()); + } + + private static ProfessionalTitleDto mapToDto(ProfessionalTitle professionalTitle) { + if (professionalTitle == null) { + return null; + } + + return switch (professionalTitle) { + case DOCTORS -> ProfessionalTitleDto.DOCTORS; + case DENTISTS -> ProfessionalTitleDto.DENTISTS; + case PSYCHOLOGICAL_PSYCHOTHERAPISTS -> ProfessionalTitleDto.PSYCHOLOGICAL_PSYCHOTHERAPISTS; + case NURSING_ASSISTANTS -> ProfessionalTitleDto.NURSING_ASSISTANTS; + case GERIATRIC_NURSES -> ProfessionalTitleDto.GERIATRIC_NURSES; + case DIETICIANS -> ProfessionalTitleDto.DIETICIANS; + case DISINFECTORS -> ProfessionalTitleDto.DISINFECTORS; + case OCCUPATIONAL_THERAPISTS -> ProfessionalTitleDto.OCCUPATIONAL_THERAPISTS; + case HEALTH_SUPERVISORS -> ProfessionalTitleDto.HEALTH_SUPERVISORS; + case HEALTHCARE_AND_PEDIATRIC_NURSES -> ProfessionalTitleDto.HEALTHCARE_AND_PEDIATRIC_NURSES; + case HEALTHCARE_AND_NURSING_ASSISTANTS -> + ProfessionalTitleDto.HEALTHCARE_AND_NURSING_ASSISTANTS; + case HEALTHCARE_AND_NURSING_ASSISTANTS_HELPER -> + ProfessionalTitleDto.HEALTHCARE_AND_NURSING_ASSISTANTS_HELPER; + case MIDWIVES_MATERNITY_NURSES -> ProfessionalTitleDto.MIDWIVES_MATERNITY_NURSES; + case ALTERNATIVE_PRACTITIONERS -> ProfessionalTitleDto.ALTERNATIVE_PRACTITIONERS; + case NON_MEDICAL_PRACTITIONER_FOR_CHIROPRACTIC -> + ProfessionalTitleDto.NON_MEDICAL_PRACTITIONER_FOR_CHIROPRACTIC; + case ALTERNATIVE_PRACTITIONER_FOR_SPEECH_THERAPY -> + ProfessionalTitleDto.ALTERNATIVE_PRACTITIONER_FOR_SPEECH_THERAPY; + case NON_MEDICAL_PRACTITIONER_FOR_PHYSIOTHERAPY -> + ProfessionalTitleDto.NON_MEDICAL_PRACTITIONER_FOR_PHYSIOTHERAPY; + case NON_MEDICAL_PRACTITIONERS_FOR_PSYCHOTHERAPY -> + ProfessionalTitleDto.NON_MEDICAL_PRACTITIONERS_FOR_PSYCHOTHERAPY; + case CHILD_AND_YOUTH_PSYCHOTHERAPISTS -> + ProfessionalTitleDto.CHILD_AND_YOUTH_PSYCHOTHERAPISTS; + case SPEECH_THERAPISTS -> ProfessionalTitleDto.SPEECH_THERAPISTS; + case MASSEURS_AND_MEDICAL_BATH_ATTENDANTS -> + ProfessionalTitleDto.MASSEURS_AND_MEDICAL_BATH_ATTENDANTS; + case MEDICAL_DOCUMENTALISTS -> ProfessionalTitleDto.MEDICAL_DOCUMENTALISTS; + case MEDICAL_TECHNICAL_LABORATORY_ASSISTANTS -> + ProfessionalTitleDto.MEDICAL_TECHNICAL_LABORATORY_ASSISTANTS; + case MEDICAL_TECHNICAL_RADIOLOGY_ASSISTANTS -> + ProfessionalTitleDto.MEDICAL_TECHNICAL_RADIOLOGY_ASSISTANTS; + case MEDICAL_TECHNICAL_ASSISTANTS_FOR_FUNCTIONAL_DIAGNOSTICS -> + ProfessionalTitleDto.MEDICAL_TECHNICAL_ASSISTANTS_FOR_FUNCTIONAL_DIAGNOSTICS; + case EMERGENCY_PARAMEDICS -> ProfessionalTitleDto.EMERGENCY_PARAMEDICS; + case ORTHOPTISTS -> ProfessionalTitleDto.ORTHOPTISTS; + case CARE_ASSISTANTS -> ProfessionalTitleDto.CARE_ASSISTANTS; + case NURSING_SERVICES -> ProfessionalTitleDto.NURSING_SERVICES; + case NURSING_SERVICE_MANAGERS -> ProfessionalTitleDto.NURSING_SERVICE_MANAGERS; + case PHARMACEUTICAL_TECHNICAL_ASSISTANTS -> + ProfessionalTitleDto.PHARMACEUTICAL_TECHNICAL_ASSISTANTS; + case PHYSIOTHERAPISTS -> ProfessionalTitleDto.PHYSIOTHERAPISTS; + case PODIATRISTS -> ProfessionalTitleDto.PODIATRISTS; + case RADIOLOGY_ASSISTANTS -> ProfessionalTitleDto.RADIOLOGY_ASSISTANTS; + case SPORTS_THERAPISTS -> ProfessionalTitleDto.SPORTS_THERAPISTS; + case PHARMACISTS -> ProfessionalTitleDto.PHARMACISTS; + case VETERINARIANS -> ProfessionalTitleDto.VETERINARIANS; + }; + } + + private static EmploymentTypeDto mapToDto(EmploymentType employmentType) { + if (employmentType == null) { + return null; + } + + return switch (employmentType) { + case FULL_TIME -> EmploymentTypeDto.FULL_TIME; + case PART_TIME -> EmploymentTypeDto.PART_TIME; + }; + } + + private static EmploymentStatusDto mapToDto(EmploymentStatus employmentStatus) { + if (employmentStatus == null) { + return null; + } + + return switch (employmentStatus) { + case SELF_EMPLOYED -> EmploymentStatusDto.SELF_EMPLOYED; + case FREELANCE -> EmploymentStatusDto.FREELANCE; + case EMPLOYEE -> EmploymentStatusDto.EMPLOYEE; + }; + } + + public static ProfessionalTitle mapToDomain(ProfessionalTitleDto professionalTitleDto) { + if (professionalTitleDto == null) { + return null; + } + + return switch (professionalTitleDto) { + case DOCTORS -> ProfessionalTitle.DOCTORS; + case DENTISTS -> ProfessionalTitle.DENTISTS; + case PSYCHOLOGICAL_PSYCHOTHERAPISTS -> ProfessionalTitle.PSYCHOLOGICAL_PSYCHOTHERAPISTS; + case NURSING_ASSISTANTS -> ProfessionalTitle.NURSING_ASSISTANTS; + case GERIATRIC_NURSES -> ProfessionalTitle.GERIATRIC_NURSES; + case DIETICIANS -> ProfessionalTitle.DIETICIANS; + case DISINFECTORS -> ProfessionalTitle.DISINFECTORS; + case OCCUPATIONAL_THERAPISTS -> ProfessionalTitle.OCCUPATIONAL_THERAPISTS; + case HEALTH_SUPERVISORS -> ProfessionalTitle.HEALTH_SUPERVISORS; + case HEALTHCARE_AND_PEDIATRIC_NURSES -> ProfessionalTitle.HEALTHCARE_AND_PEDIATRIC_NURSES; + case HEALTHCARE_AND_NURSING_ASSISTANTS -> ProfessionalTitle.HEALTHCARE_AND_NURSING_ASSISTANTS; + case HEALTHCARE_AND_NURSING_ASSISTANTS_HELPER -> + ProfessionalTitle.HEALTHCARE_AND_NURSING_ASSISTANTS_HELPER; + case MIDWIVES_MATERNITY_NURSES -> ProfessionalTitle.MIDWIVES_MATERNITY_NURSES; + case ALTERNATIVE_PRACTITIONERS -> ProfessionalTitle.ALTERNATIVE_PRACTITIONERS; + case NON_MEDICAL_PRACTITIONER_FOR_CHIROPRACTIC -> + ProfessionalTitle.NON_MEDICAL_PRACTITIONER_FOR_CHIROPRACTIC; + case ALTERNATIVE_PRACTITIONER_FOR_SPEECH_THERAPY -> + ProfessionalTitle.ALTERNATIVE_PRACTITIONER_FOR_SPEECH_THERAPY; + case NON_MEDICAL_PRACTITIONER_FOR_PHYSIOTHERAPY -> + ProfessionalTitle.NON_MEDICAL_PRACTITIONER_FOR_PHYSIOTHERAPY; + case NON_MEDICAL_PRACTITIONERS_FOR_PSYCHOTHERAPY -> + ProfessionalTitle.NON_MEDICAL_PRACTITIONERS_FOR_PSYCHOTHERAPY; + case CHILD_AND_YOUTH_PSYCHOTHERAPISTS -> ProfessionalTitle.CHILD_AND_YOUTH_PSYCHOTHERAPISTS; + case SPEECH_THERAPISTS -> ProfessionalTitle.SPEECH_THERAPISTS; + case MASSEURS_AND_MEDICAL_BATH_ATTENDANTS -> + ProfessionalTitle.MASSEURS_AND_MEDICAL_BATH_ATTENDANTS; + case MEDICAL_DOCUMENTALISTS -> ProfessionalTitle.MEDICAL_DOCUMENTALISTS; + case MEDICAL_TECHNICAL_LABORATORY_ASSISTANTS -> + ProfessionalTitle.MEDICAL_TECHNICAL_LABORATORY_ASSISTANTS; + case MEDICAL_TECHNICAL_RADIOLOGY_ASSISTANTS -> + ProfessionalTitle.MEDICAL_TECHNICAL_RADIOLOGY_ASSISTANTS; + case MEDICAL_TECHNICAL_ASSISTANTS_FOR_FUNCTIONAL_DIAGNOSTICS -> + ProfessionalTitle.MEDICAL_TECHNICAL_ASSISTANTS_FOR_FUNCTIONAL_DIAGNOSTICS; + case EMERGENCY_PARAMEDICS -> ProfessionalTitle.EMERGENCY_PARAMEDICS; + case ORTHOPTISTS -> ProfessionalTitle.ORTHOPTISTS; + case CARE_ASSISTANTS -> ProfessionalTitle.CARE_ASSISTANTS; + case NURSING_SERVICES -> ProfessionalTitle.NURSING_SERVICES; + case NURSING_SERVICE_MANAGERS -> ProfessionalTitle.NURSING_SERVICE_MANAGERS; + case PHARMACEUTICAL_TECHNICAL_ASSISTANTS -> + ProfessionalTitle.PHARMACEUTICAL_TECHNICAL_ASSISTANTS; + case PHYSIOTHERAPISTS -> ProfessionalTitle.PHYSIOTHERAPISTS; + case PODIATRISTS -> ProfessionalTitle.PODIATRISTS; + case RADIOLOGY_ASSISTANTS -> ProfessionalTitle.RADIOLOGY_ASSISTANTS; + case SPORTS_THERAPISTS -> ProfessionalTitle.SPORTS_THERAPISTS; + case PHARMACISTS -> ProfessionalTitle.PHARMACISTS; + case VETERINARIANS -> ProfessionalTitle.VETERINARIANS; + }; + } + + public static EmploymentType mapToDomain(EmploymentTypeDto employmentTypeDto) { + if (employmentTypeDto == null) { + return null; + } + + return switch (employmentTypeDto) { + case FULL_TIME -> EmploymentType.FULL_TIME; + case PART_TIME -> EmploymentType.PART_TIME; + }; + } + + public static EmploymentStatus mapToDomain(EmploymentStatusDto employmentStatusDto) { + if (employmentStatusDto == null) { + return null; + } + + return switch (employmentStatusDto) { + case SELF_EMPLOYED -> EmploymentStatus.SELF_EMPLOYED; + case FREELANCE -> EmploymentStatus.FREELANCE; + case EMPLOYEE -> EmploymentStatus.EMPLOYEE; + }; + } +} diff --git a/backend/medical-registry/src/main/java/de/eshg/medicalregistry/util/MapperUtils.java b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/util/MapperUtils.java new file mode 100644 index 000000000..1ab53ed32 --- /dev/null +++ b/backend/medical-registry/src/main/java/de/eshg/medicalregistry/util/MapperUtils.java @@ -0,0 +1,17 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.medicalregistry.util; + +import de.cronn.commons.lang.StreamUtil; +import java.util.List; + +public final class MapperUtils { + public MapperUtils() {} + + public static String singleElementOrNull(List<String> items) { + return items.stream().collect(StreamUtil.toSingleOptionalElement()).orElse(null); + } +} diff --git a/backend/opendata/build.gradle b/backend/opendata/build.gradle index 953c36054..0e57dfff5 100644 --- a/backend/opendata/build.gradle +++ b/backend/opendata/build.gradle @@ -4,11 +4,18 @@ plugins { dependencies { implementation project(':business-module-commons') + implementation project(':file-commons') implementation project(':business-module-persistence-commons') + implementation project(':base-api') + implementation project(':auditlog-api') + implementation project(':lib-base-client') + implementation 'org.springdoc:springdoc-openapi-starter-common:latest.release' + implementation 'commons-io:commons-io:latest.release' runtimeOnly 'org.postgresql:postgresql' testImplementation project(':test-commons') testImplementation testFixtures(project(':business-module-commons')) testImplementation testFixtures(project(':business-module-persistence-commons')) + annotationProcessor 'org.hibernate.orm:hibernate-jpamodelgen' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:postgresql' @@ -19,6 +26,12 @@ dockerCompose { startedServices = ['opendata', 'opendata-db'] } +tasks.named("test").configure { + dependsOn project(':base').tasks.named("composeUp") + inputs.files(project(':base').tasks.named("bootJar")).withNormalizer(ClasspathNormalizer.class) + inputs.files(project(':keycloak').tasks.named("jar")).withNormalizer(ClasspathNormalizer.class) +} + dependencyTrack { projectId = project.findProperty('dependency-track-project-id-opendata') ?: "unspecified" } diff --git a/backend/opendata/gradle.lockfile b/backend/opendata/gradle.lockfile index c8ed73a44..2b89862a1 100644 --- a/backend/opendata/gradle.lockfile +++ b/backend/opendata/gradle.lockfile @@ -6,18 +6,18 @@ ch.qos.logback:logback-core:1.5.8=compileClasspath,productionRuntimeClasspath,ru com.fasterxml.jackson.core:jackson-annotations:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson.core:jackson-core:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson.core:jackson-databind:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml:classmate:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -25,28 +25,28 @@ com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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.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 -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.sun.istack:istack-commons-runtime:4.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.17.0=testRuntimeClasspath +commons-io:commons-io:2.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-jakarta9:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -58,30 +58,32 @@ io.prometheus:prometheus-metrics-exposition-formats:1.2.1=productionRuntimeClass io.prometheus:prometheus-metrics-model:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -io.smallrye:jandex:3.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jakarta.annotation:jakarta.annotation-api:2.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jakarta.inject:jakarta.inject-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -jakarta.persistence:jakarta.persistence-api:3.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jakarta.transaction:jakarta.transaction-api:2.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.annotation:jakarta.annotation-api:2.1.1=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +jakarta.persistence:jakarta.persistence-api:3.1.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.transaction:jakarta.transaction-api:2.0.1=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.14.19=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,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.antlr:antlr4-runtime:4.13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.24.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.11.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -89,65 +91,68 @@ org.apache.httpcomponents.core5:httpcore5-h2:5.2.5=productionRuntimeClasspath,ru org.apache.httpcomponents.core5:httpcore5:5.2.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.23.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.23.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-core:2.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.30=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcpkix-jdk18on:1.78.1=testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.78.1=testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.78.1=testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.78.1=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 -org.glassfish.jaxb:jaxb-runtime:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.glassfish.jaxb:txw2:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,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 +org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hibernate.orm:hibernate-core:6.5.3.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-envers:6.5.3.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hibernate.orm:hibernate-jpamodelgen:6.5.3.Final=annotationProcessor org.hibernate.validator:hibernate-validator:8.0.1.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt -org.jboss.logging:jboss-logging:3.5.3.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspath +org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springdoc:springdoc-openapi-starter-common:2.6.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springdoc:springdoc-openapi-starter-common:2.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springdoc:springdoc-openapi-starter-webmvc-api:2.6.0=testCompileClasspath,testRuntimeClasspath org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -162,7 +167,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -184,7 +189,7 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -192,7 +197,14 @@ org.testcontainers:database-commons:1.19.8=testCompileClasspath,testRuntimeClass org.testcontainers:jdbc:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:junit-jupiter:1.19.8=testCompileClasspath,testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testCompileClasspath,testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:parser:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:pdf-model:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:validation-model-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:verapdf-xmp-core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.webjars:swagger-ui:5.17.14=testCompileClasspath,testRuntimeClasspath org.xmlunit:xmlunit-core:2.9.1=testCompileClasspath,testRuntimeClasspath org.yaml:snakeyaml:2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -206,4 +218,4 @@ org.zalando:logbook-servlet:3.9.0=productionRuntimeClasspath,runtimeClasspath,te org.zalando:logbook-spring-boot-autoconfigure:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.zalando:logbook-spring-boot-starter:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.zalando:logbook-spring:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath +empty=developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath diff --git a/backend/opendata/openApi.yaml b/backend/opendata/openApi.yaml new file mode 100644 index 000000000..e3c33c0c8 --- /dev/null +++ b/backend/opendata/openApi.yaml @@ -0,0 +1,559 @@ +# Copyright 2024 cronn GmbH +# SPDX-License-Identifier: Apache-2.0 + +openapi: 3.0.1 +info: + description: This is the API for the opendata service + title: OpenData Service API + version: "0.1" +servers: +- url: http://localhost:8096 +paths: + /open-documents: + get: + description: | + Gets all open documents aka all resources including their versions. + It is possible to filter by `fileType`, `sources` and the year of + `statisticsStartDate` and `statisticsEndDate` + operationId: getOpenDocuments + parameters: + - description: | + If set only versions with a `statisticsStartDate` or `statisticsEndDate` + within the given year or whose period from `statisticsStartDate` to + `statisticsEndDate` covers the given year + in: query + name: statisticsYearFilter + required: false + schema: + type: string + - description: | + If set, versions with at least one of the given business modules are returned + in: query + name: sourcesFilter + required: false + schema: + type: array + items: + $ref: "#/components/schemas/BusinessModule" + - description: "If set, versions with the given file type are returned" + in: query + name: fileTypeFilter + required: false + schema: + $ref: "#/components/schemas/OpenDataFileType" + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/GetOpenDocumentsResponse" + description: OK + summary: Gets open documents + tags: + - OpenData + post: + description: | + Creates a resource as well if there is no existing resource with the + given `resourceName`. If resourceName is null, a UUID based one is generated + operationId: createOpenDocument + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + postOpenDocumentRequest: + $ref: "#/components/schemas/PostOpenDocumentRequest" + required: + - file + - postOpenDocumentRequest + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/Resource" + description: OK + summary: Creates a new version + tags: + - OpenData + /open-documents/search: + get: + description: Returns versions with matching `fileName` or `description` grouped + by their resource + operationId: searchOpenDocuments + parameters: + - in: query + name: searchString + required: true + schema: + type: string + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/GetOpenDocumentsResponse" + description: OK + summary: Searches for `searchString` in open documents + tags: + - OpenData + /open-documents/{versionId}: + delete: + description: Deletes correlating resource as well if there are no other versions + left + operationId: deleteVersion + parameters: + - in: path + name: versionId + required: true + schema: + type: string + format: uuid + responses: + "200": + description: OK + summary: Deletes a version + tags: + - OpenData + put: + description: "Updates `versionName`, `fileName`, `description`, `licence` and/or\ + \ `sources`" + operationId: updateVersionMetadata + parameters: + - in: path + name: versionId + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateVersionMetaDataRequest" + required: true + responses: + "200": + description: OK + summary: Updates meta data of a version + tags: + - OpenData + /open-documents/{versionId}/download: + get: + operationId: downloadDocument + parameters: + - in: path + name: versionId + required: true + schema: + type: string + format: uuid + responses: + "200": + content: + application/octet-stream: + schema: + type: string + format: binary + description: OK + summary: Downloads document of a version + tags: + - OpenData + /test-helper/population: + post: + operationId: populateDefaults + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/DefaultPopulationResponse" + description: OK + tags: + - TestHelper + /test-helper/request-interceptor: + post: + operationId: interceptNextRequest + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/InsertRequestInterceptionTestHelperRequest" + required: true + responses: + "200": + description: OK + tags: + - TestHelper + /test-helper/request-interceptor/barriers: + post: + operationId: addBarrier + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TestHelperInterceptionRequestFilter" + required: true + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/AddBarrierTestHelperResponse" + description: OK + tags: + - TestHelper + /test-helper/request-interceptor/barriers/{barrierId}/await: + post: + operationId: awaitBarrier + parameters: + - in: path + name: barrierId + required: true + schema: + type: integer + format: int64 + - in: query + name: timeoutInMillis + required: false + schema: + type: integer + format: int64 + minimum: 0 + responses: + "200": + description: OK + tags: + - TestHelper + /test-helper/request-interceptor/reset: + post: + operationId: resetInterceptionsAndBarriers + responses: + "200": + description: OK + tags: + - TestHelper + /test-helper/reset: + post: + operationId: reset + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/TestHelperClockUpdateResponse" + description: OK + tags: + - TestHelper + /test-helper/set-clock: + patch: + operationId: setClock + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TestHelperClockSetRequest" + required: true + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/TestHelperClockUpdateResponse" + description: OK + tags: + - TestHelper + /test-helper/wind-clock: + patch: + operationId: windClockForward + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TestHelperClockWindForwardRequest" + required: true + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/TestHelperClockUpdateResponse" + description: OK + tags: + - TestHelper +components: + schemas: + AddBarrierTestHelperResponse: + type: object + properties: + barrierId: + type: integer + format: int64 + minimum: 1 + required: + - barrierId + BusinessModule: + type: string + enum: + - INSPECTION + - SCHOOL_ENTRY + - TRAVEL_MEDICINE + - MEASLES_PROTECTION + - STI_PROTECTION + - MEDICAL_REGISTRY + DefaultPopulationResponse: + type: object + properties: + populations: + type: array + items: + $ref: "#/components/schemas/Population" + GetOpenDocumentsResponse: + type: object + properties: + elements: + type: array + items: + $ref: "#/components/schemas/Resource" + required: + - elements + HttpMethod: + type: string + enum: + - GET + - HEAD + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + - TRACE + InsertRequestInterceptionTestHelperRequest: + type: object + properties: + filter: + $ref: "#/components/schemas/TestHelperInterceptionRequestFilter" + type: + $ref: "#/components/schemas/InterceptionType" + required: + - type + InterceptionType: + type: string + enum: + - BAD_REQUEST + - UNAUTHORIZED + - FORBIDDEN + - NOT_FOUND + - INTERNAL_SERVER_ERROR + OpenDataFileType: + type: string + enum: + - PDF + - CSV + Population: + type: object + properties: + numberOfPopulatedEntities: + type: integer + format: int32 + populatorName: + type: string + totalNumberOfEntities: + type: integer + format: int64 + required: + - numberOfPopulatedEntities + - totalNumberOfEntities + PostOpenDocumentRequest: + type: object + properties: + description: + type: string + licence: + type: string + resourceName: + type: string + description: | + If set, and a resource with the same value already exists in the database, + the version is added to this resource. Otherwise, a new resource is created. + + If this value is not set, a new resource with a generated UUID as `resourceName` is created. + sources: + type: array + items: + $ref: "#/components/schemas/BusinessModule" + uniqueItems: true + statisticEndDate: + type: string + format: date + description: Either set `statisticsStartDate` and `statisticsEndDate` together + or not at all. + statisticStartDate: + type: string + format: date + description: Either set `statisticsStartDate` and `statisticsEndDate` together + or not at all. + versionName: + type: string + required: + - licence + - sources + - versionName + Resource: + type: object + properties: + resourceName: + type: string + versions: + type: array + items: + $ref: "#/components/schemas/Version" + required: + - resourceName + - versions + TestHelperClockSetRequest: + type: object + properties: + newInstant: + type: string + format: date-time + required: + - newInstant + TestHelperClockUpdateResponse: + type: object + properties: + instant: + type: string + format: date-time + required: + - instant + TestHelperClockWindForwardRequest: + type: object + properties: + days: + type: integer + format: int32 + minimum: 0 + hours: + type: integer + format: int32 + minimum: 0 + minutes: + type: integer + format: int32 + minimum: 0 + months: + type: integer + format: int32 + minimum: 0 + seconds: + type: integer + format: int32 + minimum: 0 + weeks: + type: integer + format: int32 + minimum: 0 + required: + - days + - hours + - minutes + - months + - seconds + - weeks + TestHelperInterceptionRequestFilter: + type: object + properties: + httpMethodFilter: + $ref: "#/components/schemas/HttpMethod" + queryPatternFilter: + type: string + urlPatternFilter: + type: string + UpdateVersionMetaDataRequest: + type: object + properties: + description: + type: string + fileName: + type: string + pattern: "^[\\w\\-\\. ]+$" + licence: + type: string + sources: + type: array + items: + $ref: "#/components/schemas/BusinessModule" + uniqueItems: true + version: + type: integer + format: int64 + versionName: + type: string + required: + - licence + - sources + - version + - versionName + Version: + type: object + properties: + author: + type: string + description: + type: string + externalId: + type: string + format: uuid + fileName: + type: string + fileSize: + type: integer + format: int32 + fileType: + $ref: "#/components/schemas/OpenDataFileType" + licence: + type: string + major: + type: integer + format: int32 + minor: + type: integer + format: int32 + publicationDate: + type: string + format: date-time + sources: + type: array + items: + $ref: "#/components/schemas/BusinessModule" + uniqueItems: true + statisticEndDate: + type: string + format: date + statisticStartDate: + type: string + format: date + version: + type: integer + format: int64 + description: "Version of the entity. Each time the entity is changed, it\ + \ is incremented by one." + versionName: + type: string + required: + - externalId + - fileName + - fileSize + - fileType + - licence + - major + - minor + - publicationDate + - sources + - version + - versionName diff --git a/backend/opendata/src/main/java/de/eshg/opendata/FileType.java b/backend/opendata/src/main/java/de/eshg/opendata/FileType.java deleted file mode 100644 index 9189da21d..000000000 --- a/backend/opendata/src/main/java/de/eshg/opendata/FileType.java +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: Apache-2.0 - */ - -package de.eshg.opendata; - -public enum FileType { - CSV, - PDF -} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/OpenDataApplication.java b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataApplication.java index 929d37939..32cec4b25 100644 --- a/backend/opendata/src/main/java/de/eshg/opendata/OpenDataApplication.java +++ b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataApplication.java @@ -5,9 +5,12 @@ package de.eshg.opendata; +import de.eshg.rest.service.security.config.OpenDataPublicSecurityConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; +@Import(OpenDataPublicSecurityConfig.class) @SpringBootApplication public class OpenDataApplication { public static void main(String[] args) { diff --git a/backend/opendata/src/main/java/de/eshg/opendata/OpenDataController.java b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataController.java new file mode 100644 index 000000000..cedbf7c65 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataController.java @@ -0,0 +1,146 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata; + +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; + +import de.eshg.api.commons.InlineParameterObject; +import de.eshg.base.feature.BaseFeature; +import de.eshg.base.feature.BaseFeatureTogglesApi; +import de.eshg.opendata.api.GetOpenDocumentsRequest; +import de.eshg.opendata.api.GetOpenDocumentsResponse; +import de.eshg.opendata.api.PostOpenDocumentRequest; +import de.eshg.opendata.api.ResourceDto; +import de.eshg.opendata.api.UpdateVersionMetaDataRequest; +import de.eshg.rest.service.error.BadRequestException; +import de.eshg.rest.service.security.config.BaseUrls; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.UUID; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping(BaseUrls.OpenData.OPEN_DATA_CONTROLLER) +@Tag(name = "OpenData") +public class OpenDataController { + + private final OpenDataService openDataService; + + private final BaseFeatureTogglesApi baseFeatureTogglesApi; + + public OpenDataController( + OpenDataService openDataService, BaseFeatureTogglesApi baseFeatureTogglesApi) { + this.openDataService = openDataService; + this.baseFeatureTogglesApi = baseFeatureTogglesApi; + } + + @GetMapping() + @Transactional(readOnly = true) + @Operation( + summary = "Gets open documents", + description = + """ + Gets all open documents aka all resources including their versions. + It is possible to filter by `fileType`, `sources` and the year of + `statisticsStartDate` and `statisticsEndDate` + """) + public GetOpenDocumentsResponse getOpenDocuments( + @InlineParameterObject @ParameterObject @Valid GetOpenDocumentsRequest request) { + validateOpenDataEnabled(); + return new GetOpenDocumentsResponse(openDataService.getOpenDocuments(request)); + } + + @GetMapping("/search") + @Transactional(readOnly = true) + @Operation( + summary = "Searches for `searchString` in open documents", + description = + "Returns versions with matching `fileName` or `description` grouped by their resource") + public GetOpenDocumentsResponse searchOpenDocuments(@RequestParam String searchString) { + validateOpenDataEnabled(); + return new GetOpenDocumentsResponse(openDataService.searchOpenDocuments(searchString)); + } + + @GetMapping("/{versionId}/download") + @ApiResponse( + responseCode = "200", + content = + @Content( + mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, + schema = @Schema(format = "binary"))) + @Transactional(readOnly = true) + @Operation(summary = "Downloads document of a version") + public ResponseEntity<byte[]> downloadDocument(@PathVariable("versionId") UUID versionId) { + validateOpenDataEnabled(); + return openDataService.downloadDocument(versionId); + } + + @PutMapping("/{versionId}") + @Transactional + @Operation( + summary = "Updates meta data of a version", + description = "Updates `versionName`, `fileName`, `description`, `licence` and/or `sources`") + public void updateVersionMetadata( + @PathVariable("versionId") UUID versionId, + @RequestBody @Valid UpdateVersionMetaDataRequest updateRequest) { + validateOpenDataEnabled(); + openDataService.updateVersionMetadata(versionId, updateRequest); + } + + @DeleteMapping("/{versionId}") + @Transactional + @Operation( + summary = "Deletes a version", + description = "Deletes correlating resource as well if there are no other versions left") + public void deleteVersion(@PathVariable("versionId") UUID versionId) { + validateOpenDataEnabled(); + openDataService.deleteVersion(versionId); + } + + @PostMapping(consumes = MULTIPART_FORM_DATA_VALUE) + @Transactional + @Operation( + summary = "Creates a new version", + description = + """ + Creates a resource as well if there is no existing resource with the + given `resourceName`. If resourceName is null, a UUID based one is generated + """) + public ResourceDto createOpenDocument( + @RequestPart(name = "postOpenDocumentRequest") @Valid PostOpenDocumentRequest postRequest, + @RequestPart(name = "file") MultipartFile file) { + validateOpenDataEnabled(); + return openDataService.createOpenDocument(postRequest, file); + } + + private void validateOpenDataEnabled() { + if (!baseFeatureTogglesApi + .getFeatureToggles() + .enabledNewFeatures() + .contains(BaseFeature.OPEN_DATA)) { + throw new BadRequestException( + "New feature %s is not enabled".formatted(BaseFeature.OPEN_DATA)); + } + } +} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/OpenDataMapper.java b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataMapper.java new file mode 100644 index 000000000..34c2a4bee --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata; + +import de.eshg.file.common.FileType; +import de.eshg.opendata.api.ResourceDto; +import de.eshg.opendata.api.VersionDto; +import de.eshg.opendata.domain.model.OpenDataFileType; +import de.eshg.opendata.domain.model.Resource; +import de.eshg.opendata.domain.model.Version; +import de.eshg.rest.service.error.BadRequestException; +import java.util.LinkedHashSet; +import java.util.List; + +class OpenDataMapper { + + private OpenDataMapper() {} + + static ResourceDto toInterfaceType(Resource resource) { + return new ResourceDto(resource.getResourceName(), toInterfaceType(resource.getVersions())); + } + + private static VersionDto toInterfaceType(Version version) { + return new VersionDto( + version.getVersionName(), + version.getVersion(), + version.getExternalId(), + version.getMajor(), + version.getMinor(), + version.getPublicationDate(), + version.getStatisticStartDate(), + version.getStatisticEndDate(), + new LinkedHashSet<>(version.getSources()), + version.getAuthor(), + version.getDescription(), + version.getFileType(), + version.getFileName(), + version.getFileSize(), + version.getLicence()); + } + + static ResourceDto toInterfaceWithVersions(Resource resource, List<Version> version) { + return new ResourceDto(resource.getResourceName(), toInterfaceType(version)); + } + + private static List<VersionDto> toInterfaceType(List<Version> versions) { + return versions.stream().map(OpenDataMapper::toInterfaceType).toList(); + } + + public static OpenDataFileType mapToOpenDataFileType(FileType fileType) { + return switch (fileType) { + case PDF -> OpenDataFileType.PDF; + case CSV -> OpenDataFileType.CSV; + default -> throw new BadRequestException("File type not permitted"); + }; + } +} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/OpenDataService.java b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataService.java new file mode 100644 index 000000000..7712a1063 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/OpenDataService.java @@ -0,0 +1,288 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata; + +import static de.eshg.domain.model.BaseEntity_.ID; +import static de.eshg.domain.model.BaseEntity_.id; +import static de.eshg.opendata.VersionFilterSpecification.fetchingResourcesAndSources; +import static de.eshg.opendata.VersionFilterSpecification.filterByFileType; +import static de.eshg.opendata.VersionFilterSpecification.filterBySource; +import static de.eshg.opendata.VersionFilterSpecification.filterStatisticsStartAndEndDatesByYear; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; +import static org.springframework.data.jpa.domain.JpaSort.of; +import static org.springframework.data.jpa.domain.JpaSort.path; +import static org.springframework.data.jpa.domain.Specification.allOf; + +import de.eshg.file.common.FileTypeDetector; +import de.eshg.file.common.PdfAConformanceValidator; +import de.eshg.opendata.api.GetOpenDocumentsRequest; +import de.eshg.opendata.api.PostOpenDocumentRequest; +import de.eshg.opendata.api.ResourceDto; +import de.eshg.opendata.api.UpdateVersionMetaDataRequest; +import de.eshg.opendata.domain.model.FileContent; +import de.eshg.opendata.domain.model.OpenDataFileType; +import de.eshg.opendata.domain.model.Resource; +import de.eshg.opendata.domain.model.Resource_; +import de.eshg.opendata.domain.model.Version; +import de.eshg.opendata.domain.model.Version_; +import de.eshg.opendata.domain.repository.ResourceRepository; +import de.eshg.opendata.domain.repository.VersionRepository; +import de.eshg.rest.service.error.BadRequestException; +import de.eshg.rest.service.error.NotFoundException; +import de.eshg.validation.ValidationUtil; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import org.apache.commons.io.FilenameUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public class OpenDataService { + + private static final Logger log = LoggerFactory.getLogger(OpenDataService.class); + + private static final String DEFAULT_AUTHOR = "GA Frankfurt"; + + private final ResourceRepository resourceRepository; + private final VersionRepository versionRepository; + private final Clock clock; + + public OpenDataService( + ResourceRepository resourceRepository, VersionRepository versionRepository, Clock clock) { + this.resourceRepository = resourceRepository; + this.versionRepository = versionRepository; + this.clock = clock; + } + + public List<ResourceDto> getOpenDocuments(GetOpenDocumentsRequest filterOptions) { + List<Specification<Version>> specifications = new ArrayList<>(); + + if (filterOptions.statisticsYearFilter() != null) { + specifications.add( + filterStatisticsStartAndEndDatesByYear(filterOptions.statisticsYearFilter())); + } + + if (filterOptions.sourcesFilter() != null) { + specifications.add(filterBySource(filterOptions.sourcesFilter())); + } + + if (filterOptions.fileTypeFilter() != null) { + specifications.add(filterByFileType(filterOptions.fileTypeFilter())); + } + + specifications.add(fetchingResourcesAndSources()); + + return versionRepository + .findAll( + Specification.where(allOf(specifications)), + of(Direction.ASC, path(Version_.resource).dot(Resource_.resourceName)) + .andUnsafe(Direction.ASC, "%s.%s".formatted(Version_.RESOURCE, ID)) + .and(of(Direction.DESC, path(Version_.publicationDate))) + .and(of(Direction.ASC, path(id)))) + .stream() + .collect(groupingBy(Version::getResource, LinkedHashMap::new, toList())) + .entrySet() + .stream() + .map(entry -> OpenDataMapper.toInterfaceWithVersions(entry.getKey(), entry.getValue())) + .toList(); + } + + public List<ResourceDto> searchOpenDocuments(String searchString) { + return resourceRepository.findByResourceNameOrVersionDescription(searchString).stream() + .map(OpenDataMapper::toInterfaceType) + .toList(); + } + + public ResponseEntity<byte[]> downloadDocument(UUID versionId) { + Version version = getVersionByExternalIdOrThrow(versionId); + ContentDisposition contentDisposition = + ContentDisposition.attachment() + .filename(version.getFileName(), StandardCharsets.UTF_8) + .build(); + return ResponseEntity.ok() + .contentType(version.getFileType().getCommonFileType().getMediaType()) + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) + .body(version.getDocument().getContent()); + } + + public void updateVersionMetadata(UUID versionId, UpdateVersionMetaDataRequest updateRequest) { + Version version = getVersionForUpdateOrThrow(versionId, updateRequest.version()); + validateNewFileNameExtension(updateRequest.fileName(), version); + + version.setVersionName(updateRequest.versionName()); + version.setFileName(updateRequest.fileName()); + version.setDescription(updateRequest.description()); + version.setLicence(updateRequest.licence()); + version.setSources(updateRequest.sources()); + } + + public void deleteVersion(UUID versionId) { + Version version = getVersionByExternalIdOrThrow(versionId); + Resource correlatingResource = version.getResource(); + + if (correlatingResource.getVersions().size() == 1) { + resourceRepository.delete(correlatingResource); + } else { + correlatingResource.getVersions().remove(version); + } + } + + public ResourceDto createOpenDocument(PostOpenDocumentRequest postRequest, MultipartFile file) { + validateStatisticsDates(postRequest.statisticStartDate(), postRequest.statisticEndDate()); + OpenDataFileType fileType = getFileTypeAndValidateFile(file); + + Resource correlatingResource = + Optional.ofNullable(postRequest.resourceName()) + .map(this::findOrCreateResource) + .orElseGet(() -> createResource(UUID.randomUUID().toString())); + + Version version = new Version(); + + version.setStatisticStartDate(postRequest.statisticStartDate()); + version.setStatisticEndDate(postRequest.statisticEndDate()); + version.setSources(postRequest.sources()); + version.setAuthor(DEFAULT_AUTHOR); + version.setDescription(postRequest.description()); + version.setPublicationDate(Instant.now(clock)); + version.setVersionName(postRequest.versionName()); + version.setFileName(file.getOriginalFilename()); + + FileContent fileContent = new FileContent(); + byte[] fileContentBytes = getBytes(file); + fileContent.setContent(fileContentBytes); + version.setDocument(fileContent); + version.setFileSize(fileContentBytes.length); + + version.setFileType(fileType); + version.setLicence(postRequest.licence()); + version.setMajor(calculateNextMajorVersion(correlatingResource, version)); + version.setMinor(calculateNextMinorVersion(correlatingResource, version)); + + correlatingResource.addVersion(version); + versionRepository.flush(); + + return OpenDataMapper.toInterfaceType(correlatingResource); + } + + private Resource findOrCreateResource(String resourceName) { + return resourceRepository + .findByResourceNameFetchingVersions(resourceName) + .orElseGet(() -> createResource(resourceName)); + } + + private byte[] getBytes(MultipartFile file) { + try { + return file.getBytes(); + } catch (IOException e) { + log.error("Corrupt file content", e); + throw new BadRequestException("Corrupt file content"); + } + } + + private OpenDataFileType getFileTypeAndValidateFile(MultipartFile file) { + try { + OpenDataFileType fileType = + OpenDataMapper.mapToOpenDataFileType(FileTypeDetector.getSupportedFileTypeOrThrow(file)); + + if (fileType.equals(OpenDataFileType.PDF)) { + PdfAConformanceValidator.validate(file.getBytes()); + } + return fileType; + } catch (IOException e) { + log.error("File header was corrupt", e); + throw new BadRequestException("File header was corrupt"); + } + } + + private Resource createResource(String resourceName) { + Resource resource = new Resource(); + resource.setResourceName(resourceName); + return resourceRepository.save(resource); + } + + private Version getVersionByExternalIdOrThrow(UUID versionId) { + return versionRepository + .findByExternalId(versionId) + .orElseThrow(() -> new NotFoundException("Version not found")); + } + + public int calculateNextMajorVersion(Resource resource, Version version) { + List<Version> versionsWithSameMajor = getSameMajorVersions(resource, version); + if (versionsWithSameMajor.isEmpty()) { + return resource.getVersions().stream() + .mapToInt(Version::getMajor) + .map(major -> major + 1) + .max() + .orElse(1); + } else { + return versionsWithSameMajor.getFirst().getMajor(); + } + } + + public int calculateNextMinorVersion(Resource resource, Version version) { + List<Version> versionsWithSameMajor = getSameMajorVersions(resource, version); + if (versionsWithSameMajor.isEmpty()) { + return 0; + } else { + return versionsWithSameMajor.stream().mapToInt(Version::getMinor).max().getAsInt() + 1; + } + } + + private List<Version> getSameMajorVersions(Resource resource, Version version) { + return resource.getVersions().stream() + .filter(entry -> hasTheSameTimeframe(version, entry)) + .toList(); + } + + private boolean hasTheSameTimeframe(Version version, Version entry) { + return Objects.equals(entry.getStatisticStartDate(), version.getStatisticStartDate()) + && Objects.equals(entry.getStatisticEndDate(), version.getStatisticEndDate()); + } + + private void validateStatisticsDates(LocalDate start, LocalDate end) { + if ((start != null && end == null) || (start == null && end != null)) { + throw new BadRequestException("Both date fields must be either set together or not at all."); + } + + if (end != null && end.isBefore(start)) { + throw new BadRequestException("StatisticEndDate is before StatisticStartDate."); + } + } + + private void validateNewFileNameExtension(String newFileName, Version version) { + String currentExtension = FilenameUtils.getExtension(version.getFileName()); + String newExtension = FilenameUtils.getExtension(newFileName); + if (!Objects.equals(currentExtension, newExtension)) { + throw new BadRequestException("It is forbidden to change or remove the file extension"); + } + } + + private Version getVersionForUpdateOrThrow(UUID versionId, Long entityVersion) { + Version version = + versionRepository + .findByExternalIdForUpdate(versionId) + .orElseThrow(() -> new NotFoundException("Version not found")); + ValidationUtil.validateVersion(entityVersion, version); + return version; + } +} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/VersionFilterSpecification.java b/backend/opendata/src/main/java/de/eshg/opendata/VersionFilterSpecification.java new file mode 100644 index 000000000..9a185f900 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/VersionFilterSpecification.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata; + +import de.eshg.lib.common.BusinessModule; +import de.eshg.opendata.domain.model.OpenDataFileType; +import de.eshg.opendata.domain.model.Version; +import de.eshg.opendata.domain.model.Version_; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import java.time.LocalDate; +import java.time.Year; +import java.util.ArrayList; +import java.util.List; +import org.springframework.data.jpa.domain.Specification; + +public class VersionFilterSpecification { + + private VersionFilterSpecification() {} + + public static Specification<Version> filterStatisticsStartAndEndDatesByYear(Year year) { + return (version, query, cb) -> + cb.or( + startOfYearIsInStatistic(version, cb, year), + endOfYearIsInStatistic(version, cb, year), + statisticWithinYear(version, cb, year)); + } + + private static Predicate startOfYearIsInStatistic( + Root<Version> version, CriteriaBuilder cb, Year year) { + Expression<LocalDate> firstDayOfYear = cb.literal(year.atDay(1)); + return cb.and( + cb.between( + firstDayOfYear, + version.get(Version_.statisticStartDate), + version.get(Version_.statisticEndDate))); + } + + private static Predicate endOfYearIsInStatistic( + Root<Version> version, CriteriaBuilder cb, Year year) { + Expression<LocalDate> lastDayOfYear = cb.literal(year.plusYears(1).atDay(1).minusDays(1)); + return cb.between( + lastDayOfYear, + version.get(Version_.statisticStartDate), + version.get(Version_.statisticEndDate)); + } + + private static Predicate statisticWithinYear( + Root<Version> version, CriteriaBuilder cb, Year year) { + Expression<LocalDate> firstDayOfYear = cb.literal(year.atDay(1)); + Expression<LocalDate> lastDayOfYear = cb.literal(year.plusYears(1).atDay(1).minusDays(1)); + + return cb.and( + cb.greaterThanOrEqualTo(version.get(Version_.statisticStartDate), firstDayOfYear), + cb.lessThanOrEqualTo(version.get(Version_.statisticEndDate), lastDayOfYear)); + } + + public static Specification<Version> filterBySource(List<BusinessModule> sources) { + return (version, query, cb) -> { + List<Predicate> predicates = new ArrayList<>(); + for (BusinessModule module : sources) { + predicates.add(cb.isMember(module, version.get(Version_.sources))); + } + + return cb.or(predicates.toArray(Predicate[]::new)); + }; + } + + public static Specification<Version> filterByFileType(OpenDataFileType fileType) { + return (version, query, cb) -> cb.equal(version.get(Version_.fileType), fileType); + } + + public static Specification<Version> fetchingResourcesAndSources() { + return (root, query, criteriaBuilder) -> { + root.fetch(Version_.resource); + root.fetch(Version_.sources, JoinType.LEFT); + return null; + }; + } +} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/api/GetOpenDocumentsRequest.java b/backend/opendata/src/main/java/de/eshg/opendata/api/GetOpenDocumentsRequest.java new file mode 100644 index 000000000..9f9f56525 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/api/GetOpenDocumentsRequest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata.api; + +import de.eshg.lib.common.BusinessModule; +import de.eshg.opendata.domain.model.OpenDataFileType; +import io.swagger.v3.oas.annotations.Parameter; +import java.time.Year; +import java.util.List; + +public record GetOpenDocumentsRequest( + @Parameter( + description = + """ + If set only versions with a `statisticsStartDate` or `statisticsEndDate` + within the given year or whose period from `statisticsStartDate` to + `statisticsEndDate` covers the given year + """) + Year statisticsYearFilter, + @Parameter( + description = + """ + If set, versions with at least one of the given business modules are returned + """) + List<BusinessModule> sourcesFilter, + @Parameter(description = "If set, versions with the given file type are returned") + OpenDataFileType fileTypeFilter) {} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/api/GetOpenDocumentsResponse.java b/backend/opendata/src/main/java/de/eshg/opendata/api/GetOpenDocumentsResponse.java new file mode 100644 index 000000000..963a21025 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/api/GetOpenDocumentsResponse.java @@ -0,0 +1,12 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata.api; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record GetOpenDocumentsResponse(@Valid @NotNull List<ResourceDto> elements) {} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/api/PostOpenDocumentRequest.java b/backend/opendata/src/main/java/de/eshg/opendata/api/PostOpenDocumentRequest.java new file mode 100644 index 000000000..e29a1d2f4 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/api/PostOpenDocumentRequest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata.api; + +import de.eshg.lib.common.BusinessModule; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.Set; +import org.hibernate.validator.constraints.URL; + +public record PostOpenDocumentRequest( + @NotEmpty String versionName, + @Schema( + description = + """ + If set, and a resource with the same value already exists in the database, + the version is added to this resource. Otherwise, a new resource is created. + + If this value is not set, a new resource with a generated UUID as `resourceName` is created. + """) + String resourceName, + @Schema( + description = + "Either set `statisticsStartDate` and `statisticsEndDate` together or not at all.") + LocalDate statisticStartDate, + @Schema( + description = + "Either set `statisticsStartDate` and `statisticsEndDate` together or not at all.") + LocalDate statisticEndDate, + @NotNull Set<BusinessModule> sources, + String description, + @NotEmpty @URL String licence) {} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/api/ResourceDto.java b/backend/opendata/src/main/java/de/eshg/opendata/api/ResourceDto.java new file mode 100644 index 000000000..8e16c9aaf --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/api/ResourceDto.java @@ -0,0 +1,15 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@Schema(name = "Resource") +public record ResourceDto( + @NotNull String resourceName, @NotNull @Valid List<VersionDto> versions) {} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/api/UpdateVersionMetaDataRequest.java b/backend/opendata/src/main/java/de/eshg/opendata/api/UpdateVersionMetaDataRequest.java new file mode 100644 index 000000000..f1ccc48e8 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/api/UpdateVersionMetaDataRequest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata.api; + +import de.eshg.lib.common.BusinessModule; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import java.util.Set; +import org.hibernate.validator.constraints.URL; + +public record UpdateVersionMetaDataRequest( + @NotNull long version, + @NotEmpty String versionName, + @Pattern( + regexp = "^[\\w\\-\\. ]+$", + message = + "Invalid file name. Only alphanumeric characters, hyphens, dots, and spaces are allowed.") + String fileName, + String description, + @NotEmpty @URL String licence, + @NotNull Set<BusinessModule> sources) {} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/api/VersionDto.java b/backend/opendata/src/main/java/de/eshg/opendata/api/VersionDto.java new file mode 100644 index 000000000..3ba0c5116 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/api/VersionDto.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata.api; + +import de.eshg.lib.common.BusinessModule; +import de.eshg.opendata.domain.model.OpenDataFileType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.time.Instant; +import java.time.LocalDate; +import java.util.Set; +import java.util.UUID; + +@Schema(name = "Version") +public record VersionDto( + @NotEmpty String versionName, + @NotNull + @Schema( + description = + "Version of the entity. Each time the entity is changed, it is incremented by one.") + long version, + @NotNull UUID externalId, + @NotNull int major, + @NotNull int minor, + @NotNull Instant publicationDate, + LocalDate statisticStartDate, + LocalDate statisticEndDate, + @NotNull Set<BusinessModule> sources, + String author, + String description, + @NotNull OpenDataFileType fileType, + @NotNull String fileName, + @NotNull int fileSize, + @NotEmpty String licence) {} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/domain/model/FileContent.java b/backend/opendata/src/main/java/de/eshg/opendata/domain/model/FileContent.java new file mode 100644 index 000000000..c1682dea7 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/domain/model/FileContent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata.domain.model; + +import de.eshg.domain.model.BaseEntity; +import de.eshg.lib.common.DataSensitivity; +import de.eshg.lib.common.SensitivityLevel; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; + +@Entity +@DataSensitivity(SensitivityLevel.PUBLIC) +public class FileContent extends BaseEntity { + + @Column(nullable = false) + private byte[] content; + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } +} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/domain/model/OpenDataFileType.java b/backend/opendata/src/main/java/de/eshg/opendata/domain/model/OpenDataFileType.java new file mode 100644 index 000000000..1c4556880 --- /dev/null +++ b/backend/opendata/src/main/java/de/eshg/opendata/domain/model/OpenDataFileType.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +package de.eshg.opendata.domain.model; + +import de.eshg.file.common.FileType; + +public enum OpenDataFileType { + PDF(FileType.PDF), + CSV(FileType.CSV); + + private final FileType fileType; + + OpenDataFileType(FileType fileType) { + this.fileType = fileType; + } + + public FileType getCommonFileType() { + return fileType; + } +} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/domain/model/Resource.java b/backend/opendata/src/main/java/de/eshg/opendata/domain/model/Resource.java index 59bd12a6d..fbcac394e 100644 --- a/backend/opendata/src/main/java/de/eshg/opendata/domain/model/Resource.java +++ b/backend/opendata/src/main/java/de/eshg/opendata/domain/model/Resource.java @@ -8,10 +8,11 @@ package de.eshg.opendata.domain.model; import de.eshg.domain.model.BaseEntity; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.OneToMany; import jakarta.persistence.OrderBy; -import jakarta.persistence.Transient; import java.util.ArrayList; import java.util.List; @@ -19,27 +20,23 @@ import java.util.List; public class Resource extends BaseEntity { @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) - private String ressourceName; + @Column(unique = true, nullable = false) + private String resourceName; @DataSensitivity(SensitivityLevel.PUBLIC) - @OneToMany(mappedBy = "resource") + @OneToMany( + mappedBy = Version_.RESOURCE, + cascade = {CascadeType.PERSIST}, + orphanRemoval = true) @OrderBy private final List<Version> versions = new ArrayList<>(); - @Transient - @DataSensitivity(SensitivityLevel.PUBLIC) - private Version latestVersion; - - public Version getLatestVersion() { - return latestVersion; - } - - public String getRessourceName() { - return ressourceName; + public String getResourceName() { + return resourceName; } - public void setRessourceName(String ressourceName) { - this.ressourceName = ressourceName; + public void setResourceName(String resourceName) { + this.resourceName = resourceName; } public List<Version> getVersions() { @@ -47,11 +44,10 @@ public class Resource extends BaseEntity { } public void addVersion(Version version) { - this.versions.add(version); - setLatestVersion(version); - } - - public void setLatestVersion(Version latestVersion) { - this.latestVersion = latestVersion; + if (version == null) { + return; + } + this.versions.addFirst(version); + version.setResource(this); } } diff --git a/backend/opendata/src/main/java/de/eshg/opendata/domain/model/Version.java b/backend/opendata/src/main/java/de/eshg/opendata/domain/model/Version.java index 0300edda4..eb6e224b0 100644 --- a/backend/opendata/src/main/java/de/eshg/opendata/domain/model/Version.java +++ b/backend/opendata/src/main/java/de/eshg/opendata/domain/model/Version.java @@ -5,20 +5,25 @@ package de.eshg.opendata.domain.model; -import de.eshg.domain.model.BaseEntity; +import de.eshg.domain.model.BaseEntityWithExternalId; import de.eshg.lib.common.BusinessModule; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; -import de.eshg.opendata.FileType; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.Index; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.OrderBy; import jakarta.persistence.Table; -import java.time.LocalDateTime; -import java.util.Date; +import jakarta.validation.constraints.NotNull; +import java.time.Instant; +import java.time.LocalDate; +import java.util.LinkedHashSet; import java.util.Set; import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLEnumJdbcType; @@ -26,48 +31,61 @@ import org.hibernate.dialect.PostgreSQLEnumJdbcType; @DataSensitivity(SensitivityLevel.PUBLIC) @Table(indexes = @Index(columnList = "resource_id")) @Entity -public class Version extends BaseEntity { +public class Version extends BaseEntityWithExternalId { public Version() {} - public Version(int major, int minor, Resource resource) { - this.major = major; - this.minor = minor; - this.resource = resource; - this.publicationDate = LocalDateTime.now(); - } + @NotNull private String versionName; - private int major; + @NotNull private int major; - private int minor; + @NotNull private int minor; - private LocalDateTime publicationDate; + @NotNull private Instant publicationDate; - private Date statisticStartDate; + private LocalDate statisticStartDate; - private Date statisticEndDate; + private LocalDate statisticEndDate; - private byte[] document; + @NotNull private int fileSize; - @ElementCollection(fetch = FetchType.EAGER) + @OneToOne( + optional = false, + fetch = FetchType.LAZY, + orphanRemoval = true, + cascade = CascadeType.PERSIST) + private FileContent document; + + @ElementCollection @JdbcType(PostgreSQLEnumJdbcType.class) - private Set<BusinessModule> sources; + @OrderBy + @Column(nullable = false) + private Set<BusinessModule> sources = new LinkedHashSet<>(); private String author; private String description; + @NotNull @JdbcType(PostgreSQLEnumJdbcType.class) - private FileType fileType; + private OpenDataFileType fileType; - private String fileName; + @NotNull private String fileName; - private String licence = "https://creativecommons.org/licenses/by/4.0/deed.de"; + @NotNull private String licence; @ManyToOne(optional = false) @JoinColumn(name = "resource_id") private Resource resource; + public String getVersionName() { + return versionName; + } + + public void setVersionName(String versionName) { + this.versionName = versionName; + } + public int getMajor() { return major; } @@ -84,35 +102,35 @@ public class Version extends BaseEntity { this.minor = minor; } - public LocalDateTime getPublicationDate() { + public Instant getPublicationDate() { return publicationDate; } - public void setPublicationDate(LocalDateTime publicationDate) { + public void setPublicationDate(Instant publicationDate) { this.publicationDate = publicationDate; } - public Date getStatisticStartDate() { + public LocalDate getStatisticStartDate() { return statisticStartDate; } - public void setStatisticStartDate(Date statisticStartDate) { + public void setStatisticStartDate(LocalDate statisticStartDate) { this.statisticStartDate = statisticStartDate; } - public Date getStatisticEndDate() { + public LocalDate getStatisticEndDate() { return statisticEndDate; } - public void setStatisticEndDate(Date statisticEndDate) { + public void setStatisticEndDate(LocalDate statisticEndDate) { this.statisticEndDate = statisticEndDate; } - public byte[] getDocument() { + public FileContent getDocument() { return document; } - public void setDocument(byte[] document) { + public void setDocument(FileContent document) { this.document = document; } @@ -140,11 +158,11 @@ public class Version extends BaseEntity { this.description = description; } - public FileType getFileType() { + public OpenDataFileType getFileType() { return fileType; } - public void setFileType(FileType fileType) { + public void setFileType(OpenDataFileType fileType) { this.fileType = fileType; } @@ -164,15 +182,24 @@ public class Version extends BaseEntity { this.licence = licence; } - public Resource getRessource() { + public Resource getResource() { return resource; } - public void setRessource(Resource resource) { + public void setResource(Resource resource) { this.resource = resource; } public String getFullVersionNumber() { return major + "." + minor; } + + @NotNull + public int getFileSize() { + return fileSize; + } + + public void setFileSize(@NotNull int fileSize) { + this.fileSize = fileSize; + } } diff --git a/backend/opendata/src/main/java/de/eshg/opendata/domain/repository/ResourceRepository.java b/backend/opendata/src/main/java/de/eshg/opendata/domain/repository/ResourceRepository.java index 1aba07dc8..b73be65b1 100644 --- a/backend/opendata/src/main/java/de/eshg/opendata/domain/repository/ResourceRepository.java +++ b/backend/opendata/src/main/java/de/eshg/opendata/domain/repository/ResourceRepository.java @@ -6,10 +6,34 @@ package de.eshg.opendata.domain.repository; import de.eshg.opendata.domain.model.Resource; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface ResourceRepository - extends JpaRepository<Resource, Long>, JpaSpecificationExecutor<Resource> {} + extends JpaRepository<Resource, Long>, JpaSpecificationExecutor<Resource> { + + @Query( + """ + SELECT r FROM Resource r + JOIN FETCH r.versions v + WHERE r.resourceName = :resourceName + ORDER BY v.publicationDate DESC + """) + Optional<Resource> findByResourceNameFetchingVersions(@Param("resourceName") String resourceName); + + @Query( + """ + SELECT DISTINCT r FROM Resource r + JOIN FETCH r.versions v + WHERE LOWER(v.fileName) LIKE LOWER(CONCAT('%', :searchTerm, '%')) + OR LOWER(v.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) + ORDER BY v.publicationDate DESC + """) + List<Resource> findByResourceNameOrVersionDescription(@Param("searchTerm") String searchTerm); +} diff --git a/backend/opendata/src/main/java/de/eshg/opendata/domain/repository/VersionRepository.java b/backend/opendata/src/main/java/de/eshg/opendata/domain/repository/VersionRepository.java index 7f9b95e23..4c89d60b8 100644 --- a/backend/opendata/src/main/java/de/eshg/opendata/domain/repository/VersionRepository.java +++ b/backend/opendata/src/main/java/de/eshg/opendata/domain/repository/VersionRepository.java @@ -6,10 +6,22 @@ package de.eshg.opendata.domain.repository; import de.eshg.opendata.domain.model.Version; +import jakarta.persistence.LockModeType; +import java.util.Optional; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface VersionRepository - extends JpaRepository<Version, Long>, JpaSpecificationExecutor<Version> {} + extends JpaRepository<Version, Long>, JpaSpecificationExecutor<Version> { + + @Query("select v from Version v where v.externalId = ?1") + @Lock(LockModeType.PESSIMISTIC_WRITE) + Optional<Version> findByExternalIdForUpdate(UUID externalId); + + Optional<Version> findByExternalId(UUID externalId); +} diff --git a/backend/opendata/src/main/resources/application.properties b/backend/opendata/src/main/resources/application.properties index dbef17751..ddc22581c 100644 --- a/backend/opendata/src/main/resources/application.properties +++ b/backend/opendata/src/main/resources/application.properties @@ -7,3 +7,4 @@ spring.datasource.username=testuser spring.datasource.password=testpassword spring.jpa.hibernate.ddl-auto=create + diff --git a/backend/relay-server/gradle.lockfile b/backend/relay-server/gradle.lockfile index 10099ecc0..3ea9f82b8 100644 --- a/backend/relay-server/gradle.lockfile +++ b/backend/relay-server/gradle.lockfile @@ -59,6 +59,7 @@ io.projectreactor.netty:reactor-netty-core:1.1.22=testCompileClasspath,testRunti io.projectreactor.netty:reactor-netty-http:1.1.22=testCompileClasspath,testRuntimeClasspath io.projectreactor:reactor-core:3.6.10=testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/resources/matrix/synapse/homeserver.yaml b/backend/resources/matrix/synapse/homeserver.yaml index 1196599cb..e3b6a7966 100644 --- a/backend/resources/matrix/synapse/homeserver.yaml +++ b/backend/resources/matrix/synapse/homeserver.yaml @@ -13,6 +13,8 @@ # each option, go to docs/usage/configuration/config_documentation.md or # https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html server_name: "synapse.local.dev" +public_baseurl: "http://localhost:8008" + pid_file: /data/homeserver.pid listeners: - port: 8008 @@ -22,6 +24,7 @@ listeners: resources: - names: [client, federation] compress: false + database: name: psycopg2 args: @@ -32,9 +35,12 @@ database: port: 5432 cp_min: 5 cp_max: 10 + log_config: "/data/synapse.local.dev.log.config" + +report_stats: false + registration_shared_secret: "k.@ukx06IL;5RcXHIo=^m4LI7lF*x-BgNegdB367MEyR@oe&~K" -report_stats: true macaroon_secret_key: "N;bGo,h:OFkx4IFfkB;S^:y;~3DGp*a.z2Nbp;k#Ik.,DcIQ*R" form_secret: "uedeOma**;.d~-MyHLTPpL+GiGUf9Sr:29UKDMEvX3jo6Vxk~i" signing_key_path: "/data/synapse.local.dev.signing.key" @@ -61,7 +67,10 @@ rc_login: per_second: 100 burst_count: 100 -public_baseurl: "http://localhost:8008" +max_upload_size: "10M" +max_image_pixels: "32M" +media_store_path: "/data/media_store" +uploads_path: "/home/synapse/config/uploads" sso: client_whitelist: # A list of client URLs which are whitelisted so that the user does not have to confirm giving access to their account to the URL @@ -88,7 +97,7 @@ oidc_providers: scopes: ["openid", "profile", "email"] user_mapping_provider: config: - localpart_template: "{{ user.given_name }}.{{ user.family_name }}" + localpart_template: "{{ user.preferred_username }}" display_name_template: "{{ user.name }}" email_template: "{{ user.email }}" # confirm_localpart: true # User can choose his Matrix ID @@ -98,10 +107,7 @@ oidc_providers: allow_insecure_http: true # WARNING -max_upload_size: "10M" -max_image_pixels: "32M" -media_store_path: "/data/media_store" -uploads_path: "/home/synapse/config/uploads" + #templates: # custom_template_directory: "/data/templates/" diff --git a/backend/rest-service-commons/gradle.lockfile b/backend/rest-service-commons/gradle.lockfile index 9cfaeacfe..375096831 100644 --- a/backend/rest-service-commons/gradle.lockfile +++ b/backend/rest-service-commons/gradle.lockfile @@ -32,7 +32,7 @@ org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt -org.jetbrains:annotations:26.0.0=compileClasspath +org.jetbrains:annotations:26.0.1=compileClasspath org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath @@ -79,4 +79,4 @@ org.zalando:logbook-servlet:3.9.0=productionRuntimeClasspath,runtimeClasspath,te org.zalando:logbook-spring-boot-autoconfigure:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.zalando:logbook-spring-boot-starter:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.zalando:logbook-spring:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath +empty=annotationProcessor,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath diff --git a/backend/rest-service-errors/gradle.lockfile b/backend/rest-service-errors/gradle.lockfile index 4c6f0e9d2..293cb179f 100644 --- a/backend/rest-service-errors/gradle.lockfile +++ b/backend/rest-service-errors/gradle.lockfile @@ -7,6 +7,7 @@ com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.13.4=testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.13.4=testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/school-entry/build.gradle b/backend/school-entry/build.gradle index 086778e09..c2f1417c0 100644 --- a/backend/school-entry/build.gradle +++ b/backend/school-entry/build.gradle @@ -5,15 +5,15 @@ plugins { dependencies { implementation project(':lib-appointmentblock') implementation project(':lib-base-client') - implementation project(':lib-procedures') implementation project(':lib-calendar') - implementation project(":lib-statistics") implementation project(":lib-document-generator") + implementation project(':lib-procedures') + implementation project(":lib-statistics") + implementation project(":lib-xlsx-import") implementation project(':business-module-persistence-commons') + implementation project(':file-commons') implementation 'org.apache.commons:commons-math3:latest.release' - implementation 'org.apache.poi:poi:latest.release' - implementation 'org.apache.poi:poi-ooxml:latest.release' implementation 'de.cronn:reflection-util:latest.release' implementation 'com.google.zxing:core:latest.release' implementation 'org.apache.xmlgraphics:batik-svggen:latest.release' @@ -29,9 +29,9 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' testImplementation testFixtures(project(':business-module-persistence-commons')) - testImplementation 'jakarta.mail:jakarta.mail-api' testImplementation 'com.google.zxing:javase:latest.release' testImplementation testFixtures(project(':lib-document-generator')) + testImplementation testFixtures(project(':lib-xlsx-import')) } dockerCompose { diff --git a/backend/school-entry/gradle.lockfile b/backend/school-entry/gradle.lockfile index c814de14f..590befa8c 100644 --- a/backend/school-entry/gradle.lockfile +++ b/backend/school-entry/gradle.lockfile @@ -17,11 +17,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.jai-imageio:jai-imageio-core:1.4.0=testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.virtuald:curvesapi:1.08=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -33,7 +33,7 @@ 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.google.zxing:core:3.5.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.zxing:javase:3.5.3=testCompileClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=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 @@ -42,11 +42,11 @@ com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath, com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,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=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -57,10 +57,10 @@ de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeCla de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.rototor.pdfbox:graphics2d:3.0.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.github.openhtmltopdf:openhtmltopdf-core:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.github.openhtmltopdf:openhtmltopdf-pdfbox:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -79,27 +79,28 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.inject:jakarta.inject-api:2.0.1=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -jakarta.mail:jakarta.mail-api:2.1.3=testCompileClasspath,testRuntimeClasspath +jakarta.mail:jakarta.mail-api:2.1.3=testRuntimeClasspath jakarta.persistence:jakarta.persistence-api:3.1.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.transaction:jakarta.transaction-api:2.0.1=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.14.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.26.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -151,7 +152,7 @@ org.apache.xmlgraphics:batik-xml:1.18=compileClasspath,productionRuntimeClasspat org.apache.xmlgraphics:xmlgraphics-commons:2.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcmail-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -164,8 +165,8 @@ org.freemarker:freemarker:2.3.33=productionRuntimeClasspath,runtimeClasspath,tes org.glassfish.jaxb:jaxb-core:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -177,16 +178,15 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:26.0.0=compileClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:26.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath @@ -194,12 +194,12 @@ org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspa org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -209,7 +209,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -225,7 +225,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -248,14 +248,14 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.testcontainers:database-commons:1.19.8=testRuntimeClasspath org.testcontainers:jdbc:1.19.8=testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath diff --git a/backend/school-entry/openApi.yaml b/backend/school-entry/openApi.yaml index fad0a2a71..73f7edd2a 100644 --- a/backend/school-entry/openApi.yaml +++ b/backend/school-entry/openApi.yaml @@ -2864,6 +2864,18 @@ paths: description: OK tags: - TestHelper + /test-helper/school-entries/closed: + get: + operationId: getIdsOfClosedProcedures + responses: + "200": + content: + '*/*': + schema: + $ref: "#/components/schemas/GetClosedProceduresResponse" + description: OK + tags: + - TestHelper /test-helper/school-entries/{procedureId}/citizen-user-id: delete: operationId: clearCitizenUserId @@ -4886,6 +4898,16 @@ components: - appointmentStart - child - isClosedProcedure + GetClosedProceduresResponse: + type: object + properties: + procedureIds: + type: array + items: + type: string + format: uuid + required: + - procedureIds GetCountryCodesResponse: type: object description: "Possible countries and their group. Expected keys: SchoolEntryCountryCode" @@ -6144,6 +6166,7 @@ components: enum: - PATIENT - PARENT + - PROFESSIONAL PhysicalExamination: type: object description: Physical examination results. @@ -6408,6 +6431,9 @@ components: - TM_VACCINATION_CONSULTATION - MEASLES_PROTECTION - STI_PROTECTION + - MEDICAL_REGISTRY_ENTRY + - MEDICAL_REGISTRY_CITIZEN_DRAFT + - MEDICAL_REGISTRY_EMPLOYEE_DRAFT ProcedureWithDuration: type: object properties: @@ -6872,15 +6898,8 @@ components: SchoolEntryFeature: type: string enum: - - MEDICAL_REPORT - - MERGE_PROCEDURES_ON_IMPORT - CLOSE_PROCEDURE - REOPEN_PROCEDURE - - DELETE_PROCEDURE - - SEARCH_BY_KNOWLEDGE_FACTORS - - SCHOOL_INFO_LETTER - - SCHOOL_YEAR - - WAITING_ROOM - IMPORT_PAST_PROCEDURES SchoolEntryLabel: type: object @@ -7743,4 +7762,4 @@ components: - IN_EXAMINATION_DOCTOR - IN_EXAMINATION_MFA - DONE - - CANCELLED + - CANCELLED \ No newline at end of file diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/ProceduresHelper.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/ProceduresHelper.java index e55311768..10b8514bf 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/ProceduresHelper.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/ProceduresHelper.java @@ -42,7 +42,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.Assert; @Component -class ProceduresHelper { +public class ProceduresHelper { private final SchoolEntryProcedureRepository schoolEntryProcedureRepository; private final PersonClient personClient; diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryCitizenService.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryCitizenService.java index 17be45c61..d2fa4d37a 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryCitizenService.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryCitizenService.java @@ -162,7 +162,7 @@ public class SchoolEntryCitizenService { public AppointmentAddressDto getAppointmentAddress(SchoolEntryProcedure procedure) { return Optional.ofNullable(schoolEntryService.getAppointmentLocation(procedure)) - .map((contactId) -> mapToAppointmentAddress(contactClient.getContact(contactId))) + .map(contactId -> mapToAppointmentAddress(contactClient.getContact(contactId))) .orElse(mapToAppointmentAddress(departmentClient.getDepartmentInfo())); } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryController.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryController.java index 8078d71d7..6fcd10ad5 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryController.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryController.java @@ -5,24 +5,26 @@ package de.eshg.schoolentry; -import static de.eshg.schoolentry.importer.ImportValidator.validateFileExistsAndHasCorrectType; -import static de.eshg.schoolentry.importer.ImportValidator.validateHeaderExists; -import static de.eshg.schoolentry.importer.ImportValidator.validateSheet; +import static de.eshg.lib.xlsximport.ImportValidator.validateFileExistsAndHasCorrectType; +import static de.eshg.lib.xlsximport.ImportValidator.validateHeaderExists; +import static de.eshg.lib.xlsximport.ImportValidator.validateSheet; import static de.eshg.schoolentry.util.SchoolEntrySystemProgressEntryType.MEDICAL_REPORT_GENERATED; import static de.eshg.schoolentry.util.SchoolEntrySystemProgressEntryType.SCHOOL_INFO_LETTER_GENERATED; -import de.base.rest.CustomMediaTypes; import de.eshg.api.commons.InlineParameterObject; +import de.eshg.file.common.CustomMediaTypes; import de.eshg.lib.appointmentblock.LocationSelectionMode; import de.eshg.lib.appointmentblock.api.AppointmentDto; import de.eshg.lib.appointmentblock.api.GetFreeAppointmentsResponse; import de.eshg.lib.appointmentblock.spring.AppointmentBlockProperties; import de.eshg.lib.procedure.domain.model.Pdf; +import de.eshg.lib.procedure.domain.model.TaskType; +import de.eshg.lib.xlsximport.model.ImportResult; +import de.eshg.lib.xlsximport.util.FileResponseUtil; import de.eshg.rest.service.error.BadRequestException; import de.eshg.rest.service.security.config.BaseUrls; import de.eshg.schoolentry.api.*; import de.eshg.schoolentry.api.anamnesis.AnamnesisDto; -import de.eshg.schoolentry.business.model.ImportResult; import de.eshg.schoolentry.business.model.ProcedureDetailsData; import de.eshg.schoolentry.config.SchoolEntryFeature; import de.eshg.schoolentry.config.SchoolEntryFeatureToggle; @@ -36,6 +38,7 @@ import de.eshg.schoolentry.pdf.schoolinfoletter.SchoolInfoLetterGenerator; import de.eshg.schoolentry.pdf.schoolinfoletter.SchoolInfoLetterValidator; import de.eshg.schoolentry.util.ExceptionUtil; import de.eshg.schoolentry.util.ProgressEntryUtil; +import de.eshg.schoolentry.util.TaskUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -60,12 +63,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.http.ContentDisposition; -import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -137,13 +138,9 @@ public class SchoolEntryController { @InlineParameterObject @ParameterObject @Valid ProcedurePaginationAndSortParameters paginationAndSortParameters, @InlineParameterObject @ParameterObject @Valid ProcedureSearchParameters searchParameters) { - if (featureToggle.isNewFeatureEnabled(SchoolEntryFeature.SEARCH_BY_KNOWLEDGE_FACTORS)) { - Validator.validateOnlyOneOfSearchAndFilterParametersAreSet( - filterParameters, searchParameters); - Validator.validateSearchParametersAreComplete(searchParameters); - } else { - validator.validateSearchParametersAreNull(searchParameters); - } + + Validator.validateOnlyOneOfSearchAndFilterParametersAreSet(filterParameters, searchParameters); + Validator.validateSearchParametersAreComplete(searchParameters); PagedProcedures pagedProcedures = schoolEntryService.getProcedures( filterParameters, paginationAndSortParameters, searchParameters); @@ -185,7 +182,6 @@ public class SchoolEntryController { public ProcedureDetailsDto closeProcedure( @PathVariable("procedureId") UUID procedureId, @Valid @RequestBody CloseProcedureRequest request) { - featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.SCHOOL_INFO_LETTER); featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.CLOSE_PROCEDURE); SchoolEntryProcedure procedure = @@ -226,7 +222,6 @@ public class SchoolEntryController { public void deleteProcedure( @PathVariable("procedureId") UUID procedureId, @Valid @RequestBody DeleteProcedureRequest request) { - featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.DELETE_PROCEDURE); SchoolEntryProcedure procedure = schoolEntryService.findProcedureByExternalIdForUpdate(procedureId, request.version()); Validator.validateDeletionOfProcedure(schoolEntryService.augmentWithDetails(procedure)); @@ -485,7 +480,6 @@ public class SchoolEntryController { @RequestParam(value = "schoolYear") @Min(1900) int schoolYear, @RequestPart("file") MultipartFile file) throws IOException { - featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.SCHOOL_YEAR); featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.IMPORT_PAST_PROCEDURES); validator.validateSchoolExists(schoolId); return importData(file, ImportType.PAST_PROCEDURE_LIST, schoolId, null, Year.of(schoolYear)); @@ -496,9 +490,7 @@ public class SchoolEntryController { throws IOException { validateFileExistsAndHasCorrectType(file); - if (featureToggle.isNewFeatureEnabled(SchoolEntryFeature.SCHOOL_YEAR)) { - validator.validateSchoolYear(schoolYear); - } + validator.validateSchoolYear(schoolYear); try (InputStream inputStream = file.getInputStream(); XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) { @@ -513,18 +505,7 @@ public class SchoolEntryController { importService.processSheetAndPersistProcedures( sheet, importType, schoolId, locationId, schoolYear); - MultiValueMap<String, Object> multipart = new LinkedMultiValueMap<>(); - - HttpHeaders statisticsHeaders = new HttpHeaders(); - statisticsHeaders.setContentType(MediaType.APPLICATION_JSON); - multipart.add("statistics", new HttpEntity<>(result.statistics(), statisticsHeaders)); - - HttpHeaders fileHeaders = new HttpHeaders(); - fileHeaders.setContentType(CustomMediaTypes.APPLICATION_XLSX); - fileHeaders.setContentDisposition(fileFormData(filename())); - multipart.add("file", new HttpEntity<>(result.file(), fileHeaders)); - - return ResponseEntity.ok().contentType(MediaType.MULTIPART_FORM_DATA).body(multipart); + return FileResponseUtil.mapImportResultToMultipartResponse(result, filename()); } } @@ -537,7 +518,7 @@ public class SchoolEntryController { produces = CustomMediaTypes.APPLICATION_XLSX_VALUE) @Operation(summary = "Get the XLSX citizen list template.") public ResponseEntity<Resource> getCitizenListTemplate() { - return getListTemplate(citizenListTemplate); + return FileResponseUtil.getTemplateFileResponse(citizenListTemplate); } @GetMapping( @@ -545,28 +526,7 @@ public class SchoolEntryController { produces = CustomMediaTypes.APPLICATION_XLSX_VALUE) @Operation(summary = "Get the XLSX school list template.") public ResponseEntity<Resource> getSchoolListTemplate() { - return getListTemplate(schoolListTemplate); - } - - private static ResponseEntity<Resource> getListTemplate(Resource citizenListTemplate) { - return ResponseEntity.ok() - .header( - HttpHeaders.CONTENT_DISPOSITION, - fileAttachment(citizenListTemplate.getFilename()).toString()) - .header(HttpHeaders.CONTENT_TYPE, CustomMediaTypes.APPLICATION_XLSX_VALUE) - .body(citizenListTemplate); - } - - private static ContentDisposition fileFormData(String filename) { - return file(filename, ContentDisposition.formData()); - } - - private static ContentDisposition fileAttachment(String filename) { - return file(filename, ContentDisposition.attachment()); - } - - private static ContentDisposition file(String filename, ContentDisposition.Builder builder) { - return builder.name("file").filename(filename).build(); + return FileResponseUtil.getTemplateFileResponse(schoolListTemplate); } @GetMapping("/{procedureId}/vaccination-status") @@ -620,7 +580,6 @@ public class SchoolEntryController { public ResponseEntity<Resource> createMedicalReport( @PathVariable("procedureId") UUID procedureId, @Valid @RequestBody CreateMedicalReportRequest request) { - featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.MEDICAL_REPORT); SchoolEntryProcedure procedure = schoolEntryService.findProcedureByExternalId(procedureId); Validator.validateProcedureStatusNotClosed(procedure); ProcedureDetailsData procedureDetailsData = schoolEntryService.augmentWithDetails(procedure); @@ -641,7 +600,6 @@ public class SchoolEntryController { public ResponseEntity<Resource> createSchoolInfoLetter( @PathVariable("procedureId") UUID procedureId, @Valid @RequestBody CreateSchoolInfoLetterRequest request) { - featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.SCHOOL_INFO_LETTER); SchoolEntryProcedure procedure = schoolEntryService.findProcedureByExternalId(procedureId); Validator.validateProcedureStatusNotClosed(procedure); @@ -652,6 +610,7 @@ public class SchoolEntryController { procedure, procedureDetailsData, request); ProgressEntryUtil.addProgressEntry(procedure, SCHOOL_INFO_LETTER_GENERATED, pdf); procedure.setschoolInfoLetterCreatedAt(Instant.now(clock)); + TaskUtil.closeOptionalTaskOfType(procedure, TaskType.PERFORM_SCHOOL_ENTRY_EXAMINATION); return ResponseEntity.ok() .header( @@ -666,7 +625,6 @@ public class SchoolEntryController { @Operation(summary = "Update waiting room details for a procedure.") public WaitingRoomDto updateWaitingRoomDetails( @PathVariable("procedureId") UUID procedureId, @Valid @RequestBody WaitingRoomDto request) { - featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.WAITING_ROOM); assertLocationModeNotSet(); WaitingRoom waitingRoom = schoolEntryService.findWaitingRoomForUpdate(procedureId, request.version()); @@ -683,7 +641,6 @@ public class SchoolEntryController { @Transactional(readOnly = true) public ResponseEntity<ValidateRequiredProcedureDataResponse> validateCompleteness( @PathVariable("procedureId") UUID procedureId) { - featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.SCHOOL_INFO_LETTER); SchoolEntryProcedure procedure = schoolEntryService.findProcedureByExternalId(procedureId); Map<RequiredProcedureData, Boolean> validationResult = @@ -704,7 +661,6 @@ public class SchoolEntryController { public GetWaitingRoomProceduresResponse getWaitingRoomProcedures( @InlineParameterObject @ParameterObject @Valid WaitingRoomProcedurePaginationAndSortParameters paginationAndSortParameters) { - featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.WAITING_ROOM); assertLocationModeNotSet(); PagedWaitingRoomProcedures pagedProcedures = diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryService.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryService.java index 541e35c8b..8d223f792 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryService.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/SchoolEntryService.java @@ -35,11 +35,9 @@ import de.eshg.lib.procedure.procedures.ProcedureDeletionService; import de.eshg.lib.procedure.procedures.ProcedureSearchService; import de.eshg.rest.service.error.BadRequestException; import de.eshg.rest.service.error.NotFoundException; -import de.eshg.rest.service.security.CurrentUserHelper; import de.eshg.schoolentry.api.*; import de.eshg.schoolentry.business.model.*; import de.eshg.schoolentry.client.PersonClient; -import de.eshg.schoolentry.config.SchoolEntryFeature; import de.eshg.schoolentry.config.SchoolEntryFeatureToggle; import de.eshg.schoolentry.config.SchoolEntryProperties; import de.eshg.schoolentry.domain.model.*; @@ -58,6 +56,7 @@ import de.eshg.schoolentry.util.ProcedureSortKey; import de.eshg.schoolentry.util.ProcedureTypeAssignmentHelper; import de.eshg.schoolentry.util.ProgressEntryUtil; import de.eshg.schoolentry.util.SchoolEntrySystemProgressEntryType; +import de.eshg.schoolentry.util.TaskUtil; import de.eshg.validation.ValidationUtil; import java.time.Clock; import java.time.Instant; @@ -107,6 +106,7 @@ public class SchoolEntryService { private final ProceduresHelper proceduresHelper; private final ProcedureDeletionService<SchoolEntryProcedure> procedureDeletionService; private final ProcedureTypeAssignmentHelper procedureTypeAssignmentHelper; + private final TaskUtil taskUtil; public SchoolEntryService( SchoolEntryProcedureRepository schoolEntryProcedureRepository, @@ -136,7 +136,8 @@ public class SchoolEntryService { SchoolEntryFeatureToggle schoolEntryFeatureToggle, ProceduresHelper proceduresHelper, ProcedureDeletionService<SchoolEntryProcedure> procedureDeletionService, - ProcedureTypeAssignmentHelper procedureTypeAssignmentHelper) { + ProcedureTypeAssignmentHelper procedureTypeAssignmentHelper, + TaskUtil taskUtil) { this.schoolEntryProcedureRepository = schoolEntryProcedureRepository; this.personRepository = personRepository; this.hearingTestResultRepository = hearingTestResultRepository; @@ -165,6 +166,7 @@ public class SchoolEntryService { this.proceduresHelper = proceduresHelper; this.procedureDeletionService = procedureDeletionService; this.procedureTypeAssignmentHelper = procedureTypeAssignmentHelper; + this.taskUtil = taskUtil; } public SchoolEntryProcedure createProcedure(CreateProcedureRequest request) { @@ -187,18 +189,31 @@ public class SchoolEntryService { Year schoolYear, DataOrigin dataOrigin) { List<SchoolEntryProcedure> createdProcedures = - createProcedures(procedures, schoolId, locationId, schoolYear, dataOrigin); + createProcedures( + procedures, schoolId, locationId, schoolYear, dataOrigin, ProcedureStatus.OPEN); createdProcedures.forEach( - procedure -> addOpenTaskWithType(procedure, TaskType.BOOK_APPOINTMENT)); + procedure -> taskUtil.addOpenTaskOfType(procedure, TaskType.BOOK_APPOINTMENT)); return createdProcedures; } + public List<SchoolEntryProcedure> createProceduresFromDataImport( + List<ImportProcedureData> procedures, UUID schoolId, UUID locationId, Year schoolYear) { + return createProcedures( + procedures, + schoolId, + locationId, + schoolYear, + DataOrigin.DATA_IMPORT, + ProcedureStatus.CLOSED); + } + public List<SchoolEntryProcedure> createProcedures( List<ImportProcedureData> procedures, UUID schoolId, UUID locationId, Year schoolYear, - DataOrigin dataOrigin) { + DataOrigin dataOrigin, + ProcedureStatus initialProcedureStatus) { List<SchoolEntryProcedure> result = new ArrayList<>(); Label specialNeedsLabel = null; @@ -226,7 +241,8 @@ public class SchoolEntryService { locationId, schoolYear, procedure.isEntryLevel(), - procedure.examinationDate()); + procedure.examinationDate(), + initialProcedureStatus); if (procedure.isEarlyExamination()) { Assert.notNull(specialNeedsLabel, "specialNeedsLabel must be fetched at this point"); @@ -268,9 +284,10 @@ public class SchoolEntryService { UUID locationId, Year schoolYear, boolean isEntryLevel, - LocalDate examinationDate) { + LocalDate examinationDate, + ProcedureStatus initialProcedureStatus) { SchoolEntryProcedure schoolEntryProcedure = new SchoolEntryProcedure(); - schoolEntryProcedure.updateProcedureStatus(ProcedureStatus.OPEN, clock, auditLogger); + schoolEntryProcedure.updateProcedureStatus(initialProcedureStatus, clock, auditLogger); schoolEntryProcedure.setProcedureType(type); schoolEntryProcedure.setSchoolId(schoolId); schoolEntryProcedure.setLocationId(locationId); @@ -290,10 +307,7 @@ public class SchoolEntryService { schoolEntryProcedure.setVaccinationStatus(new VaccinationStatus()); schoolEntryProcedure.setAnamnesis(new Anamnesis()); schoolEntryProcedure.setWaitingRoom(new WaitingRoom()); - - if (schoolEntryFeatureToggle.isNewFeatureEnabled(SchoolEntryFeature.SCHOOL_YEAR)) { - schoolEntryProcedure.setSchoolYear(schoolYear); - } + schoolEntryProcedure.setSchoolYear(schoolYear); return schoolEntryProcedureRepository.save(schoolEntryProcedure); } @@ -315,17 +329,6 @@ public class SchoolEntryService { return person; } - private void addOpenTaskWithType(SchoolEntryProcedure schoolEntryProcedure, TaskType type) { - SchoolEntryTask task = new SchoolEntryTask(); - task.assign( - CurrentUserHelper.getCurrentUserId(), - CurrentUserHelper.getCurrentUserId(), - Instant.now(clock)); - task.setTaskStatus(TaskStatus.OPEN); - task.setTaskType(type); - schoolEntryProcedure.addTask(task); - } - ProcedureDetailsData findAndAugmentProcedureByExternalId(UUID procedureId) { SchoolEntryProcedure schoolEntryProcedure = findProcedureByExternalId(procedureId); return augmentWithDetails(schoolEntryProcedure); @@ -468,9 +471,9 @@ public class SchoolEntryService { start.atZone(clock.getZone()).format(ReportGeneratorConstants.DATE_FORMAT_DE)), invitation); - closeSingleTaskOfType(procedure, TaskType.BOOK_APPOINTMENT); + TaskUtil.closeSingleTaskOfType(procedure, TaskType.BOOK_APPOINTMENT); if (!procedure.hasTaskOfType(TaskType.PERFORM_SCHOOL_ENTRY_EXAMINATION)) { - addOpenTaskWithType(procedure, TaskType.PERFORM_SCHOOL_ENTRY_EXAMINATION); + taskUtil.addOpenTaskOfType(procedure, TaskType.PERFORM_SCHOOL_ENTRY_EXAMINATION); } procedure.getTaskOfType(TaskType.PERFORM_SCHOOL_ENTRY_EXAMINATION).updateDueAt(start); @@ -546,7 +549,6 @@ public class SchoolEntryService { ProcedurePageSpec pageSpec = createPageSpec(paginationAndSortParameters); if (filterParameters.schoolYearFilter() != null) { - schoolEntryFeatureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.SCHOOL_YEAR); validator.validateSchoolYear(Year.of(filterParameters.schoolYearFilter())); } @@ -576,10 +578,6 @@ public class SchoolEntryService { private ProcedurePageSpec createPageSpec( ProcedurePaginationAndSortParameters paginationAndSortParameters) { - if (Objects.equals( - paginationAndSortParameters.sortKey(), SchoolEntryProcedureSortKey.SCHOOL_YEAR)) { - schoolEntryFeatureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.SCHOOL_YEAR); - } return ProcedureMapper.mapToPageSpec( paginationAndSortParameters.pageNumberOrFallback(0), paginationAndSortParameters.pageSizeOrFallback(25), @@ -808,16 +806,10 @@ public class SchoolEntryService { private void updateSchoolYear( SchoolEntryProcedure procedure, Year persistedSchoolYear, Year requestedSchoolYear) { - if (schoolEntryFeatureToggle.isNewFeatureEnabled(SchoolEntryFeature.SCHOOL_YEAR)) { - if (!Objects.equals(persistedSchoolYear, requestedSchoolYear)) { - validator.validateSchoolYear(requestedSchoolYear); - log.info("Modifying schoolYear {} to {}", persistedSchoolYear, requestedSchoolYear); - procedure.setSchoolYear(requestedSchoolYear); - } - } else { - if (requestedSchoolYear != null) { - log.warn("Ignoring given school year since the feature toggle is disabled"); - } + if (!Objects.equals(persistedSchoolYear, requestedSchoolYear)) { + validator.validateSchoolYear(requestedSchoolYear); + log.info("Modifying schoolYear {} to {}", persistedSchoolYear, requestedSchoolYear); + procedure.setSchoolYear(requestedSchoolYear); } } @@ -827,6 +819,7 @@ public class SchoolEntryService { UUID citizenUserId = procedure.getCitizenUserId(); if (citizenUserId != null) { removeCitizenUserAccess(citizenUserId); + procedure.setCitizenUserId(null); } schoolEntryProcedureRepository.flush(); @@ -837,7 +830,8 @@ public class SchoolEntryService { CitizenAccessCodeUserDto citizenUser = citizenAccessCodeUserApi.getCitizenAccessCodeUser(citizenUserId); citizenAccessCodeUserApi.deleteCitizenAccessCodeUser(citizenUser.userId()); - } catch (NotFoundException ignored) { + } catch (HttpClientErrorException.NotFound ignored) { + // Access Code User is not present, so there is nothing to do for deletion } } @@ -845,6 +839,8 @@ public class SchoolEntryService { SchoolEntryProcedure procedure = findProcedureByExternalIdForUpdate(procedureId, version); Validator.validateProcedureStatusIsClosed(procedure); procedure.updateProcedureStatus(ProcedureStatus.OPEN, clock, auditLogger); + + schoolEntryProcedureRepository.flush(); return procedure; } @@ -862,11 +858,6 @@ public class SchoolEntryService { procedureDeletionService.deleteProcedure(procedure.getExternalId()); } - private void closeSingleTaskOfType(SchoolEntryProcedure procedure, TaskType type) { - SchoolEntryTask taskToUpdate = procedure.getTaskOfType(type); - taskToUpdate.setTaskStatus(TaskStatus.CLOSED); - } - public SchoolEntryProcedure updateChildData( SchoolEntryProcedure procedure, Person child, UpdatePersonRequest request) { UUID updatedFileStateId = personClient.updateChild(child.getCentralFileStateId(), request); @@ -888,6 +879,7 @@ public class SchoolEntryService { switch (person.getPersonType()) { case PATIENT -> CHILD_SYNCED_WITH_CENTRAL_FILE; case PARENT -> CUSTODIAN_SYNCED_WITH_CENTRAL_FILE; + default -> throw new IllegalStateException("Unknown person type"); }; ProgressEntryUtil.addProgressEntry(procedure, progressEntryType); @@ -1473,8 +1465,7 @@ public class SchoolEntryService { procedure.setLocationId(locationId); } - if (schoolEntryFeatureToggle.isNewFeatureEnabled(SchoolEntryFeature.SCHOOL_YEAR) - && schoolYear != null) { + if (schoolYear != null) { procedure.setSchoolYear(schoolYear); } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/Validator.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/Validator.java index ef419d0b7..ecdea080d 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/Validator.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/Validator.java @@ -24,8 +24,6 @@ import de.eshg.schoolentry.api.*; import de.eshg.schoolentry.api.anamnesis.AnamnesisDto; import de.eshg.schoolentry.business.model.ChildData; import de.eshg.schoolentry.business.model.ProcedureDetailsData; -import de.eshg.schoolentry.config.SchoolEntryFeature; -import de.eshg.schoolentry.config.SchoolEntryFeatureToggle; import de.eshg.schoolentry.config.SchoolEntryProperties; import de.eshg.schoolentry.domain.model.SchoolEntryProcedure; import de.eshg.schoolentry.domain.repository.Icd10CodeRepository; @@ -63,7 +61,6 @@ public class Validator { private final Icd10GroupRepository icd10GroupRepository; private final ContactClient contactClient; private final Clock clock; - private final SchoolEntryFeatureToggle featureToggle; private final SchoolEntryProperties schoolEntryProperties; private final AppointmentBlockProperties appointmentBlockProperties; @@ -72,24 +69,16 @@ public class Validator { Icd10GroupRepository icd10GroupRepository, ContactClient contactClient, Clock clock, - SchoolEntryFeatureToggle featureToggle, SchoolEntryProperties schoolEntryProperties, AppointmentBlockProperties appointmentBlockProperties) { this.icd10CodeRepository = icd10CodeRepository; this.icd10GroupRepository = icd10GroupRepository; this.contactClient = contactClient; this.clock = clock; - this.featureToggle = featureToggle; this.schoolEntryProperties = schoolEntryProperties; this.appointmentBlockProperties = appointmentBlockProperties; } - void validateSearchParametersAreNull(ProcedureSearchParameters searchParameters) { - if (hasNonNullValue(searchParameters)) { - featureToggle.assertNewFeatureIsEnabled(SchoolEntryFeature.SEARCH_BY_KNOWLEDGE_FACTORS); - } - } - static void validateOnlyOneOfSearchAndFilterParametersAreSet( ProcedureFilterParameters filterParameters, ProcedureSearchParameters searchParameters) { if (hasNonNullValue(filterParameters) && hasNonNullValue(searchParameters)) { diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/api/CreatePersonDto.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/api/CreatePersonDto.java index 3cde7dee3..9cfdd300a 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/api/CreatePersonDto.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/api/CreatePersonDto.java @@ -5,10 +5,10 @@ package de.eshg.schoolentry.api; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -28,7 +28,7 @@ public record CreatePersonDto( @NotNull LocalDate dateOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, List<@NotNull @Size(min = 6, max = 254) String> emailAddresses, List<@NotNull @Size(min = 1, max = 23) String> phoneNumbers, @Valid AddressDto contactAddress, @@ -54,7 +54,7 @@ public record CreatePersonDto( String lastName, LocalDate dateOfBirth, String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, List<String> phoneNumbers, AddressDto contactAddress) { this( diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/api/GetClosedProceduresResponse.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/api/GetClosedProceduresResponse.java new file mode 100644 index 000000000..d7a14511b --- /dev/null +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/api/GetClosedProceduresResponse.java @@ -0,0 +1,12 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.schoolentry.api; + +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.UUID; + +public record GetClosedProceduresResponse(@NotNull List<UUID> procedureIds) {} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/api/PersonDetailsDto.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/api/PersonDetailsDto.java index 76c64f407..0105e4860 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/api/PersonDetailsDto.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/api/PersonDetailsDto.java @@ -5,10 +5,10 @@ package de.eshg.schoolentry.api; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -30,7 +30,7 @@ public record PersonDetailsDto( @NotNull LocalDate dateOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, @NotNull List<@NotNull @Size(min = 6, max = 254) String> emailAddresses, @NotNull List<@NotNull @Size(min = 1, max = 23) String> phoneNumbers, @Valid AddressDto contactAddress, diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/api/UpdatePersonRequest.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/api/UpdatePersonRequest.java index 6673299cc..0d86e562a 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/api/UpdatePersonRequest.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/api/UpdatePersonRequest.java @@ -5,10 +5,10 @@ package de.eshg.schoolentry.api; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -25,7 +25,7 @@ public record UpdatePersonRequest( @NotNull LocalDate dateOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, List<@NotNull @Size(min = 6, max = 254) String> emailAddresses, List<@NotNull @Size(min = 1, max = 23) String> phoneNumbers, @Valid AddressDto contactAddress, diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ChildData.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ChildData.java index fc771fe93..e83954756 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ChildData.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ChildData.java @@ -5,9 +5,9 @@ package de.eshg.schoolentry.business.model; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import java.time.LocalDate; import java.util.List; @@ -16,7 +16,7 @@ public record ChildData( String lastName, LocalDate dateOfBirth, String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, GenderDto gender, AddressDto address, List<String> phoneNumbers) { @@ -26,7 +26,7 @@ public record ChildData( String lastName, LocalDate dateOfBirth, String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, GenderDto gender, AddressDto address) { this(firstName, lastName, dateOfBirth, placeOfBirth, countryOfBirth, gender, address, null); diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ChildDetailsData.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ChildDetailsData.java index 326196241..10bd997a1 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ChildDetailsData.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ChildDetailsData.java @@ -5,10 +5,10 @@ package de.eshg.schoolentry.business.model; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -25,7 +25,7 @@ public record ChildDetailsData( LocalDate dateOfBirth, String nameAtBirth, String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, List<String> emailAddresses, List<String> phoneNumbers, AddressDto contactAddress, diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/CustodianDetailsData.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/CustodianDetailsData.java index 7a8fac447..f584fc2e4 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/CustodianDetailsData.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/CustodianDetailsData.java @@ -5,10 +5,10 @@ package de.eshg.schoolentry.business.model; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.AddressDto; +import de.eshg.lib.common.CountryCode; import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -25,7 +25,7 @@ public record CustodianDetailsData( LocalDate dateOfBirth, String nameAtBirth, String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, List<String> emailAddresses, List<String> phoneNumbers, AddressDto contactAddress, diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportChildData.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportChildData.java index c470cf1e4..c28ec0911 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportChildData.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportChildData.java @@ -5,8 +5,9 @@ package de.eshg.schoolentry.business.model; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; +import de.eshg.lib.common.CountryCode; +import de.eshg.lib.xlsximport.model.AddressData; import java.time.LocalDate; public record ImportChildData( @@ -14,7 +15,7 @@ public record ImportChildData( String lastName, LocalDate dateOfBirth, String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, GenderDto gender, AddressData address, String phoneNumber) { @@ -33,7 +34,7 @@ public record ImportChildData( String lastName, LocalDate dateOfBirth, String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, GenderDto gender, AddressData address) { this(firstName, lastName, dateOfBirth, placeOfBirth, countryOfBirth, gender, address, null); diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportCustodianData.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportCustodianData.java index 135227b18..dbc1a5f92 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportCustodianData.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/ImportCustodianData.java @@ -7,6 +7,7 @@ package de.eshg.schoolentry.business.model; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; +import de.eshg.lib.xlsximport.model.AddressData; import java.time.LocalDate; public record ImportCustodianData( diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/MergeProcedureData.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/MergeProcedureData.java index 9f96510f0..7c75a2cb6 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/MergeProcedureData.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/business/model/MergeProcedureData.java @@ -5,14 +5,14 @@ package de.eshg.schoolentry.business.model; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import java.util.List; import java.util.UUID; public record MergeProcedureData( UUID procedureId, String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, List<ImportCustodianData> custodians, String phoneNumber, Boolean isEntryLevel, diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/client/PersonClient.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/client/PersonClient.java index 75fbb4fbd..3b5a88156 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/client/PersonClient.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/client/PersonClient.java @@ -7,7 +7,6 @@ package de.eshg.schoolentry.client; import com.google.common.collect.Lists; import de.cronn.commons.lang.StreamUtil; -import de.eshg.base.CountryCodeDto; import de.eshg.base.SortDirection; import de.eshg.base.centralfile.PersonApi; import de.eshg.base.centralfile.api.DataOriginDto; @@ -17,6 +16,7 @@ import de.eshg.base.centralfile.api.person.AddPersonFileStateRequest; import de.eshg.base.centralfile.api.person.AddPersonFileStateResponse; import de.eshg.base.centralfile.api.person.GetPersonFileStatesRequest; import de.eshg.base.centralfile.api.person.GetPersonFileStatesResponse; +import de.eshg.lib.common.CountryCode; import de.eshg.rest.service.error.BadRequestException; import de.eshg.rest.service.error.ErrorCode; import de.eshg.rest.service.error.ErrorResponse; @@ -408,7 +408,7 @@ public class PersonClient { public UUID updateChild( SchoolEntryProcedure procedure, String placeOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, String phoneNumber) { UUID existingFileStateId = procedure.getChildIdFromCentralFile(); diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/config/SchoolEntryFeature.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/config/SchoolEntryFeature.java index cc27c35cf..75c2817a8 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/config/SchoolEntryFeature.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/config/SchoolEntryFeature.java @@ -6,14 +6,7 @@ package de.eshg.schoolentry.config; public enum SchoolEntryFeature { - MEDICAL_REPORT, - MERGE_PROCEDURES_ON_IMPORT, CLOSE_PROCEDURE, REOPEN_PROCEDURE, - DELETE_PROCEDURE, - SEARCH_BY_KNOWLEDGE_FACTORS, - SCHOOL_INFO_LETTER, - SCHOOL_YEAR, - WAITING_ROOM, IMPORT_PAST_PROCEDURES, } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/domain/model/SchoolEntryProcedure.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/domain/model/SchoolEntryProcedure.java index 8793c44ac..303a654af 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/domain/model/SchoolEntryProcedure.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/domain/model/SchoolEntryProcedure.java @@ -21,6 +21,7 @@ import java.time.LocalDate; import java.time.Year; import java.util.*; import java.util.function.Predicate; +import java.util.stream.Stream; import org.hibernate.annotations.BatchSize; @Entity @@ -271,9 +272,15 @@ public class SchoolEntryProcedure } public SchoolEntryTask getTaskOfType(TaskType taskType) { - return getTasks().stream() - .filter(task -> task.getTaskType() == taskType) - .collect(StreamUtil.toSingleElement()); + return getTasksOfType(taskType).collect(StreamUtil.toSingleElement()); + } + + public Optional<SchoolEntryTask> getOptionalTaskOfType(TaskType taskType) { + return getTasksOfType(taskType).collect(StreamUtil.toSingleOptionalElement()); + } + + private Stream<SchoolEntryTask> getTasksOfType(TaskType taskType) { + return getTasks().stream().filter(task -> task.getTaskType() == taskType); } public boolean hasLabel(String labelName) { diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/domain/repository/SchoolEntryProcedureRepository.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/domain/repository/SchoolEntryProcedureRepository.java index 25bb57ee0..adac393ac 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/domain/repository/SchoolEntryProcedureRepository.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/domain/repository/SchoolEntryProcedureRepository.java @@ -53,4 +53,8 @@ public interface SchoolEntryProcedureRepository extends ProcedureRepository<Scho @Query( "update SchoolEntryProcedure p set p.citizenUserId = null where p.externalId = :externalId") void clearCitizenUserId(@Param("externalId") UUID externalId); + + @Query( + "select p.externalId from SchoolEntryProcedure p where p.procedureStatus = de.eshg.lib.procedure.domain.model.ProcedureStatus.CLOSED order by p.id") + List<UUID> findExternalIdsOfClosedProcedures(); } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListColumn.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListColumn.java index b6ca20a4e..ef7db35a2 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListColumn.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListColumn.java @@ -5,6 +5,8 @@ package de.eshg.schoolentry.importer; +import de.eshg.lib.xlsximport.XlsxColumn; + public enum CitizenListColumn implements XlsxColumn { LAST_NAME("Nachname"), FIST_NAME("Vorname"), diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowProcessor.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowReader.java similarity index 65% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowProcessor.java rename to backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowReader.java index 38706b9c9..9b55b5538 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowProcessor.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowReader.java @@ -7,25 +7,22 @@ package de.eshg.schoolentry.importer; import static de.eshg.schoolentry.importer.CitizenListColumn.*; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; -import de.eshg.lib.procedure.domain.model.ProcedureType; -import de.eshg.schoolentry.business.model.AddressData; +import de.eshg.lib.common.CountryCode; +import de.eshg.lib.xlsximport.ColumnAccessor; +import de.eshg.lib.xlsximport.RowReader; +import de.eshg.lib.xlsximport.model.AddressData; import de.eshg.schoolentry.business.model.ImportChildData; import de.eshg.schoolentry.business.model.ImportCustodianData; -import de.eshg.schoolentry.business.model.ImportProcedureData; -import de.eshg.schoolentry.business.model.MergeProcedureData; -import de.eshg.schoolentry.mapper.PersonMapper; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.function.BiConsumer; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Sheet; -public class CitizenListRowProcessor extends RowProcessor<CitizenListRowValues, CitizenListColumn> { +public class CitizenListRowReader extends RowReader<CitizenListRowValues, CitizenListColumn> { private static final AddressColumns<CitizenListColumn> CHILD_ADDRESS_COLUMNS = new AddressColumns<>(STREET, HOUSE_NUMBER, POSTAL_CODE, CITY, ADDRESS_ADDITION); @@ -59,70 +56,40 @@ public class CitizenListRowProcessor extends RowProcessor<CitizenListRowValues, SALUTATION_CUSTODIAN_2, GENDER_CUSTODIAN_2)); - public CitizenListRowProcessor(Sheet sheet, List<CitizenListColumn> actualColumns) { + public CitizenListRowReader(Sheet sheet, List<CitizenListColumn> actualColumns) { super(sheet, actualColumns); } @Override - protected CitizenListRowValues process(ColumnAccessor<CitizenListColumn> col) { + protected CitizenListRowValues read(ColumnAccessor<CitizenListColumn> col) { CitizenListRowValues result = new CitizenListRowValues(); BiConsumer<Cell, String> errorHandler = createErrorHandler(result); - result.setChild(processChildData(col, errorHandler)); + result.setChild(readChildData(col, errorHandler)); if (col.hasColumn(INFORMATION_BLOCK)) { result.setInformationBlock(cellAsFlag(col, INFORMATION_BLOCK, errorHandler)); } - result.setCustodians(processCustodiansData(col, errorHandler)); - result.setStatus(processStatus(col, STATUS, errorHandler)); - result.setProcedureId(processProcedureId(col, PROCEDURE_ID, errorHandler)); + result.setCustodians(readCustodiansData(col, errorHandler)); + result.setStatus(readStatus(col, STATUS, errorHandler)); + result.setProcedureId(readProcedureId(col, PROCEDURE_ID, errorHandler)); return result; } - @Override - public boolean equalRowValues(CitizenListRowValues values1, CitizenListRowValues values2) { - return Objects.equals(values1.getChild(), values2.getChild()) - && Objects.equals(values1.getCustodians(), values2.getCustodians()); - } - - @Override - public ImportProcedureData mapValuesToImportData(CitizenListRowValues values) { - return new ImportProcedureData( - PersonMapper.mapImportChildDataToCreatePersonDto(values.getChild()), - values.getCustodians(), - ProcedureType.DRAFT_CITIZEN_OFFICE_IMPORT, - null, - false, - false, - values.hasInformationBlock()); - } - - @Override - public MergeProcedureData mapValuesToMergeData(CitizenListRowValues values) { - return new MergeProcedureData( - values.getProcedureId(), - values.getChild().placeOfBirth(), - values.getChild().countryOfBirth(), - values.getCustodians(), - values.getChild().phoneNumber(), - null, - null); - } - - private ImportChildData processChildData( + private ImportChildData readChildData( ColumnAccessor<CitizenListColumn> col, BiConsumer<Cell, String> errorHandler) { String lastName = cellAsString(col, LAST_NAME, errorHandler); String firstName = cellAsString(col, FIST_NAME, errorHandler); - AddressData addressData = processAddressData(col, CHILD_ADDRESS_COLUMNS, errorHandler, true); + AddressData addressData = readAddressData(col, CHILD_ADDRESS_COLUMNS, errorHandler, true); LocalDate birthDate = cellAsDate(col, DATE_OF_BIRTH, errorHandler); String placeOfBirth = cellAsString(col, PLACE_OF_BIRTH, true, false, errorHandler); - CountryCodeDto countryCodeDto = cellAsCountryCode(col, COUNTRY_OF_BIRTH, errorHandler); + CountryCode countryCode = cellAsCountryCode(col, COUNTRY_OF_BIRTH, errorHandler); GenderDto genderDto = cellAsGender(col, GENDER, errorHandler); return new ImportChildData( - firstName, lastName, birthDate, placeOfBirth, countryCodeDto, genderDto, addressData); + firstName, lastName, birthDate, placeOfBirth, countryCode, genderDto, addressData); } - private List<ImportCustodianData> processCustodiansData( + private List<ImportCustodianData> readCustodiansData( ColumnAccessor<CitizenListColumn> col, BiConsumer<Cell, String> errorHandler) { List<ImportCustodianData> custodians = new ArrayList<>(); @@ -130,7 +97,7 @@ public class CitizenListRowProcessor extends RowProcessor<CitizenListRowValues, if (anyValueInRange(col, custodian, errorHandler)) { String firstName = cellAsString(col, custodian.firstName(), errorHandler); String lastName = cellAsString(col, custodian.lastName(), errorHandler); - AddressData address = processAddressData(col, custodian.address(), errorHandler, false); + AddressData address = readAddressData(col, custodian.address(), errorHandler, false); LocalDate dateOfBirth = cellAsDate(col, custodian.dateOfBirth(), errorHandler); String title = cellAsString(col, custodian.title(), true, false, errorHandler); SalutationDto salutation = cellAsSalutation(col, custodian.salutation(), errorHandler); diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowValueMapper.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowValueMapper.java new file mode 100644 index 000000000..2266653f0 --- /dev/null +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowValueMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.schoolentry.importer; + +import de.eshg.lib.procedure.domain.model.ProcedureType; +import de.eshg.schoolentry.business.model.ImportProcedureData; +import de.eshg.schoolentry.business.model.MergeProcedureData; +import de.eshg.schoolentry.mapper.PersonMapper; + +public class CitizenListRowValueMapper implements RowValueMapper<CitizenListRowValues> { + + @Override + public ImportProcedureData mapValuesToImportData(CitizenListRowValues values) { + return new ImportProcedureData( + PersonMapper.mapImportChildDataToCreatePersonDto(values.getChild()), + values.getCustodians(), + ProcedureType.DRAFT_CITIZEN_OFFICE_IMPORT, + null, + false, + false, + values.hasInformationBlock()); + } + + @Override + public MergeProcedureData mapValuesToMergeData(CitizenListRowValues values) { + return new MergeProcedureData( + values.getProcedureId(), + values.getChild().placeOfBirth(), + values.getChild().countryOfBirth(), + values.getCustodians(), + values.getChild().phoneNumber(), + null, + null); + } +} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowValues.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowValues.java index f922116a6..999f68295 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowValues.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/CitizenListRowValues.java @@ -7,8 +7,9 @@ package de.eshg.schoolentry.importer; import de.eshg.schoolentry.business.model.ImportCustodianData; import java.util.List; +import java.util.Objects; -public final class CitizenListRowValues extends RowValues { +public final class CitizenListRowValues extends SchoolEntryRowValues { private List<ImportCustodianData> custodians; private boolean informationBlock; @@ -28,4 +29,11 @@ public final class CitizenListRowValues extends RowValues { public void setInformationBlock(boolean informationBlock) { this.informationBlock = informationBlock; } + + @Override + public boolean isDuplicateRow(Object other) { + return (other instanceof CitizenListRowValues citizenListRowValues) + && Objects.equals(this.getChild(), citizenListRowValues.getChild()) + && Objects.equals(this.getCustodians(), citizenListRowValues.getCustodians()); + } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/EqualityComparator.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/EqualityComparator.java deleted file mode 100644 index 391d3d8dd..000000000 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/EqualityComparator.java +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package de.eshg.schoolentry.importer; - -public interface EqualityComparator<T extends RowValues> { - boolean equalRowValues(T values1, T values2); -} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportService.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportService.java index 1279f7d46..2453070c8 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportService.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/ImportService.java @@ -5,60 +5,33 @@ package de.eshg.schoolentry.importer; -import static de.eshg.schoolentry.importer.ImportStatus.*; -import static de.eshg.schoolentry.importer.XlsxUtil.writeValue; - -import de.cronn.commons.lang.StreamUtil; -import de.eshg.base.GenderDto; -import de.eshg.base.address.AddressDto; -import de.eshg.base.address.DomesticAddressDto; -import de.eshg.base.address.PostboxAddressDto; -import de.eshg.base.centralfile.api.person.PersonKeyAttributes; -import de.eshg.lib.procedure.domain.model.ProcedureType; +import de.eshg.lib.xlsximport.FeedbackColumnAccessor; +import de.eshg.lib.xlsximport.ImportValidator; +import de.eshg.lib.xlsximport.XlsxColumn; +import de.eshg.lib.xlsximport.XlsxNormalizer; +import de.eshg.lib.xlsximport.model.ImportResult; import de.eshg.schoolentry.SchoolEntryService; -import de.eshg.schoolentry.api.ImportStatisticsDto; -import de.eshg.schoolentry.business.model.*; -import de.eshg.schoolentry.config.SchoolEntryFeature; -import de.eshg.schoolentry.config.SchoolEntryFeatureToggle; import de.eshg.schoolentry.config.SchoolEntryProperties; -import de.eshg.schoolentry.domain.model.SchoolEntryProcedure; -import de.eshg.schoolentry.util.ExceptionUtil; import de.eshg.schoolentry.util.ProcedureTypeAssignmentHelper; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.Year; import java.util.*; -import java.util.Map.Entry; -import java.util.function.Consumer; -import java.util.stream.Stream; import org.apache.poi.ss.usermodel.*; -import org.apache.poi.xssf.usermodel.XSSFCellStyle; -import org.apache.poi.xssf.usermodel.XSSFFont; 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.ByteArrayResource; import org.springframework.stereotype.Service; -import org.springframework.util.Assert; @Service public class ImportService { - private static final Logger log = LoggerFactory.getLogger(ImportService.class); - private final SchoolEntryService schoolEntryService; - private final SchoolEntryFeatureToggle schoolEntryFeatureToggle; private final SchoolEntryProperties schoolEntryProperties; private final ProcedureTypeAssignmentHelper procedureTypeAssignmentHelper; public ImportService( SchoolEntryService schoolEntryService, - SchoolEntryFeatureToggle schoolEntryFeatureToggle, SchoolEntryProperties schoolEntryProperties, ProcedureTypeAssignmentHelper procedureTypeAssignmentHelper) { this.schoolEntryService = schoolEntryService; - this.schoolEntryFeatureToggle = schoolEntryFeatureToggle; this.schoolEntryProperties = schoolEntryProperties; this.procedureTypeAssignmentHelper = procedureTypeAssignmentHelper; } @@ -70,435 +43,60 @@ public class ImportService { try (XlsxNormalizer xlsxNormalizer = new XlsxNormalizer()) { XSSFSheet normalizedSheet = xlsxNormalizer.normalize(sheet); - RowProcessor<? extends RowValues, ? extends XlsxColumn> rowProcessor = - switch (importType) { - case CITIZEN_LIST -> { - List<CitizenListColumn> actualColumns = - ImportValidator.validateHeaderFormat(CitizenListColumn.values(), normalizedSheet); - yield new CitizenListRowProcessor(normalizedSheet, actualColumns); - } - case SCHOOL_LIST -> { - List<SchoolListColumn> actualColumns = - ImportValidator.validateHeaderFormat(SchoolListColumn.values(), normalizedSheet); - yield new SchoolListRowProcessor( - normalizedSheet, actualColumns, schoolYear, procedureTypeAssignmentHelper); - } - case PAST_PROCEDURE_LIST -> { - List<PastProcedureListColumn> actualColumns = - ImportValidator.validateHeaderFormat( - PastProcedureListColumn.values(), normalizedSheet); - yield new PastProcedureListRowProcessor(normalizedSheet, actualColumns); - } - }; - - FeedbackColumnAccessor feedbackColumnAccessor = - new FeedbackColumnAccessor(rowProcessor.getActualColumns()); - - return new Importer<>( - normalizedSheet, - importType, - rowProcessor, - feedbackColumnAccessor, - schoolId, - locationId, - schoolYear) - .process(); - } - } - - private class Importer<T extends RowValues> { - private final XSSFSheet sheet; - private final XSSFCellStyle defaultCellStyle; - private final ImportStatistics stats = new ImportStatistics(); - private final ValidRows<T> validRows = new ValidRows<>(new ArrayList<>(), new ArrayList<>()); - private final ImportType importType; - private final RowProcessor<T, ?> rowProcessor; - private final FeedbackColumnAccessor col; - private final UUID schoolId; - private final UUID locationId; - private final Year schoolYear; - private final XSSFCellStyle importedSuccessfullyCellStyle; - private final XSSFCellStyle importFailedCellStyle; - private final XSSFCellStyle importWarningCellStyle; - - private Importer( - XSSFSheet sheet, - ImportType importType, - RowProcessor<T, ?> rowProcessor, - FeedbackColumnAccessor feedbackColumnAccessor, - UUID schoolId, - UUID locationId, - Year schoolYear) { - this.sheet = sheet; - this.locationId = locationId; - this.defaultCellStyle = createDefaultCellStyle(); - - this.importedSuccessfullyCellStyle = - createCellStyle( - font -> { - font.setBold(true); - font.setColor(XlsxUtil.newColor(76, 175, 80)); - }); - - this.importFailedCellStyle = - createCellStyle( - font -> { - font.setBold(true); - font.setColor(XlsxUtil.newColor(176, 0, 0)); - }); - - importWarningCellStyle = - createCellStyle( - font -> { - font.setBold(true); - font.setColor(XlsxUtil.newColor(228, 114, 0)); - }); - - this.importType = importType; - this.rowProcessor = rowProcessor; - this.col = feedbackColumnAccessor; - this.schoolId = schoolId; - this.schoolYear = schoolYear; - } - - private XSSFCellStyle createCellStyle(Consumer<XSSFFont> fontCustomizer) { - XSSFCellStyle cellStyle = createDefaultCellStyle(); - XSSFFont font = cellStyle.getFont(); - fontCustomizer.accept(font); - return cellStyle; - } - - private XSSFCellStyle createDefaultCellStyle() { - XSSFWorkbook workbook = sheet.getWorkbook(); - XSSFCellStyle cellStyle = workbook.createCellStyle(); - cellStyle.setFont(XlsxUtil.createDefaultFont(workbook)); - return cellStyle; - } - - private ImportResult process() throws IOException { - processRows(); - - List<T> importableRows = validRows.importableRows(); - List<ImportProcedureData> importData = - importableRows.stream().map(rowProcessor::mapValuesToImportData).toList(); - try { - List<SchoolEntryProcedure> createdProcedures = - switch (importType) { - case CITIZEN_LIST, SCHOOL_LIST -> - schoolEntryService.createProceduresWithBookAppointmentTask( - importData, schoolId, locationId, schoolYear, DataOrigin.DATA_IMPORT); - case PAST_PROCEDURE_LIST -> - schoolEntryService.createProcedures( - importData, schoolId, locationId, schoolYear, DataOrigin.DATA_IMPORT); - }; - writeProcedureIdsInSheet(importableRows, createdProcedures); - } catch (Exception e) { - log.error("Failure during creating new procedures.", e); - writeFailedStatusInSheet(importableRows); - stats.correctCreatedToFailed(importableRows.size()); - } - - if (schoolEntryFeatureToggle.isNewFeatureEnabled( - SchoolEntryFeature.MERGE_PROCEDURES_ON_IMPORT) - && importType.supportsMerge()) { - List<T> mergeableRows = validRows.mergeableRows(); - List<MergeProcedureData> mergeData = - mergeableRows.stream().map(rowProcessor::mapValuesToMergeData).toList(); - List<UUID> failedIds = - schoolEntryService.mergeProcedures( - mergeData, importType, schoolId, locationId, schoolYear); - writeMergedFailedStatusInSheet(mergeableRows, failedIds); - stats.correctMergeToFailed(failedIds.size()); - } - - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - sheet.getWorkbook().write(outputStream); - ByteArrayResource resource = new ByteArrayResource(outputStream.toByteArray()); - - return new ImportResult(stats.mapToDto(), resource); - } - } - - private void processRows() { - Map<Row, T> rowValues = new LinkedHashMap<>(); - for (Row row : sheet) { - if (row.getRowNum() == 0) { - // skip the header - continue; - } - deleteReferenceId(row); - try { - rowValues.put(row, rowProcessor.processRow(row)); - } catch (Exception e) { - log.error("Error in reading row %d".formatted(row.getRowNum()), e); - writeStatus(row, EXCEPTION); - stats.countFailed(); - } - } - - List<UUID> procedureIds = - rowValues.values().stream() - .map(RowValues::getProcedureId) - .filter(Objects::nonNull) - .toList(); - List<UUID> existingProcedureIds = schoolEntryService.collectExistingProcedures(procedureIds); - Map<PersonKeyAttributes, List<ProcedureWithChildData>> mergeCandidates; - if (schoolEntryFeatureToggle.isNewFeatureEnabled( - SchoolEntryFeature.MERGE_PROCEDURES_ON_IMPORT) - && importType.supportsMerge()) { - Set<PersonKeyAttributes> rowsToSearchFor = - rowValues.values().stream() - .filter(row -> row.getProcedureId() == null) - .filter(RowValues::isValid) - .map(Importer::getChildKeyAttributes) - .collect(StreamUtil.toLinkedHashSet()); - mergeCandidates = schoolEntryService.searchForMergeCandidates(rowsToSearchFor); - } else { - mergeCandidates = Map.of(); - } - - for (Entry<Row, T> entry : rowValues.entrySet()) { - Row row = entry.getKey(); - T value = entry.getValue(); - if (value.getProcedureId() != null) { - if (existingProcedureIds.contains(value.getProcedureId())) { - writeStatus(row, IMPORTED_PREVIOUSLY); - stats.countPreviouslyImported(); - } else { - writeStatus(row, INVALID_PROCEDURE_ID); - stats.countFailed(); - } - } else if (value.getStatus() == DUPLICATE_WITHIN_LIST - || containsMatchingRow(validRows, value)) { - writeStatus(row, DUPLICATE_WITHIN_LIST); - stats.countDuplicated(); - } else if (value.isValid()) { - if (schoolEntryFeatureToggle.isNewFeatureEnabled( - SchoolEntryFeature.MERGE_PROCEDURES_ON_IMPORT) - && importType.supportsMerge()) { - evaluateActionsWhenMergeIsEnabled(row, value, mergeCandidates); - } else { - validRows.importableRows().add(value); - stats.countCreated(); - } - } else { - writeStatus(row, ERROR_INPUT_DATA); - stats.countFailed(); - } - } - } - - private static PersonKeyAttributes getChildKeyAttributes(RowValues rowValues) { - ImportChildData child = rowValues.getChild(); - return new PersonKeyAttributes(child.firstName(), child.lastName(), child.dateOfBirth()); - } - - private void evaluateActionsWhenMergeIsEnabled( - Row row, T value, Map<PersonKeyAttributes, List<ProcedureWithChildData>> mergeCandidates) { - List<ProcedureWithChildData> procedures = - mergeCandidates.getOrDefault(getChildKeyAttributes(value), List.of()); - if (procedures.isEmpty()) { - validRows.importableRows().add(value); - stats.countCreated(); - } else if (procedures.size() > 1 - || procedures.getFirst().procedure().getProcedureType() != procedureTypeToMergeWith()) { - writeStatusAndReferenceId( - row, DUPLICATE_IN_ASSET, procedures.getFirst().procedure().getExternalId()); - stats.countMergeFailed(); - } else { - Assert.isTrue( - !schoolEntryProperties.isDirectProcedureTypeAssignmentOnImport(), - "Procedures of a draft type should not exist when direct procedure type assignment is enabled."); - ProcedureWithChildData procedure = procedures.getFirst(); - if (procedureMatchesImportValues(procedure, value)) { - value.setProcedureId(procedure.procedure().getExternalId()); - validRows.mergeableRows().add(value); - writeStatusAndProcedureId(row, MERGED_SUCCESSFULLY, procedure.procedure()); - stats.countMerged(); - } else { - writeStatusAndReferenceId(row, DUPLICATE_IN_ASSET, procedure.procedure().getExternalId()); - stats.countMergeFailed(); - } - } - } - - private ProcedureType procedureTypeToMergeWith() { - return switch (importType) { - case CITIZEN_LIST -> ProcedureType.DRAFT_SCHOOL_IMPORT; - case SCHOOL_LIST -> ProcedureType.DRAFT_CITIZEN_OFFICE_IMPORT; - case PAST_PROCEDURE_LIST -> throw ExceptionUtil.mergeNotSupportedForPastProcedureImport(); - }; - } - - record ValidRows<T>(List<T> importableRows, List<T> mergeableRows) {} - - private void writeStatusAndProcedureId( - Row row, ImportStatus status, SchoolEntryProcedure procedure) { - writeStatus(row, status); - writeValue(col.getProcedureId(row), procedure.getExternalId().toString(), defaultCellStyle); - } - - private void deleteReferenceId(Row row) { - if (col.hasReferenceIdColum()) { - writeValue(col.getReferenceId(row), "", defaultCellStyle); - } - } - - private void deleteProcedureId(Row row) { - writeValue(col.getProcedureId(row), "", defaultCellStyle); - } - - private void writeStatusAndReferenceId(Row row, ImportStatus status, UUID referenceId) { - writeStatus(row, status); - writeValue(col.getReferenceId(row), referenceId.toString(), defaultCellStyle); - } - - private boolean procedureMatchesImportValues(ProcedureWithChildData procedure, T values) { - AddressDto address = procedure.child().address(); - if (address instanceof PostboxAddressDto) { - return false; - } - DomesticAddressDto domesticAddressDto = (DomesticAddressDto) address; - AddressData importAddress = values.getChild().address(); - boolean commonFieldsMatch = - Objects.equals(domesticAddressDto.street(), importAddress.street()) - && Objects.equals(domesticAddressDto.city(), importAddress.city()) - && Objects.equals(domesticAddressDto.houseNumber(), importAddress.houseNumber()) - && Objects.equals(domesticAddressDto.postalCode(), importAddress.postalCode()) - && Objects.equals( - domesticAddressDto.addressAddition(), importAddress.addressAddition()) - && Objects.equals( - procedure.child().gender(), - Optional.ofNullable(values.getChild().gender()).orElse(GenderDto.NOT_SPECIFIED)) - && (procedure.procedure().getSchoolYear() == null - || Objects.equals(procedure.procedure().getSchoolYear(), schoolYear)); - - if (!commonFieldsMatch) { - return false; - } - - return switch (importType) { - case SCHOOL_LIST -> - (procedure.procedure().getSchoolId() == null - || Objects.equals(procedure.procedure().getSchoolId(), schoolId)) - && (procedure.procedure().getLocationId() == null - || Objects.equals(procedure.procedure().getLocationId(), locationId)); - case CITIZEN_LIST -> - (procedure.child().placeOfBirth() == null - || Objects.equals( - procedure.child().placeOfBirth(), values.getChild().placeOfBirth())) - && (procedure.child().countryOfBirth() == null - || Objects.equals( - procedure.child().countryOfBirth(), values.getChild().countryOfBirth())); - case PAST_PROCEDURE_LIST -> throw ExceptionUtil.mergeNotSupportedForPastProcedureImport(); - }; - } - - private void writeStatus(Row row, ImportStatus importStatus) { - writeValue(col.getStatus(row), importStatus.getDescription(), getCellStyle(importStatus)); - } - - private XSSFCellStyle getCellStyle(ImportStatus importStatus) { - return switch (importStatus) { - case IMPORTED_SUCCESSFULLY, MERGED_SUCCESSFULLY -> importedSuccessfullyCellStyle; - case ERROR_INPUT_DATA, INVALID_PROCEDURE_ID, EXCEPTION, MERGE_FAILED -> - importFailedCellStyle; - case IMPORTED_PREVIOUSLY, DUPLICATE_WITHIN_LIST, DUPLICATE_IN_ASSET -> - importWarningCellStyle; - }; - } - - private void writeProcedureIdsInSheet( - List<T> importableRows, List<SchoolEntryProcedure> createdProcedures) { - for (int i = 0; i < importableRows.size(); i++) { - T rowValues = importableRows.get(i); - SchoolEntryProcedure createdProcedure = createdProcedures.get(i); - - Row row = rowValues.getRow(); - writeStatusAndProcedureId(row, IMPORTED_SUCCESSFULLY, createdProcedure); - } - } - - private void writeMergedFailedStatusInSheet(List<T> mergeableRows, List<UUID> failedIds) { - for (UUID uuid : failedIds) { - Row row = - mergeableRows.stream() - .filter(values -> Objects.equals(uuid, values.getProcedureId())) - .collect(StreamUtil.toSingleElement()) - .getRow(); - deleteProcedureId(row); - writeStatusAndReferenceId(row, MERGE_FAILED, uuid); - } - } - - private void writeFailedStatusInSheet(List<T> importableRows) { - for (T rowValues : importableRows) { - writeStatus(rowValues.getRow(), EXCEPTION); - } - } - - private boolean containsMatchingRow(ValidRows<T> rows, T values) { - return Stream.concat(rows.importableRows().stream(), rows.mergeableRows().stream()) - .anyMatch(row -> rowProcessor.equalRowValues(row, values)); - } - } - - static class ImportStatistics { - private int created = 0; - private int merged = 0; - private int mergeFailed = 0; - private int duplicated = 0; - private int failed = 0; - private int previouslyImported = 0; - - void countCreated() { - created++; - } - - void countMerged() { - merged++; - } - - void countMergeFailed() { - mergeFailed++; - } - - void countDuplicated() { - duplicated++; - } - - void countFailed() { - failed++; - } - - void countPreviouslyImported() { - previouslyImported++; - } - - void correctMergeToFailed(int count) { - if (merged < count) { - throw new IllegalStateException("Count correction failed."); - } - merged -= count; - mergeFailed += count; - } - - void correctCreatedToFailed(int count) { - if (created < count) { - throw new IllegalStateException("Count correction failed."); - } - created -= count; - failed += count; - } - - ImportStatisticsDto mapToDto() { - return new ImportStatisticsDto( - created + merged + mergeFailed + duplicated + failed + previouslyImported, - created, - merged, - mergeFailed, - duplicated, - failed); + SchoolEntryImporter<? extends SchoolEntryRowValues, ? extends XlsxColumn> + schoolEntryImporter = + switch (importType) { + case CITIZEN_LIST -> { + List<CitizenListColumn> actualColumns = + ImportValidator.validateHeaderFormat( + CitizenListColumn.values(), normalizedSheet); + yield new SchoolEntryImporter<>( + normalizedSheet, + importType, + new CitizenListRowReader(normalizedSheet, actualColumns), + new FeedbackColumnAccessor(actualColumns), + new CitizenListRowValueMapper(), + schoolId, + locationId, + schoolYear, + schoolEntryService, + schoolEntryProperties); + } + case SCHOOL_LIST -> { + List<SchoolListColumn> actualColumns = + ImportValidator.validateHeaderFormat( + SchoolListColumn.values(), normalizedSheet); + yield new SchoolEntryImporter<>( + normalizedSheet, + importType, + new SchoolListRowReader(normalizedSheet, actualColumns), + new FeedbackColumnAccessor(actualColumns), + new SchoolListRowValueMapper(schoolYear, procedureTypeAssignmentHelper), + schoolId, + locationId, + schoolYear, + schoolEntryService, + schoolEntryProperties); + } + case PAST_PROCEDURE_LIST -> { + List<PastProcedureListColumn> actualColumns = + ImportValidator.validateHeaderFormat( + PastProcedureListColumn.values(), normalizedSheet); + yield new SchoolEntryImporter<>( + normalizedSheet, + importType, + new PastProcedureListRowReader(normalizedSheet, actualColumns), + new FeedbackColumnAccessor(actualColumns), + new PastProcedureListRowValueMapper(), + schoolId, + locationId, + schoolYear, + schoolEntryService, + schoolEntryProperties); + } + }; + + return schoolEntryImporter.process(); } } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListColumn.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListColumn.java index 445e8fb6a..da046106d 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListColumn.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListColumn.java @@ -5,6 +5,8 @@ package de.eshg.schoolentry.importer; +import de.eshg.lib.xlsximport.XlsxColumn; + public enum PastProcedureListColumn implements XlsxColumn { LAST_NAME("Nachname"), FIRST_NAME("Vorname"), diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowProcessor.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowReader.java similarity index 62% rename from backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowProcessor.java rename to backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowReader.java index 0169467da..40adef418 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowProcessor.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowReader.java @@ -20,46 +20,43 @@ import static de.eshg.schoolentry.importer.PastProcedureListColumn.STATUS; import static de.eshg.schoolentry.importer.PastProcedureListColumn.STREET; import de.eshg.lib.procedure.domain.model.ProcedureType; +import de.eshg.lib.xlsximport.ColumnAccessor; +import de.eshg.lib.xlsximport.RowReader; import de.eshg.schoolentry.business.model.ImportChildData; -import de.eshg.schoolentry.business.model.ImportProcedureData; -import de.eshg.schoolentry.business.model.MergeProcedureData; -import de.eshg.schoolentry.mapper.PersonMapper; -import de.eshg.schoolentry.util.ExceptionUtil; import java.util.List; -import java.util.Objects; import java.util.function.BiConsumer; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Sheet; -public class PastProcedureListRowProcessor - extends RowProcessor<PastProcedureListRowValues, PastProcedureListColumn> { +public class PastProcedureListRowReader + extends RowReader<PastProcedureListRowValues, PastProcedureListColumn> { - public PastProcedureListRowProcessor(Sheet sheet, List<PastProcedureListColumn> actualColumns) { + public PastProcedureListRowReader(Sheet sheet, List<PastProcedureListColumn> actualColumns) { super(sheet, actualColumns); } @Override - protected PastProcedureListRowValues process(ColumnAccessor<PastProcedureListColumn> col) { + protected PastProcedureListRowValues read(ColumnAccessor<PastProcedureListColumn> col) { PastProcedureListRowValues result = new PastProcedureListRowValues(); BiConsumer<Cell, String> errorHandler = createErrorHandler(result); - result.setChild(processChildData(col, errorHandler)); - result.setProcedureType(processProcedureType(col, errorHandler)); + result.setChild(readChildData(col, errorHandler)); + result.setProcedureType(readProcedureType(col, errorHandler)); result.setExaminationDate(cellAsDate(col, EXAMINATION_DATE, errorHandler)); - result.setStatus(processStatus(col, STATUS, errorHandler)); - result.setProcedureId(processProcedureId(col, PROCEDURE_ID, errorHandler)); + result.setStatus(readStatus(col, STATUS, errorHandler)); + result.setProcedureId(readProcedureId(col, PROCEDURE_ID, errorHandler)); return result; } - private ImportChildData processChildData( + private ImportChildData readChildData( ColumnAccessor<PastProcedureListColumn> col, BiConsumer<Cell, String> errorHandler) { return new ImportChildData( cellAsString(col, FIRST_NAME, errorHandler), cellAsString(col, LAST_NAME, errorHandler), cellAsDate(col, DATE_OF_BIRTH, errorHandler), cellAsGender(col, GENDER, errorHandler), - processAddressData( + readAddressData( col, new AddressColumns<>(STREET, HOUSE_NUMBER, POSTAL_CODE, CITY, ADDRESS_ADDITION), errorHandler, @@ -67,7 +64,7 @@ public class PastProcedureListRowProcessor null); } - private ProcedureType processProcedureType( + private ProcedureType readProcedureType( ColumnAccessor<PastProcedureListColumn> col, BiConsumer<Cell, String> errorHandler) { Cell cell = col.get(PROCEDURE_TYPE); String string = cellAsString(cell, errorHandler); @@ -82,26 +79,4 @@ public class PastProcedureListRowProcessor } }; } - - @Override - public boolean equalRowValues( - PastProcedureListRowValues values1, PastProcedureListRowValues values2) { - return Objects.equals(values1.getChild(), values2.getChild()); - } - - @Override - public ImportProcedureData mapValuesToImportData(PastProcedureListRowValues values) { - return new ImportProcedureData( - PersonMapper.mapImportChildDataToCreatePersonDto(values.getChild()), - values.getProcedureType(), - values.getExaminationDate(), - false, - false, - false); - } - - @Override - public MergeProcedureData mapValuesToMergeData(PastProcedureListRowValues values) { - throw ExceptionUtil.mergeNotSupportedForPastProcedureImport(); - } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowValueMapper.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowValueMapper.java new file mode 100644 index 000000000..0575c466d --- /dev/null +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowValueMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.schoolentry.importer; + +import de.eshg.schoolentry.business.model.ImportProcedureData; +import de.eshg.schoolentry.business.model.MergeProcedureData; +import de.eshg.schoolentry.mapper.PersonMapper; +import de.eshg.schoolentry.util.ExceptionUtil; + +public class PastProcedureListRowValueMapper implements RowValueMapper<PastProcedureListRowValues> { + + @Override + public ImportProcedureData mapValuesToImportData(PastProcedureListRowValues values) { + return new ImportProcedureData( + PersonMapper.mapImportChildDataToCreatePersonDto(values.getChild()), + values.getProcedureType(), + values.getExaminationDate(), + false, + false, + false); + } + + @Override + public MergeProcedureData mapValuesToMergeData(PastProcedureListRowValues values) { + throw ExceptionUtil.mergeNotSupportedForPastProcedureImport(); + } +} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowValues.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowValues.java index 37892f4f9..c9d287491 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowValues.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/PastProcedureListRowValues.java @@ -7,8 +7,9 @@ package de.eshg.schoolentry.importer; import de.eshg.lib.procedure.domain.model.ProcedureType; import java.time.LocalDate; +import java.util.Objects; -public final class PastProcedureListRowValues extends RowValues { +public final class PastProcedureListRowValues extends SchoolEntryRowValues { private ProcedureType procedureType; @@ -29,4 +30,10 @@ public final class PastProcedureListRowValues extends RowValues { public void setExaminationDate(LocalDate examinationDate) { this.examinationDate = examinationDate; } + + @Override + boolean isDuplicateRow(Object other) { + return (other instanceof PastProcedureListRowValues pastProcedureListRowValues) + && Objects.equals(this.getChild(), pastProcedureListRowValues.getChild()); + } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolEntryImporter.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolEntryImporter.java new file mode 100644 index 000000000..5da1cdd4b --- /dev/null +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolEntryImporter.java @@ -0,0 +1,291 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.schoolentry.importer; + +import static de.eshg.lib.xlsximport.ImportStatus.DUPLICATE_IN_ASSET; +import static de.eshg.lib.xlsximport.ImportStatus.DUPLICATE_WITHIN_LIST; +import static de.eshg.lib.xlsximport.ImportStatus.ERROR_INPUT_DATA; +import static de.eshg.lib.xlsximport.ImportStatus.IMPORTED_PREVIOUSLY; +import static de.eshg.lib.xlsximport.ImportStatus.IMPORTED_SUCCESSFULLY; +import static de.eshg.lib.xlsximport.ImportStatus.INVALID_PROCEDURE_ID; +import static de.eshg.lib.xlsximport.ImportStatus.MERGED_SUCCESSFULLY; + +import de.cronn.commons.lang.StreamUtil; +import de.eshg.base.GenderDto; +import de.eshg.base.address.AddressDto; +import de.eshg.base.address.DomesticAddressDto; +import de.eshg.base.address.PostboxAddressDto; +import de.eshg.base.centralfile.api.person.PersonKeyAttributes; +import de.eshg.lib.procedure.domain.model.ProcedureType; +import de.eshg.lib.xlsximport.FeedbackColumnAccessor; +import de.eshg.lib.xlsximport.Importer; +import de.eshg.lib.xlsximport.RowReader; +import de.eshg.lib.xlsximport.XlsxColumn; +import de.eshg.lib.xlsximport.model.AddressData; +import de.eshg.schoolentry.SchoolEntryService; +import de.eshg.schoolentry.business.model.DataOrigin; +import de.eshg.schoolentry.business.model.ImportChildData; +import de.eshg.schoolentry.business.model.ImportProcedureData; +import de.eshg.schoolentry.business.model.MergeProcedureData; +import de.eshg.schoolentry.business.model.ProcedureWithChildData; +import de.eshg.schoolentry.config.SchoolEntryProperties; +import de.eshg.schoolentry.domain.model.SchoolEntryProcedure; +import de.eshg.schoolentry.util.ExceptionUtil; +import java.time.Year; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; + +public class SchoolEntryImporter<T extends SchoolEntryRowValues, C extends XlsxColumn> + extends Importer<T, C> { + + private static final Logger log = LoggerFactory.getLogger(SchoolEntryImporter.class); + + private final ImportType importType; + private final UUID schoolId; + private final UUID locationId; + private final Year schoolYear; + private final SchoolEntryService schoolEntryService; + private final SchoolEntryProperties schoolEntryProperties; + private final RowValueMapper<T> rowValueMapper; + + public SchoolEntryImporter( + XSSFSheet sheet, + ImportType importType, + RowReader<T, C> rowReader, + FeedbackColumnAccessor feedbackColumnAccessor, + RowValueMapper<T> rowValueMapper, + UUID schoolId, + UUID locationId, + Year schoolYear, + SchoolEntryService schoolEntryService, + SchoolEntryProperties schoolEntryProperties) { + super(sheet, rowReader, feedbackColumnAccessor); + this.locationId = locationId; + this.schoolEntryService = schoolEntryService; + this.schoolEntryProperties = schoolEntryProperties; + this.importType = importType; + this.schoolId = schoolId; + this.schoolYear = schoolYear; + this.rowValueMapper = rowValueMapper; + } + + @Override + protected void readRowsAndEvaluateActions() { + Map<Row, T> rowValues = readRows(); + + List<UUID> existingProcedureIds = fetchExistingProceduresIfNecessary(rowValues); + Map<PersonKeyAttributes, List<ProcedureWithChildData>> mergeCandidates = + fetchMergeCandidatesIfNecessary(rowValues); + + for (Entry<Row, T> entry : rowValues.entrySet()) { + evaluateActionForRow(entry.getKey(), entry.getValue(), existingProcedureIds, mergeCandidates); + } + } + + private List<UUID> fetchExistingProceduresIfNecessary(Map<Row, T> rowValues) { + List<UUID> procedureIds = + rowValues.values().stream() + .map(SchoolEntryRowValues::getProcedureId) + .filter(Objects::nonNull) + .toList(); + return schoolEntryService.collectExistingProcedures(procedureIds); + } + + private Map<PersonKeyAttributes, List<ProcedureWithChildData>> fetchMergeCandidatesIfNecessary( + Map<Row, T> rowValues) { + Map<PersonKeyAttributes, List<ProcedureWithChildData>> mergeCandidates; + if (importType.supportsMerge()) { + Set<PersonKeyAttributes> rowsToSearchFor = + rowValues.values().stream() + .filter(row -> row.getProcedureId() == null) + .filter(SchoolEntryRowValues::isValid) + .map(SchoolEntryImporter::getChildKeyAttributes) + .collect(StreamUtil.toLinkedHashSet()); + mergeCandidates = schoolEntryService.searchForMergeCandidates(rowsToSearchFor); + } else { + mergeCandidates = Map.of(); + } + return mergeCandidates; + } + + private void evaluateActionForRow( + Row row, + T rowValues, + List<UUID> existingProcedureIds, + Map<PersonKeyAttributes, List<ProcedureWithChildData>> mergeCandidates) { + + if (rowValues.getProcedureId() != null) { + if (existingProcedureIds.contains(rowValues.getProcedureId())) { + writeStatus(row, IMPORTED_PREVIOUSLY); + stats.countPreviouslyImported(); + } else { + writeStatus(row, INVALID_PROCEDURE_ID); + stats.countFailed(); + } + } else if (rowValues.getStatus() == DUPLICATE_WITHIN_LIST + || containsMatchingRow(validRows, rowValues)) { + writeStatus(row, DUPLICATE_WITHIN_LIST); + stats.countDuplicated(); + } else if (rowValues.isValid()) { + if (importType.supportsMerge()) { + evaluateActionWhenMergeIsEnabled(row, rowValues, mergeCandidates); + } else { + validRows.importableRows().add(rowValues); + stats.countCreated(); + } + } else { + writeStatus(row, ERROR_INPUT_DATA); + stats.countFailed(); + } + } + + private boolean containsMatchingRow(ValidRows<T> rows, T values) { + return Stream.concat(rows.importableRows().stream(), rows.mergeableRows().stream()) + .anyMatch(row -> row.isDuplicateRow(values)); + } + + private static PersonKeyAttributes getChildKeyAttributes(SchoolEntryRowValues rowValues) { + ImportChildData child = rowValues.getChild(); + return new PersonKeyAttributes(child.firstName(), child.lastName(), child.dateOfBirth()); + } + + private void evaluateActionWhenMergeIsEnabled( + Row row, T value, Map<PersonKeyAttributes, List<ProcedureWithChildData>> mergeCandidates) { + List<ProcedureWithChildData> procedures = + mergeCandidates.getOrDefault(getChildKeyAttributes(value), List.of()); + if (procedures.isEmpty()) { + validRows.importableRows().add(value); + stats.countCreated(); + } else if (procedures.size() > 1 + || procedures.getFirst().procedure().getProcedureType() != procedureTypeToMergeWith()) { + writeStatusAndReferenceId( + row, DUPLICATE_IN_ASSET, procedures.getFirst().procedure().getExternalId()); + stats.countMergeFailed(); + } else { + Assert.isTrue( + !schoolEntryProperties.isDirectProcedureTypeAssignmentOnImport(), + "Procedures of a draft type should not exist when direct procedure type assignment is enabled."); + ProcedureWithChildData procedure = procedures.getFirst(); + if (mergeCandidateMatchesImportValues(procedure, value)) { + value.setProcedureId(procedure.procedure().getExternalId()); + validRows.mergeableRows().add(value); + writeStatusAndProcedureId(row, MERGED_SUCCESSFULLY, procedure.procedure().getExternalId()); + stats.countMerged(); + } else { + writeStatusAndReferenceId(row, DUPLICATE_IN_ASSET, procedure.procedure().getExternalId()); + stats.countMergeFailed(); + } + } + } + + private ProcedureType procedureTypeToMergeWith() { + return switch (importType) { + case CITIZEN_LIST -> ProcedureType.DRAFT_SCHOOL_IMPORT; + case SCHOOL_LIST -> ProcedureType.DRAFT_CITIZEN_OFFICE_IMPORT; + case PAST_PROCEDURE_LIST -> throw ExceptionUtil.mergeNotSupportedForPastProcedureImport(); + }; + } + + private boolean mergeCandidateMatchesImportValues( + ProcedureWithChildData mergeCandidate, T values) { + AddressDto address = mergeCandidate.child().address(); + if (address instanceof PostboxAddressDto) { + return false; + } + DomesticAddressDto domesticAddressDto = (DomesticAddressDto) address; + AddressData importAddress = values.getChild().address(); + boolean commonFieldsMatch = + Objects.equals(domesticAddressDto.street(), importAddress.street()) + && Objects.equals(domesticAddressDto.city(), importAddress.city()) + && Objects.equals(domesticAddressDto.houseNumber(), importAddress.houseNumber()) + && Objects.equals(domesticAddressDto.postalCode(), importAddress.postalCode()) + && Objects.equals(domesticAddressDto.addressAddition(), importAddress.addressAddition()) + && Objects.equals( + mergeCandidate.child().gender(), + Optional.ofNullable(values.getChild().gender()).orElse(GenderDto.NOT_SPECIFIED)) + && (mergeCandidate.procedure().getSchoolYear() == null + || Objects.equals(mergeCandidate.procedure().getSchoolYear(), schoolYear)); + + if (!commonFieldsMatch) { + return false; + } + + return switch (importType) { + case SCHOOL_LIST -> + (mergeCandidate.procedure().getSchoolId() == null + || Objects.equals(mergeCandidate.procedure().getSchoolId(), schoolId)) + && (mergeCandidate.procedure().getLocationId() == null + || Objects.equals(mergeCandidate.procedure().getLocationId(), locationId)); + case CITIZEN_LIST -> + (mergeCandidate.child().placeOfBirth() == null + || Objects.equals( + mergeCandidate.child().placeOfBirth(), values.getChild().placeOfBirth())) + && (mergeCandidate.child().countryOfBirth() == null + || Objects.equals( + mergeCandidate.child().countryOfBirth(), values.getChild().countryOfBirth())); + case PAST_PROCEDURE_LIST -> throw ExceptionUtil.mergeNotSupportedForPastProcedureImport(); + }; + } + + @Override + protected void createProceduresAndWriteResults() { + List<T> importableRows = validRows.importableRows(); + List<ImportProcedureData> importData = + importableRows.stream().map(rowValueMapper::mapValuesToImportData).toList(); + try { + List<SchoolEntryProcedure> createdProcedures = + switch (importType) { + case CITIZEN_LIST, SCHOOL_LIST -> + schoolEntryService.createProceduresWithBookAppointmentTask( + importData, schoolId, locationId, schoolYear, DataOrigin.DATA_IMPORT); + case PAST_PROCEDURE_LIST -> + schoolEntryService.createProceduresFromDataImport( + importData, schoolId, locationId, schoolYear); + }; + writeProcedureIdsInSheet(importableRows, createdProcedures); + } catch (Exception e) { + log.error("Failure during creating new procedures.", e); + writeFailedStatusInSheet(importableRows); + stats.correctCreatedToFailed(importableRows.size()); + } + } + + private void writeProcedureIdsInSheet( + List<T> importableRows, List<SchoolEntryProcedure> createdProcedures) { + for (int i = 0; i < importableRows.size(); i++) { + T rowValues = importableRows.get(i); + SchoolEntryProcedure createdProcedure = createdProcedures.get(i); + + Row row = rowValues.getRow(); + writeStatusAndProcedureId(row, IMPORTED_SUCCESSFULLY, createdProcedure.getExternalId()); + } + } + + @Override + protected void mergeProceduresAndWriteResults() { + if (importType.supportsMerge()) { + List<T> mergeableRows = validRows.mergeableRows(); + List<MergeProcedureData> mergeData = + mergeableRows.stream().map(rowValueMapper::mapValuesToMergeData).toList(); + List<UUID> failedIds = + schoolEntryService.mergeProcedures( + mergeData, importType, schoolId, locationId, schoolYear); + writeMergedFailedStatusInSheet(mergeableRows, failedIds); + stats.correctMergeToFailed(failedIds.size()); + } + } +} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolEntryRowValues.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolEntryRowValues.java new file mode 100644 index 000000000..40d10aa53 --- /dev/null +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolEntryRowValues.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.schoolentry.importer; + +import de.eshg.lib.xlsximport.RowValues; +import de.eshg.schoolentry.business.model.ImportChildData; + +public abstract class SchoolEntryRowValues extends RowValues { + + private ImportChildData child; + + public ImportChildData getChild() { + return child; + } + + public void setChild(ImportChildData child) { + this.child = child; + } + + abstract boolean isDuplicateRow(Object other); +} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListColumn.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListColumn.java index 935fa165f..db1d1c2d5 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListColumn.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListColumn.java @@ -5,6 +5,8 @@ package de.eshg.schoolentry.importer; +import de.eshg.lib.xlsximport.XlsxColumn; + public enum SchoolListColumn implements XlsxColumn { LAST_NAME("Name"), FIRST_NAME("Vorname"), diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowProcessor.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowProcessor.java deleted file mode 100644 index a9c3a9b79..000000000 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowProcessor.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package de.eshg.schoolentry.importer; - -import static de.eshg.schoolentry.importer.SchoolListColumn.*; - -import de.eshg.lib.procedure.domain.model.ProcedureType; -import de.eshg.schoolentry.business.model.ImportChildData; -import de.eshg.schoolentry.business.model.ImportProcedureData; -import de.eshg.schoolentry.business.model.MergeProcedureData; -import de.eshg.schoolentry.mapper.PersonMapper; -import de.eshg.schoolentry.util.ProcedureTypeAssignmentHelper; -import java.time.Year; -import java.util.List; -import java.util.Objects; -import java.util.function.BiConsumer; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.Sheet; - -public class SchoolListRowProcessor extends RowProcessor<SchoolListRowValues, SchoolListColumn> { - - private final Year schoolYear; - private final ProcedureTypeAssignmentHelper procedureTypeAssignmentHelper; - - public SchoolListRowProcessor( - Sheet sheet, - List<SchoolListColumn> actualColumns, - Year schoolYear, - ProcedureTypeAssignmentHelper procedureTypeAssignmentHelper) { - super(sheet, actualColumns); - this.schoolYear = schoolYear; - this.procedureTypeAssignmentHelper = procedureTypeAssignmentHelper; - } - - @Override - protected SchoolListRowValues process(ColumnAccessor<SchoolListColumn> col) { - SchoolListRowValues result = new SchoolListRowValues(); - BiConsumer<Cell, String> errorHandler = createErrorHandler(result); - - result.setChild(processChildData(col, errorHandler)); - result.setStatus(processStatus(col, STATUS, errorHandler)); - result.setProcedureId(processProcedureId(col, PROCEDURE_ID, errorHandler)); - result.setEntryLevel(processEntryLevel(col, errorHandler)); - result.setEarlyExamination(processEarlyExamination(col, errorHandler)); - - return result; - } - - @Override - public boolean equalRowValues(SchoolListRowValues values1, SchoolListRowValues values2) { - return Objects.equals(values1.getChild(), values2.getChild()); - } - - @Override - public ImportProcedureData mapValuesToImportData(SchoolListRowValues values) { - ProcedureType procedureType = - procedureTypeAssignmentHelper.getProcedureTypeForSchoolListImport( - values.isEntryLevel(), values.getChild().dateOfBirth(), schoolYear); - return new ImportProcedureData( - PersonMapper.mapImportChildDataToCreatePersonDto(values.getChild()), - procedureType, - null, - values.isEntryLevel(), - values.isEarlyExamination(), - false); - } - - @Override - public MergeProcedureData mapValuesToMergeData(SchoolListRowValues values) { - return new MergeProcedureData( - values.getProcedureId(), - null, - null, - null, - values.getChild().phoneNumber(), - values.isEntryLevel(), - values.isEarlyExamination()); - } - - private ImportChildData processChildData( - ColumnAccessor<SchoolListColumn> col, BiConsumer<Cell, String> errorHandler) { - return new ImportChildData( - cellAsString(col, FIRST_NAME, errorHandler), - cellAsString(col, LAST_NAME, errorHandler), - cellAsDate(col, DATE_OF_BIRTH, errorHandler), - cellAsGender(col, GENDER, errorHandler), - processAddressData( - col, - new AddressColumns<>(STREET, HOUSE_NUMBER, POSTAL_CODE, CITY, ADDRESS_ADDITION), - errorHandler, - true), - processPhoneNumber(col, errorHandler)); - } - - private String processPhoneNumber( - ColumnAccessor<SchoolListColumn> col, BiConsumer<Cell, String> errorHandler) { - return cellAsString(col, PHONE_NUMBER, true, true, errorHandler); - } - - private boolean processEntryLevel( - ColumnAccessor<SchoolListColumn> col, BiConsumer<Cell, String> errorHandler) { - return cellAsFlag(col, ENTRY_LEVEL, errorHandler); - } - - private boolean processEarlyExamination( - ColumnAccessor<SchoolListColumn> col, BiConsumer<Cell, String> errorHandler) { - return cellAsFlag(col, EARLY_EXAMINATION, errorHandler); - } -} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowReader.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowReader.java new file mode 100644 index 000000000..046a13836 --- /dev/null +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowReader.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.schoolentry.importer; + +import static de.eshg.schoolentry.importer.SchoolListColumn.*; + +import de.eshg.lib.xlsximport.ColumnAccessor; +import de.eshg.lib.xlsximport.RowReader; +import de.eshg.schoolentry.business.model.ImportChildData; +import java.util.List; +import java.util.function.BiConsumer; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Sheet; + +public class SchoolListRowReader extends RowReader<SchoolListRowValues, SchoolListColumn> { + + public SchoolListRowReader(Sheet sheet, List<SchoolListColumn> actualColumns) { + super(sheet, actualColumns); + } + + @Override + protected SchoolListRowValues read(ColumnAccessor<SchoolListColumn> col) { + SchoolListRowValues result = new SchoolListRowValues(); + BiConsumer<Cell, String> errorHandler = createErrorHandler(result); + + result.setChild(readChildData(col, errorHandler)); + result.setStatus(readStatus(col, STATUS, errorHandler)); + result.setProcedureId(readProcedureId(col, PROCEDURE_ID, errorHandler)); + result.setEntryLevel(readEntryLevelFlag(col, errorHandler)); + result.setEarlyExamination(readEarlyExaminationFlag(col, errorHandler)); + + return result; + } + + private ImportChildData readChildData( + ColumnAccessor<SchoolListColumn> col, BiConsumer<Cell, String> errorHandler) { + return new ImportChildData( + cellAsString(col, FIRST_NAME, errorHandler), + cellAsString(col, LAST_NAME, errorHandler), + cellAsDate(col, DATE_OF_BIRTH, errorHandler), + cellAsGender(col, GENDER, errorHandler), + readAddressData( + col, + new AddressColumns<>(STREET, HOUSE_NUMBER, POSTAL_CODE, CITY, ADDRESS_ADDITION), + errorHandler, + true), + readPhoneNumber(col, errorHandler)); + } + + private String readPhoneNumber( + ColumnAccessor<SchoolListColumn> col, BiConsumer<Cell, String> errorHandler) { + return cellAsString(col, PHONE_NUMBER, true, true, errorHandler); + } + + private boolean readEntryLevelFlag( + ColumnAccessor<SchoolListColumn> col, BiConsumer<Cell, String> errorHandler) { + return cellAsFlag(col, ENTRY_LEVEL, errorHandler); + } + + private boolean readEarlyExaminationFlag( + ColumnAccessor<SchoolListColumn> col, BiConsumer<Cell, String> errorHandler) { + return cellAsFlag(col, EARLY_EXAMINATION, errorHandler); + } +} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowValueMapper.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowValueMapper.java new file mode 100644 index 000000000..bf36ee839 --- /dev/null +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowValueMapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.schoolentry.importer; + +import de.eshg.lib.procedure.domain.model.ProcedureType; +import de.eshg.schoolentry.business.model.ImportProcedureData; +import de.eshg.schoolentry.business.model.MergeProcedureData; +import de.eshg.schoolentry.mapper.PersonMapper; +import de.eshg.schoolentry.util.ProcedureTypeAssignmentHelper; +import java.time.Year; + +public class SchoolListRowValueMapper implements RowValueMapper<SchoolListRowValues> { + + private final Year schoolYear; + private final ProcedureTypeAssignmentHelper procedureTypeAssignmentHelper; + + public SchoolListRowValueMapper( + Year schoolYear, ProcedureTypeAssignmentHelper procedureTypeAssignmentHelper) { + this.schoolYear = schoolYear; + this.procedureTypeAssignmentHelper = procedureTypeAssignmentHelper; + } + + @Override + public ImportProcedureData mapValuesToImportData(SchoolListRowValues values) { + ProcedureType procedureType = + procedureTypeAssignmentHelper.getProcedureTypeForSchoolListImport( + values.isEntryLevel(), values.getChild().dateOfBirth(), schoolYear); + return new ImportProcedureData( + PersonMapper.mapImportChildDataToCreatePersonDto(values.getChild()), + procedureType, + null, + values.isEntryLevel(), + values.isEarlyExamination(), + false); + } + + @Override + public MergeProcedureData mapValuesToMergeData(SchoolListRowValues values) { + return new MergeProcedureData( + values.getProcedureId(), + null, + null, + null, + values.getChild().phoneNumber(), + values.isEntryLevel(), + values.isEarlyExamination()); + } +} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowValues.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowValues.java index bfe9785c7..09f02addd 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowValues.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/importer/SchoolListRowValues.java @@ -5,7 +5,9 @@ package de.eshg.schoolentry.importer; -public final class SchoolListRowValues extends RowValues { +import java.util.Objects; + +public final class SchoolListRowValues extends SchoolEntryRowValues { private boolean isEntryLevel; @@ -26,4 +28,10 @@ public final class SchoolListRowValues extends RowValues { public void setEarlyExamination(boolean earlyExamination) { isEarlyExamination = earlyExamination; } + + @Override + boolean isDuplicateRow(Object other) { + return (other instanceof SchoolListRowValues schoolListRowValues) + && Objects.equals(this.getChild(), schoolListRowValues.getChild()); + } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/mapper/AddressMapper.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/mapper/AddressMapper.java index 722252957..3c9fa4796 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/mapper/AddressMapper.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/mapper/AddressMapper.java @@ -7,7 +7,7 @@ package de.eshg.schoolentry.mapper; import de.eshg.base.address.AddressDto; import de.eshg.base.address.DomesticAddressDto; -import de.eshg.schoolentry.business.model.AddressData; +import de.eshg.lib.xlsximport.model.AddressData; public final class AddressMapper { private AddressMapper() {} diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/invitation/InvitationGenerator.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/invitation/InvitationGenerator.java index 74a69280b..a63fe477d 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/invitation/InvitationGenerator.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/invitation/InvitationGenerator.java @@ -14,9 +14,9 @@ import de.eshg.lib.appointmentblock.spring.AppointmentBlockProperties; import de.eshg.lib.document.generator.DocumentGenerator; import de.eshg.lib.document.generator.department.DepartmentClient; import de.eshg.lib.document.generator.department.DepartmentLogo; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.Pdf; import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.lib.procedure.file.FileFactory; import de.eshg.schoolentry.business.model.ChildData; import de.eshg.schoolentry.pdf.AbstractGenerator; @@ -189,6 +189,7 @@ public class InvitationGenerator extends AbstractGenerator { .formatted( invitationData.child().name().replace(" ", "_"), now.format(ReportGeneratorConstants.FILENAME_TIMESTAMP_FORMAT)); - return FileFactory.createPdfWithMetaData(filename, FileType.PDF, bytes, pdfMetaData, false); + return FileFactory.createPdfWithMetaData( + filename, ProcedureFileType.PDF, bytes, pdfMetaData, false); } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/medicalreport/MedicalReportGenerator.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/medicalreport/MedicalReportGenerator.java index ef57085ef..fd12f8e14 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/medicalreport/MedicalReportGenerator.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/medicalreport/MedicalReportGenerator.java @@ -9,9 +9,9 @@ import de.eshg.base.client.ContactClient; import de.eshg.lib.document.generator.DocumentGenerator; import de.eshg.lib.document.generator.department.DepartmentClient; import de.eshg.lib.document.generator.department.DepartmentLogo; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.Pdf; import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.lib.procedure.file.FileFactory; import de.eshg.schoolentry.api.CreateMedicalReportRequest; import de.eshg.schoolentry.business.model.ChildDetailsData; @@ -88,6 +88,7 @@ public class MedicalReportGenerator extends AbstractGenerator { addressee, medicalReportData.child().name().replace(" ", "_"), now.format(ReportGeneratorConstants.FILENAME_TIMESTAMP_FORMAT)); - return FileFactory.createPdfWithMetaData(filename, FileType.PDF, bytes, pdfMetaData, false); + return FileFactory.createPdfWithMetaData( + filename, ProcedureFileType.PDF, bytes, pdfMetaData, false); } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/schoolinfoletter/SchoolInfoLetterExaminationMapper.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/schoolinfoletter/SchoolInfoLetterExaminationMapper.java index 327a08960..76a0a90ed 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/schoolinfoletter/SchoolInfoLetterExaminationMapper.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/schoolinfoletter/SchoolInfoLetterExaminationMapper.java @@ -11,15 +11,12 @@ import static de.eshg.schoolentry.domain.model.SchoolRecommendation.BACK_REGULAR import de.eshg.lib.procedure.domain.model.ProcedureType; import de.eshg.schoolentry.api.CreateSchoolInfoLetterRequest; import de.eshg.schoolentry.business.model.ProcedureDetailsData; -import de.eshg.schoolentry.config.SchoolEntryFeature; -import de.eshg.schoolentry.config.SchoolEntryFeatureToggle; import de.eshg.schoolentry.domain.model.*; import de.eshg.schoolentry.pdf.schoolinfoletter.model.*; import de.eshg.schoolentry.pdf.schoolinfoletter.model.SchoolInfoLetterExaminationType.Type; import de.eshg.schoolentry.statistics.StatisticsValueMappers; import de.eshg.schoolentry.statistics.options.EvaluationResult; import java.time.Clock; -import java.time.Year; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Objects; @@ -37,11 +34,9 @@ public class SchoolInfoLetterExaminationMapper { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy"); private static final DateTimeFormatter YEAR_FORMATTER = DateTimeFormatter.ofPattern("yyyy"); private final Clock clock; - private final SchoolEntryFeatureToggle featureToggle; - public SchoolInfoLetterExaminationMapper(Clock clock, SchoolEntryFeatureToggle featureToggle) { + public SchoolInfoLetterExaminationMapper(Clock clock) { this.clock = clock; - this.featureToggle = featureToggle; } SchoolInfoLetterExamination mapToData( @@ -56,7 +51,7 @@ public class SchoolInfoLetterExaminationMapper { new SchoolInfoLetterChild( concat(procedureDetails.child().firstName(), procedureDetails.child().lastName()), procedureDetails.child().dateOfBirth().format(DATE_FORMATTER)), - YEAR_FORMATTER.format(getSchoolYear(procedureDetails)), + YEAR_FORMATTER.format(procedureDetails.schoolYear()), DATE_FORMATTER.format( procedureDetails.appointment().getAppointmentEnd().atZone(clock.getZone())), new SchoolInfoLetterExaminationType( @@ -76,14 +71,6 @@ public class SchoolInfoLetterExaminationMapper { request.parentsWishNote(), request.referredToFurtherConsultationFromSchool())); } - private Year getSchoolYear(ProcedureDetailsData procedureDetails) { - if (featureToggle.isNewFeatureDisabled(SchoolEntryFeature.SCHOOL_YEAR)) { - log.warn("Using current year since feature toggle is disabled"); - return Year.now(clock); - } - return procedureDetails.schoolYear(); - } - private static String concat(String... parts) { return String.join(" ", parts); } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/schoolinfoletter/SchoolInfoLetterGenerator.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/schoolinfoletter/SchoolInfoLetterGenerator.java index dcfdf6c00..451b8f3bd 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/schoolinfoletter/SchoolInfoLetterGenerator.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/pdf/schoolinfoletter/SchoolInfoLetterGenerator.java @@ -9,9 +9,9 @@ import de.eshg.base.client.ContactClient; import de.eshg.lib.document.generator.DocumentGenerator; import de.eshg.lib.document.generator.department.DepartmentClient; import de.eshg.lib.document.generator.department.DepartmentLogo; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.Pdf; import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.lib.procedure.file.FileFactory; import de.eshg.schoolentry.api.CreateSchoolInfoLetterRequest; import de.eshg.schoolentry.business.model.ProcedureDetailsData; @@ -74,7 +74,7 @@ public class SchoolInfoLetterGenerator extends AbstractGenerator { "Schulinfobrief_%s.pdf" .formatted(now.format(ReportGeneratorConstants.FILENAME_TIMESTAMP_FORMAT)); return FileFactory.createPdfWithMetaData( - filename, FileType.PDF, baos.toByteArray(), pdfMetaData, false); + filename, ProcedureFileType.PDF, baos.toByteArray(), pdfMetaData, false); } @VisibleForTesting diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/statistics/StatisticsValueMappers.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/statistics/StatisticsValueMappers.java index 4f58e90ea..5e737132b 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/statistics/StatisticsValueMappers.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/statistics/StatisticsValueMappers.java @@ -15,6 +15,8 @@ import org.springframework.data.domain.Range; public class StatisticsValueMappers { + private StatisticsValueMappers() {} + public static Function<Integer, EvaluationResult> jumpCountAssessment() { return getEvaluationResult(Range.closed(0, 6), Range.closed(7, 8), Range.closed(9, 30)); } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryProceduresPopulator.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryProceduresPopulator.java index d7d4a9169..d455c4e64 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryProceduresPopulator.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryProceduresPopulator.java @@ -7,18 +7,20 @@ package de.eshg.schoolentry.testhelper; import static de.eshg.base.util.ClassNameUtil.getClassNameAsPropertyKey; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; import de.eshg.base.address.DomesticAddressDto; -import de.eshg.base.contact.api.SearchContactsResponse; +import de.eshg.base.contact.ContactApi; +import de.eshg.base.contact.api.*; import de.eshg.base.testhelper.BaseTestHelperApi; +import de.eshg.lib.appointmentblock.LocationSelectionMode; +import de.eshg.lib.appointmentblock.spring.AppointmentBlockProperties; import de.eshg.lib.appointmentblock.testhelper.AppointmentBlockGroupsPopulator; +import de.eshg.lib.common.CountryCode; import de.eshg.schoolentry.LabelController; import de.eshg.schoolentry.SchoolEntryController; import de.eshg.schoolentry.api.*; import de.eshg.schoolentry.api.anamnesis.*; -import de.eshg.schoolentry.config.SchoolEntryFeature; import de.eshg.schoolentry.config.SchoolEntryFeatureToggle; import de.eshg.schoolentry.domain.model.SchoolEntryProcedure; import de.eshg.schoolentry.domain.repository.SchoolEntryProcedureRepository; @@ -34,12 +36,7 @@ import jakarta.validation.constraints.NotNull; import java.time.Clock; import java.time.LocalDate; import java.time.Year; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.stream.Stream; import net.datafaker.Faker; import net.datafaker.providers.base.Address; @@ -57,6 +54,8 @@ public class SchoolEntryProceduresPopulator extends BasePopulator<CreateProcedur private final LabelController labelController; private final BaseTestHelperApi baseTestHelperApi; private final SchoolEntryFeatureToggle featureToggle; + private final ContactApi contactApi; + private final AppointmentBlockProperties appointmentBlockProperties; public SchoolEntryProceduresPopulator( Clock clock, @@ -69,7 +68,9 @@ public class SchoolEntryProceduresPopulator extends BasePopulator<CreateProcedur SchoolEntryFeatureToggle featureToggle, @SuppressWarnings("unused") // Used to define a dependency AppointmentBlockGroupsPopulator appointmentBlockGroupsPopulator, - EnvironmentConfig environmentConfig) { + EnvironmentConfig environmentConfig, + ContactApi contactApi, + AppointmentBlockProperties appointmentBlockProperties) { super( clock, environment, @@ -82,6 +83,8 @@ public class SchoolEntryProceduresPopulator extends BasePopulator<CreateProcedur this.labelController = labelController; this.baseTestHelperApi = baseTestHelperApi; this.featureToggle = featureToggle; + this.contactApi = contactApi; + this.appointmentBlockProperties = appointmentBlockProperties; } @Override @@ -90,12 +93,16 @@ public class SchoolEntryProceduresPopulator extends BasePopulator<CreateProcedur () -> { ListWithTotalNumber<CreateProcedureResponse> response = populateWithAuthentication(numberOfEntitiesToPopulate); - createAppointments(response.entities()); - SearchContactsResponse responseSearchContacts = - baseTestHelperApi.populateSchoolContacts(new PopulationRequest(5)); - UUID schoolId = responseSearchContacts.elements().getFirst().id(); - assignSpecialNeedsLabel(response.entities().subList(0, numberOfEntitiesToPopulate / 5)); + UUID schoolId = getIdOfFirstContactForCategory(InstitutionContactCategoryDto.SCHOOL); assignSchool(response.entities().subList(0, numberOfEntitiesToPopulate / 2), schoolId); + if (appointmentBlockProperties.getLocationSelectionMode() + == LocationSelectionMode.HEALTH_DEPARTMENT) { + assignLocationId( + response.entities().subList(0, numberOfEntitiesToPopulate / 2), + getIdOfFirstContactForCategory(InstitutionContactCategoryDto.HEALTH_DEPARTMENT)); + } + assignSpecialNeedsLabel(response.entities().subList(0, numberOfEntitiesToPopulate / 5)); + createAppointments(response.entities()); return response; }); } @@ -155,6 +162,26 @@ public class SchoolEntryProceduresPopulator extends BasePopulator<CreateProcedur } } + private void assignLocationId(List<CreateProcedureResponse> procedures, UUID locationId) { + for (CreateProcedureResponse procedure : procedures) { + ProcedureDetailsDto procedureToUpdate = + schoolEntryController.getProcedure(procedure.procedureId()); + schoolEntryController.updateProcedure( + procedure.procedureId(), + new UpdateProcedureRequest( + procedureToUpdate.version(), + procedureToUpdate.type(), + procedureToUpdate.labels().stream().map(LabelDto::id).toList(), + procedureToUpdate.appointment(), + procedureToUpdate.isInvitationSent(), + procedureToUpdate.school().id(), + locationId, + procedureToUpdate.isDeceased(), + procedureToUpdate.deceased(), + procedureToUpdate.schoolYear())); + } + } + @Override protected CreateProcedureResponse populate( int index, @@ -203,8 +230,8 @@ public class SchoolEntryProceduresPopulator extends BasePopulator<CreateProcedur optional(faker, address.secondaryAddress(), 0.1)); } - private static CountryCodeDto randomCountryBase(Faker faker) { - return randomElement(faker, CountryCodeDto.values()); + private static CountryCode randomCountryBase(Faker faker) { + return randomElement(faker, CountryCode.values()); } private void createRandomExaminationsAndAnamnesisForProcedure(UUID procedureId, Faker faker) { @@ -240,23 +267,21 @@ public class SchoolEntryProceduresPopulator extends BasePopulator<CreateProcedur } private void setRandomSchoolYear(@NotNull UUID procedureId, Faker faker) { - if (featureToggle.isNewFeatureEnabled(SchoolEntryFeature.SCHOOL_YEAR)) { - ProcedureDetailsDto procedureToUpdate = schoolEntryController.getProcedure(procedureId); - int currentYear = Year.now(clock).getValue(); - schoolEntryController.updateProcedure( - procedureId, - new UpdateProcedureRequest( - procedureToUpdate.version(), - procedureToUpdate.type(), - procedureToUpdate.labels().stream().map(LabelDto::id).toList(), - procedureToUpdate.appointment(), - procedureToUpdate.isInvitationSent(), - getSchoolId(procedureToUpdate), - getLocationId(procedureToUpdate), - procedureToUpdate.isDeceased(), - procedureToUpdate.deceased(), - faker.number().numberBetween(currentYear - 10, currentYear + 10))); - } + ProcedureDetailsDto procedureToUpdate = schoolEntryController.getProcedure(procedureId); + int currentYear = Year.now(clock).getValue(); + schoolEntryController.updateProcedure( + procedureId, + new UpdateProcedureRequest( + procedureToUpdate.version(), + procedureToUpdate.type(), + procedureToUpdate.labels().stream().map(LabelDto::id).toList(), + procedureToUpdate.appointment(), + procedureToUpdate.isInvitationSent(), + getSchoolId(procedureToUpdate), + getLocationId(procedureToUpdate), + procedureToUpdate.isDeceased(), + procedureToUpdate.deceased(), + faker.number().numberBetween(currentYear - 5, currentYear + 5))); } /* hearing test */ @@ -619,4 +644,29 @@ public class SchoolEntryProceduresPopulator extends BasePopulator<CreateProcedur protected long countExistingEntities() { return this.schoolEntryProcedureRepository.count(); } + + private UUID getIdOfFirstContactForCategory(InstitutionContactCategoryDto category) { + return contactApi + .getContacts( + new ContactFilterParameters( + null, null, ContactTypeDto.INSTITUTION, category, null, null, null, null)) + .elements() + .stream() + .findFirst() + .map(ContactDto::id) + .orElseGet(() -> populateOneContactOfCategoryAndGetId(category)); + } + + private UUID populateOneContactOfCategoryAndGetId(InstitutionContactCategoryDto category) { + SearchContactsResponse response = + switch (category) { + case SCHOOL -> baseTestHelperApi.populateSchoolContacts(new PopulationRequest(1)); + case HEALTH_DEPARTMENT -> + baseTestHelperApi.populateHealthDepartmentContacts(new PopulationRequest(1)); + case null, default -> + throw new IllegalStateException( + "Expected only to be used with SCHOOL or HEALTH_DEPARTMENT. Got: " + category); + }; + return response.elements().getFirst().id(); + } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryTestHelperController.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryTestHelperController.java index 3650b0914..cd63ced78 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryTestHelperController.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryTestHelperController.java @@ -12,6 +12,7 @@ import de.eshg.lib.appointmentblock.spring.AppointmentBlockProperties; import de.eshg.lib.appointmentblock.testhelper.AppointmentBlockGroupsPopulator; import de.eshg.lib.auditlog.AuditLogTestHelperService; import de.eshg.schoolentry.api.CreateProcedureResponse; +import de.eshg.schoolentry.api.GetClosedProceduresResponse; import de.eshg.schoolentry.api.SchoolEntryAppointmentBlockPopulationResult; import de.eshg.schoolentry.api.SchoolEntryProcedurePopulationResult; import de.eshg.schoolentry.config.SchoolEntryFeature; @@ -77,6 +78,12 @@ public class SchoolEntryTestHelperController extends TestHelperController schoolEntryTestHelperService.clearCitizenUserId(procedureId); } + @GetExchange("/school-entries/closed") + @Transactional(readOnly = true) + public GetClosedProceduresResponse getIdsOfClosedProcedures() { + return new GetClosedProceduresResponse(schoolEntryTestHelperService.getIdsOfClosedProcedures()); + } + @PostExchange("/enabled-new-features/{featureToEnable}") public void enableNewFeature( @PathVariable("featureToEnable") SchoolEntryFeature featureToEnable) { diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryTestHelperService.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryTestHelperService.java index 3e5a2c160..a14aa2155 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryTestHelperService.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/testhelper/SchoolEntryTestHelperService.java @@ -78,4 +78,8 @@ public class SchoolEntryTestHelperService extends DefaultTestHelperService { environmentConfig.assertIsNotProduction(); schoolEntryProcedureRepository.clearCitizenUserId(procedureId); } + + public List<UUID> getIdsOfClosedProcedures() { + return schoolEntryProcedureRepository.findExternalIdsOfClosedProcedures(); + } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/util/ProcedureTypeAssignmentHelper.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/util/ProcedureTypeAssignmentHelper.java index a11b1d9a2..b55999e8b 100644 --- a/backend/school-entry/src/main/java/de/eshg/schoolentry/util/ProcedureTypeAssignmentHelper.java +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/util/ProcedureTypeAssignmentHelper.java @@ -6,7 +6,6 @@ package de.eshg.schoolentry.util; import de.eshg.lib.procedure.domain.model.ProcedureType; -import de.eshg.schoolentry.config.SchoolEntryFeature; import de.eshg.schoolentry.config.SchoolEntryFeatureToggle; import de.eshg.schoolentry.config.SchoolEntryProperties; import java.time.LocalDate; @@ -53,20 +52,12 @@ public class ProcedureTypeAssignmentHelper { boolean isRegularSchoolEntry(LocalDate dateOfBirth, Year schoolYear) { MonthDay maxDateOfBirthForRegularSchoolEntry = schoolEntryProperties.getMaxDateOfBirthForRegularSchoolEntry(); - if (schoolEntryFeatureToggle.isNewFeatureEnabled(SchoolEntryFeature.SCHOOL_YEAR)) { - LocalDate maxDateOfBirthForRegularSchoolEntryWithYear = - schoolYear.minusYears(6).atMonthDay(maxDateOfBirthForRegularSchoolEntry); - if (schoolEntryProperties.isMaxDateOfBirthForRegularSchoolEntryIsInclusive()) { - return !dateOfBirth.isAfter(maxDateOfBirthForRegularSchoolEntryWithYear); - } else { - return dateOfBirth.isBefore(maxDateOfBirthForRegularSchoolEntryWithYear); - } + LocalDate maxDateOfBirthForRegularSchoolEntryWithYear = + schoolYear.minusYears(6).atMonthDay(maxDateOfBirthForRegularSchoolEntry); + if (schoolEntryProperties.isMaxDateOfBirthForRegularSchoolEntryIsInclusive()) { + return !dateOfBirth.isAfter(maxDateOfBirthForRegularSchoolEntryWithYear); } else { - if (schoolEntryProperties.isMaxDateOfBirthForRegularSchoolEntryIsInclusive()) { - return MonthDay.from(dateOfBirth).compareTo(maxDateOfBirthForRegularSchoolEntry) <= 0; - } else { - return MonthDay.from(dateOfBirth).compareTo(maxDateOfBirthForRegularSchoolEntry) < 0; - } + return dateOfBirth.isBefore(maxDateOfBirthForRegularSchoolEntryWithYear); } } } diff --git a/backend/school-entry/src/main/java/de/eshg/schoolentry/util/TaskUtil.java b/backend/school-entry/src/main/java/de/eshg/schoolentry/util/TaskUtil.java new file mode 100644 index 000000000..246b011d9 --- /dev/null +++ b/backend/school-entry/src/main/java/de/eshg/schoolentry/util/TaskUtil.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.schoolentry.util; + +import de.eshg.lib.procedure.domain.model.TaskStatus; +import de.eshg.lib.procedure.domain.model.TaskType; +import de.eshg.rest.service.security.CurrentUserHelper; +import de.eshg.schoolentry.domain.model.SchoolEntryProcedure; +import de.eshg.schoolentry.domain.model.SchoolEntryTask; +import java.time.Clock; +import java.time.Instant; +import org.springframework.stereotype.Component; + +@Component +public class TaskUtil { + + private final Clock clock; + + public TaskUtil(Clock clock) { + this.clock = clock; + } + + public void addOpenTaskOfType(SchoolEntryProcedure schoolEntryProcedure, TaskType type) { + SchoolEntryTask task = new SchoolEntryTask(); + task.assign( + CurrentUserHelper.getCurrentUserId(), + CurrentUserHelper.getCurrentUserId(), + Instant.now(clock)); + task.setTaskStatus(TaskStatus.OPEN); + task.setTaskType(type); + schoolEntryProcedure.addTask(task); + } + + public static void closeSingleTaskOfType(SchoolEntryProcedure procedure, TaskType type) { + SchoolEntryTask taskToUpdate = procedure.getTaskOfType(type); + taskToUpdate.setTaskStatus(TaskStatus.CLOSED); + } + + public static void closeOptionalTaskOfType(SchoolEntryProcedure procedure, TaskType type) { + procedure.getOptionalTaskOfType(type).ifPresent(task -> task.setTaskStatus(TaskStatus.CLOSED)); + } +} diff --git a/backend/school-entry/src/main/resources/application-preview-features.properties b/backend/school-entry/src/main/resources/application-preview-features.properties index 404ac134e..af44e656e 100644 --- a/backend/school-entry/src/main/resources/application-preview-features.properties +++ b/backend/school-entry/src/main/resources/application-preview-features.properties @@ -1,10 +1,3 @@ de.eshg.schoolentry.feature-toggle.enabled-new-features=\ - MEDICAL_REPORT,\ - MERGE_PROCEDURES_ON_IMPORT,\ CLOSE_PROCEDURE,\ - SEARCH_BY_KNOWLEDGE_FACTORS,\ - SCHOOL_YEAR,\ - SCHOOL_INFO_LETTER,\ - DELETE_PROCEDURE,\ - WAITING_ROOM,\ REOPEN_PROCEDURE diff --git a/backend/school-entry/src/main/resources/migrations/0047_introduce_procedure_file_type.xml b/backend/school-entry/src/main/resources/migrations/0047_introduce_procedure_file_type.xml new file mode 100644 index 000000000..33e3f6e4d --- /dev/null +++ b/backend/school-entry/src/main/resources/migrations/0047_introduce_procedure_file_type.xml @@ -0,0 +1,11 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1728921636471-1"> + <ext:renamePostgresEnumType oldName="fileType" newName="procedureFileType"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/school-entry/src/main/resources/migrations/0048_add_medical_registry_procedure_types.xml b/backend/school-entry/src/main/resources/migrations/0048_add_medical_registry_procedure_types.xml new file mode 100644 index 000000000..21b419fae --- /dev/null +++ b/backend/school-entry/src/main/resources/migrations/0048_add_medical_registry_procedure_types.xml @@ -0,0 +1,14 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1728647075086-1"> + <ext:addPostgresEnumValues enumTypeName="persontype" valuesToAdd="PROFESSIONAL"/> + </changeSet> + <changeSet author="GA-Lotse" id="1728647075086-2"> + <ext:addPostgresEnumValues enumTypeName="proceduretype" valuesToAdd="MEDICAL_REGISTRY_CITIZEN_DRAFT, MEDICAL_REGISTRY_EMPLOYEE_DRAFT, MEDICAL_REGISTRY_ENTRY"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/school-entry/src/main/resources/migrations/changelog.xml b/backend/school-entry/src/main/resources/migrations/changelog.xml index 9acd2eb9d..34a7a23b5 100644 --- a/backend/school-entry/src/main/resources/migrations/changelog.xml +++ b/backend/school-entry/src/main/resources/migrations/changelog.xml @@ -56,5 +56,7 @@ <include file="migrations/0044_add_columns_measles_contra_indication.xml"/> <include file="migrations/0045_add_examination_date.xml"/> <include file="migrations/0046_add_general_country_options.xml"/> + <include file="migrations/0047_introduce_procedure_file_type.xml"/> + <include file="migrations/0048_add_medical_registry_procedure_types.xml"/> </databaseChangeLog> diff --git a/backend/school-entry/src/main/resources/templates/medicalreport.ftlx b/backend/school-entry/src/main/resources/templates/medicalreport.ftlx index 343db5c21..5a0aebed8 100644 --- a/backend/school-entry/src/main/resources/templates/medicalreport.ftlx +++ b/backend/school-entry/src/main/resources/templates/medicalreport.ftlx @@ -163,6 +163,7 @@ <div class="addressee"> An den/die Arzt:in des Kindes </div> + <div>${child.name}</div> </td> <td valign="top" class="sender"> <div class="sender-address"> diff --git a/backend/service-directory/gradle.lockfile b/backend/service-directory/gradle.lockfile index 8cac3402f..af063bf04 100644 --- a/backend/service-directory/gradle.lockfile +++ b/backend/service-directory/gradle.lockfile @@ -60,6 +60,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/settings.gradle b/backend/settings.gradle index 44956270c..4ec5c3f90 100644 --- a/backend/settings.gradle +++ b/backend/settings.gradle @@ -50,6 +50,7 @@ include 'business-module-persistence-commons' include 'central-repository' include 'chat-management' include 'compliance-test' +include 'file-commons' include 'inspection' include 'keycloak' include 'keycloak-api' @@ -81,6 +82,7 @@ include 'lib-service-directory-admin-api' include 'lib-service-directory-api' include 'lib-statistics' include 'lib-statistics-api' +include 'lib-xlsx-import' include 'local-service-directory' include 'logging-commons' include 'measles-protection' diff --git a/backend/spatz/gradle.lockfile b/backend/spatz/gradle.lockfile index f72534388..f89f7547e 100644 --- a/backend/spatz/gradle.lockfile +++ b/backend/spatz/gradle.lockfile @@ -107,6 +107,7 @@ io.projectreactor.netty:reactor-netty-core:1.1.22=compileClasspath,productionRun io.projectreactor.netty:reactor-netty-http:1.1.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.projectreactor:reactor-core:3.6.10=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:2.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.mail:jakarta.mail-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/statistics/build.gradle b/backend/statistics/build.gradle index 19adc1938..0f8351216 100644 --- a/backend/statistics/build.gradle +++ b/backend/statistics/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation project(':lib-base-client') implementation project(':lib-statistics-api') implementation project(':rest-oauth-client-commons') + implementation project(':file-commons') implementation("org.springframework.boot:spring-boot-starter-web") implementation 'org.apache.poi:poi:latest.release' @@ -35,4 +36,4 @@ tasks.named("test").configure { dependencyTrack { projectId = project.findProperty('dependency-track-project-id-statistics') ?: "unspecified" -} \ No newline at end of file +} diff --git a/backend/statistics/gradle.lockfile b/backend/statistics/gradle.lockfile index 3b603092e..ccad08a54 100644 --- a/backend/statistics/gradle.lockfile +++ b/backend/statistics/gradle.lockfile @@ -13,11 +13,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.virtuald:curvesapi:1.08=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -26,7 +26,7 @@ com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=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 @@ -35,11 +35,11 @@ com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath, com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,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=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -49,10 +49,10 @@ de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeCla de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-jakarta9:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -66,6 +66,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -76,15 +77,16 @@ jakarta.transaction:jakarta.transaction-api:2.0.1=annotationProcessor,compileCla jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.13.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.26.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -99,6 +101,7 @@ org.apache.logging.log4j:log4j-to-slf4j:2.23.1=compileClasspath,productionRuntim org.apache.poi:poi-ooxml-lite:5.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.poi:poi-ooxml:5.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.poi:poi:5.3.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tika:tika-core:2.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -106,18 +109,18 @@ org.apache.tomcat:tomcat-annotations-api:10.1.30=productionRuntimeClasspath,runt org.apache.xmlbeans:xmlbeans:5.2.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.bouncycastle:bcpkix-jdk18on:1.78.1=testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.78.1=testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.78.1=testRuntimeClasspath +org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.78.1=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 org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -129,27 +132,28 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspath +org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -159,7 +163,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -175,7 +179,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -198,14 +202,21 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.testcontainers:database-commons:1.19.8=testRuntimeClasspath org.testcontainers:jdbc:1.19.8=testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:parser:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:pdf-model:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:validation-model-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.verapdf:verapdf-xmp-core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.webjars:swagger-ui:5.17.14=testCompileClasspath,testRuntimeClasspath org.xmlunit:xmlunit-core:2.9.1=testCompileClasspath,testRuntimeClasspath org.yaml:snakeyaml:2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/statistics/openApi.yaml b/backend/statistics/openApi.yaml index 3f1f6ef73..1c6c62c6a 100644 --- a/backend/statistics/openApi.yaml +++ b/backend/statistics/openApi.yaml @@ -62,7 +62,9 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AddEvaluationTemplateRequest" + oneOf: + - $ref: "#/components/schemas/AddEvaluationTemplateFromEvaluationRequest" + - $ref: "#/components/schemas/AddEvaluationTemplateWithDataSourcesRequest" required: true responses: "200": @@ -974,6 +976,15 @@ components: type: string required: - '@type' + AbstractAddEvaluationTemplateRequest: + type: object + discriminator: + propertyName: '@type' + properties: + '@type': + type: string + required: + - '@type' AbstractAddReportSeriesRequest: type: object discriminator: @@ -1155,17 +1166,34 @@ components: - chartConfiguration - name - statisticId - AddEvaluationTemplateRequest: + AddEvaluationTemplateFromEvaluationRequest: type: object - properties: - dataSources: - type: array - items: - $ref: "#/components/schemas/DataSource" - maxItems: 1 - minItems: 1 - name: - type: string + allOf: + - $ref: "#/components/schemas/AbstractAddEvaluationTemplateRequest" + - type: object + properties: + evaluationId: + type: string + format: uuid + name: + type: string + required: + - evaluationId + - name + AddEvaluationTemplateWithDataSourcesRequest: + type: object + allOf: + - $ref: "#/components/schemas/AbstractAddEvaluationTemplateRequest" + - type: object + properties: + dataSources: + type: array + items: + $ref: "#/components/schemas/DataSource" + maxItems: 1 + minItems: 1 + name: + type: string required: - dataSources - name @@ -2134,15 +2162,15 @@ components: GetReportDetailPageResponse: type: object properties: - createdAt: - type: string - format: date-time description: type: string evaluation: type: array items: $ref: "#/components/schemas/Evaluation" + executionDate: + type: string + format: date id: type: string format: uuid @@ -2178,8 +2206,8 @@ components: userReportSeries: $ref: "#/components/schemas/User" required: - - createdAt - evaluation + - executionDate - id - name - numberOfReportsInSeries @@ -2679,6 +2707,7 @@ components: format: int64 minimum: 0 required: + - executionDate - id - name - state @@ -2735,6 +2764,7 @@ components: - COMPLETED - FAILED - CREATING + - DELETING ReportType: type: string enum: diff --git a/backend/statistics/src/main/java/de/eshg/statistics/AttributeCodeToNameMapping.java b/backend/statistics/src/main/java/de/eshg/statistics/AttributeCodeToNameMapping.java deleted file mode 100644 index 368069753..000000000 --- a/backend/statistics/src/main/java/de/eshg/statistics/AttributeCodeToNameMapping.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package de.eshg.statistics; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.util.Map; - -public record AttributeCodeToNameMapping( - @NotBlank String businessAttributeCode, - @NotBlank String businessAttributeName, - @NotNull Map<String, String> baseAttributes) {} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/EvaluationTemplateController.java b/backend/statistics/src/main/java/de/eshg/statistics/EvaluationTemplateController.java index 136aa35cb..958d32bf2 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/EvaluationTemplateController.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/EvaluationTemplateController.java @@ -10,13 +10,19 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import de.eshg.rest.service.security.config.BaseUrls; import de.eshg.statistics.aggregation.DataSourceValidator; -import de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateRequest; +import de.eshg.statistics.aggregation.StatisticService; +import de.eshg.statistics.api.AvailableDataSource; +import de.eshg.statistics.api.evaluationtemplate.AbstractAddEvaluationTemplateRequest; +import de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateFromEvaluationRequest; +import de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateWithDataSourcesRequest; import de.eshg.statistics.api.evaluationtemplate.EvaluationTemplateDto; import de.eshg.statistics.api.evaluationtemplate.GetEvaluationTemplatesResponse; +import de.eshg.statistics.datatransfer.EvaluationTemplateData; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; import java.util.UUID; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -33,12 +39,15 @@ public class EvaluationTemplateController { public static final String BASE_URL = BaseUrls.Statistics.EVALUATION_TEMPLATE_CONTROLLER; private final EvaluationTemplateService evaluationTemplateService; + private final StatisticService statisticService; private final DataSourceValidator dataSourceValidator; public EvaluationTemplateController( EvaluationTemplateService evaluationTemplateService, + StatisticService statisticService, DataSourceValidator dataSourceValidator) { this.evaluationTemplateService = evaluationTemplateService; + this.statisticService = statisticService; this.dataSourceValidator = dataSourceValidator; } @@ -46,9 +55,35 @@ public class EvaluationTemplateController { @ApiResponse(responseCode = "200", description = "The added evaluation template") @Operation(summary = "Add an evaluation template") public EvaluationTemplateDto addEvaluationTemplate( - @Valid @RequestBody AddEvaluationTemplateRequest addEvaluationTemplateRequest) { - dataSourceValidator.validateDataSources(addEvaluationTemplateRequest.dataSources()); - return evaluationTemplateService.addEvaluationTemplate(addEvaluationTemplateRequest); + @Valid @RequestBody AbstractAddEvaluationTemplateRequest addEvaluationTemplateRequest) { + return switch (addEvaluationTemplateRequest) { + case AddEvaluationTemplateFromEvaluationRequest + addEvaluationTemplateFromEvaluationRequest -> { + UUID evaluationId = addEvaluationTemplateFromEvaluationRequest.evaluationId(); + EvaluationTemplateData evaluationTemplateData = + statisticService.getEvaluationTemplateData(evaluationId); + List<AvailableDataSource> relevantAvailableDataSources = + dataSourceValidator.getRelevantAvailableDataSources( + evaluationTemplateData.dataSources()); + dataSourceValidator.validateDataSources( + evaluationTemplateData.dataSources(), relevantAvailableDataSources); + yield evaluationTemplateService.addEvaluationTemplate( + addEvaluationTemplateFromEvaluationRequest, + evaluationTemplateData, + relevantAvailableDataSources); + } + case AddEvaluationTemplateWithDataSourcesRequest + addEvaluationTemplateWithDataSourcesRequest -> { + List<AvailableDataSource> relevantAvailableDataSources = + dataSourceValidator.getRelevantAvailableDataSources( + addEvaluationTemplateWithDataSourcesRequest.dataSources()); + dataSourceValidator.validateDataSources( + addEvaluationTemplateWithDataSourcesRequest.dataSources(), + relevantAvailableDataSources); + yield evaluationTemplateService.addEvaluationTemplate( + addEvaluationTemplateWithDataSourcesRequest, relevantAvailableDataSources); + } + }; } @GetExchange(accept = APPLICATION_JSON_VALUE) diff --git a/backend/statistics/src/main/java/de/eshg/statistics/EvaluationTemplateService.java b/backend/statistics/src/main/java/de/eshg/statistics/EvaluationTemplateService.java index 80b71c712..c9104b1ba 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/EvaluationTemplateService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/EvaluationTemplateService.java @@ -7,25 +7,18 @@ package de.eshg.statistics; import de.eshg.domain.model.BaseEntity_; import de.eshg.rest.service.error.NotFoundException; -import de.eshg.statistics.aggregation.DataSourceAggregationService; -import de.eshg.statistics.aggregation.DataSourceValidator; import de.eshg.statistics.api.AvailableDataSource; -import de.eshg.statistics.api.BaseDataSourceAttribute; -import de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateRequest; +import de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateFromEvaluationRequest; +import de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateWithDataSourcesRequest; import de.eshg.statistics.api.evaluationtemplate.EvaluationTemplateDto; +import de.eshg.statistics.datatransfer.EvaluationTemplateData; import de.eshg.statistics.mapper.EvaluationTemplateMapper; -import de.eshg.statistics.persistence.entity.evaluationtemplate.DataSource; import de.eshg.statistics.persistence.entity.evaluationtemplate.EvaluationTemplate; import de.eshg.statistics.persistence.repository.EvaluationTemplateRepository; import java.time.Clock; import java.time.Instant; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,33 +26,39 @@ import org.springframework.transaction.annotation.Transactional; @Service public class EvaluationTemplateService { private final EvaluationTemplateRepository evaluationTemplateRepository; - private final DataSourceAggregationService dataSourceAggregationService; - private final DataSourceValidator dataSourceValidator; private final Clock clock; public EvaluationTemplateService( - EvaluationTemplateRepository evaluationTemplateRepository, - DataSourceAggregationService dataSourceAggregationService, - DataSourceValidator dataSourceValidator, - Clock clock) { + EvaluationTemplateRepository evaluationTemplateRepository, Clock clock) { this.evaluationTemplateRepository = evaluationTemplateRepository; - this.dataSourceAggregationService = dataSourceAggregationService; - this.dataSourceValidator = dataSourceValidator; this.clock = clock; } @Transactional public EvaluationTemplateDto addEvaluationTemplate( - AddEvaluationTemplateRequest addEvaluationTemplateRequest) { - dataSourceValidator.validateDataSources(addEvaluationTemplateRequest.dataSources()); + AddEvaluationTemplateFromEvaluationRequest addEvaluationTemplateFromEvaluationRequest, + EvaluationTemplateData evaluationTemplateData, + List<AvailableDataSource> availableDataSources) { + EvaluationTemplate evaluationTemplate = + evaluationTemplateRepository.save( + EvaluationTemplateMapper.mapToPersistence( + addEvaluationTemplateFromEvaluationRequest.name(), + evaluationTemplateData, + availableDataSources)); + return EvaluationTemplateMapper.mapToApi(evaluationTemplate); + } + @Transactional + public EvaluationTemplateDto addEvaluationTemplate( + AddEvaluationTemplateWithDataSourcesRequest addEvaluationTemplateWithDataSourcesRequest, + List<AvailableDataSource> availableDataSources) { EvaluationTemplate evaluationTemplate = evaluationTemplateRepository.save( - EvaluationTemplateMapper.mapToPersistence(addEvaluationTemplateRequest)); - return EvaluationTemplateMapper.mapToApi( - evaluationTemplate, - getCodeToNameMappingsByDataSourceId( - getGetAvailableDataSources(Collections.singletonList(evaluationTemplate)))); + EvaluationTemplateMapper.mapToPersistence( + addEvaluationTemplateWithDataSourcesRequest.name(), + addEvaluationTemplateWithDataSourcesRequest.dataSources(), + availableDataSources)); + return EvaluationTemplateMapper.mapToApi(evaluationTemplate); } @Transactional(readOnly = true) @@ -67,23 +66,13 @@ public class EvaluationTemplateService { List<EvaluationTemplate> evaluationTemplates = evaluationTemplateRepository.findAll(Sort.by(Sort.Direction.DESC, BaseEntity_.ID)); - Map<UUID, List<AttributeCodeToNameMapping>> codeToNameMappings = - getCodeToNameMappingsByDataSourceId(getGetAvailableDataSources(evaluationTemplates)); - - return evaluationTemplates.stream() - .map( - evaluationTemplate -> - EvaluationTemplateMapper.mapToApi(evaluationTemplate, codeToNameMappings)) - .toList(); + return evaluationTemplates.stream().map(EvaluationTemplateMapper::mapToApi).toList(); } @Transactional(readOnly = true) public EvaluationTemplateDto getEvaluationTemplate(UUID templateId) { EvaluationTemplate evaluationTemplate = getEvaluationTemplateInternal(templateId); - return EvaluationTemplateMapper.mapToApi( - evaluationTemplate, - getCodeToNameMappingsByDataSourceId( - getGetAvailableDataSources(Collections.singletonList(evaluationTemplate)))); + return EvaluationTemplateMapper.mapToApi(evaluationTemplate); } @Transactional @@ -91,41 +80,6 @@ public class EvaluationTemplateService { evaluationTemplateRepository.delete(getEvaluationTemplateInternal(templateId)); } - private List<AvailableDataSource> getGetAvailableDataSources( - List<EvaluationTemplate> evaluationTemplates) { - Set<String> relevantBusinessModules = - evaluationTemplates.stream() - .flatMap(template -> template.getDataSources().stream()) - .map(DataSource::getBusinessModuleName) - .collect(Collectors.toSet()); - return dataSourceAggregationService - .getAvailableDataSources(relevantBusinessModules) - .availableDataSources(); - } - - private Map<UUID, List<AttributeCodeToNameMapping>> getCodeToNameMappingsByDataSourceId( - List<AvailableDataSource> availableDataSources) { - return availableDataSources.stream() - .collect(Collectors.toMap(AvailableDataSource::id, this::getCodeToNameMappings)); - } - - private List<AttributeCodeToNameMapping> getCodeToNameMappings( - AvailableDataSource availableDataSource) { - return availableDataSource.attributes().stream() - .map( - dataSourceAttribute -> - new AttributeCodeToNameMapping( - dataSourceAttribute.code(), - dataSourceAttribute.name(), - dataSourceAttribute.baseAttributes() == null - ? new HashMap<>() - : dataSourceAttribute.baseAttributes().stream() - .collect( - Collectors.toMap( - BaseDataSourceAttribute::code, BaseDataSourceAttribute::name)))) - .toList(); - } - private EvaluationTemplate getEvaluationTemplateInternal(UUID templateId) { return evaluationTemplateRepository .findByExternalId(templateId) diff --git a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/DataAggregationService.java b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/DataAggregationService.java index 8fccf2784..2f62e3e35 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/DataAggregationService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/DataAggregationService.java @@ -883,11 +883,11 @@ public class DataAggregationService { .collect(Collectors.joining(", "))); } - public void removeTableRows(Statistic statistic) { + public void removeTableRows(AbstractAggregationResult aggregationResult) { tableRowRepository.deleteAll( tableRowRepository .findAllByAggregationResult( - statistic, Pageable.ofSize(pageSizeForBusinessModuleDataRequest)) + aggregationResult, Pageable.ofSize(pageSizeForBusinessModuleDataRequest)) .getContent()); } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/DataSourceValidator.java b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/DataSourceValidator.java index 2baee1ef8..53af28a3f 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/DataSourceValidator.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/DataSourceValidator.java @@ -34,23 +34,34 @@ public class DataSourceValidator { this.dataSourceAggregationService = dataSourceAggregationService; } - public void validateDataSources(List<DataSourceDto> dataSources) { - checkForAttributeDuplicates(dataSources); - validateBusinessModulesExist(dataSources); + public List<AvailableDataSource> getRelevantAvailableDataSources( + List<DataSourceDto> dataSources) { + Set<String> relevantBusinessModules = getRelevantBusinessModules(dataSources); + validateBusinessModulesExist(relevantBusinessModules); + + GetAvailableDataSourcesResponse availableDataSources = + dataSourceAggregationService.getAvailableDataSources(relevantBusinessModules); - List<AvailableDataSource> relevantAvailableDataSources = - getRelevantAvailableDataSources(dataSources); + handleErrorResponses(availableDataSources); - dataSources.forEach(dataSource -> validateDataSource(dataSource, relevantAvailableDataSources)); + Set<UUID> relevantDataSources = getRelevantDataSources(dataSources); + return availableDataSources.availableDataSources().stream() + .filter(availableDataSource -> relevantDataSources.contains(availableDataSource.id())) + .toList(); } - private void validateBusinessModulesExist(List<DataSourceDto> dataSources) { - Set<String> businessModuleNames = - dataSources.stream().map(DataSourceDto::businessModuleName).collect(Collectors.toSet()); + private void validateBusinessModulesExist(Set<String> businessModuleNames) { businessModuleNames.forEach( businessModuleAggregationHelper::validateBusinessModuleIsRegistered); } + public void validateDataSources( + List<DataSourceDto> dataSources, List<AvailableDataSource> relevantAvailableDataSources) { + checkForAttributeDuplicates(dataSources); + + dataSources.forEach(dataSource -> validateDataSource(dataSource, relevantAvailableDataSources)); + } + private void checkForAttributeDuplicates(List<DataSourceDto> dataSources) { dataSources.forEach(this::checkForBusinessAttributeDuplicates); } @@ -86,20 +97,6 @@ public class DataSourceValidator { }); } - private List<AvailableDataSource> getRelevantAvailableDataSources( - List<DataSourceDto> dataSources) { - Set<String> relevantBusinessModules = getRelevantBusinessModules(dataSources); - GetAvailableDataSourcesResponse availableDataSources = - dataSourceAggregationService.getAvailableDataSources(relevantBusinessModules); - - handleErrorResponses(availableDataSources); - - Set<UUID> relevantDataSources = getRelevantDataSources(dataSources); - return availableDataSources.availableDataSources().stream() - .filter(availableDataSource -> relevantDataSources.contains(availableDataSource.id())) - .toList(); - } - private Set<String> getRelevantBusinessModules(List<DataSourceDto> dataSources) { return dataSources.stream().map(DataSourceDto::businessModuleName).collect(Collectors.toSet()); } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/InspectionSimulator.java b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/InspectionSimulator.java index 5a8c54c34..835e3f4a9 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/InspectionSimulator.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/InspectionSimulator.java @@ -70,6 +70,8 @@ public class InspectionSimulator implements StatisticsApi { private static final UUID FIRST_UUID = UUID.fromString("7efebca3-1780-4ec0-9ff6-df15afeccfbf"); private static final UUID SECOND_UUID = UUID.fromString("de31b6bd-b704-460b-a276-b4f53824a03c"); + private static final UUID FACILITY_UUID = UUID.fromString("dddddddd-bbbb-4444-aaaa-bbbbbbbbbbbb"); + @Override public GetDataSourcesResponse getAvailableDataSources() { return new GetDataSourcesResponse( @@ -84,14 +86,26 @@ public class InspectionSimulator implements StatisticsApi { @Override public GetSpecificDataResponse getSpecificData(GetSpecificDataRequest getSpecificDataRequest) { - return new GetSpecificDataResponse( - "INSPECTION", - getSpecificDataRequest.timeRangeStart(), - getSpecificDataRequest.timeRangeEnd(), - new DataTableHeader(List.of(PROCEDURE_ID_ATTRIBUTE, RESULT_ATTRIBUTE, LOCATION_ATTRIBUTE)), - List.of( - new DataRow(Arrays.asList(FIRST_UUID, "I", "Schulkantine")), - new DataRow(Arrays.asList(SECOND_UUID, "F", "Tattoostudio"))), - 2); + if (getSpecificDataRequest.dataSourceId().equals(DATA_SOURCE_UUID)) { + return new GetSpecificDataResponse( + "INSPECTION", + getSpecificDataRequest.timeRangeStart(), + getSpecificDataRequest.timeRangeEnd(), + new DataTableHeader( + List.of(PROCEDURE_ID_ATTRIBUTE, FACILITY_ATTRIBUTE, LOCATION_ATTRIBUTE)), + List.of(new DataRow(Arrays.asList(FIRST_UUID, FACILITY_UUID, "Frankfurt"))), + 1); + } else { + return new GetSpecificDataResponse( + "INSPECTION2", + getSpecificDataRequest.timeRangeStart(), + getSpecificDataRequest.timeRangeEnd(), + new DataTableHeader( + List.of(PROCEDURE_ID_ATTRIBUTE, RESULT_ATTRIBUTE, LOCATION_ATTRIBUTE)), + List.of( + new DataRow(Arrays.asList(FIRST_UUID, "I", "Schulkantine")), + new DataRow(Arrays.asList(SECOND_UUID, "F", "Tattoostudio"))), + 2); + } } } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportController.java b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportController.java index 066a83590..11499de40 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportController.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportController.java @@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.service.annotation.DeleteExchange; @@ -27,11 +28,15 @@ import org.springframework.web.service.annotation.HttpExchange; public class ReportController { private final StatisticsFeatureToggle statisticsFeatureToggle; private final ReportService reportService; + private final ReportExecution reportExecution; public ReportController( - StatisticsFeatureToggle statisticsFeatureToggle, ReportService reportService) { + StatisticsFeatureToggle statisticsFeatureToggle, + ReportService reportService, + ReportExecution reportExecution) { this.statisticsFeatureToggle = statisticsFeatureToggle; this.reportService = reportService; + this.reportExecution = reportExecution; } @GetExchange(value = "/{reportId}", accept = APPLICATION_JSON_VALUE) @@ -49,6 +54,7 @@ public class ReportController { @Operation(summary = "Delete a report") public void deleteReport(@PathVariable(name = "reportId") UUID reportId) { statisticsFeatureToggle.assertNewFeatureIsEnabled(StatisticsFeature.REPORTS); - reportService.deleteReport(reportId); + reportService.flagReportForDeletion(reportId); + CompletableFuture.runAsync(() -> reportExecution.deleteReport(reportId)); } } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportExecution.java b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportExecution.java index cd7ca7bce..225b06045 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportExecution.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportExecution.java @@ -12,6 +12,7 @@ import de.eshg.statistics.persistence.entity.AggregationResultPendingState; import de.eshg.statistics.persistence.entity.AggregationResultState; import java.util.Map; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; @@ -99,4 +100,17 @@ public class ReportExecution { moduleClientAuthenticator.doWithModuleClientAuthentication( () -> reportService.setStateToFailed(reportId)); } + + public void deleteReport(UUID reportId) { + try { + AtomicBoolean deletionFinished = new AtomicBoolean(false); + while (!deletionFinished.get()) { + moduleClientAuthenticator.doWithModuleClientAuthentication( + () -> deletionFinished.set(reportService.deleteReport(reportId))); + } + } catch (Exception e) { + log.error("Could not delete report {}", reportId, e); + setToFailed(reportId); + } + } } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportSeriesService.java b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportSeriesService.java index 4ce06ee49..d22b9113b 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportSeriesService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportSeriesService.java @@ -82,7 +82,7 @@ public class ReportSeriesService { return ReportMapper.mapToApi(reportSeries); } - private static ReportSeries createManualReportSeries( + private ReportSeries createManualReportSeries( Statistic statistic, AddManualReportSeriesRequest addManualReportSeriesRequest) { AggregationResultUtil.validateTimeRange( addManualReportSeriesRequest.timeRangeStart(), addManualReportSeriesRequest.timeRangeEnd()); @@ -100,7 +100,7 @@ public class ReportSeriesService { addManualReportSeriesRequest.timeRangeStart(), addManualReportSeriesRequest.timeRangeEnd(), AggregationResultState.CREATING, - null, + LocalDate.now(clock), statistic)); return reportSeries; diff --git a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportService.java b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportService.java index b2ffda413..8e4e80781 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/ReportService.java @@ -128,7 +128,7 @@ public class ReportService { report.getReportSeries().getReports().size(), report.getTimeRangeStart(), report.getTimeRangeEnd(), - report.getCreatedAt(), + report.getExecutionDate(), StatisticMapper.mapToApi(report.getTableColumns()), report.getNumberOfTableRows(), resolvedUsers.get(reportSeriesUserId), @@ -521,18 +521,36 @@ public class ReportService { } @Transactional - public void deleteReport(UUID reportId) { + public void flagReportForDeletion(UUID reportId) { Report report = getReportInternal(reportId); ReportSeries reportSeries = report.getReportSeries(); ReportSeriesService.validateBelongsToCurrentUserOrIsAdmin(reportSeries); if (report.getState().equals(AggregationResultState.PLANNED)) { throw new BadRequestException( "Report is in state 'PLANNED', deactivate report series to remove this report"); + } else if (report.getState().equals(AggregationResultState.DELETING)) { + throw new BadRequestException("Report is already in the process of being deleted"); } - if (reportSeries.getReportType().equals(ReportType.MANUAL)) { - reportSeriesRepository.delete(reportSeries); - } else { - reportRepository.delete(report); + + report.setState(AggregationResultState.DELETING); + } + + @Transactional + public boolean deleteReport(UUID reportId) { + Report report = getReportInternal(reportId); + + dataAggregationService.removeTableRows(report); + + if (dataAggregationService.countTableRows(report) <= 0) { + ReportSeries reportSeries = report.getReportSeries(); + if (reportSeries.getReportType().equals(ReportType.MANUAL)) { + reportSeriesRepository.delete(reportSeries); + } else { + reportRepository.delete(report); + } + return true; } + + return false; } } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/StatisticService.java b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/StatisticService.java index ee6716ab7..997467b07 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/aggregation/StatisticService.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/aggregation/StatisticService.java @@ -27,6 +27,8 @@ import de.eshg.statistics.api.AbstractUpdateStatisticRequest; import de.eshg.statistics.api.AddStatisticWithDataSourcesRequest; import de.eshg.statistics.api.AddStatisticWithTemplateRequest; import de.eshg.statistics.api.AttributeSelectionDto; +import de.eshg.statistics.api.AvailableDataSource; +import de.eshg.statistics.api.BusinessDataAttribute; import de.eshg.statistics.api.DataSourceDto; import de.eshg.statistics.api.EvaluationDto; import de.eshg.statistics.api.GetDetailPageInformationResponse; @@ -40,16 +42,23 @@ import de.eshg.statistics.api.completeness.CompletenessOfAttribute; import de.eshg.statistics.api.completeness.CompletenessOfBaseAttribute; import de.eshg.statistics.api.completeness.CompletenessOfBusinessAttribute; import de.eshg.statistics.api.completeness.GetCompletenessDataResponse; -import de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateRequest; +import de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateWithDataSourcesRequest; import de.eshg.statistics.api.evaluationtemplate.EvaluationTemplateDto; import de.eshg.statistics.api.report.GetReportSeriesEntriesOfStatisticResponse; import de.eshg.statistics.api.report.ReportSeriesDto; +import de.eshg.statistics.datatransfer.AnalysisTemplateData; +import de.eshg.statistics.datatransfer.DiagramTemplateData; +import de.eshg.statistics.datatransfer.EvaluationTemplateData; import de.eshg.statistics.mapper.EvaluationMapper; +import de.eshg.statistics.mapper.FilterParameterMapper; import de.eshg.statistics.mapper.ReportMapper; import de.eshg.statistics.mapper.StatisticMapper; import de.eshg.statistics.persistence.entity.AbstractAggregationResult; import de.eshg.statistics.persistence.entity.AggregationResultPendingState; import de.eshg.statistics.persistence.entity.AggregationResultState; +import de.eshg.statistics.persistence.entity.ChartConfiguration; +import de.eshg.statistics.persistence.entity.Diagram; +import de.eshg.statistics.persistence.entity.Evaluation; import de.eshg.statistics.persistence.entity.MinMaxNullUnknownValues; import de.eshg.statistics.persistence.entity.Statistic; import de.eshg.statistics.persistence.entity.TableColumn; @@ -59,13 +68,17 @@ import de.eshg.statistics.persistence.repository.TableRowRepository; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Instant; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.hibernate.Hibernate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -114,7 +127,9 @@ public class StatisticService { addStatisticWithTemplateRequest.templateId()); DataSourceDto dataSourceDto = StatisticMapper.mapToDataSourceCode(evaluationTemplate.dataSources().getFirst()); - dataSourceValidator.validateDataSources(List.of(dataSourceDto)); + dataSourceValidator.validateDataSources( + List.of(dataSourceDto), + dataSourceValidator.getRelevantAvailableDataSources(List.of(dataSourceDto))); yield addStatistic( dataSourceDto, addStatisticWithTemplateRequest.name(), @@ -127,13 +142,17 @@ public class StatisticService { private UUID addStatistic(AddStatisticWithDataSourcesRequest request) { UUID templateId = null; - dataSourceValidator.validateDataSources(request.dataSources()); + List<AvailableDataSource> relevantAvailableDataSources = + dataSourceValidator.getRelevantAvailableDataSources(request.dataSources()); + dataSourceValidator.validateDataSources(request.dataSources(), relevantAvailableDataSources); if (request.templateName() != null) { templateId = evaluationTemplateService .addEvaluationTemplate( - new AddEvaluationTemplateRequest(request.templateName(), request.dataSources())) + new AddEvaluationTemplateWithDataSourcesRequest( + request.templateName(), request.dataSources()), + relevantAvailableDataSources) .id(); } @@ -475,4 +494,77 @@ public class StatisticService { statistic.setPendingState(AggregationResultPendingState.DATA_AGGREGATION); } } + + @Transactional(readOnly = true) + public EvaluationTemplateData getEvaluationTemplateData(UUID statisticId) { + Statistic statistic = getStatisticInternal(statisticId); + List<DataSourceDto> dataSourceDtos = determineDataSources(statistic.getTableColumns()); + List<AnalysisTemplateData> analysisTemplateDatas = + determineAnalysisTemplateDatas(statistic.getEvaluations()); + return new EvaluationTemplateData(dataSourceDtos, analysisTemplateDatas); + } + + private List<DataSourceDto> determineDataSources(List<TableColumn> tableColumns) { + Map<String, DataSourceDto> keyToDataSourceMap = new LinkedHashMap<>(); + tableColumns.stream() + .filter(tableColumn -> !tableColumn.getValueType().equals(ValueType.CENTRAL_FILE_ID)) + .forEach( + tableColumn -> { + String key = + "%s-%s" + .formatted( + tableColumn.getDataSourceId(), tableColumn.getBusinessModuleName()); + keyToDataSourceMap.computeIfAbsent( + key, + k -> + new DataSourceDto( + tableColumn.getBusinessModuleName(), + tableColumn.getDataSourceId(), + new ArrayList<>())); + Optional<BusinessDataAttribute> businessDataAttributeOptional = + keyToDataSourceMap.get(key).attributeCodes().stream() + .filter( + attribute -> + attribute.code().equals(tableColumn.getBusinessModuleAttributeCode())) + .findFirst(); + BusinessDataAttribute attribute; + if (businessDataAttributeOptional.isEmpty()) { + attribute = + new BusinessDataAttribute( + tableColumn.getBusinessModuleAttributeCode(), new ArrayList<>()); + keyToDataSourceMap.get(key).attributeCodes().add(attribute); + } else { + attribute = businessDataAttributeOptional.get(); + } + if (tableColumn.getBaseModuleAttributeCode() != null) { + attribute.baseAttributeCodes().add(tableColumn.getBaseModuleAttributeCode()); + } + }); + return keyToDataSourceMap.keySet().stream().map(keyToDataSourceMap::get).toList(); + } + + private List<AnalysisTemplateData> determineAnalysisTemplateDatas(List<Evaluation> evaluations) { + return evaluations.stream() + .map( + evaluation -> + new AnalysisTemplateData( + evaluation.getName(), + EvaluationMapper.mapToChartConfigurationDto( + Hibernate.unproxy( + evaluation.getChartConfiguration(), ChartConfiguration.class), + true), + determineDiagramTemplateDatas(evaluation.getDiagrams()))) + .toList(); + } + + private List<DiagramTemplateData> determineDiagramTemplateDatas(List<Diagram> diagrams) { + return diagrams.stream() + .map( + diagram -> + new DiagramTemplateData( + diagram.getTitle(), + diagram.getDescription(), + FilterParameterMapper.mapToApi(diagram.getFilters()))) + .toList(); + } } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AbstractAddEvaluationTemplateRequest.java b/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AbstractAddEvaluationTemplateRequest.java new file mode 100644 index 000000000..c3b7a4cb4 --- /dev/null +++ b/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AbstractAddEvaluationTemplateRequest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.statistics.api.evaluationtemplate; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(name = "AbstractAddEvaluationTemplateRequest") +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "@type", + include = JsonTypeInfo.As.EXISTING_PROPERTY) +@JsonSubTypes({ + @JsonSubTypes.Type( + value = AddEvaluationTemplateFromEvaluationRequest.class, + name = AddEvaluationTemplateFromEvaluationRequest.SCHEMA_NAME), + @JsonSubTypes.Type( + value = AddEvaluationTemplateWithDataSourcesRequest.class, + name = AddEvaluationTemplateWithDataSourcesRequest.SCHEMA_NAME), +}) +public sealed interface AbstractAddEvaluationTemplateRequest + permits AddEvaluationTemplateFromEvaluationRequest, + AddEvaluationTemplateWithDataSourcesRequest { + @Hidden + @NotNull + @JsonProperty("@type") + String type(); + + String name(); +} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateFromEvaluationRequest.java b/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateFromEvaluationRequest.java new file mode 100644 index 000000000..b602f50d4 --- /dev/null +++ b/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateFromEvaluationRequest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.statistics.api.evaluationtemplate; + +import static de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateFromEvaluationRequest.SCHEMA_NAME; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +@Schema(name = SCHEMA_NAME) +public record AddEvaluationTemplateFromEvaluationRequest( + @NotBlank String name, @NotNull UUID evaluationId) + implements AbstractAddEvaluationTemplateRequest { + public static final String SCHEMA_NAME = "AddEvaluationTemplateFromEvaluationRequest"; + + @Override + public String type() { + return SCHEMA_NAME; + } +} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateRequest.java b/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateRequest.java deleted file mode 100644 index 4c607b9b1..000000000 --- a/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package de.eshg.statistics.api.evaluationtemplate; - -import de.eshg.statistics.api.DataSourceDto; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import java.util.List; - -public record AddEvaluationTemplateRequest( - @NotBlank String name, - @NotNull @Size(min = 1, max = 1) @Valid List<DataSourceDto> dataSources) {} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateWithDataSourcesRequest.java b/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateWithDataSourcesRequest.java new file mode 100644 index 000000000..71fa11c97 --- /dev/null +++ b/backend/statistics/src/main/java/de/eshg/statistics/api/evaluationtemplate/AddEvaluationTemplateWithDataSourcesRequest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.statistics.api.evaluationtemplate; + +import static de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateWithDataSourcesRequest.SCHEMA_NAME; + +import de.eshg.statistics.api.DataSourceDto; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; + +@Schema(name = SCHEMA_NAME) +public record AddEvaluationTemplateWithDataSourcesRequest( + @NotBlank String name, @NotNull @Size(min = 1, max = 1) @Valid List<DataSourceDto> dataSources) + implements AbstractAddEvaluationTemplateRequest { + public static final String SCHEMA_NAME = "AddEvaluationTemplateWithDataSourcesRequest"; + + @Override + public String type() { + return SCHEMA_NAME; + } +} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/api/report/GetReportDetailPageResponse.java b/backend/statistics/src/main/java/de/eshg/statistics/api/report/GetReportDetailPageResponse.java index 719ee1909..1030acf41 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/api/report/GetReportDetailPageResponse.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/api/report/GetReportDetailPageResponse.java @@ -13,6 +13,7 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.Instant; +import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -25,7 +26,7 @@ public record GetReportDetailPageResponse( @NotNull @Min(1) int numberOfReportsInSeries, @NotNull Instant timeRangeStart, @NotNull Instant timeRangeEnd, - @NotNull Instant createdAt, + @NotNull LocalDate executionDate, @NotNull @Valid List<TableColumnHeader> tableColumnHeaders, @NotNull @Min(0) long totalNumberOfElements, @Valid UserDto userReportSeries, diff --git a/backend/statistics/src/main/java/de/eshg/statistics/api/report/ReportInfoDto.java b/backend/statistics/src/main/java/de/eshg/statistics/api/report/ReportInfoDto.java index ace2b4b4c..24a606a1e 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/api/report/ReportInfoDto.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/api/report/ReportInfoDto.java @@ -20,5 +20,5 @@ public record ReportInfoDto( @NotNull Instant timeRangeStart, @NotNull Instant timeRangeEnd, @NotNull ReportStateDto state, - LocalDate executionDate, + @NotNull LocalDate executionDate, @Min(0) Long totalNumberOfElements) {} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/api/report/ReportStateDto.java b/backend/statistics/src/main/java/de/eshg/statistics/api/report/ReportStateDto.java index ea07d187e..1171ad375 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/api/report/ReportStateDto.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/api/report/ReportStateDto.java @@ -12,5 +12,6 @@ public enum ReportStateDto { PLANNED, COMPLETED, FAILED, - CREATING + CREATING, + DELETING } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/datatransfer/AnalysisTemplateData.java b/backend/statistics/src/main/java/de/eshg/statistics/datatransfer/AnalysisTemplateData.java new file mode 100644 index 000000000..0e5fcdbb5 --- /dev/null +++ b/backend/statistics/src/main/java/de/eshg/statistics/datatransfer/AnalysisTemplateData.java @@ -0,0 +1,14 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.statistics.datatransfer; + +import de.eshg.statistics.api.chart.ChartConfigurationDto; +import java.util.List; + +public record AnalysisTemplateData( + String name, + ChartConfigurationDto chartConfiguration, + List<DiagramTemplateData> diagramTemplateDatas) {} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/datatransfer/DiagramTemplateData.java b/backend/statistics/src/main/java/de/eshg/statistics/datatransfer/DiagramTemplateData.java new file mode 100644 index 000000000..a4613254c --- /dev/null +++ b/backend/statistics/src/main/java/de/eshg/statistics/datatransfer/DiagramTemplateData.java @@ -0,0 +1,12 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.statistics.datatransfer; + +import de.eshg.statistics.api.filter.TableColumnFilterParameter; +import java.util.List; + +public record DiagramTemplateData( + String title, String description, List<TableColumnFilterParameter> filters) {} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/datatransfer/EvaluationTemplateData.java b/backend/statistics/src/main/java/de/eshg/statistics/datatransfer/EvaluationTemplateData.java new file mode 100644 index 000000000..ee2a37253 --- /dev/null +++ b/backend/statistics/src/main/java/de/eshg/statistics/datatransfer/EvaluationTemplateData.java @@ -0,0 +1,12 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.statistics.datatransfer; + +import de.eshg.statistics.api.DataSourceDto; +import java.util.List; + +public record EvaluationTemplateData( + List<DataSourceDto> dataSources, List<AnalysisTemplateData> analysisTemplateDatas) {} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/export/DataExportController.java b/backend/statistics/src/main/java/de/eshg/statistics/export/DataExportController.java index d3d7c8b41..e62b7f449 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/export/DataExportController.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/export/DataExportController.java @@ -7,7 +7,7 @@ package de.eshg.statistics.export; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import de.base.rest.CustomMediaTypes; +import de.eshg.file.common.CustomMediaTypes; import de.eshg.rest.service.security.config.BaseUrls; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; diff --git a/backend/statistics/src/main/java/de/eshg/statistics/mapper/EvaluationMapper.java b/backend/statistics/src/main/java/de/eshg/statistics/mapper/EvaluationMapper.java index 65215b7ac..b8044b7f1 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/mapper/EvaluationMapper.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/mapper/EvaluationMapper.java @@ -77,6 +77,7 @@ import de.eshg.statistics.persistence.entity.diagramdata.KeyToValue; import de.eshg.statistics.persistence.entity.diagramdata.LineOrScatterChartData; import de.eshg.statistics.persistence.entity.diagramdata.PieChartData; import de.eshg.statistics.persistence.entity.diagramdata.TrendLine; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -119,6 +120,31 @@ public class EvaluationMapper { }; } + @SuppressWarnings("java:S2637") + public static ChartConfiguration mapToPersistence(ChartConfigurationDto chartConfiguration) { + return switch (chartConfiguration) { + case BarChartConfigurationDto barChartConfigurationDto -> + mapToBarChartConfiguration(barChartConfigurationDto); + case ChoroplethMapConfigurationDto choroplethMapConfigurationDto -> + mapToChoroplethMapConfiguration( + new AddChoroplethMapConfigurationDto( + choroplethMapConfigurationDto.primaryAttribute(), + choroplethMapConfigurationDto.secondaryAttribute(), + choroplethMapConfigurationDto.calculation(), + null, + choroplethMapConfigurationDto.colorScheme()), + choroplethMapConfigurationDto.geoJson()); + case HistogramChartConfigurationDto histogramChartConfigurationDto -> + mapToHistogramChartConfiguration(histogramChartConfigurationDto, Collections.emptyList()); + case LineChartConfigurationDto lineChartConfigurationDto -> + mapToLineChartConfiguration(lineChartConfigurationDto); + case PieChartConfigurationDto pieChartConfigurationDto -> + mapToPieChartConfiguration(pieChartConfigurationDto); + case ScatterChartConfigurationDto scatterChartConfigurationDto -> + mapToScatterChartConfiguration(scatterChartConfigurationDto); + }; + } + private static BarChartConfiguration mapToBarChartConfiguration( BarChartConfigurationDto barChartConfigurationDto) { BarChartConfiguration barChartConfiguration = new BarChartConfiguration(); @@ -284,7 +310,7 @@ public class EvaluationMapper { .toList()); } - private static ChartConfigurationDto mapToChartConfigurationDto( + public static ChartConfigurationDto mapToChartConfigurationDto( ChartConfiguration chartConfiguration, boolean withJson) { return switch (chartConfiguration) { case BarChartConfiguration barChartConfiguration -> diff --git a/backend/statistics/src/main/java/de/eshg/statistics/mapper/EvaluationTemplateMapper.java b/backend/statistics/src/main/java/de/eshg/statistics/mapper/EvaluationTemplateMapper.java index bf8d97a81..862e7afbd 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/mapper/EvaluationTemplateMapper.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/mapper/EvaluationTemplateMapper.java @@ -6,8 +6,10 @@ package de.eshg.statistics.mapper; import de.eshg.rest.service.error.BadRequestException; -import de.eshg.statistics.AttributeCodeToNameMapping; +import de.eshg.statistics.api.AvailableDataSource; +import de.eshg.statistics.api.BaseDataSourceAttribute; import de.eshg.statistics.api.BusinessDataAttribute; +import de.eshg.statistics.api.BusinessDataSourceAttribute; import de.eshg.statistics.api.DataSourceDto; import de.eshg.statistics.api.chart.BarChartConfigurationDto; import de.eshg.statistics.api.chart.ChoroplethMapConfigurationDto; @@ -15,12 +17,14 @@ import de.eshg.statistics.api.chart.HistogramChartConfigurationDto; import de.eshg.statistics.api.chart.LineChartConfigurationDto; import de.eshg.statistics.api.chart.PieChartConfigurationDto; import de.eshg.statistics.api.chart.ScatterChartConfigurationDto; -import de.eshg.statistics.api.evaluationtemplate.AddEvaluationTemplateRequest; import de.eshg.statistics.api.evaluationtemplate.AnalysisInfo; import de.eshg.statistics.api.evaluationtemplate.BaseDataAttributeWithName; import de.eshg.statistics.api.evaluationtemplate.BusinessDataAttributeWithName; import de.eshg.statistics.api.evaluationtemplate.DataSourceWithAttributeNames; import de.eshg.statistics.api.evaluationtemplate.EvaluationTemplateDto; +import de.eshg.statistics.datatransfer.AnalysisTemplateData; +import de.eshg.statistics.datatransfer.DiagramTemplateData; +import de.eshg.statistics.datatransfer.EvaluationTemplateData; import de.eshg.statistics.exception.InvalidDataSourceException; import de.eshg.statistics.persistence.entity.ChartConfiguration; import de.eshg.statistics.persistence.entity.chart.BarChartConfiguration; @@ -30,15 +34,12 @@ import de.eshg.statistics.persistence.entity.chart.LineChartConfiguration; import de.eshg.statistics.persistence.entity.chart.PieChartConfiguration; import de.eshg.statistics.persistence.entity.chart.ScatterChartConfiguration; import de.eshg.statistics.persistence.entity.evaluationtemplate.AnalysisTemplate; +import de.eshg.statistics.persistence.entity.evaluationtemplate.BaseDataAttribute; import de.eshg.statistics.persistence.entity.evaluationtemplate.DataAttribute; import de.eshg.statistics.persistence.entity.evaluationtemplate.DataSource; import de.eshg.statistics.persistence.entity.evaluationtemplate.DiagramTemplate; import de.eshg.statistics.persistence.entity.evaluationtemplate.EvaluationTemplate; -import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; import org.hibernate.Hibernate; public class EvaluationTemplateMapper { @@ -46,46 +47,113 @@ public class EvaluationTemplateMapper { private EvaluationTemplateMapper() {} public static EvaluationTemplate mapToPersistence( - AddEvaluationTemplateRequest addEvaluationTemplateRequest) { - EvaluationTemplate evaluationTemplate = new EvaluationTemplate(); - evaluationTemplate.setName(addEvaluationTemplateRequest.name()); - evaluationTemplate.addDataSources(mapToPersistence(addEvaluationTemplateRequest.dataSources())); + String name, + EvaluationTemplateData evaluationTemplateData, + List<AvailableDataSource> availableDataSources) { + EvaluationTemplate evaluationTemplate = + mapToPersistence(name, evaluationTemplateData.dataSources(), availableDataSources); + evaluationTemplate.addAnalysisTemplates( + evaluationTemplateData.analysisTemplateDatas().stream() + .map(EvaluationTemplateMapper::mapToAnalysisTemplate) + .toList()); return evaluationTemplate; } - private static List<DataSource> mapToPersistence(List<DataSourceDto> dataSourceDtos) { - return dataSourceDtos.stream().map(EvaluationTemplateMapper::mapToPersistence).toList(); + private static AnalysisTemplate mapToAnalysisTemplate(AnalysisTemplateData analysisTemplateData) { + AnalysisTemplate analysisTemplate = new AnalysisTemplate(); + analysisTemplate.setName(analysisTemplateData.name()); + analysisTemplate.setChartConfiguration( + EvaluationMapper.mapToPersistence(analysisTemplateData.chartConfiguration())); + analysisTemplate.addDiagramTemplates( + analysisTemplateData.diagramTemplateDatas().stream() + .map(EvaluationTemplateMapper::mapToDiagramTemplate) + .toList()); + return analysisTemplate; + } + + private static DiagramTemplate mapToDiagramTemplate(DiagramTemplateData diagramTemplateData) { + DiagramTemplate diagramTemplate = new DiagramTemplate(); + diagramTemplate.setTitle(diagramTemplateData.title()); + diagramTemplate.setDescription(diagramTemplateData.description()); + diagramTemplate.addFilters( + diagramTemplateData.filters().stream() + .map(FilterParameterMapper::mapToPersistence) + .toList()); + return diagramTemplate; + } + + public static EvaluationTemplate mapToPersistence( + String name, + List<DataSourceDto> dataSourceDtos, + List<AvailableDataSource> availableDataSources) { + EvaluationTemplate evaluationTemplate = new EvaluationTemplate(); + evaluationTemplate.setName(name); + evaluationTemplate.addDataSources( + dataSourceDtos.stream() + .map(dataSourceDto -> mapToPersistence(dataSourceDto, availableDataSources)) + .toList()); + return evaluationTemplate; } - private static DataSource mapToPersistence(DataSourceDto dataSourceDto) { + private static DataSource mapToPersistence( + DataSourceDto dataSourceDto, List<AvailableDataSource> availableDataSources) { + AvailableDataSource availableDataSource = + availableDataSources.stream() + .filter( + source -> + source.id().equals(dataSourceDto.id()) + && source.businessModule().equals(dataSourceDto.businessModuleName())) + .findFirst() + .orElseThrow(InvalidDataSourceException::new); + DataSource dataSource = new DataSource(); dataSource.setBusinessModuleName(dataSourceDto.businessModuleName()); dataSource.setExternalDataSourceId(dataSourceDto.id()); + dataSource.setDataSourceName(availableDataSource.name()); dataSource.addAttributes( dataSourceDto.attributeCodes().stream() - .map(EvaluationTemplateMapper::mapToPersistence) + .map( + businessDataAttribute -> + mapToPersistence(businessDataAttribute, availableDataSource)) .toList()); return dataSource; } - private static DataAttribute mapToPersistence(BusinessDataAttribute businessDataAttribute) { + private static DataAttribute mapToPersistence( + BusinessDataAttribute businessDataAttribute, AvailableDataSource availableDataSource) { + BusinessDataSourceAttribute businessDataSourceAttribute = + availableDataSource.attributes().stream() + .filter(attribute -> attribute.code().equals(businessDataAttribute.code())) + .findFirst() + .orElseThrow(InvalidDataSourceException::new); + DataAttribute dataAttribute = new DataAttribute(); dataAttribute.setCode(businessDataAttribute.code()); - dataAttribute.addBaseAttributeCodes(businessDataAttribute.baseAttributeCodes()); + dataAttribute.setName(businessDataSourceAttribute.name()); + dataAttribute.addBaseAttributes( + businessDataAttribute.baseAttributeCodes().stream() + .map(code -> mapToBaseDataAttribute(code, businessDataSourceAttribute)) + .toList()); return dataAttribute; } - public static EvaluationTemplateDto mapToApi( - EvaluationTemplate evaluationTemplate, - Map<UUID, List<AttributeCodeToNameMapping>> codeToNameMappingsByDataSourceId) { - List<DataSourceWithAttributeNames> dataSources; - try { - dataSources = - mapToDataSourceDtos( - evaluationTemplate.getDataSources(), codeToNameMappingsByDataSourceId); - } catch (InvalidDataSourceException e) { - dataSources = new ArrayList<>(); - } + private static BaseDataAttribute mapToBaseDataAttribute( + String code, BusinessDataSourceAttribute businessDataSourceAttribute) { + BaseDataSourceAttribute baseDataSourceAttribute = + businessDataSourceAttribute.baseAttributes().stream() + .filter(attribute -> attribute.code().equals(code)) + .findFirst() + .orElseThrow(InvalidDataSourceException::new); + + BaseDataAttribute baseDataAttribute = new BaseDataAttribute(); + baseDataAttribute.setCode(code); + baseDataAttribute.setName(baseDataSourceAttribute.name()); + return baseDataAttribute; + } + + public static EvaluationTemplateDto mapToApi(EvaluationTemplate evaluationTemplate) { + List<DataSourceWithAttributeNames> dataSources = + mapToDataSourceDtos(evaluationTemplate.getDataSources()); return new EvaluationTemplateDto( evaluationTemplate.getExternalId(), @@ -97,64 +165,33 @@ public class EvaluationTemplateMapper { } private static List<DataSourceWithAttributeNames> mapToDataSourceDtos( - List<DataSource> dataSources, - Map<UUID, List<AttributeCodeToNameMapping>> codeToNameMappingsByDataSourceId) { - return dataSources.stream() - .map( - dataSource -> - EvaluationTemplateMapper.mapToDataSourceDto( - dataSource, - codeToNameMappingsByDataSourceId.get(dataSource.getExternalDataSourceId()))) - .toList(); + List<DataSource> dataSources) { + return dataSources.stream().map(EvaluationTemplateMapper::mapToDataSourceDto).toList(); } - private static DataSourceWithAttributeNames mapToDataSourceDto( - DataSource dataSource, List<AttributeCodeToNameMapping> attributeCodeToNameMappings) { - if (attributeCodeToNameMappings == null) { - throw new InvalidDataSourceException(); - } + private static DataSourceWithAttributeNames mapToDataSourceDto(DataSource dataSource) { return new DataSourceWithAttributeNames( dataSource.getBusinessModuleName(), dataSource.getExternalDataSourceId(), dataSource.getAttributes().stream() - .map( - attribute -> - EvaluationTemplateMapper.mapToBusinessDataAttribute( - attribute, attributeCodeToNameMappings)) + .map(EvaluationTemplateMapper::mapToBusinessDataAttribute) .toList()); } private static BusinessDataAttributeWithName mapToBusinessDataAttribute( - DataAttribute dataAttribute, List<AttributeCodeToNameMapping> attributeCodeToNameMappings) { - String businessAttributeCode = dataAttribute.getCode(); - - return Optional.ofNullable(attributeCodeToNameMappings) - .flatMap( - mappings -> - mappings.stream() - .filter( - mapping -> mapping.businessAttributeCode().equals(businessAttributeCode)) - .findFirst()) - .map( - attributeCodeToNameMapping -> - new BusinessDataAttributeWithName( - attributeCodeToNameMapping.businessAttributeCode(), - attributeCodeToNameMapping.businessAttributeName(), - dataAttribute.getBaseAttributeCodes().stream() - .map( - baseAttributeCode -> - EvaluationTemplateMapper.mapToBaseDataAttribute( - baseAttributeCode, attributeCodeToNameMapping.baseAttributes())) - .toList())) - .orElseThrow(InvalidDataSourceException::new); + DataAttribute dataAttribute) { + return new BusinessDataAttributeWithName( + dataAttribute.getCode(), + dataAttribute.getName(), + dataAttribute.getBaseAttributes().stream() + .map(EvaluationTemplateMapper::mapToBaseDataAttribute) + .toList()); } private static BaseDataAttributeWithName mapToBaseDataAttribute( - String baseAttributeCode, Map<String, String> baseAttributeCodeToNameMap) { - return Optional.ofNullable(baseAttributeCodeToNameMap.get(baseAttributeCode)) - .map(name -> new BaseDataAttributeWithName(baseAttributeCode, name)) - .orElseThrow(InvalidDataSourceException::new); + BaseDataAttribute baseDataAttribute) { + return new BaseDataAttributeWithName(baseDataAttribute.getCode(), baseDataAttribute.getName()); } private static List<AnalysisInfo> mapToAnalysisInfos(List<AnalysisTemplate> analysisTemplates) { diff --git a/backend/statistics/src/main/java/de/eshg/statistics/mapper/ReportMapper.java b/backend/statistics/src/main/java/de/eshg/statistics/mapper/ReportMapper.java index bf9d758a1..5ab1271d0 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/mapper/ReportMapper.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/mapper/ReportMapper.java @@ -44,10 +44,6 @@ public class ReportMapper { reportStream.map(ReportMapper::mapToReportInfoDto).toList()); } - private static ReportStateDto mapToReportStateDto(AggregationResultState state) { - return ReportStateDto.valueOf(state.name()); - } - public static ReportTypeDto mapToReportTypeDto(ReportType reportType) { return ReportTypeDto.valueOf(reportType.name()); } @@ -75,6 +71,10 @@ public class ReportMapper { : null); } + private static ReportStateDto mapToReportStateDto(AggregationResultState state) { + return ReportStateDto.valueOf(state.name()); + } + public static ReportType mapToReportType(ReportTypeDto reportType) { return ReportType.valueOf(reportType.name()); } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/AggregationResultState.java b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/AggregationResultState.java index 581e79c39..c7153633f 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/AggregationResultState.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/AggregationResultState.java @@ -11,5 +11,6 @@ public enum AggregationResultState { FAILED, CREATING, UPDATING, - COPY_ONGOING + COPY_ONGOING, + DELETING } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/BaseDataAttribute.java b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/BaseDataAttribute.java new file mode 100644 index 000000000..6e2ebabe8 --- /dev/null +++ b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/BaseDataAttribute.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.statistics.persistence.entity.evaluationtemplate; + +import static de.eshg.lib.common.SensitivityLevel.PUBLIC; + +import de.eshg.domain.model.BaseEntity; +import de.eshg.lib.common.DataSensitivity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@DataSensitivity(PUBLIC) +@Table(indexes = @Index(columnList = "data_attribute_id")) +public class BaseDataAttribute extends BaseEntity { + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "data_attribute_id") + private DataAttribute dataAttribute; + + @Column(nullable = false) + private String code; + + @Column(nullable = false) + private String name; + + public void setDataAttribute(DataAttribute dataAttribute) { + this.dataAttribute = dataAttribute; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/DataAttribute.java b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/DataAttribute.java index 708a4cf75..cac9ce768 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/DataAttribute.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/DataAttribute.java @@ -9,15 +9,14 @@ import static de.eshg.lib.common.SensitivityLevel.PUBLIC; import de.eshg.domain.model.BaseEntity; import de.eshg.lib.common.DataSensitivity; -import jakarta.persistence.CollectionTable; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; -import jakarta.persistence.ForeignKey; import jakarta.persistence.Index; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.OrderColumn; import jakarta.persistence.Table; import java.util.ArrayList; @@ -34,14 +33,16 @@ public class DataAttribute extends BaseEntity { @Column(nullable = false) private String code; - @ElementCollection - @CollectionTable( - name = "attribute_to_base_attributes", - joinColumns = @JoinColumn(name = "id"), - foreignKey = @ForeignKey(name = "fk_attribute_to_base_attributes")) + @Column(nullable = false) + private String name; + + @OneToMany( + cascade = CascadeType.PERSIST, + fetch = FetchType.LAZY, + mappedBy = BaseDataAttribute_.DATA_ATTRIBUTE, + orphanRemoval = true) @OrderColumn - @Column(name = "base_attribute_code", nullable = false) - private List<String> baseAttributeCodes = new ArrayList<>(); + private final List<BaseDataAttribute> baseAttributes = new ArrayList<>(); void setDataSource(DataSource dataSource) { this.dataSource = dataSource; @@ -55,11 +56,20 @@ public class DataAttribute extends BaseEntity { this.code = code; } - public List<String> getBaseAttributeCodes() { - return baseAttributeCodes; + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List<BaseDataAttribute> getBaseAttributes() { + return baseAttributes; } - public void addBaseAttributeCodes(List<String> baseAttributeCodes) { - this.baseAttributeCodes.addAll(baseAttributeCodes); + public void addBaseAttributes(List<BaseDataAttribute> baseAttributes) { + baseAttributes.forEach(attribute -> attribute.setDataAttribute(this)); + this.baseAttributes.addAll(baseAttributes); } } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/DataSource.java b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/DataSource.java index 696fa7de6..fdbe07b64 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/DataSource.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/DataSource.java @@ -37,6 +37,9 @@ public class DataSource extends BaseEntity { @Column(nullable = false) private UUID externalDataSourceId; + @Column(nullable = false) + private String dataSourceName; + @OneToMany( cascade = CascadeType.PERSIST, fetch = FetchType.LAZY, @@ -65,6 +68,14 @@ public class DataSource extends BaseEntity { this.externalDataSourceId = externalDataSourceId; } + public String getDataSourceName() { + return dataSourceName; + } + + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + public List<DataAttribute> getAttributes() { return attributes; } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/EvaluationTemplate.java b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/EvaluationTemplate.java index 49835f3dc..e1ba19c60 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/EvaluationTemplate.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/evaluationtemplate/EvaluationTemplate.java @@ -21,6 +21,8 @@ import jakarta.persistence.OrderColumn; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.UUID; +import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -33,6 +35,11 @@ public class EvaluationTemplate extends BaseEntityWithExternalId { @Column(nullable = false) private Instant createdAt; + @DataSensitivity(PROTECTED) + @CreatedBy + @Column(nullable = false) + private UUID createdByUserId; + @DataSensitivity(PUBLIC) @Column(nullable = false) private String name; @@ -63,6 +70,10 @@ public class EvaluationTemplate extends BaseEntityWithExternalId { return createdAt; } + public UUID getCreatedByUserId() { + return createdByUserId; + } + public String getName() { return name; } diff --git a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/report/Report.java b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/report/Report.java index 4f3ab9803..9d50bb6ef 100644 --- a/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/report/Report.java +++ b/backend/statistics/src/main/java/de/eshg/statistics/persistence/entity/report/Report.java @@ -30,7 +30,7 @@ public class Report extends AbstractAggregationResult { private ReportSeries reportSeries; @DataSensitivity(PUBLIC) - @Column + @Column(nullable = false) private LocalDate executionDate; void setReportSeries(ReportSeries reportSeries) { diff --git a/backend/statistics/src/main/resources/migrations/0028_add_deleting_aggregation_result_state.xml b/backend/statistics/src/main/resources/migrations/0028_add_deleting_aggregation_result_state.xml new file mode 100644 index 000000000..3a24067d8 --- /dev/null +++ b/backend/statistics/src/main/resources/migrations/0028_add_deleting_aggregation_result_state.xml @@ -0,0 +1,14 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1728554210283-1"> + <ext:addPostgresEnumValues enumTypeName="aggregationresultstate" valuesToAdd="DELETING"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/statistics/src/main/resources/migrations/0029_execution_date_mandatory.xml b/backend/statistics/src/main/resources/migrations/0029_execution_date_mandatory.xml new file mode 100644 index 000000000..e8cd3c643 --- /dev/null +++ b/backend/statistics/src/main/resources/migrations/0029_execution_date_mandatory.xml @@ -0,0 +1,19 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1729062568681-1"> + <sql> + UPDATE report r + SET execution_date = DATE(aar.created_at) + FROM abstract_aggregation_result aar + WHERE r.id = aar.id + AND r.execution_date IS NULL; + </sql> + + <addNotNullConstraint columnDataType="date" columnName="execution_date" tableName="report" validate="true"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/statistics/src/main/resources/migrations/0030_add_attribute_names_to_evaluation_templates.xml b/backend/statistics/src/main/resources/migrations/0030_add_attribute_names_to_evaluation_templates.xml new file mode 100644 index 000000000..1b71d82f1 --- /dev/null +++ b/backend/statistics/src/main/resources/migrations/0030_add_attribute_names_to_evaluation_templates.xml @@ -0,0 +1,56 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1728562253921-6"> + <delete tableName="attribute_to_base_attributes" /> + <delete tableName="data_attribute" /> + <delete tableName="data_source" /> + <delete tableName="evaluation_template" /> + + <dropForeignKeyConstraint baseTableName="attribute_to_base_attributes" constraintName="fk_attribute_to_base_attributes_data_attribute"/> + <dropTable tableName="attribute_to_base_attributes"/> + + <addColumn tableName="evaluation_template"> + <column name="created_by_user_id" type="uuid"> + <constraints nullable="false"/> + </column> + </addColumn> + <addColumn tableName="data_source"> + <column name="data_source_name" type="text"> + <constraints nullable="false"/> + </column> + </addColumn> + <addColumn tableName="data_attribute"> + <column name="name" type="text"> + <constraints nullable="false"/> + </column> + </addColumn> + + <createTable tableName="base_data_attribute"> + <column autoIncrement="true" name="id" type="BIGINT"> + <constraints nullable="false" primaryKey="true" primaryKeyName="pk_base_data_attribute"/> + </column> + <column name="base_attributes_order" type="INTEGER"/> + <column name="data_attribute_id" type="BIGINT"> + <constraints nullable="false"/> + </column> + <column name="version" type="BIGINT"> + <constraints nullable="false"/> + </column> + <column name="code" type="TEXT"> + <constraints nullable="false"/> + </column> + <column name="name" type="TEXT"> + <constraints nullable="false"/> + </column> + </createTable> + <addForeignKeyConstraint baseColumnNames="data_attribute_id" baseTableName="base_data_attribute" constraintName="fk_base_data_attribute_data_attribute" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="data_attribute" validate="true"/> + <createIndex indexName="idx_base_data_attribute_data_attribute_id" tableName="base_data_attribute"> + <column name="data_attribute_id"/> + </createIndex> + </changeSet> +</databaseChangeLog> diff --git a/backend/statistics/src/main/resources/migrations/changelog.xml b/backend/statistics/src/main/resources/migrations/changelog.xml index fd524c3db..1f150d4f7 100644 --- a/backend/statistics/src/main/resources/migrations/changelog.xml +++ b/backend/statistics/src/main/resources/migrations/changelog.xml @@ -35,5 +35,8 @@ <include file="migrations/0025_add_table_rows_removal_state.xml"/> <include file="migrations/0026_enhance_evaluation_templates.xml"/> <include file="migrations/0027_refactor_aggregation_result_states.xml"/> + <include file="migrations/0028_add_deleting_aggregation_result_state.xml"/> + <include file="migrations/0029_execution_date_mandatory.xml"/> + <include file="migrations/0030_add_attribute_names_to_evaluation_templates.xml"/> </databaseChangeLog> diff --git a/backend/sti-protection/build.gradle b/backend/sti-protection/build.gradle index b60378d0f..b58e0d6c3 100644 --- a/backend/sti-protection/build.gradle +++ b/backend/sti-protection/build.gradle @@ -8,7 +8,7 @@ dependencies { implementation project(':lib-appointmentblock') implementation project(':lib-calendar') implementation project(':business-module-persistence-commons') - + implementation project(":lib-document-generator") implementation 'org.springdoc:springdoc-openapi-starter-common:latest.release' annotationProcessor 'org.hibernate.orm:hibernate-jpamodelgen' @@ -16,6 +16,7 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' testImplementation testFixtures(project(':business-module-persistence-commons')) + testImplementation testFixtures(project(':lib-document-generator')) } dockerCompose { diff --git a/backend/sti-protection/gradle.lockfile b/backend/sti-protection/gradle.lockfile index 055b23719..70fb75a2d 100644 --- a/backend/sti-protection/gradle.lockfile +++ b/backend/sti-protection/gradle.lockfile @@ -16,11 +16,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -28,7 +28,7 @@ com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=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 @@ -37,23 +37,29 @@ com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath, com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -commons-logging:commons-logging:1.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.16.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-logging:commons-logging:1.3.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.rototor.pdfbox:graphics2d:3.0.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +io.github.openhtmltopdf:openhtmltopdf-core:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +io.github.openhtmltopdf:openhtmltopdf-pdfbox:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +io.github.openhtmltopdf:openhtmltopdf-slf4j:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +io.github.openhtmltopdf:openhtmltopdf-svg-support:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-jakarta9:1.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -67,6 +73,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -77,19 +84,19 @@ jakarta.transaction:jakarta.transaction-api:2.0.1=annotationProcessor,compileCla jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.14.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.26.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.12.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -97,11 +104,12 @@ org.apache.httpcomponents.core5:httpcore5-h2:5.2.5=productionRuntimeClasspath,ru org.apache.httpcomponents.core5:httpcore5:5.2.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.23.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.23.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.pdfbox:fontbox:2.0.31=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.pdfbox:fontbox:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:jempbox:1.8.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.pdfbox:pdfbox-io:3.0.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.pdfbox:pdfbox-tools:2.0.31=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.apache.pdfbox:pdfbox:2.0.31=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.apache.pdfbox:xmpbox:2.0.31=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.tika:tika-bom:2.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-core:2.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.apache.tika:tika-parser-pdf-module:2.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -110,9 +118,28 @@ org.apache.tomcat.embed:tomcat-embed-core:10.1.30=compileClasspath,productionRun org.apache.tomcat.embed:tomcat-embed-el:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:10.1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.tomcat:tomcat-annotations-api:10.1.30=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 +org.apache.xmlgraphics:batik-codec:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-constants:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-css:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-dom:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-ext:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-gvt:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-i18n:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-parser:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-script:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-shared-resources:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-svg-dom:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-svggen:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-transcoder:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-util:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:batik-xml:1.17=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.apache.xmlgraphics:xmlgraphics-commons:2.9=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcmail-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -121,11 +148,12 @@ org.bouncycastle:bcutil-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspa org.checkerframework:checker-qual:3.43.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.eclipse.angus:angus-activation:2.0.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.eclipse.angus:jakarta.mail:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.freemarker:freemarker:2.3.33=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-core:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -137,15 +165,15 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath @@ -153,12 +181,12 @@ org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspa org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -168,7 +196,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -184,7 +212,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -207,14 +235,14 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.testcontainers:database-commons:1.19.8=testRuntimeClasspath org.testcontainers:jdbc:1.19.8=testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -235,4 +263,6 @@ org.zalando:logbook-servlet:3.9.0=productionRuntimeClasspath,runtimeClasspath,te org.zalando:logbook-spring-boot-autoconfigure:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.zalando:logbook-spring-boot-starter:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.zalando:logbook-spring:3.9.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +xml-apis:xml-apis-ext:1.3.04=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +xml-apis:xml-apis:1.4.01=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath empty=developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath diff --git a/backend/sti-protection/openApi.yaml b/backend/sti-protection/openApi.yaml index b2760e445..aee2b6351 100644 --- a/backend/sti-protection/openApi.yaml +++ b/backend/sti-protection/openApi.yaml @@ -3907,6 +3907,7 @@ components: enum: - PATIENT - PARENT + - PROFESSIONAL Population: type: object properties: @@ -4054,6 +4055,9 @@ components: - TM_VACCINATION_CONSULTATION - MEASLES_PROTECTION - STI_PROTECTION + - MEDICAL_REGISTRY_ENTRY + - MEDICAL_REGISTRY_CITIZEN_DRAFT + - MEDICAL_REGISTRY_EMPLOYEE_DRAFT ProcedureWithDuration: type: object properties: @@ -4259,6 +4263,8 @@ components: $ref: "#/components/schemas/Appointment" concern: $ref: "#/components/schemas/Concern" + countryOfBirth: + $ref: "#/components/schemas/CountryCode" createdAt: type: string format: date-time 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 c938353f4..dbceba09f 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 @@ -7,6 +7,7 @@ package de.eshg.stiprotection; import de.eshg.api.commons.InlineParameterObject; import de.eshg.rest.service.security.config.BaseUrls; +import de.eshg.stiprotection.annotations.ProcedureStatusTransition; import de.eshg.stiprotection.api.CreateProcedureRequest; import de.eshg.stiprotection.api.CreateProcedureResponse; import de.eshg.stiprotection.api.GetStiProtectionProceduresPaginationOptions; @@ -108,6 +109,7 @@ public class StiProtectionProcedureController { @PutMapping("/{id}/close") @Operation(summary = "Close an STI procedure.") @Transactional + @ProcedureStatusTransition public StiProtectionProcedureDto closeProcedure(@PathVariable("id") UUID procedureId) { stiProtectionService.closeProcedure(procedureId); return StiProtectionProcedureMapper.toInterfaceType( @@ -117,6 +119,7 @@ public class StiProtectionProcedureController { @PutMapping("/{id}/reopen") @Operation(summary = "Re-open an STI procedure.") @Transactional + @ProcedureStatusTransition public StiProtectionProcedureDto reopenProcedure(@PathVariable("id") UUID procedureId) { stiProtectionService.reopenProcedure(procedureId); return StiProtectionProcedureMapper.toInterfaceType( 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 9eb330f6f..5bc68b970 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 @@ -201,7 +201,11 @@ public class StiProtectionProcedureService { } public MedicalHistory getMedicalHistory(UUID procedureId) { - return findProcedureByExternalId(procedureId).getMedicalHistory(); + MedicalHistory medicalHistory = findProcedureByExternalId(procedureId).getMedicalHistory(); + if (medicalHistory == null) { + throw new NotFoundException(procedureId + ": no medical history found"); + } + return medicalHistory; } private void bookAppointment(StiProtectionProcedure procedure, CreateProcedureRequest request) { diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/annotations/ProcedureStatusTransition.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/annotations/ProcedureStatusTransition.java new file mode 100644 index 000000000..29aa41918 --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/annotations/ProcedureStatusTransition.java @@ -0,0 +1,15 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ProcedureStatusTransition {} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/CreateProcedureRequest.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/CreateProcedureRequest.java index 1563e09dc..bc59e952b 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/CreateProcedureRequest.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/CreateProcedureRequest.java @@ -6,8 +6,8 @@ package de.eshg.stiprotection.api; import com.fasterxml.jackson.annotation.JsonIgnore; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; @@ -21,7 +21,7 @@ public record CreateProcedureRequest( @NotNull ConcernDto concern, @NotNull GenderDto gender, @NotNull @Past @Schema(type = "integer") Year yearOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, @Schema( type = "integer", description = "The year since the person has been residing in Germany.", diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/PersonDto.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/PersonDto.java index e22a09376..584cefb34 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/PersonDto.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/PersonDto.java @@ -5,8 +5,8 @@ package de.eshg.stiprotection.api; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import java.time.Year; @@ -17,5 +17,5 @@ public record PersonDto( @NotNull UUID id, @NotNull GenderDto gender, @Schema(type = "integer") @NotNull Year yearOfBirth, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, @Schema(type = "integer") Year inGermanySince) {} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/StiProtectionProcedureOverviewDto.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/StiProtectionProcedureOverviewDto.java index faba04777..ca801a26c 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/StiProtectionProcedureOverviewDto.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/api/StiProtectionProcedureOverviewDto.java @@ -7,6 +7,7 @@ package de.eshg.stiprotection.api; import de.eshg.base.GenderDto; import de.eshg.lib.appointmentblock.api.AppointmentDto; +import de.eshg.lib.common.CountryCode; import de.eshg.lib.procedure.model.ProcedureStatusDto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; @@ -22,5 +23,6 @@ public record StiProtectionProcedureOverviewDto( @NotNull ProcedureStatusDto status, @NotNull ConcernDto concern, @Schema(type = "integer") @NotNull Year yearOfBirth, + CountryCode countryOfBirth, @NotNull GenderDto gender, @NotNull @Valid AppointmentDto appointment) {} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/aspect/ProtectedProcedureAspect.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/aspect/ProtectedProcedureAspect.java new file mode 100644 index 000000000..3e0da0761 --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/aspect/ProtectedProcedureAspect.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.aspect; + +import de.eshg.lib.procedure.domain.model.ProcedureStatus; +import de.eshg.rest.service.error.BadRequestException; +import de.eshg.stiprotection.persistence.db.StiProtectionProcedure; +import de.eshg.stiprotection.persistence.db.StiProtectionProcedureRepository; +import java.util.Optional; +import java.util.UUID; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +@Aspect +@Configuration(proxyBeanMethods = false) +@EnableAspectJAutoProxy +public class ProtectedProcedureAspect { + + private static final Logger log = LoggerFactory.getLogger(ProtectedProcedureAspect.class); + + private final StiProtectionProcedureRepository procedures; + + public ProtectedProcedureAspect(StiProtectionProcedureRepository procedures) { + this.procedures = procedures; + } + + @Pointcut("args(externalId, ..)") + public void hasExternalId(UUID externalId) {} + + @Pointcut("@annotation(de.eshg.stiprotection.annotations.ProcedureStatusTransition)") + public void statusTransition() {} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") + public void post() {} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)") + public void put() {} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PatchMapping)") + public void patch() {} + + @Pointcut("post() || put() || patch()") + public void write() {} + + @Pointcut("within(de.eshg.stiprotection..*)") + public void withinSti() {} + + @Before( + value = "withinSti() && !statusTransition() && hasExternalId(externalId) && write()", + argNames = "joinPoint,externalId") + public void denyModificationIfClosed(JoinPoint joinPoint, UUID externalId) { + log.trace("externalId = {}, joinPoint = {}", externalId, joinPoint); + Optional<StiProtectionProcedure> procedure = procedures.findByExternalId(externalId); + if (procedure.isEmpty()) { + return; + } + if (ProcedureStatus.isClosed(procedure.get().getProcedureStatus())) { + throw new BadRequestException(externalId + ": Access denied: procedure closed."); + } + } +} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/config/DateTimeConstants.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/config/DateTimeConstants.java new file mode 100644 index 000000000..fefb9f34b --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/config/DateTimeConstants.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.config; + +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +public final class DateTimeConstants { + private DateTimeConstants() {} + + public static final DateTimeFormatter DATE_FORMAT_DE = + DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY); + + public static final DateTimeFormatter TIME_FORMAT_DE = DateTimeFormatter.ofPattern("HH:mm"); +} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/mapper/StiProtectionProcedureMapper.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/mapper/StiProtectionProcedureMapper.java index 983b21dcf..6fe53fdf9 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/mapper/StiProtectionProcedureMapper.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/mapper/StiProtectionProcedureMapper.java @@ -40,6 +40,7 @@ public class StiProtectionProcedureMapper { ProcedureMapper.toInterfaceType(procedureData.status()), ConcernMapper.toInterfaceType(procedureData.concern()), procedureData.person().getYearOfBirth(), + procedureData.person().getCountryOfBirth(), GenderMapper.toInterfaceType(procedureData.person().getGender()), AppointmentMapper.toInterfaceType( procedureData.appointment(), procedureData.userDefinedAppointment())); diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/AnonymousIdentificationDocumentData.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/AnonymousIdentificationDocumentData.java new file mode 100644 index 000000000..6040b415e --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/AnonymousIdentificationDocumentData.java @@ -0,0 +1,8 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.pdf.identification; + +public record AnonymousIdentificationDocumentData(DocumentSender sender, Appointment appointment) {} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/AnonymousIdentificationDocumentService.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/AnonymousIdentificationDocumentService.java new file mode 100644 index 000000000..ce4fbf988 --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/AnonymousIdentificationDocumentService.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.pdf.identification; + +import de.eshg.lib.document.generator.DocumentGenerator; +import de.eshg.lib.procedure.domain.model.Pdf; +import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; +import de.eshg.lib.procedure.file.FileFactory; +import java.io.ByteArrayOutputStream; +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +@Service +public class AnonymousIdentificationDocumentService { + public static final String IDENTIFICATION_TEMPLATES_ROOT = "/templates/identification/"; + + private final DocumentGenerator reportBuilder; + private final Clock clock; + + private static final DateTimeFormatter FILENAME_TIMESTAMP_SUFFIX = + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss", Locale.GERMANY); + + public AnonymousIdentificationDocumentService(DocumentGenerator reportBuilder, Clock clock) { + this.reportBuilder = reportBuilder; + this.clock = clock; + } + + public Pdf createPdf(AnonymousIdentificationDocumentData data) { + byte[] bytes = createPdfFromTemplate(data); + String fileName = fileName(); + PdfMetaData pdfMetaData = pdfMetaData(); + return FileFactory.createPdfWithMetaData( + fileName, ProcedureFileType.PDF, bytes, pdfMetaData, false); + } + + private String fileName() { + return "Anonyme_Beratung_%s.pdf" + .formatted(ZonedDateTime.now(clock).format(FILENAME_TIMESTAMP_SUFFIX)); + } + + private byte[] createPdfFromTemplate(AnonymousIdentificationDocumentData data) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + String template = IDENTIFICATION_TEMPLATES_ROOT + "anon_indent.ftlx"; + reportBuilder.createPdfFromTemplate(new ClassPathResource(template), data, baos); + return baos.toByteArray(); + } + + private PdfMetaData pdfMetaData() { + PdfMetaData pdfMetaData = new PdfMetaData(); + pdfMetaData.setCreatedDate(clock.instant()); + pdfMetaData.setDescription("Anonyme Beratung"); + return pdfMetaData; + } +} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/Appointment.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/Appointment.java new file mode 100644 index 000000000..401b1ca58 --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/Appointment.java @@ -0,0 +1,15 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.pdf.identification; + +public record Appointment( + Department department, + String date, + String time, + String durationMinutes, + String room, + String url, + String ticketNumber) {} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/Department.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/Department.java new file mode 100644 index 000000000..806924dbc --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/Department.java @@ -0,0 +1,17 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.pdf.identification; + +public record Department( + String name, + String abbreviation, + String street, + String houseNumber, + String postalCode, + String city, + String phoneNumber, + String homepage, + String email) {} diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/DocumentSender.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/DocumentSender.java new file mode 100644 index 000000000..c4c6f5d58 --- /dev/null +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/pdf/identification/DocumentSender.java @@ -0,0 +1,8 @@ +/* + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package de.eshg.stiprotection.pdf.identification; + +public record DocumentSender(Department department, String documentDate, String referenceNumber) {} 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 c0e557bd3..f8c943711 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 @@ -5,7 +5,7 @@ package de.eshg.stiprotection.persistence.db; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; import de.eshg.lib.procedure.domain.model.RelatedPerson; @@ -32,7 +32,7 @@ public class Person extends RelatedPerson<StiProtectionProcedure> { @JdbcType(PostgreSQLEnumJdbcType.class) @DataSensitivity(SensitivityLevel.UNDEFINED) - private CountryCodeDto countryOfBirth; + private CountryCode countryOfBirth; @DataSensitivity(SensitivityLevel.UNDEFINED) private Year inGermanySince; @@ -53,11 +53,11 @@ public class Person extends RelatedPerson<StiProtectionProcedure> { this.yearOfBirth = yearOfBirth; } - public CountryCodeDto getCountryOfBirth() { + public CountryCode getCountryOfBirth() { return countryOfBirth; } - public void setCountryOfBirth(CountryCodeDto countryOfBirth) { + public void setCountryOfBirth(CountryCode countryOfBirth) { this.countryOfBirth = countryOfBirth; } diff --git a/backend/sti-protection/src/main/java/de/eshg/stiprotection/testhelper/StiProtectionPopulator.java b/backend/sti-protection/src/main/java/de/eshg/stiprotection/testhelper/StiProtectionPopulator.java index 7ed0e29d0..ff9ff55af 100644 --- a/backend/sti-protection/src/main/java/de/eshg/stiprotection/testhelper/StiProtectionPopulator.java +++ b/backend/sti-protection/src/main/java/de/eshg/stiprotection/testhelper/StiProtectionPopulator.java @@ -7,8 +7,8 @@ package de.eshg.stiprotection.testhelper; import static de.eshg.base.util.ClassNameUtil.getClassNameAsPropertyKey; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; +import de.eshg.lib.common.CountryCode; import de.eshg.stiprotection.StiProtectionProcedureController; import de.eshg.stiprotection.api.AppointmentBookingTypeDto; import de.eshg.stiprotection.api.ConcernDto; @@ -108,8 +108,8 @@ public class StiProtectionPopulator extends BasePopulator<CreateProcedureRespons return Year.of(LocalDate.now(clock).minusYears(faker.random().nextInt(age)).getYear()); } - private static CountryCodeDto countryOfBirth(Faker faker) { - return BasePopulator.randomElement(faker, CountryCodeDto.values()); + private static CountryCode countryOfBirth(Faker faker) { + return BasePopulator.randomElement(faker, CountryCode.values()); } private static AppointmentBookingTypeDto appointmentBookingType() { diff --git a/backend/sti-protection/src/main/resources/migrations/0002_introduce_procedure_file_type.xml b/backend/sti-protection/src/main/resources/migrations/0002_introduce_procedure_file_type.xml new file mode 100644 index 000000000..3d2ff852f --- /dev/null +++ b/backend/sti-protection/src/main/resources/migrations/0002_introduce_procedure_file_type.xml @@ -0,0 +1,11 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1728922179310-1"> + <ext:renamePostgresEnumType oldName="fileType" newName="procedureFileType"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/sti-protection/src/main/resources/migrations/0003_rename_country_code_dto.xml b/backend/sti-protection/src/main/resources/migrations/0003_rename_country_code_dto.xml new file mode 100644 index 000000000..b89c3980f --- /dev/null +++ b/backend/sti-protection/src/main/resources/migrations/0003_rename_country_code_dto.xml @@ -0,0 +1,11 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1728390259649-2"> + <ext:renamePostgresEnumType oldName="countrycodedto" newName="countrycode" /> + </changeSet> +</databaseChangeLog> diff --git a/backend/sti-protection/src/main/resources/migrations/0004_add_medical_registry_procedure_types.xml b/backend/sti-protection/src/main/resources/migrations/0004_add_medical_registry_procedure_types.xml new file mode 100644 index 000000000..21b419fae --- /dev/null +++ b/backend/sti-protection/src/main/resources/migrations/0004_add_medical_registry_procedure_types.xml @@ -0,0 +1,14 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 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="1728647075086-1"> + <ext:addPostgresEnumValues enumTypeName="persontype" valuesToAdd="PROFESSIONAL"/> + </changeSet> + <changeSet author="GA-Lotse" id="1728647075086-2"> + <ext:addPostgresEnumValues enumTypeName="proceduretype" valuesToAdd="MEDICAL_REGISTRY_CITIZEN_DRAFT, MEDICAL_REGISTRY_EMPLOYEE_DRAFT, MEDICAL_REGISTRY_ENTRY"/> + </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 fb8d48987..52f525fb5 100644 --- a/backend/sti-protection/src/main/resources/migrations/changelog.xml +++ b/backend/sti-protection/src/main/resources/migrations/changelog.xml @@ -9,5 +9,8 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> <include file="migrations/0001_initial.xml"/> + <include file="migrations/0002_introduce_procedure_file_type.xml"/> + <include file="migrations/0003_rename_country_code_dto.xml"/> + <include file="migrations/0004_add_medical_registry_procedure_types.xml"/> </databaseChangeLog> diff --git a/backend/sti-protection/src/main/resources/templates/identification/.editorconfig b/backend/sti-protection/src/main/resources/templates/identification/.editorconfig new file mode 100644 index 000000000..f8daf8852 --- /dev/null +++ b/backend/sti-protection/src/main/resources/templates/identification/.editorconfig @@ -0,0 +1,3 @@ +[{*.ftlx,*.css}] +max_line_length = 120 +ij_continuation_indent_size = 2 diff --git a/backend/sti-protection/src/main/resources/templates/identification/anon-ident.css b/backend/sti-protection/src/main/resources/templates/identification/anon-ident.css new file mode 100644 index 000000000..b1c7f4ad3 --- /dev/null +++ b/backend/sti-protection/src/main/resources/templates/identification/anon-ident.css @@ -0,0 +1,143 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +@page { + size: A4 portrait; + margin-bottom: 25mm; + + @bottom-left { + content: element(--var-footerRunning); + font-family: sans-serif; + font-size: 8pt; + border-top: black solid 1px; + vertical-align: top; + } + + @bottom-right { + content: "Seite " counter(page) " von " counter(pages); + font-family: sans-serif; + font-size: 8pt; + border-top: black solid 1px; + vertical-align: top; + } +} + +html { + font-family: sans-serif; + font-size: 8pt; +} + +/* the footer is a running element, which is used in the @bottom-center margin box */ +/* see https://pagedjs.org/documentation/7-generated-content-in-margin-boxes/ */ +footer { + position: running(--var-footerRunning); +} + +.header .officeName { + font-size: 16pt; +} + +.header .objectType { + font-size: 12pt; +} + +.header .letterhead td { + padding: 0; + border-spacing: 0; +} + +.header .receiver { + padding-right: 20px; + width: 75%; + font-size: 8pt; +} + +.header .receiver .officeAddress { + font-size: 7pt; + padding-top: 60px; + margin-bottom: 10px; +} + +.header .sender { + font-size: 8pt; + margin-top: 10px; +} + +.subject p { + padding-top: 20px; + font-weight: bold; +} + +.participants ul { + margin: 0; + list-style-type: none; +} + +.access-code { + padding-right: 40px; +} + +.important { + font-weight: bold; +} + +.box { + border: 1px solid black; + padding: 6px; +} + +.officeInfoHeader { + font-size: 7pt; +} + +.officeInfoContent { + font-size: 8pt; + font-weight: bold; +} + +.footerContent { + font-size: 7pt; +} + +.info-table td { + border: none; + padding-top: 20px; + padding-left: 20px; + padding-right: 20px; + vertical-align: top; +} + +.highlight { + background: #EBEBEB; + border-left: 2px solid #21BBEF; +} + +.section { + font-size: 7px; + font-weight: 400; + line-height: 10.5px; +} + +.strong { + font-weight: 600; +} + +.ticket-number { + padding: 8px; + border: 1px solid black; + font-weight: 600; + letter-spacing: 1px; + width: 120px; +} + +.qr-code { + background: black; + width: 70px; + height: 70px; +} + +ol { + padding: 10pt; +} diff --git a/backend/sti-protection/src/main/resources/templates/identification/anon_indent.ftlx b/backend/sti-protection/src/main/resources/templates/identification/anon_indent.ftlx new file mode 100644 index 000000000..bc5d105e9 --- /dev/null +++ b/backend/sti-protection/src/main/resources/templates/identification/anon_indent.ftlx @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + +<#-- @ftlvariable name="sender" type="de.eshg.stiprotection.pdf.identification.DocumentSender" --> +<#-- @ftlvariable name="appointment" type="de.eshg.stiprotection.pdf.identification.Appointment" --> +<html xml:lang="de" lang="de"> + <head> + <link rel="stylesheet" type="text/css" href="anon-ident.css"/> + </head> + + <body> + <#include "footer.ftlx"> + <#include "header.ftlx"> + + <p>Sehr geehrte:r Bürger:in,</p> + + <p>Vielen Dank, dass Sie unser anonymes Beratungsangebot in Anspruch nehmen.</p> + + <p>Unten stehend finden Sie Ihre Ticketnummer. Damit können Sie</p> + + <ul> + <li>sich vor Ort bei Mitarbeitenden des Gesundheitsamts anonym bei Ihrer Untersuchung und Ihrem Beratungsgespräch + identifizieren, + </li> + <li>Ihren Termin ändern oder stornieren und</li> + <li>nach der Untersuchung online einsehen, ob Ergebnisse für Ihren Vorgang vorliegen.</li> + </ul> + + <p>Bewahren Sie die Ticketnummer gut auf und achten Sie darauf, dass dieser nicht in die Hände von unbefugten + Personen gelangt. Wenn Sie zu Ihrem Termin erscheinen, bringen Sie bitte Ihre Ticketnummer mit. + </p> + + <table class="highlight"> + <tr> + <td colspan="3"> + <table class="info-table"> + <tr> + <td style="width: 75px;"> + <div>Ihr Termin</div> + </td> + <td style="width: 300px;"> + ${appointment.date} + <br/> + ${appointment.time()} Uhr + <br/> + <span>Planen Sie für den Termin inkl. Wartezeit bitte etwa ${appointment.durationMinutes} Minuten ein.</span> + </td> + <td style="width: 350px;"> + ${sender.department.name} + <br/> + ${sender.department.street} ${sender.department.houseNumber} + <br/> + ${appointment.room()} + <br/> + ${sender.department.postalCode} ${sender.department.city} + </td> + </tr> + <tr> + <td style="width: 75px;"> + <div>QR-Code</div> + <div class="qr-code"></div> + </td> + <td style="width: 200px;"> + <div>URL</div> + <div class="officeInfoContent">${appointment.url}</div> + <div style="padding-top: 20px;">Ticketnummer</div> + <div class="ticket-number">${appointment.ticketNumber}</div> + </td> + <td style="width: 300px;"> + <span>Über die links stehende URL können Sie nach der Untersuchung Informationen erhalten, ob Ergebnisse vorliegen. + </span> + <br/> + <ol> + <li>Scannen Sie mit Ihrem Smartphone den QR‑Code oder rufen Sie die nebenstehende URL auf.</li> + <li>Geben Sie die Ticketnummer und Ihr Passwort ein.</li> + </ol> + </td> + </tr> + </table> + </td> + </tr> + </table> + + <p> + Wichtig: Um absolute Anonymität zu gewährleisten, fragen wir Sie niemals nach Ihren Kontaktdaten, + Ausweisdokumenten oder Versicherungskarten. Aus diesem Grund erhalten Sie keine Benachrichtigung, wenn Ergebnisse + vorliegen. Bitte rufen Sie proaktiv die genannte Website auf und geben Sie Ihre Ticketnummer ein, um Ihre + Ergebnisse einzusehen. + </p> + <p> + Mit freundlichen Grüßen + <br/> + Im Auftrag, Untersuchungs- und Beratungsstelle für sexuelle und reproduktive Gesundheit (STI) + <br/> + (Dieses Schreiben wurde maschinell ohne Unterschrift erstellt) + </p> + </body> +</html> diff --git a/backend/sti-protection/src/main/resources/templates/identification/footer.ftlx b/backend/sti-protection/src/main/resources/templates/identification/footer.ftlx new file mode 100644 index 000000000..687cbf62e --- /dev/null +++ b/backend/sti-protection/src/main/resources/templates/identification/footer.ftlx @@ -0,0 +1,12 @@ +<#-- @ftlvariable name="sender" type="de.eshg.stiprotection.pdf.identification.DocumentSender" --> +<footer> + <div class="footerContent"> + <div> + ${sender.department.name} • ${sender.department.street} ${sender.department.houseNumber} ${sender.department.postalCode} ${sender.department.city} + </div> + <div> + Webseite: ${sender.department.homepage} + E-Mail: ${sender.department.email} + </div> + </div> +</footer> diff --git a/backend/sti-protection/src/main/resources/templates/identification/header.ftlx b/backend/sti-protection/src/main/resources/templates/identification/header.ftlx new file mode 100644 index 000000000..4af368bf0 --- /dev/null +++ b/backend/sti-protection/src/main/resources/templates/identification/header.ftlx @@ -0,0 +1,53 @@ +<#-- @ftlvariable name="sender" type="de.eshg.stiprotection.pdf.identification.DocumentSender" --> +<table class="header"> + <tr> + <td valign="top"/> + <td valign="top"> + <img src="logo-gesundheitsamt-ffm.svg" width="114px" height="52px" alt="logo"/> + </td> + </tr> + <tr class="letterhead"> + <td valign="top" class="receiver"> + <div class="officeAddress"> + ${sender.department.name}, ${sender.department.street} ${sender.department.houseNumber} + , ${sender.department.postalCode} ${sender.department.city} + </div> + </td> + <td valign="top" class="sender"> + <div class="officeInfoHeader">${sender.department.name}</div> + <div class="officeInfoHeader">${sender.department.street} ${sender.department.houseNumber}</div> + <div class="officeInfoHeader">${sender.department.postalCode} ${sender.department.city}</div> + <table> + <tr> + <td> + <div class="officeInfoHeader">Telefon</div> + <div class="officeInfoContent">${sender.department.phoneNumber}</div> + </td> + </tr> + <tr> + <td> + <div class="officeInfoHeader">E-Mail</div> + <div class="officeInfoContent">${sender.department.email}</div> + </td> + </tr> + <tr> + <td> + <div class="officeInfoHeader">Datum</div> + <div class="officeInfoContent">${sender.documentDate}</div> + </td> + </tr> + <tr> + <td> + <#if sender.referenceNumber??> + <div class="officeInfoHeader">Aktenzeichen</div> + <div class="officeInfoContent">${sender.referenceNumber}</div> + <div class="officeInfoHeader">(bitte bei Antwort angeben)</div> + <#else> + <br/><br/><br/> + </#if> + </td> + </tr> + </table> + </td> + </tr> +</table> diff --git a/backend/sti-protection/src/main/resources/templates/identification/logo-gesundheitsamt-ffm.svg b/backend/sti-protection/src/main/resources/templates/identification/logo-gesundheitsamt-ffm.svg new file mode 100644 index 000000000..57ab78430 --- /dev/null +++ b/backend/sti-protection/src/main/resources/templates/identification/logo-gesundheitsamt-ffm.svg @@ -0,0 +1,12 @@ +<svg width="114" height="52" viewBox="0 0 114 52" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_6063_613)"> +<path d="M21.5433 16.2142C21.5433 19.3449 19.0046 21.8835 15.8739 21.8835C12.7433 21.8835 10.2046 19.3449 10.2046 16.2142C10.2046 13.0835 12.7433 10.5449 15.8739 10.5449C19.0046 10.5449 21.5433 13.0835 21.5433 16.2142Z" fill="black"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0.953125C0 0.614459 0.274615 0.339844 0.613281 0.339844H31.1348C31.4734 0.339844 31.748 0.614459 31.748 0.953125V16.2148C31.748 26.6082 21.7587 34.6696 10.8574 31.3203C4.85343 29.4763 0 22.6472 0 16.3672V0.953125ZM26.0781 16.2148C26.0781 10.5789 21.509 6.00977 15.873 6.00977C10.2384 6.00977 5.66992 10.5789 5.66992 16.2148C5.66992 21.8508 10.2384 26.4199 15.873 26.4199C21.509 26.4199 26.0781 21.8508 26.0781 16.2148ZM5.90728 37.3478C6.91262 41.9278 10.9926 45.3544 15.874 45.3544C20.7553 45.3544 24.8353 41.9278 25.8406 37.3478C26.1246 36.0584 27.2806 35.1491 28.6019 35.1491C30.4193 35.1491 31.7646 36.8358 31.3699 38.6104C29.7913 45.7131 23.4526 51.0238 15.874 51.0238C8.29528 51.0238 1.95662 45.7131 0.377951 38.6104C-0.0167154 36.8358 1.32862 35.1491 3.14595 35.1491C4.46729 35.1491 5.62328 36.0584 5.90728 37.3478Z" fill="#21BBEF"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.2957 9.80133V4.45467C43.2957 1.624 45.077 0 48.2063 0C51.453 0 52.997 1.34667 52.997 4.17733V4.396C52.997 4.67333 52.8397 4.85067 52.5623 4.85067H51.097C50.8197 4.85067 50.661 4.69333 50.661 4.416V4.17733C50.661 2.61333 49.969 1.96 48.2063 1.96C46.5037 1.96 45.6717 2.772 45.6717 4.376V9.92C45.6717 11.524 46.5037 12.336 48.2063 12.336C50.0077 12.336 50.721 11.6627 50.721 10.0187V8.81067H48.8197C48.305 8.81067 47.9877 8.51467 47.9877 8.03867V7.58267C47.9877 7.088 48.305 6.792 48.8197 6.792H52.1663C52.7197 6.792 52.997 7.06933 52.997 7.62267V10.0187C52.997 12.9093 51.453 14.296 48.2063 14.296C45.0583 14.296 43.2957 12.652 43.2957 9.80133ZM56.2246 12.9889V1.30756C56.2246 0.634226 56.6406 0.218225 57.3139 0.218225H64.4806C64.9166 0.218225 65.1539 0.455559 65.1539 0.891559V1.56489C65.1539 1.99956 64.9166 2.23823 64.4806 2.23823H58.6006V5.97956H63.2139C63.8273 5.97956 64.2633 6.33556 64.2633 6.95023V7.04889C64.2633 7.66223 63.8273 8.01956 63.2139 8.01956H58.6006V12.0582H64.4806C64.9166 12.0582 65.1539 12.2956 65.1539 12.7316V13.4049C65.1539 13.8409 64.9166 14.0782 64.4806 14.0782H57.3139C56.6406 14.0782 56.2246 13.6622 56.2246 12.9889ZM77.0934 10.2562V9.76158C77.0934 7.30691 75.8654 6.73224 73.9654 6.31624L71.3907 5.76158C70.4401 5.56424 69.9454 5.28691 69.9454 4.29624V3.84158C69.9454 2.35624 70.5201 1.90158 72.3014 1.90158C74.0041 1.90158 74.6374 2.43624 74.6374 3.76291V4.15891C74.6374 4.47491 74.7961 4.67358 75.1134 4.67358H76.4601C76.7761 4.67358 76.9347 4.47491 76.9347 4.13891V3.74291C76.9347 1.12824 75.5294 0.000244357 72.3014 0.000244357C69.2134 0.000244357 67.6294 1.34691 67.6294 4.02024V4.49491C67.6294 6.96958 68.7974 7.52424 70.6987 7.94024L73.2921 8.49491C74.2814 8.71224 74.7761 8.96958 74.7761 9.96024V10.4349C74.7761 11.9202 74.1241 12.3949 72.2627 12.3949C70.4601 12.3949 69.8067 11.8602 69.8067 10.4949V10.0589C69.8067 9.74158 69.6481 9.54424 69.3321 9.54424H67.9854C67.6681 9.54424 67.5107 9.74158 67.5107 10.0789V10.5336C67.5107 13.1669 68.9561 14.2962 72.2627 14.2962C75.4107 14.2962 77.0934 12.9296 77.0934 10.2562ZM79.7266 9.8606V0.911268C79.7266 0.495268 80.0039 0.217935 80.4199 0.217935H81.4092C81.8252 0.217935 82.1026 0.495268 82.1026 0.911268V9.97927C82.1026 11.5433 82.8546 12.3353 84.3799 12.3353C85.8839 12.3353 86.6559 11.5433 86.6559 9.97927V0.911268C86.6559 0.495268 86.9332 0.217935 87.3492 0.217935H88.3399C88.7546 0.217935 89.0319 0.495268 89.0319 0.911268V9.8606C89.0319 12.6726 87.3492 14.2953 84.3799 14.2953C81.4092 14.2953 79.7266 12.6726 79.7266 9.8606ZM91.8044 1.30756V13.3849C91.8044 13.8009 92.0818 14.0782 92.4978 14.0782H93.2898C93.7058 14.0782 93.9818 13.8009 93.9818 13.3849V3.76223L97.7644 13.1662C98.0218 13.8209 98.3578 14.0782 99.0311 14.0782H99.9818C100.694 14.0782 101.11 13.6622 101.11 12.9889V0.911559C101.11 0.495559 100.832 0.218225 100.418 0.218225H99.6258C99.2098 0.218225 98.9324 0.495559 98.9324 0.911559V10.6529L95.1711 1.12889C94.9138 0.475559 94.5764 0.218225 93.9031 0.218225H92.9324C92.2204 0.218225 91.8044 0.634227 91.8044 1.30756ZM107.98 12.0782C110.039 12.0782 111.01 11.1476 111.01 9.18756V5.10889C111.01 3.14756 110.039 2.21823 107.98 2.21823H106.258V12.0782H107.98ZM103.882 12.9889V1.30756C103.882 0.634226 104.278 0.218225 104.951 0.218225H107.94C111.564 0.218225 113.386 1.90089 113.386 5.20756V9.08889C113.386 12.3956 111.564 14.0782 107.94 14.0782H104.951C104.278 14.0782 103.882 13.6622 103.882 12.9889ZM43.4937 17.7116V30.1849C43.4937 30.6009 43.771 30.8783 44.187 30.8783H45.1763C45.5923 30.8783 45.8697 30.6009 45.8697 30.1849V24.8196H50.423V30.1849C50.423 30.6009 50.7003 30.8783 51.1163 30.8783H52.107C52.5217 30.8783 52.799 30.6009 52.799 30.1849V17.7116C52.799 17.2956 52.5217 17.0183 52.107 17.0183H51.1163C50.7003 17.0183 50.423 17.2956 50.423 17.7116V22.7796H45.8697V17.7116C45.8697 17.2956 45.5923 17.0183 45.1763 17.0183H44.187C43.771 17.0183 43.4937 17.2956 43.4937 17.7116ZM56.2246 29.7887V18.1074C56.2246 17.434 56.6406 17.018 57.3139 17.018H64.4806C64.9166 17.018 65.1539 17.2554 65.1539 17.6914V18.3647C65.1539 18.7994 64.9166 19.038 64.4806 19.038H58.6006V22.7794H63.2139C63.8273 22.7794 64.2633 23.1354 64.2633 23.75V23.8487C64.2633 24.462 63.8273 24.8194 63.2139 24.8194H58.6006V28.858H64.4806C64.9166 28.858 65.1539 29.0954 65.1539 29.5314V30.2047C65.1539 30.6407 64.9166 30.878 64.4806 30.878H57.3139C56.6406 30.878 56.2246 30.462 56.2246 29.7887ZM67.8472 29.5314V30.2047C67.8472 30.6407 68.0845 30.878 68.5205 30.878H76.0845C76.5192 30.878 76.7565 30.6407 76.7565 30.2047V29.5314C76.7565 29.0954 76.5192 28.858 76.0845 28.858H73.4898V19.038H76.0845C76.5192 19.038 76.7565 18.7994 76.7565 18.3647V17.6914C76.7565 17.2554 76.5192 17.018 76.0845 17.018H68.5205C68.0845 17.018 67.8472 17.2554 67.8472 17.6914V18.3647C67.8472 18.7994 68.0845 19.038 68.5205 19.038H71.1138V28.858H68.5205C68.0845 28.858 67.8472 29.0954 67.8472 29.5314ZM83.1917 30.1849V19.0369H79.905C79.469 19.0369 79.2317 18.7996 79.2317 18.3649V17.6916C79.2317 17.2556 79.469 17.0183 79.905 17.0183H88.8544C89.2904 17.0183 89.5277 17.2556 89.5277 17.6916V18.3649C89.5277 18.7996 89.2904 19.0369 88.8544 19.0369H85.5677V30.1849C85.5677 30.6009 85.2904 30.8783 84.8544 30.8783H83.905C83.469 30.8783 83.1917 30.6009 83.1917 30.1849ZM101.249 27.056V26.5614C101.249 24.1067 100.021 23.532 98.1209 23.116L95.5463 22.5614C94.5956 22.364 94.1009 22.0867 94.1009 21.096V20.6414C94.1009 19.156 94.6756 18.7014 96.4569 18.7014C98.1596 18.7014 98.7929 19.236 98.7929 20.5627V20.9587C98.7929 21.2747 98.9516 21.472 99.2689 21.472H100.616C100.932 21.472 101.09 21.2747 101.09 20.9387V20.5427C101.09 17.928 99.6849 16.8 96.4569 16.8C93.3689 16.8 91.7849 18.1467 91.7849 20.82V21.2947C91.7849 23.7694 92.9529 24.324 94.8543 24.74L97.4476 25.2947C98.4369 25.512 98.9316 25.7694 98.9316 26.7587V27.2347C98.9316 28.72 98.2796 29.1947 96.4183 29.1947C94.6156 29.1947 93.9623 28.66 93.9623 27.2947V26.8587C93.9623 26.5414 93.8036 26.344 93.4876 26.344H92.1409C91.8236 26.344 91.6663 26.5414 91.6663 26.8787V27.3334C91.6663 29.9667 93.1116 31.096 96.4183 31.096C99.5663 31.096 101.249 29.7294 101.249 27.056ZM50.0274 41.6587L48.1074 36.016L46.1861 41.6587H50.0274ZM43.1968 46.9853V45.124C43.1968 43.5987 43.3354 43.044 43.8301 41.6387L46.2848 34.7093C46.5234 34.056 46.7808 33.7587 47.5128 33.7587H48.7794C49.5128 33.7587 49.7701 34.056 50.0074 34.7093L52.4621 41.6387C52.9581 43.044 53.0968 43.5987 53.0968 45.124V46.9853C53.0968 47.4 52.7994 47.6773 52.3834 47.6773H51.4328C51.0168 47.6773 50.7394 47.4 50.7394 46.9853V45.064C50.7394 44.5693 50.7008 44.1333 50.6408 43.7173H45.5928C45.5328 44.1333 45.5128 44.5493 45.5128 45.064V46.9853C45.5128 47.4 45.2354 47.6773 44.8208 47.6773H43.8888C43.4741 47.6773 43.1968 47.4 43.1968 46.9853ZM55.7393 34.9267V47.044C55.7393 47.44 55.9766 47.6773 56.3339 47.6773H57.2446C57.6006 47.6773 57.8379 47.44 57.8379 47.044V37.916L59.9566 42.8667C60.1353 43.2627 60.3326 43.46 60.6699 43.46C61.0059 43.46 61.2046 43.2627 61.3819 42.8667L63.5006 37.916V47.044C63.5006 47.44 63.7379 47.6773 64.0953 47.6773H65.0446C65.4019 47.6773 65.6393 47.44 65.6393 47.044V34.9267C65.6393 34.2533 65.2233 33.8173 64.5499 33.8173H63.9166C63.2233 33.8173 62.9659 34.016 62.7086 34.6493L60.6886 39.6387L58.6699 34.6493C58.4126 34.016 58.1553 33.8173 57.4619 33.8173H56.8286C56.1553 33.8173 55.7393 34.2533 55.7393 34.9267ZM71.1138 46.985V35.8383H67.8271C67.3911 35.8383 67.1538 35.5997 67.1538 35.165V34.4917C67.1538 34.0557 67.3911 33.8183 67.8271 33.8183H76.7765C77.2125 33.8183 77.4498 34.0557 77.4498 34.4917V35.165C77.4498 35.5997 77.2125 35.8383 76.7765 35.8383H73.4898V46.985C73.4898 47.401 73.2125 47.6783 72.7765 47.6783H71.8271C71.3911 47.6783 71.1138 47.401 71.1138 46.985ZM80.3599 34.4123V47.3216C80.3599 47.5389 80.5185 47.6776 80.7172 47.6776H81.1519C81.3705 47.6776 81.5279 47.5389 81.5279 47.3216V41.3416H87.2105C87.4879 41.3416 87.6865 41.1643 87.6865 40.8869V40.8069C87.6865 40.5496 87.4879 40.3523 87.2105 40.3523H81.5279V34.8083H88.0425C88.2599 34.8083 88.3985 34.6896 88.3985 34.4709V34.1749C88.3985 33.9563 88.2599 33.8176 88.0425 33.8176H80.9545C80.5585 33.8176 80.3599 34.0363 80.3599 34.4123ZM92.438 47.3216V34.4123C92.438 34.0363 92.6353 33.8176 93.0313 33.8176H100.121C100.338 33.8176 100.477 33.9563 100.477 34.1749V34.4709C100.477 34.6896 100.338 34.8083 100.121 34.8083H93.606V40.3523H99.2887C99.566 40.3523 99.7633 40.5496 99.7633 40.8069V40.8869C99.7633 41.1643 99.566 41.3416 99.2887 41.3416H93.606V47.3216C93.606 47.5389 93.4473 47.6776 93.23 47.6776H92.794C92.5967 47.6776 92.438 47.5389 92.438 47.3216ZM103.981 34.4119V47.3412C103.981 47.5386 104.12 47.6786 104.317 47.6786H104.733C104.932 47.6786 105.07 47.5386 105.07 47.3412V35.6986L108.118 42.6879C108.218 42.9266 108.396 43.0652 108.634 43.0652C108.852 43.0652 109.01 42.9266 109.109 42.6879L112.198 35.6786V47.3412C112.198 47.5386 112.317 47.6786 112.534 47.6786H112.95C113.148 47.6786 113.286 47.5386 113.286 47.3412V34.4119C113.286 34.0559 113.069 33.8186 112.693 33.8186H112.317C111.941 33.8186 111.782 33.9572 111.644 34.2732L108.634 41.3226L105.624 34.2732C105.485 33.9572 105.348 33.8186 104.952 33.8186H104.574C104.198 33.8186 103.981 34.0559 103.981 34.4119Z" fill="black"/> +</g> +<defs> +<clipPath id="clip0_6063_613"> +<rect width="114" height="52.0001" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/backend/test-commons/gradle.lockfile b/backend/test-commons/gradle.lockfile index 4cc8f443a..0b7b5dcda 100644 --- a/backend/test-commons/gradle.lockfile +++ b/backend/test-commons/gradle.lockfile @@ -65,7 +65,7 @@ org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt -org.jetbrains:annotations:26.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:26.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.10.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath diff --git a/backend/test-helper-commons/gradle.lockfile b/backend/test-helper-commons/gradle.lockfile index 1ea39b2f6..ea850649a 100644 --- a/backend/test-helper-commons/gradle.lockfile +++ b/backend/test-helper-commons/gradle.lockfile @@ -55,6 +55,7 @@ io.prometheus:prometheus-metrics-model:1.2.1=testRuntimeClasspath io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.2.1=testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.22=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/backend/travel-medicine/build.gradle b/backend/travel-medicine/build.gradle index 7850f353a..8976ed791 100644 --- a/backend/travel-medicine/build.gradle +++ b/backend/travel-medicine/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation project(':business-module-persistence-commons') implementation project(':lib-document-generator') implementation project(':rest-oauth-client-commons') + implementation project(':file-commons') annotationProcessor 'org.hibernate.orm:hibernate-jpamodelgen' diff --git a/backend/travel-medicine/gradle.lockfile b/backend/travel-medicine/gradle.lockfile index af4798513..c0e0456af 100644 --- a/backend/travel-medicine/gradle.lockfile +++ b/backend/travel-medicine/gradle.lockfile @@ -16,11 +16,11 @@ com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClassp com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.0=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.curious-odd-man:rgxgen:2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-api:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport-zerodep:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.docker-java:docker-java-transport:3.3.6=testCompileClasspath,testRuntimeClasspath -com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=testRuntimeClasspath -com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=testRuntimeClasspath +com.github.docker-java:docker-java-api:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport-zerodep:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.docker-java:docker-java-transport:3.3.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-decorator-spring-boot-autoconfigure:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.28.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -28,7 +28,7 @@ com.google.guava:failureaccess:1.0.2=productionRuntimeClasspath,runtimeClasspath com.google.guava:guava:33.3.1-jre=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.0.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -com.googlecode.java-diff-utils:diffutils:1.3.0=testCompileClasspath,testRuntimeClasspath +com.googlecode.java-diff-utils:diffutils:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.googlecode.libphonenumber:libphonenumber:8.13.46=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 @@ -37,23 +37,24 @@ com.nimbusds:nimbus-jose-jwt:9.37.3=compileClasspath,productionRuntimeClasspath, com.nimbusds:oauth2-oidc-sdk:9.43.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.opencsv:opencsv:5.9=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,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 -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.tngtech.archunit:archunit-junit5-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5-engine:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit-junit5:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.3.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath com.zaxxer:HikariCP:5.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.16.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.17.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.3.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:commons-lang:1.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator-postgresql:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-changelog-generator:1.0=testCompileClasspath,testRuntimeClasspath de.cronn:liquibase-postgres-enum-extension:1.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:postgres-snapshot-util:1.3.3=testRuntimeClasspath +de.cronn:postgres-snapshot-util:1.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath de.cronn:reflection-util:2.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -de.cronn:test-utils:1.1.1=testCompileClasspath,testRuntimeClasspath -de.cronn:validation-file-assertions:0.8.0=testCompileClasspath,testRuntimeClasspath +de.cronn:test-utils:1.1.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +de.cronn:validation-file-assertions:0.8.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.rototor.pdfbox:graphics2d:3.0.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.github.openhtmltopdf:openhtmltopdf-core:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.github.openhtmltopdf:openhtmltopdf-pdfbox:1.1.22=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -72,6 +73,7 @@ io.prometheus:prometheus-metrics-shaded-protobuf:1.2.1=productionRuntimeClasspat io.prometheus:prometheus-metrics-tracer-common:1.2.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.smallrye:jandex:3.1.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.25=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.25=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.3=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -82,19 +84,19 @@ jakarta.transaction:jakarta.transaction-api:2.0.1=annotationProcessor,compileCla jakarta.validation:jakarta.validation-api:3.0.2=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.19=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.19=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.datafaker:datafaker:2.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.14.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.stax-utils:stax-utils:20070216=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.logstash.logback:logstash-logback-encoder:8.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.ttddyy:datasource-proxy:1.10=testRuntimeClasspath +net.ttddyy:datasource-proxy:1.10=productionRuntimeClasspath,runtimeClasspath,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.26.1=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-lang3:3.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-text:1.12.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.httpcomponents.client5:httpclient5:5.3.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -137,7 +139,7 @@ org.apache.xmlgraphics:batik-xml:1.17=productionRuntimeClasspath,runtimeClasspat org.apache.xmlgraphics:xmlgraphics-commons:2.9=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.25.3=testCompileClasspath,testRuntimeClasspath +org.assertj:assertj-core:3.25.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcmail-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.78.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath @@ -150,8 +152,8 @@ org.freemarker:freemarker:2.3.33=productionRuntimeClasspath,runtimeClasspath,tes org.glassfish.jaxb:jaxb-core:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:jaxb-runtime:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.glassfish.jaxb:txw2:4.0.5=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest-core:2.2=testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:2.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.common:hibernate-commons-annotations:6.0.6.Final=annotationProcessor,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.hibernate.orm:hibernate-core:6.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -163,15 +165,15 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt org.jacoco:org.jacoco.core:0.8.11=jacocoAnt org.jacoco:org.jacoco.report:0.8.11=jacocoAnt org.jboss.logging:jboss-logging:3.5.3.Final=annotationProcessor,compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.10.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:17.0.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.3=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.10.3=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter:5.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.10.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.10.3=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.junit.platform:junit-platform-launcher:1.10.3=testRuntimeClasspath -org.junit:junit-bom:5.10.3=testCompileClasspath,testRuntimeClasspath +org.junit:junit-bom:5.10.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.liquibase:liquibase-core:4.27.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.mockito:mockito-core:5.11.0=testCompileClasspath,testRuntimeClasspath @@ -179,12 +181,12 @@ org.mockito:mockito-junit-jupiter:5.11.0=testCompileClasspath,testRuntimeClasspa org.mozilla:rhino:1.7.13=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-commons:9.6=jacocoAnt org.ow2.asm:asm-tree:9.6=jacocoAnt org.ow2.asm:asm:9.6=jacocoAnt,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath +org.rnorth.duct-tape:duct-tape:1.0.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.16=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -194,7 +196,7 @@ org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0=testCompileClasspath,tes org.springframework.boot:spring-boot-actuator-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-dependencies:3.3.3=testRuntimeClasspath +org.springframework.boot:spring-boot-dependencies:3.3.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-actuator:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-aop:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter-data-jpa:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -210,7 +212,7 @@ org.springframework.boot:spring-boot-starter-validation:3.3.4=compileClasspath,p org.springframework.boot:spring-boot-starter-web:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-starter:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:3.3.4=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.3.4=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.3.4=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot-testcontainers:3.3.4=testCompileClasspath,testRuntimeClasspath org.springframework.boot:spring-boot:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework.data:spring-data-commons:3.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -233,14 +235,14 @@ org.springframework:spring-expression:6.1.13=compileClasspath,productionRuntimeC org.springframework:spring-jcl:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-jdbc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-orm:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.1.13=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:6.1.13=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-tx:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-web:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.springframework:spring-webmvc:6.1.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.testcontainers:database-commons:1.19.8=testRuntimeClasspath org.testcontainers:jdbc:1.19.8=testRuntimeClasspath org.testcontainers:postgresql:1.19.8=testRuntimeClasspath -org.testcontainers:testcontainers:1.19.8=testCompileClasspath,testRuntimeClasspath +org.testcontainers:testcontainers:1.19.8=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.verapdf:core-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:feature-reporting-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.verapdf:metadata-fixer-jakarta:1.26.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath diff --git a/backend/travel-medicine/openApi.yaml b/backend/travel-medicine/openApi.yaml index ef61e9485..4c3e99532 100644 --- a/backend/travel-medicine/openApi.yaml +++ b/backend/travel-medicine/openApi.yaml @@ -472,6 +472,34 @@ paths: summary: Cancel an appointment. tags: - CitizenAuth + /citizen/auth/vaccination-consultations/{procedureId}/procedure-steps/{procedureStepId}/appointments: + put: + operationId: putAppointment + parameters: + - in: path + name: procedureId + required: true + schema: + type: string + format: uuid + - in: path + name: procedureStepId + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Appointment" + required: true + responses: + "200": + description: OK + summary: Book or rebook an appointment + tags: + - CitizenAuth /citizen/auth/vaccination-consultations/{procedureId}/procedure-steps/{procedureStepId}/medical-history: get: operationId: getMedicalHistory @@ -4260,6 +4288,9 @@ components: GetAppointmentDetailsResponse: type: object properties: + bookingsRemaining: + type: integer + format: int32 citizenHasAnswered: type: boolean dateOfBirth: @@ -4276,6 +4307,7 @@ components: summaryDto: $ref: "#/components/schemas/AppointmentSummary" required: + - bookingsRemaining - citizenHasAnswered - dateOfBirth - firstName @@ -5855,6 +5887,7 @@ components: enum: - PATIENT - PARENT + - PROFESSIONAL Population: type: object properties: @@ -6366,6 +6399,9 @@ components: - TM_VACCINATION_CONSULTATION - MEASLES_PROTECTION - STI_PROTECTION + - MEDICAL_REGISTRY_ENTRY + - MEDICAL_REGISTRY_CITIZEN_DRAFT + - MEDICAL_REGISTRY_EMPLOYEE_DRAFT ProcedureWithDuration: type: object properties: diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/certificate/CertificateService.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/certificate/CertificateService.java index 93d218d4f..18b6b7414 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/certificate/CertificateService.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/certificate/CertificateService.java @@ -10,9 +10,9 @@ import static de.eshg.travelmedicine.util.TravelMedicineProgressEntryType.CERTIF import de.eshg.lib.document.generator.DocumentGenerator; import de.eshg.lib.procedure.domain.factory.SystemProgressEntryFactory; import de.eshg.lib.procedure.domain.model.File; -import de.eshg.lib.procedure.domain.model.FileType; import de.eshg.lib.procedure.domain.model.Pdf; import de.eshg.lib.procedure.domain.model.PdfMetaData; +import de.eshg.lib.procedure.domain.model.ProcedureFileType; import de.eshg.lib.procedure.domain.model.ProgressEntry; import de.eshg.lib.procedure.domain.model.SystemProgressEntry; import de.eshg.lib.procedure.domain.model.TriggerType; @@ -228,7 +228,8 @@ public class CertificateService { pdfMetaData.setCreatedDate(Instant.now(clock)); pdfMetaData.setDescription(pdfParameters.getTitle()); - return FileFactory.createPdfWithMetaData(PDF_FILENAME, FileType.PDF, bytes, pdfMetaData, false); + return FileFactory.createPdfWithMetaData( + PDF_FILENAME, ProcedureFileType.PDF, bytes, pdfMetaData, false); } private UUID generateProgressEntry(ProcedureStep procedureStep, List<UUID> serviceIds) { diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/certificate/HealthInsuranceCertificatePdfParameters.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/certificate/HealthInsuranceCertificatePdfParameters.java index 46f4c182a..1230cf5fc 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/certificate/HealthInsuranceCertificatePdfParameters.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/certificate/HealthInsuranceCertificatePdfParameters.java @@ -5,8 +5,8 @@ package de.eshg.travelmedicine.certificate; -import de.eshg.base.CountryCodeDto; import de.eshg.base.department.GetDepartmentInfoResponse; +import de.eshg.lib.common.CountryCode; import de.eshg.lib.document.generator.department.DepartmentLogo; import java.math.BigDecimal; import java.text.NumberFormat; @@ -58,7 +58,7 @@ public class HealthInsuranceCertificatePdfParameters { String dateOfBirth, String travelDate, String travelType, - List<CountryCodeDto> travelDestinations, + List<CountryCode> travelDestinations, String travelDuration, List<PdfServiceParameters> pdfServiceParameters) { this.departmentInfo = departmentInfo; @@ -75,8 +75,7 @@ public class HealthInsuranceCertificatePdfParameters { this.dateOfBirth = dateOfBirth; this.travelDate = travelDate; this.travelType = travelType; - this.travelDestinations = - travelDestinations.stream().map(CountryCodeDto::getCountryName).toList(); + this.travelDestinations = travelDestinations.stream().map(CountryCode::getCountryName).toList(); this.travelDuration = travelDuration; this.pdfServiceParameters = pdfServiceParameters; this.serviceTotalCost = diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/citizenauth/CitizenAuthController.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/citizenauth/CitizenAuthController.java index 9398f6a5e..6e4349501 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/citizenauth/CitizenAuthController.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/citizenauth/CitizenAuthController.java @@ -5,6 +5,7 @@ package de.eshg.travelmedicine.citizenauth; +import de.eshg.lib.appointmentblock.api.AppointmentDto; import de.eshg.rest.service.security.config.BaseUrls; import de.eshg.travelmedicine.featuretoggle.TravelMedicineFeature; import de.eshg.travelmedicine.featuretoggle.TravelMedicineFeatureToggle; @@ -24,6 +25,7 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -36,6 +38,7 @@ public class CitizenAuthController { public static final String BASE_URL = BaseUrls.TravelMedicine.CITIZEN_AUTH_CONTROLLER; public static final String PROCEDURE_APPOINTMENTS_URL = "/procedure-appointments"; + public static final String APPOINTMENTS_URL = "/appointments"; public static final String VACCINATION_CONSULTATION_URL = "/vaccination-consultations"; public static final String PROCEDURE_STEP_URL = "/procedure-steps"; public static final String MEDICAL_HISTORY_URL = "/medical-history"; @@ -124,6 +127,24 @@ public class CitizenAuthController { getCitizenUserId(principal), procedureId, procedureStepId, patchMedicalHistoryContent); } + @PutMapping( + VACCINATION_CONSULTATION_URL + + "/{procedureId}" + + PROCEDURE_STEP_URL + + "/{procedureStepId}" + + APPOINTMENTS_URL) + @Operation(summary = "Book or rebook an appointment") + @Transactional() + public void putAppointment( + @AuthenticationPrincipal Jwt principal, + @PathVariable("procedureId") UUID procedureId, + @PathVariable("procedureStepId") UUID procedureStepId, + @RequestBody @Valid AppointmentDto appointmentDto) { + featureToggle.assertNewFeatureIsEnabled(TravelMedicineFeature.CITIZEN_PORTAL_PROCEDURE); + vaccinationConsultationService.bookCitizenAppointment( + getCitizenUserId(principal), procedureId, procedureStepId, appointmentDto); + } + private UUID getCitizenUserId(Jwt principal) { return UUID.fromString(principal.getSubject()); } diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/citizenpublic/DepartmentInfoProperties.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/citizenpublic/DepartmentInfoProperties.java index 0d55805d9..2a719ff51 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/citizenpublic/DepartmentInfoProperties.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/citizenpublic/DepartmentInfoProperties.java @@ -5,7 +5,7 @@ package de.eshg.travelmedicine.citizenpublic; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "de.eshg.travel-medicine.department-info") @@ -16,7 +16,7 @@ public record DepartmentInfoProperties( String houseNumber, String postalCode, String city, - CountryCodeDto country, + CountryCode country, String phoneNumber, String homepage, String email, diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/AppointmentDetailsMapper.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/AppointmentDetailsMapper.java index a0d566125..1b2c17a90 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/AppointmentDetailsMapper.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/AppointmentDetailsMapper.java @@ -5,10 +5,11 @@ package de.eshg.travelmedicine.vaccinationconsultation; -import de.eshg.travelmedicine.medicalhistory.persistence.entity.MedicalHistory; import de.eshg.travelmedicine.vaccinationconsultation.api.AppointmentSummaryDto; import de.eshg.travelmedicine.vaccinationconsultation.api.GetAppointmentDetailsResponse; import de.eshg.travelmedicine.vaccinationconsultation.api.PatientDto; +import de.eshg.travelmedicine.vaccinationconsultation.persistence.entity.ProcedureStep; +import de.eshg.travelmedicine.vaccinationconsultation.persistence.entity.VcService; import java.time.LocalDate; public class AppointmentDetailsMapper { @@ -16,18 +17,22 @@ public class AppointmentDetailsMapper { public static GetAppointmentDetailsResponse mapToDetails( AppointmentSummaryDto appointmentSummary, - boolean hasAccomplishedService, PatientDto patientDto, - MedicalHistory medicalHistory) { + ProcedureStep procedureStep) { String lastName = patientDto.lastName(); String firstName = patientDto.firstName(); LocalDate dateOfBirth = patientDto.dateOfBirth(); - boolean isMedicalHistoryCompletelyAnswered = medicalHistory.isCompletelyAnswered(); - boolean citizenHasAnswered = medicalHistory.isCitizenHasAnswered(); + boolean isMedicalHistoryCompletelyAnswered = + procedureStep.getMedicalHistory().isCompletelyAnswered(); + boolean citizenHasAnswered = procedureStep.getMedicalHistory().isCitizenHasAnswered(); + boolean hasAccomplishedService = + procedureStep.getServices().stream().anyMatch(VcService::isAccomplished); + int bookingsRemaining = procedureStep.getBookingsRemaining(); return new GetAppointmentDetailsResponse( appointmentSummary, hasAccomplishedService, + bookingsRemaining, lastName, firstName, dateOfBirth, diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/VaccinationConsultationService.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/VaccinationConsultationService.java index f6bc5d29a..7712c00ef 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/VaccinationConsultationService.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/VaccinationConsultationService.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import de.eshg.base.citizenuser.api.CitizenAccessCodeUserDto; import de.eshg.lib.appointmentblock.AppointmentTypeMapper; +import de.eshg.lib.appointmentblock.api.AppointmentDto; import de.eshg.lib.appointmentblock.persistence.AppointmentType; import de.eshg.lib.appointmentblock.persistence.entity.Appointment; import de.eshg.lib.auditlog.AuditLogger; @@ -77,6 +78,7 @@ import java.time.Clock; import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedList; @@ -750,8 +752,6 @@ public class VaccinationConsultationService { List.of( new ProcedureAccessor.CheckNotClosed(), new ProcedureAccessor.CheckCitizenUserId(citizenUserId))); - boolean hasAccomplishedService = - procedureStep.getServices().stream().anyMatch(VcService::isAccomplished); UUID patientId = procedureStep.getVaccinationConsultation().getPatientIdsFromCentralFile().getFirst(); @@ -760,8 +760,7 @@ public class VaccinationConsultationService { AppointmentSummaryDto summaryDto = vaccinationConsultationDetailsMapper.mapToAppointmentSummaryInterfaceType(procedureStep); - return AppointmentDetailsMapper.mapToDetails( - summaryDto, hasAccomplishedService, patient, procedureStep.getMedicalHistory()); + return AppointmentDetailsMapper.mapToDetails(summaryDto, patient, procedureStep); } public MedicalHistoryContentDto getMedicalHistory( @@ -819,4 +818,53 @@ public class VaccinationConsultationService { } appointmentService.deleteAppointment(procedureStep); } + + public void bookCitizenAppointment( + UUID citizenUserId, UUID procedureId, UUID procedureStepId, AppointmentDto appointmentDto) { + + ProcedureStep procedureStep = + procedureAccessor.accessProcedureStep( + procedureStepId, + procedureId, + List.of( + new ProcedureAccessor.CheckNotClosed(), + new ProcedureAccessor.CheckCitizenUserId(citizenUserId))); + if (procedureStep.getServices().stream().anyMatch(VcService::isAccomplished)) { + throw new BadRequestException( + "Appointment has accomplished services and cannot be rebooked."); + } + if (procedureStep.getEarliestDate() != null) { + if (procedureStep + .getEarliestDate() + .atStartOfDay(clock.getZone()) + .toInstant() + .isAfter(appointmentDto.start())) { + throw new BadRequestException( + "Appointment has accomplished services and cannot be rebooked."); + } + } + boolean rebook = false; + if (procedureStep.getAppointment() != null + || procedureStep.getUserDefinedAppointment() != null) { + rebook = true; + procedureStep.setAppointment(null); + procedureStep.setUserDefinedAppointment(null); + } + int remainingBookings = procedureStep.getBookingsRemaining(); + + if (remainingBookings > 0) { + appointmentService.createBlockAppointmentForStep( + procedureStep, + appointmentDto.start(), + Math.toIntExact( + ChronoUnit.MINUTES.between(appointmentDto.start(), appointmentDto.end()))); + + } else { + throw new BadRequestException("No more bookings available. 2 rebookings max. allowed."); + } + + if (rebook) { + procedureStep.setBookingsRemaining(remainingBookings - 1); + } + } } diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/GetAppointmentDetailsResponse.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/GetAppointmentDetailsResponse.java index 660458f8f..77d8fe9cd 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/GetAppointmentDetailsResponse.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/GetAppointmentDetailsResponse.java @@ -13,6 +13,7 @@ import java.time.LocalDate; public record GetAppointmentDetailsResponse( @NotNull @Valid AppointmentSummaryDto summaryDto, @NotNull boolean hasAccomplishedService, + @NotNull int bookingsRemaining, @NotBlank String lastName, @NotBlank String firstName, @NotNull LocalDate dateOfBirth, diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PatchVaccinationConsultationTravelDetailsRequest.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PatchVaccinationConsultationTravelDetailsRequest.java index da92c544b..5899291c1 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PatchVaccinationConsultationTravelDetailsRequest.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PatchVaccinationConsultationTravelDetailsRequest.java @@ -5,14 +5,14 @@ package de.eshg.travelmedicine.vaccinationconsultation.api; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.util.List; public record PatchVaccinationConsultationTravelDetailsRequest( @NotNull TravelTypeDto travelType, - @NotNull List<CountryCodeDto> travelDestinations, + @NotNull List<CountryCode> travelDestinations, LocalDate travelStartDate, Integer travelTimeAmount, TravelTimeUnitDto travelTimeUnit) {} diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PatientDto.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PatientDto.java index b85d38634..a462f1ea8 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PatientDto.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PatientDto.java @@ -5,9 +5,9 @@ package de.eshg.travelmedicine.vaccinationconsultation.api; -import de.eshg.base.CountryCodeDto; import de.eshg.base.GenderDto; import de.eshg.base.SalutationDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; @@ -25,7 +25,7 @@ public record PatientDto( @NotNull LocalDate dateOfBirth, List<@Email String> emailAddresses, List<@NotBlank @Size(min = 1, max = 23) String> phoneNumbers, - CountryCodeDto countryOfBirth, + CountryCode countryOfBirth, @Size(min = 1, max = 40) String nameAtBirth, @Size(min = 1, max = 50) String placeOfBirth, @Size(min = 1, max = 119) String title, diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PersonAddressDto.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PersonAddressDto.java index 20b01a819..e1125f0cc 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PersonAddressDto.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PersonAddressDto.java @@ -5,7 +5,7 @@ package de.eshg.travelmedicine.vaccinationconsultation.api; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -13,7 +13,7 @@ import jakarta.validation.constraints.Size; @Schema(name = "PersonAddress") public record PersonAddressDto( - @NotNull CountryCodeDto country, + @NotNull CountryCode country, @NotBlank @Size(max = 50) String city, @NotBlank String postalCode, @NotBlank @Size(max = 55) String street, diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PostVaccinationConsultationRequest.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PostVaccinationConsultationRequest.java index 3c0e9ce74..eee284d0b 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PostVaccinationConsultationRequest.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/PostVaccinationConsultationRequest.java @@ -5,8 +5,8 @@ package de.eshg.travelmedicine.vaccinationconsultation.api; -import de.eshg.base.CountryCodeDto; import de.eshg.lib.appointmentblock.api.AppointmentTypeDto; +import de.eshg.lib.common.CountryCode; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; @@ -17,7 +17,7 @@ import java.util.List; public record PostVaccinationConsultationRequest( @NotNull @Valid PatientDto patient, @NotNull TravelTypeDto travelType, - @NotNull List<@NotNull CountryCodeDto> travelDestinations, + @NotNull List<@NotNull CountryCode> travelDestinations, LocalDate travelStartDate, Integer travelTimeAmount, TravelTimeUnitDto travelTimeUnit, diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/TravelInformationDto.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/TravelInformationDto.java index 45ea6ee16..3bdb5b2c0 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/TravelInformationDto.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/api/TravelInformationDto.java @@ -5,7 +5,7 @@ package de.eshg.travelmedicine.vaccinationconsultation.api; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; @@ -14,7 +14,7 @@ import java.util.List; @Schema(name = "TravelInformation") public record TravelInformationDto( @NotNull TravelTypeDto travelType, - @NotNull List<CountryCodeDto> travelDestinations, + @NotNull List<CountryCode> travelDestinations, LocalDate travelStartDate, Integer travelTimeAmount, TravelTimeUnitDto travelTimeUnit) {} diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/persistence/entity/ProcedureStep.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/persistence/entity/ProcedureStep.java index 4e3f77f76..3654998fe 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/persistence/entity/ProcedureStep.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/persistence/entity/ProcedureStep.java @@ -55,6 +55,11 @@ public class ProcedureStep extends GloballyUniqueEntityBase implements EntityWit @Column private LocalDate earliestDate; + @DataSensitivity(SensitivityLevel.PUBLIC) + @NotNull + @Column + private int bookingsRemaining = 2; + @DataSensitivity(SensitivityLevel.PUBLIC) @OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true) private UserDefinedAppointment userDefinedAppointment; @@ -139,6 +144,14 @@ public class ProcedureStep extends GloballyUniqueEntityBase implements EntityWit this.earliestDate = earliestDate; } + public int getBookingsRemaining() { + return bookingsRemaining; + } + + public void setBookingsRemaining(int bookingsRemaining) { + this.bookingsRemaining = bookingsRemaining; + } + public UserDefinedAppointment getUserDefinedAppointment() { return userDefinedAppointment; } diff --git a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/persistence/entity/VaccinationConsultation.java b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/persistence/entity/VaccinationConsultation.java index 36c449702..65b78479b 100644 --- a/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/persistence/entity/VaccinationConsultation.java +++ b/backend/travel-medicine/src/main/java/de/eshg/travelmedicine/vaccinationconsultation/persistence/entity/VaccinationConsultation.java @@ -5,7 +5,7 @@ package de.eshg.travelmedicine.vaccinationconsultation.persistence.entity; -import de.eshg.base.CountryCodeDto; +import de.eshg.lib.common.CountryCode; import de.eshg.lib.common.DataSensitivity; import de.eshg.lib.common.SensitivityLevel; import de.eshg.lib.procedure.domain.model.Procedure; @@ -47,7 +47,7 @@ public class VaccinationConsultation @NotNull @ElementCollection @OrderColumn - private List<CountryCodeDto> travelDestinations; + private List<CountryCode> travelDestinations; @DataSensitivity(SensitivityLevel.PSEUDONYMIZED) @Column @@ -105,11 +105,11 @@ public class VaccinationConsultation this.travelType = travelType; } - public List<CountryCodeDto> getTravelDestinations() { + public List<CountryCode> getTravelDestinations() { return travelDestinations; } - public void setTravelDestinations(List<CountryCodeDto> travelDestinations) { + public void setTravelDestinations(List<CountryCode> travelDestinations) { this.travelDestinations = travelDestinations; } diff --git a/backend/travel-medicine/src/main/resources/migrations/0029_add_bookings_remaining_to_ps.xml b/backend/travel-medicine/src/main/resources/migrations/0029_add_bookings_remaining_to_ps.xml new file mode 100644 index 000000000..ceceecf0c --- /dev/null +++ b/backend/travel-medicine/src/main/resources/migrations/0029_add_bookings_remaining_to_ps.xml @@ -0,0 +1,19 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 SCOOP Software GmbH, cronn GmbH + SPDX-License-Identifier: AGPL-3.0-only +--> + +<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="1728031283614-1"> + <addColumn tableName="procedure_step"> + <column name="bookings_remaining" type="int4" defaultValue="0"> + <constraints nullable="false"/> + </column> + </addColumn> + <dropDefaultValue columnDataType="int4" columnName="bookings_remaining" + tableName="procedure_step"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/travel-medicine/src/main/resources/migrations/0030_introduce_procedure_file_type.xml b/backend/travel-medicine/src/main/resources/migrations/0030_introduce_procedure_file_type.xml new file mode 100644 index 000000000..1ee443742 --- /dev/null +++ b/backend/travel-medicine/src/main/resources/migrations/0030_introduce_procedure_file_type.xml @@ -0,0 +1,11 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 SCOOP Software GmbH, 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="1728922179310-1"> + <ext:renamePostgresEnumType oldName="fileType" newName="procedureFileType"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/travel-medicine/src/main/resources/migrations/0031_add_medical_registry_procedure_types.xml b/backend/travel-medicine/src/main/resources/migrations/0031_add_medical_registry_procedure_types.xml new file mode 100644 index 000000000..dde1c1bfc --- /dev/null +++ b/backend/travel-medicine/src/main/resources/migrations/0031_add_medical_registry_procedure_types.xml @@ -0,0 +1,14 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<!-- + Copyright 2024 SCOOP Software GmbH, 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="1728391007699-1"> + <ext:addPostgresEnumValues enumTypeName="proceduretype" valuesToAdd="MEDICAL_REGISTRY_CITIZEN_DRAFT, MEDICAL_REGISTRY_EMPLOYEE_DRAFT, MEDICAL_REGISTRY_ENTRY"/> + </changeSet> + <changeSet author="GA-Lotse" id="1728646892424-1"> + <ext:addPostgresEnumValues enumTypeName="persontype" valuesToAdd="PROFESSIONAL"/> + </changeSet> +</databaseChangeLog> diff --git a/backend/travel-medicine/src/main/resources/migrations/changelog.xml b/backend/travel-medicine/src/main/resources/migrations/changelog.xml index 14f2a4509..b84ce0a23 100644 --- a/backend/travel-medicine/src/main/resources/migrations/changelog.xml +++ b/backend/travel-medicine/src/main/resources/migrations/changelog.xml @@ -36,5 +36,8 @@ <include file="migrations/0026_add_citizen_has_answered.xml"/> <include file="migrations/0027_add_cancelled_to_uda.xml"/> <include file="migrations/0028_change_information_statement_template.xml"/> + <include file="migrations/0029_add_bookings_remaining_to_ps.xml"/> + <include file="migrations/0030_introduce_procedure_file_type.xml"/> + <include file="migrations/0031_add_medical_registry_procedure_types.xml"/> </databaseChangeLog> diff --git a/buildSrc/src/main/groovy/de/eshg/frontend/PlaywrightTask.groovy b/buildSrc/src/main/groovy/de/eshg/frontend/PlaywrightTask.groovy index a36b82dcd..1180117fc 100644 --- a/buildSrc/src/main/groovy/de/eshg/frontend/PlaywrightTask.groovy +++ b/buildSrc/src/main/groovy/de/eshg/frontend/PlaywrightTask.groovy @@ -22,11 +22,6 @@ abstract class PlaywrightTask extends PnpmTask { environment.put("RUN_A11Y_TESTS", "true") } - if (project.hasProperty("proxy")) { - environment.put("PLAYWRIGHT_PROXY", "true") - environment.put("MULTI_INSTANCE", "true") - } - def task = super.configure(closure) task.dependsOn("prepareEnvironment") @@ -34,7 +29,12 @@ abstract class PlaywrightTask extends PnpmTask { task.inputs.dir("${project.projectDir}/data/test/validation") task.outputs.dir("${project.projectDir}/data/test/output") - task.args = ['playwright'] + additionalArgs + def smokeTestEnv = project.findProperty("smokeTestEnv") + if (smokeTestEnv != null && additionalArgs.contains('test')) { + task.args = ['playwright'] + additionalArgs + ['-c', "src/config/playwright.${smokeTestEnv}.config.ts"] + } else { + task.args = ['playwright'] + additionalArgs + } return task } diff --git a/buildSrc/src/main/groovy/node.gradle b/buildSrc/src/main/groovy/node.gradle index 956bc459a..63ad67518 100644 --- a/buildSrc/src/main/groovy/node.gradle +++ b/buildSrc/src/main/groovy/node.gradle @@ -3,15 +3,15 @@ plugins { } node { - version = '20.17.0' - pnpmVersion = '9.10.0' + version = '20.18.0' + pnpmVersion = '9.12.1' download = true workDir = file("${rootProject.projectDir}/.gradle/nodejs") pnpmWorkDir = file("${rootProject.projectDir}/.gradle/pnpm") } ext { - nodeDockerImage = "node:${node.version.get()}-alpine@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45" + nodeDockerImage = "node:${node.version.get()}-alpine@sha256:c13b26e7e602ef2f1074aef304ce6e9b7dd284c419b35d89fcf3cc8e44a8def9" } tasks.register('cleanDependencies', Delete) { diff --git a/citizen-portal/.env b/citizen-portal/.env index f49a5e1e3..575edf89a 100644 --- a/citizen-portal/.env +++ b/citizen-portal/.env @@ -3,3 +3,5 @@ PUBLIC_BASE_BACKEND_URL=http://localhost:4001/api/base PUBLIC_MEASLES_PROTECTION_BACKEND_URL=http://localhost:4001/api/measles-protection PUBLIC_SCHOOL_ENTRY_BACKEND_URL=http://localhost:4001/api/school-entry PUBLIC_TRAVEL_MEDICINE_BACKEND_URL=http://localhost:4001/api/travel-medicine + +MARKDOWN_PAGE_DIRECTORY=frankfurt diff --git a/citizen-portal/package.json b/citizen-portal/package.json index 9c951ed51..6286648c5 100644 --- a/citizen-portal/package.json +++ b/citizen-portal/package.json @@ -12,26 +12,29 @@ "@mui/icons-material": "5.16.7", "@mui/joy": "5.0.0-beta.48", "@mui/material": "npm:@mui/joy@5.0.0-beta.48", - "@tanstack/react-query": "5.56.2", + "@mdx-js/mdx": "3.0.1", + "@tanstack/react-query": "5.59.10", "@tanstack/react-table": "8.20.5", "@types/negotiator": "0.6.3", - "i18next": "23.15.1", + "server-only": "0.0.1", + "i18next": "23.15.2", "i18next-resources-to-backend": "1.2.1", "negotiator": "0.6.3", - "next": "14.2.12", + "next": "14.2.14", "react": "18.3.1", "react-dom": "18.3.1", - "valibot": "0.42.0" + "valibot": "0.42.1" }, "devDependencies": { - "@next/bundle-analyzer": "14.2.12", - "@tanstack/eslint-plugin-query": "5.56.1", - "@types/react": "18.3.7", - "@types/react-dom": "18.3.0", - "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-istanbul": "2.1.1", - "eslint-config-next": "14.2.12", + "@next/bundle-analyzer": "14.2.14", + "@tanstack/eslint-plugin-query": "5.59.7", + "@types/mdx": "2.0.13", + "@types/react": "18.3.11", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "4.3.2", + "@vitest/coverage-istanbul": "2.1.2", + "eslint-config-next": "14.2.14", "vite-tsconfig-paths": "5.0.1", - "vitest": "2.1.1" + "vitest": "2.1.2" } } diff --git a/citizen-portal/public/markdown/frankfurt/accessibility.md b/citizen-portal/public/markdown/frankfurt/accessibility.md new file mode 100644 index 000000000..23b2bf19b --- /dev/null +++ b/citizen-portal/public/markdown/frankfurt/accessibility.md @@ -0,0 +1,50 @@ +Diese Erklärung zur digitalen Barrierefreiheit gilt für die unter frankfurt.ga-lotse.de veröffentlichte Webseite. + +Als öffentliche Stelle im Sinne der Richtlinie (EU) 2016/2102 sind wir bemüht, unsere Websites und mobilen Anwendungen im Einklang mit den Bestimmungen des Hessischen Behinderten-Gleichstellungsgesetzes (HessBGG) sowie der Hessischen Verordnung über barrierefreie Informationstechnik (BITV HE 2019) zur Umsetzung der Richtlinie (EU) 2016/2102 barrierefrei zugänglich zu machen. Frankfurt.ga-lotse.de ist überwiegend mit den derzeit gültigen Vorschriften zur Barrierefreiheit (BITV 2.0, 2019/WCAG 2.1) vereinbar. Inhalte und Funktionen, die dem derzeit noch nicht vollständig entsprechen, sind nachfolgend aufgeführt. + +## Stand der Vereinbarkeit mit den Anforderungen + +Die Anforderungen der Barrierefreiheit ergeben sich aus § 3 Absätze 1 bis 4 und § 4 der BITV HE 2019, die auf Grundlage von § 14 des HessBGG erlassen wurde. + +Die Überprüfung der Einhaltung der Anforderungen beruht auf einer am 16.09.2024 durchgeführten Selbstbewertung. +Nicht barrierefreie Inhalte + +## Nicht barrierefreie Inhalte + +Aufgrund der Überprüfung ist die Website mit den zuvor genannten Anforderungen nur teilweise vereinbar. + +- PDF-Dateien sind nicht vollständig barrierefrei + +Die Stadt Frankfurt am Main arbeitet daran, die barrierefreien Angebote weiter auszubauen. + +## Datum der Erstellung der Erklärung zur Barrierefreiheit + +Diese Erklärung wurde am 16.09.2024 erstellt und zuletzt am 23.09.2024 überprüft und aktualisiert. + +## Feedback und Anfragen zur digitalen Barrierefreiheit + +Sie möchten uns noch bestehende Barrieren mitteilen oder nicht barrierefreie Inhalte in einem barrierefreien Format anfordern? Sprechen Sie unsere verantwortlichen Kontaktpersonen an: + +Gesundheitsamt Frankfurt am Main +Digitale Zukunft, IT und strategische Planung ++49 (0) 800 -4256873 +[support@ga-lotse.de](mailto:support@ga-lotse.de) + +## Durchsetzungsverfahren + +Wenn auch nach Ihrem Feedback an den oben genannten Kontakt keine zufriedenstellende Lösung gefunden wurde, können Sie die Durchsetzungs- und Überwachungsstelle Barrierefreie Informationstechnik einschalten. Sie haben nach Ablauf einer Frist von sechs Wochen das Recht sich direkt an die Durchsetzungs- und Überwachungsstelle zu wenden. Unter Einbeziehung aller Beteiligten versucht die Durchsetzungsstelle, die Umstände der fehlenden Barrierefreiheit zu ermitteln, damit der Träger diese beheben kann. + + +## Durchsetzungs- und Überwachungsstelle Barrierefreie Informationstechnik Hessisches Ministerium für Soziales und Integration Sitz: Regierungspräsidium Gießen + +Prof. Dr. Erdmuthe Meyer zu Bexten +Landesbeauftragte für barrierefreie IT +Leiterin der Durchsetzungs- und Überwachungsstelle +Landgraf-Philipp-Platz 1-7 +35390 Gießen +Telefon: +49 641 303 - 2901 +E-Mail: [Durchsetzungsstelle-LBIT@rpgi.hessen.de](mailto:Durchsetzungsstelle-LBIT@rpgi.hessen.de) + + + +[Durchsetzung beantragen](https://lbit.hessen.de/Durchsetzungs-und-Ueberwachungsstelle/Durchsetzungsverfahren-beantragen/Formular-Durchsetzungsverfahren) diff --git a/citizen-portal/public/markdown/frankfurt/imprint.md b/citizen-portal/public/markdown/frankfurt/imprint.md new file mode 100644 index 000000000..23e15bdb0 --- /dev/null +++ b/citizen-portal/public/markdown/frankfurt/imprint.md @@ -0,0 +1,53 @@ +## Gesamtverantwortung: + +Stadt Frankfurt am Main +DER MAGISTRAT +Römerberg 23 +60311 Frankfurt am Main +Website: www.frankfurt.de + +USt-ID: DE 114 110 388 + +## Verantwortung für das GA-Lotse Online-Portal: + +Stadt Frankfurt am Main +DER MAGISTRAT +Gesundheitsamt Frankfurt am Main +Abteilung Digitale Zukunft, IT und strategische Planung +Breite Gasse 28 +60313 Frankfurt am Main + +GA-Lotse ist ein Kooperationsprojekt des Hessischen Ministeriums für Familie, Senioren, Sport, Gesundheit und Pflege mit dem Gesundheitsamt Frankfurt unter der EU-Förderung NextGenerationEU. + +## Telefonische Auskünfte: + +Informationen erhalten Sie über die Rufnummer: +49 (0) 800 -4256873 + +## Kontakt bei Presseanfragen: + +[gesundheitsamt.einheitliche-software@stadt-frankfurt.de](mailto:gesundheitsamt.einheitliche-software@stadt-frankfurt.de) + +## Kontakt bei Fragen zum GA-Lotse Online-Portal: + +eMail: gesundheitsamt.einheitliche-software@stadt-frankfurt.de +Die Abteilung Digitale Zukunft, IT und strategische Planung des Gesundheitsamtes der Stadt Frankfurt am Main zeichnet für ihre Inhalte auf www.ga-lotse.de redaktionell verantwortlich. + +## Verantwortung: + +Stefanie Kaulich, Abteilungsleitung Digitale Zukunft, IT und strategische Planung. +Bei Fragen oder Anregungen zu konkreten Inhalten und Seiten können Sie sich gerne an Frau Kaulich oder die unter „Kontakt“ benannte eMail wenden. + +## Technische Realisierung: + +Gesundheitsamt der Stadt Frankfurt am Main +Abteilung Digitale Zukunft, IT und strategische Planung +Breite Gasse 28 +60313 Frankfurt am Main + +## Bei Fragen oder Anmerkungen: + +[gesundheitsamt.einheitliche-software@stadt-frankfurt.de](mailto:gesundheitsamt.einheitliche-software@stadt-frankfurt.de) + +## Hinweise zum Datenschutz: + +Informationen zum Datenschutz finden Sie [hier](/datenschutz). diff --git a/citizen-portal/public/markdown/frankfurt/privacy.md b/citizen-portal/public/markdown/frankfurt/privacy.md new file mode 100644 index 000000000..dfb98cc1e --- /dev/null +++ b/citizen-portal/public/markdown/frankfurt/privacy.md @@ -0,0 +1,103 @@ +Diese Datenschutzerklärung gilt für die Webseite „frankfurt.ga-lotse.de“ (bzw. „https://frankfurt.ga-lotse.de“ sowie dazu zugehörige Subdomains) des Gesundheitsamts der Stadt Frankfurt am Main. Dieses Informationsportal bietet Informationen zu besonderen Ereignissen und wird ausschließlich zum dem Zwecke genutzt. + + +## 1. Name und Kontaktdaten des für die Verarbeitung Verantwortlichen sowie des behördlichen Datenschutzbeauftragten + +Diese Datenschutz-Information gilt für die Datenverarbeitung durch: + +Verantwortlicher: + +Verantwortlich für die Website „frankfurt.ga-lotse.de“ ist das Gesundheitsamt Frankfurt am Main: + +Gesundheitsamt Frankfurt am Main +Breite Gasse 28 +60313 Frankfurt am Main +E-Mail: [datenschutz.gesundheitsamt@stadt-frankfurt.de](mailto:datenschutz.gesundheitsamt@stadt-frankfurt.de) + +Behördlicher Datenschutzbeauftragter: + +Referat Datenschutz und IT-Sicherheit +Sandgasse 6, 60311 Frankfurt am Main + +## 2. Erhebung und Speicherung personenbezogener Daten sowie Art und Zweck von deren Verwendung" + +2.1 Beim Besuch der Website + +Beim Aufrufen unserer Website „frankfurt.ga-lotse.de“ werden durch den auf Ihrem Endgerät zum Einsatz kommenden Browser automatisch Informationen an den Server unserer Website gesendet. Diese Informationen werden temporär in einem sog. Logfile gespeichert. Folgende Informationen werden dabei ohne Ihr Zutun erfasst und bis zur automatisierten Löschung gespeichert: + + +- IP-Adresse des anfragenden Rechners +- Datum und Uhrzeit des Zugriffs +- Name und URL der abgerufenen Datei +- Website, von der aus der Zugriff erfolgt (Referrer-URL) +- verwendeter Browser und ggf. das Betriebssystem Ihres Rechners sowie der Name Ihres Access-Providers + +Die genannten Daten werden durch uns zu folgenden Zwecken verarbeitet: + +- Gewährleistung eines reibungslosen Verbindungsaufbaus der Website +- Gewährleistung einer komfortablen Nutzung unserer Website +- Auswertung der Systemsicherheit und -stabilität +- Rückverfolgung etwaiger DoS Attacken +- sowie zu weiteren administrativen Zwecken + +**2.2 Beim Verwenden des QR-Codes** + +Falls Ihnen für den Zugang zum Online-Portal ein individualisierter QR-Code gegeben wurde, hat dieser den Zweck, Informationen oder Nachrichten speziell für Sie zur Verfügung zu stellen, beispielsweise als betroffener eines gesundheitsrelevanten Ereignisses. Nach Einscannen des Codes wird man direkt auf die entsprechende Informationsseite weitergeleitet. Im System wird dabei protokolliert, zu welchem Zeitpunkt der QR-Code verwendet wurde, jedoch ohne weitere Angaben wie IP-Adresse oder Namen o.ä. + +Die Rechtsgrundlage für die Datenverarbeitung ist Art. 6 Abs. 1 S. 1 lit. f DS-GVO. Unser berechtigtes Interesse folgt aus oben aufgelisteten Zwecken zur Datenerhebung. In keinem Fall verwenden wir die erhobenen Daten zu dem Zweck, Rückschlüsse auf Ihre Person zu ziehen. + +Darüber hinaus setzen wir beim Besuch unserer Website Cookies ein. Nähere Erläuterungen dazu erhalten Sie unter den Ziff. 4 dieser Datenschutzerklärung. + +## 3. Weitergabe von Daten + +Es findet keine Weitergabe von Daten an Dritte statt. + +## 4. Cookies + +Wir setzen auf unserer Seite Cookies ein. Hierbei handelt es sich um kleine Dateien, die Ihr Browser automatisch erstellt und die auf Ihrem Endgerät (Laptop, Tablet, Smartphone o.ä.) gespeichert werden, wenn Sie unsere Seite besuchen. Cookies richten auf Ihrem Endgerät keinen Schaden an, enthalten keine Viren, Trojaner oder sonstige Schadsoftware. + +In dem Cookie werden Informationen abgelegt, die sich jeweils im Zusammenhang mit dem spezifisch eingesetzten Endgerät ergeben. Dies bedeutet jedoch nicht, dass wir dadurch unmittelbar Kenntnis von Ihrer Identität erhalten. + +Der Einsatz von Cookies dient einerseits dazu, die Nutzung unseres Angebots für Sie angenehmer zu gestalten. So setzen wir sogenannte Session-Cookies ein, um zu erkennen, dass Sie einzelne Seiten unserer Website bereits besucht haben. Diese werden nach Verlassen unserer Seite automatisch gelöscht. + +Darüber hinaus setzen wir ebenfalls zur Optimierung der Benutzerfreundlichkeit temporäre Cookies ein, die für einen bestimmten festgelegten Zeitraum auf Ihrem Endgerät gespeichert werden. Besuchen Sie unsere Seite erneut, um unsere Dienste in Anspruch zu nehmen, wird automatischerkannt, dass Sie bereits bei uns waren und welche Eingaben und Einstellungen sie getätigt haben, um diese nicht noch einmal eingeben zu müssen. + +Die durch Cookies verarbeiteten Daten sind für die genannten Zwecke zur Wahrung unserer berechtigten Interessen erforderlich. + +Die meisten Browser akzeptieren Cookies automatisch. Sie können Ihren Browser jedoch so konfigurieren, dass keine Cookies auf Ihrem Computer gespeichert werden oder stets ein Hinweis erscheint, bevor ein neuer Cookie angelegt wird. Die vollständige Deaktivierung von Cookies kann jedoch dazu führen, dass Sie nicht alle Funktionen unserer Website nutzen können. + +## 5. Analyse-Tools + +Es werden keine Analyse-Tools verwendet. + +## 6. Social Media Plug-ins + +Es werden keine Social Media Plug-Ins verwendet. + +## 7. Betroffenenrechte + +Sie haben das Recht: + +- gemäß Art. 15 DS-GVO Auskunft über Ihre von uns verarbeiteten personenbezogenen Daten zu verlangen. Insbesondere können Sie Auskunft über die Verarbeitungszwecke, die Kategorie der personenbezogenen Daten, die Kategorien von Empfängern, gegenüber denen Ihre Daten offengelegt wurden oder werden, die geplante Speicherdauer, das Bestehen eines Rechts auf Berichtigung, Löschung, Einschränkung der Verarbeitung oder Widerspruch, das Bestehen eines Beschwerderechts, die Herkunft ihrer Daten, sofern diese nicht bei uns erhoben wurden sowie über das Bestehen einer automatisierten Entscheidungsfindung einschließlich Profiling und ggf. aussagekräftigen Informationen zu deren Einzelheiten verlangen. +- gemäß Art. 16 DS-GVO unverzüglich die Berichtigung unrichtiger oder Vervollständigung Ihrer bei uns gespeicherten personenbezogenen Daten zu verlangen. +- gemäß Art. 17 DS-GVO die Löschung Ihrer bei uns gespeicherten personenbezogenen Daten zu verlangen, soweit nicht die Verarbeitung zur Ausübung des Rechts auf freie Meinungsäußerung und Information, zur Erfüllung einer rechtlichen Verpflichtung, aus Gründen des öffentlichen Interesses oder zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen erforderlich ist. +- gemäß Art. 18 DS-GVO die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen, soweit die Richtigkeit der Daten von Ihnen bestritten wird, die Verarbeitung unrechtmäßig ist, Sie aber deren Löschung ablehnen und wir die Daten nicht mehr benötigen, Sie jedoch diese zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen benötigen oder Sie gemäß Art. 21 DS-GVO Widerspruch gegen die Verarbeitung eingelegt haben. +- gemäß Art. 20 DS-GVO Ihre personenbezogenen Daten, die Sie uns bereitgestellt haben, in einem strukturierten, gängigen und maschinenlesebaren Format zu erhalten oder die Übermittlung an einen anderen Verantwortlichen zu verlangen. +- gemäß Art. 7 Abs. 3 DS-GVO Ihre einmal erteilte Einwilligung jederzeit gegenüber uns zu widerrufen. Dies hat zur Folge, dass wir die Datenverarbeitung, die auf dieser Einwilligung beruhte, für die Zukunft nicht mehr fortführen dürfen und +- gemäß Art. 77 DS-GVO sich bei der zuständigen Aufsichtsbehörde zu beschweren. Die zuständige Aufsichtsbehörde ist: Der Hessische Datenschutzbeauftragte, Postfach 3163, 65021 Wiesbaden, Telefon: 0611/1408 - 0, poststelle@datenschutz-hessen.de. + +## 8. Widerspruchsrecht + +Sofern Ihre personenbezogenen Daten auf Grundlage von berechtigten Interessen gemäß Art. 6 Abs. 1 S. 1 lit. f DS-GVO verarbeitet werden, haben Sie das Recht, gemäß Art. 21 DS-GVO Widerspruch gegen die Verarbeitung Ihrer personenbezogenen Daten einzulegen, soweit dafür Gründe vorliegen, die sich aus Ihrer besonderen Situation ergeben. Möchten Sie von Ihrem Widerrufs- oder Widerspruchsrecht Gebrauch machen, genügt eine E-Mail an datenschutz.gesundheitsamt@stadt-frankfurt.de. + +## 9. Datensicherheit + +Wir bedienen uns geeigneter technischer und organisatorischer Sicherheitsmaßnahmen, um Ihre Daten gegen zufällige oder vorsätzliche Manipulationen, teilweisen oder vollständigen Verlust, Zerstörung oder gegen den unbefugten Zugriff Dritter zu schützen. Unsere Sicherheitsmaßnahmen werden nach dem jeweiligen Stand der Technik gemäß Art. 32 DS-GVO fortlaufend angepasst. + +## 10. Auftragsverarbeitung + +Es findet keine Auftragsverarbeitung der erhobenen Daten statt. + +## 11. Aktualität und Änderung dieser Datenschutzerklärung + +Diese Datenschutzerklärung ist aktuell gültig und hat den Stand September 2024. diff --git a/citizen-portal/src/app/[lang]/(privatpersonen)/impfberatung/meine-termine/details/buchen/page.tsx b/citizen-portal/src/app/[lang]/(privatpersonen)/impfberatung/meine-termine/details/buchen/page.tsx new file mode 100644 index 000000000..2ffca7016 --- /dev/null +++ b/citizen-portal/src/app/[lang]/(privatpersonen)/impfberatung/meine-termine/details/buchen/page.tsx @@ -0,0 +1,28 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +"use client"; + +import { IdContextProvider } from "@/lib/businessModules/travelMedicine/components/shared/contexts/IdContext"; +import { AppointmentPageTitle } from "@/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentPageTitle"; +import { RebookAppointmentPageContent } from "@/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentPageContent"; +import { useTranslation } from "@/lib/i18n/client"; +import { PageContent } from "@/lib/shared/components/layout/PageContent"; +import { PageLayout } from "@/lib/shared/components/layout/page"; + +export default function RebookAppointmentPage() { + const { t } = useTranslation(["travelMedicine/rebookAppointment"]); + + return ( + <PageLayout> + <PageContent> + <IdContextProvider> + <AppointmentPageTitle title={t("header.title")} /> + <RebookAppointmentPageContent /> + </IdContextProvider> + </PageContent> + </PageLayout> + ); +} diff --git a/citizen-portal/src/app/[lang]/(static)/barrierefreiheit/page.tsx b/citizen-portal/src/app/[lang]/(static)/barrierefreiheit/page.tsx new file mode 100644 index 000000000..255e9602d --- /dev/null +++ b/citizen-portal/src/app/[lang]/(static)/barrierefreiheit/page.tsx @@ -0,0 +1,15 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownPage } from "@/lib/baseModule/components/MarkdownPage"; + +export default function AccessibilityPage() { + return ( + <MarkdownPage + pageType="accessibility" + title="Erklärung zur Barrierefreiheit" + /> + ); +} diff --git a/citizen-portal/src/app/[lang]/(static)/datenschutz/page.tsx b/citizen-portal/src/app/[lang]/(static)/datenschutz/page.tsx new file mode 100644 index 000000000..3573e8edd --- /dev/null +++ b/citizen-portal/src/app/[lang]/(static)/datenschutz/page.tsx @@ -0,0 +1,10 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownPage } from "@/lib/baseModule/components/MarkdownPage"; + +export default function PrivacyPolicyPage() { + return <MarkdownPage pageType="privacy" title="Datenschutzerklärung" />; +} diff --git a/citizen-portal/src/app/[lang]/(static)/impressum/page.tsx b/citizen-portal/src/app/[lang]/(static)/impressum/page.tsx new file mode 100644 index 000000000..1059ea230 --- /dev/null +++ b/citizen-portal/src/app/[lang]/(static)/impressum/page.tsx @@ -0,0 +1,10 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownPage } from "@/lib/baseModule/components/MarkdownPage"; + +export default function ImprintPage() { + return <MarkdownPage pageType="imprint" title="Impressum" />; +} diff --git a/citizen-portal/src/app/[lang]/kontakt/page.tsx b/citizen-portal/src/app/[lang]/(static)/kontakt/page.tsx similarity index 100% rename from citizen-portal/src/app/[lang]/kontakt/page.tsx rename to citizen-portal/src/app/[lang]/(static)/kontakt/page.tsx diff --git a/citizen-portal/src/app/[lang]/nutzungshinweise/page.tsx b/citizen-portal/src/app/[lang]/(static)/nutzungshinweise/page.tsx similarity index 100% rename from citizen-portal/src/app/[lang]/nutzungshinweise/page.tsx rename to citizen-portal/src/app/[lang]/(static)/nutzungshinweise/page.tsx diff --git a/citizen-portal/src/app/[lang]/barrierefreiheit/page.tsx b/citizen-portal/src/app/[lang]/barrierefreiheit/page.tsx deleted file mode 100644 index 37dd04c21..000000000 --- a/citizen-portal/src/app/[lang]/barrierefreiheit/page.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -"use client"; - -import { ExternalLink } from "@eshg/lib-portal/components/navigation/ExternalLink"; -import { Typography } from "@mui/joy"; - -import { TitleAndSheetContentLayout } from "@/lib/shared/components/layout/TitleAndSheetContentLayout"; - -export default function AccessibilityPage() { - return ( - <TitleAndSheetContentLayout pageTitle="Erklärung zur Barrierefreiheit"> - <Typography> - Diese Erklärung zur digitalen Barrierefreiheit gilt für die unter - frankfurt.ga-lotse.de veröffentlichte Webseite. - </Typography> - - <Typography> - Als öffentliche Stelle im Sinne der Richtlinie (EU) 2016/2102 sind wir - bemüht, unsere Websites und mobilen Anwendungen im Einklang mit den - Bestimmungen des Hessischen Behinderten-Gleichstellungsgesetzes - (HessBGG) sowie der Hessischen Verordnung über barrierefreie - Informationstechnik (BITV HE 2019) zur Umsetzung der Richtlinie (EU) - 2016/2102 barrierefrei zugänglich zu machen. Frankfurt.ga-lotse.de ist - überwiegend mit den derzeit gültigen Vorschriften zur Barrierefreiheit - (BITV 2.0, 2019/WCAG 2.1) vereinbar. Inhalte und Funktionen, die dem - derzeit noch nicht vollständig entsprechen, sind nachfolgend aufgeführt. - </Typography> - - <Typography level="h4" component="h3"> - Stand der Vereinbarkeit mit den Anforderungen - </Typography> - - <Typography> - Die Anforderungen der Barrierefreiheit ergeben sich aus § 3 Absätze 1 - bis 4 und § 4 der BITV HE 2019, die auf Grundlage von § 14 des HessBGG - erlassen wurde. - </Typography> - - <Typography> - Die Überprüfung der Einhaltung der Anforderungen beruht auf einer am - 16.09.2024 durchgeführten Selbstbewertung. - </Typography> - - <Typography level="h4" component="h3"> - Nicht barrierefreie Inhalte - </Typography> - - <Typography> - Aufgrund der Überprüfung ist die Website mit den zuvor genannten - Anforderungen nur teilweise vereinbar. - </Typography> - <ul> - <li>PDF-Dateien sind nicht vollständig barrierefrei </li> - </ul> - - <Typography> - Die Stadt Frankfurt am Main arbeitet daran, die barrierefreien Angebote - weiter auszubauen. - </Typography> - - <Typography level="h4" component="h3"> - Datum der Erstellung der Erklärung zur Barrierefreiheit - </Typography> - - <Typography> - Diese Erklärung wurde am 16.09.2024 erstellt und zuletzt am 23.09.2024 - überprüft und aktualisiert. - </Typography> - - <Typography level="h4" component="h3"> - Feedback und Anfragen zur digitalen Barrierefreiheit - </Typography> - - <Typography> - Sie möchten uns noch bestehende Barrieren mitteilen oder nicht - barrierefreie Inhalte in einem barrierefreien Format anfordern? Sprechen - Sie unsere verantwortlichen Kontaktpersonen an: - </Typography> - <Typography> - Gesundheitsamt Frankfurt am Main - <br /> - Digitale Zukunft, IT und strategische Planung - <br /> - +49 (0) 800 -4256873 - <br /> - <ExternalLink href="mailto:support@ga-lotse.de"> - support@ga-lotse.de - </ExternalLink> - <br /> - </Typography> - - <Typography level="h4" component="h3"> - Durchsetzungsverfahren - </Typography> - - <Typography> - Wenn auch nach Ihrem Feedback an den oben genannten Kontakt keine - zufriedenstellende Lösung gefunden wurde, können Sie die Durchsetzungs- - und Überwachungsstelle Barrierefreie Informationstechnik einschalten. - Sie haben nach Ablauf einer Frist von sechs Wochen das Recht sich direkt - an die Durchsetzungs- und Überwachungsstelle zu wenden. Unter - Einbeziehung aller Beteiligten versucht die Durchsetzungsstelle, die - Umstände der fehlenden Barrierefreiheit zu ermitteln, damit der Träger - diese beheben kann. - </Typography> - - <Typography> - <strong> - Durchsetzungs- und Überwachungsstelle Barrierefreie - Informationstechnik - </strong> - <br /> - <strong>Hessisches Ministerium für Soziales und Integration</strong> - <br /> - <strong>Sitz: Regierungspräsidium Gießen</strong> - <br /> - Prof. Dr. Erdmuthe Meyer zu Bexten - <br /> - Landesbeauftragte für barrierefreie IT - <br /> - Leiterin der Durchsetzungs- und Überwachungsstelle - <br /> - Landgraf-Philipp-Platz 1-7 - <br /> - 35390 Gießen - <br /> - Telefon: +49 641 303 - 2901 - <br /> - E-Mail:{" "} - <ExternalLink href="mailto:Durchsetzungsstelle-LBIT@rpgi.hessen.de"> - Durchsetzungsstelle-LBIT@rpgi.hessen.de - </ExternalLink> - <br /> - </Typography> - - <Typography> - <ExternalLink href="https://lbit.hessen.de/Durchsetzungs-und-Ueberwachungsstelle/Durchsetzungsverfahren-beantragen/Formular-Durchsetzungsverfahren"> - Durchsetzung beantragen - </ExternalLink> - </Typography> - </TitleAndSheetContentLayout> - ); -} diff --git a/citizen-portal/src/app/[lang]/datenschutz/page.tsx b/citizen-portal/src/app/[lang]/datenschutz/page.tsx deleted file mode 100644 index ba5d1099c..000000000 --- a/citizen-portal/src/app/[lang]/datenschutz/page.tsx +++ /dev/null @@ -1,264 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -"use client"; - -import { Typography } from "@mui/joy"; - -import { TitleAndSheetContentLayout } from "@/lib/shared/components/layout/TitleAndSheetContentLayout"; - -export default function PrivacyPolicyPage() { - return ( - <TitleAndSheetContentLayout pageTitle="Datenschutzerklärung"> - <Typography> - Diese Datenschutzerklärung gilt für die Webseite „frankfurt.ga-lotse.de“ - (bzw. „https://frankfurt.ga-lotse.de“ sowie dazu zugehörige Subdomains) - des Gesundheitsamts der Stadt Frankfurt am Main. Dieses - Informationsportal bietet Informationen zu besonderen Ereignissen und - wird ausschließlich zum dem Zwecke genutzt. - </Typography> - <Typography level="h4" component="h3"> - 1. Name und Kontaktdaten des für die Verarbeitung Verantwortlichen sowie - des behördlichen Datenschutzbeauftragten - </Typography> - <Typography> - Diese Datenschutz-Information gilt für die Datenverarbeitung durch: - <br /> - Verantwortlicher: - <br /> - Verantwortlich für die Website „frankfurt.ga-lotse.de“ ist das - Gesundheitsamt Frankfurt am Main: - </Typography> - <Typography> - Gesundheitsamt Frankfurt am Main - <br /> - Breite Gasse 28 - <br /> - 60313 Frankfurt am Main - <br /> - E-Mail: datenschutz.gesundheitsamt@stadt-frankfurt.de - <br /> - </Typography> - <Typography>Behördlicher Datenschutzbeauftragter:</Typography> - <Typography> - Referat Datenschutz und IT-Sicherheit - <br /> - Sandgasse 6, 60311 Frankfurt am Main - </Typography> - <Typography level="h4" component="h3"> - 2. Erhebung und Speicherung personenbezogener Daten sowie Art und Zweck - von deren Verwendung - </Typography> - <Typography level="h4" component="h4"> - 2.1 Beim Besuch der Website - </Typography> - <Typography> - Beim Aufrufen unserer Website „frankfurt.ga-lotse.de“ werden durch den - auf Ihrem Endgerät zum Einsatz kommenden Browser automatisch - Informationen an den Server unserer Website gesendet. Diese - Informationen werden temporär in einem sog. Logfile gespeichert. - Folgende Informationen werden dabei ohne Ihr Zutun erfasst und bis zur - automatisierten Löschung gespeichert: - </Typography> - <ul> - <li>IP-Adresse des anfragenden Rechners</li> - <li>Datum und Uhrzeit des Zugriffs</li> - <li>Name und URL der abgerufenen Datei</li> - <li>Website, von der aus der Zugriff erfolgt (Referrer-URL)</li> - <li> - verwendeter Browser und ggf. das Betriebssystem Ihres Rechners sowie - der Name Ihres Access-Providers - </li> - </ul> - <Typography> - Die genannten Daten werden durch uns zu folgenden Zwecken verarbeitet: - </Typography> - <ul> - <li> - Gewährleistung eines reibungslosen Verbindungsaufbaus der Website - </li> - <li>Gewährleistung einer komfortablen Nutzung unserer Website</li> - <li>Auswertung der Systemsicherheit und -stabilität</li> - <li>Rückverfolgung etwaiger DoS Attacken</li> - <li>sowie zu weiteren administrativen Zwecken</li> - </ul> - <Typography level="h4" component="h4"> - 2.2 Beim Verwenden des QR-Codes - </Typography> - <Typography> - Falls Ihnen für den Zugang zum Online-Portal ein individualisierter - QR-Code gegeben wurde, hat dieser den Zweck, Informationen oder - Nachrichten speziell für Sie zur Verfügung zu stellen, beispielsweise - als betroffener eines gesundheitsrelevanten Ereignisses. Nach Einscannen - des Codes wird man direkt auf die entsprechende Informationsseite - weitergeleitet. Im System wird dabei protokolliert, zu welchem Zeitpunkt - der QR-Code verwendet wurde, jedoch ohne weitere Angaben wie IP-Adresse - oder Namen o.ä. - </Typography> - <Typography> - Die Rechtsgrundlage für die Datenverarbeitung ist Art. 6 Abs. 1 S. 1 - lit. f DS-GVO. Unser berechtigtes Interesse folgt aus oben aufgelisteten - Zwecken zur Datenerhebung. In keinem Fall verwenden wir die erhobenen - Daten zu dem Zweck, Rückschlüsse auf Ihre Person zu ziehen. - </Typography> - <Typography> - Darüber hinaus setzen wir beim Besuch unserer Website Cookies ein. - Nähere Erläuterungen dazu erhalten Sie unter den Ziff. 4 dieser - Datenschutzerklärung. - </Typography> - <Typography level="h4" component="h3"> - 3. Weitergabe von Daten - </Typography> - Es findet keine Weitergabe von Daten an Dritte statt. - <Typography level="h4" component="h3"> - 4. Cookies - </Typography> - <Typography> - Wir setzen auf unserer Seite Cookies ein. Hierbei handelt es sich um - kleine Dateien, die Ihr Browser automatisch erstellt und die auf Ihrem - Endgerät (Laptop, Tablet, Smartphone o.ä.) gespeichert werden, wenn Sie - unsere Seite besuchen. Cookies richten auf Ihrem Endgerät keinen Schaden - an, enthalten keine Viren, Trojaner oder sonstige Schadsoftware. - </Typography> - <Typography> - In dem Cookie werden Informationen abgelegt, die sich jeweils im - Zusammenhang mit dem spezifisch eingesetzten Endgerät ergeben. Dies - bedeutet jedoch nicht, dass wir dadurch unmittelbar Kenntnis von Ihrer - Identität erhalten. - </Typography> - <Typography> - Der Einsatz von Cookies dient einerseits dazu, die Nutzung unseres - Angebots für Sie angenehmer zu gestalten. So setzen wir sogenannte - Session-Cookies ein, um zu erkennen, dass Sie einzelne Seiten unserer - Website bereits besucht haben. Diese werden nach Verlassen unserer Seite - automatisch gelöscht. - </Typography> - <Typography> - Darüber hinaus setzen wir ebenfalls zur Optimierung der - Benutzerfreundlichkeit temporäre Cookies ein, die für einen bestimmten - festgelegten Zeitraum auf Ihrem Endgerät gespeichert werden. Besuchen - Sie unsere Seite erneut, um unsere Dienste in Anspruch zu nehmen, wird - automatischerkannt, dass Sie bereits bei uns waren und welche Eingaben - und Einstellungen sie getätigt haben, um diese nicht noch einmal - eingeben zu müssen. - </Typography> - <Typography> - Die durch Cookies verarbeiteten Daten sind für die genannten Zwecke zur - Wahrung unserer berechtigten Interessen erforderlich. - </Typography> - <Typography> - Die meisten Browser akzeptieren Cookies automatisch. Sie können Ihren - Browser jedoch so konfigurieren, dass keine Cookies auf Ihrem Computer - gespeichert werden oder stets ein Hinweis erscheint, bevor ein neuer - Cookie angelegt wird. Die vollständige Deaktivierung von Cookies kann - jedoch dazu führen, dass Sie nicht alle Funktionen unserer Website - nutzen können. - </Typography> - <Typography level="h4" component="h3"> - 5. Analyse-Tools - </Typography> - <Typography>Es werden keine Analyse-Tools verwendet.</Typography> - <Typography level="h4" component="h3"> - 6. Social Media Plug-ins - </Typography> - <Typography>Es werden keine Social Media Plug-Ins verwendet.</Typography> - <Typography level="h4" component="h3"> - 7. Betroffenenrechte - </Typography> - <Typography>Sie haben das Recht:</Typography> - <ul> - <li> - gemäß Art. 15 DS-GVO Auskunft über Ihre von uns verarbeiteten - personenbezogenen Daten zu verlangen. Insbesondere können Sie Auskunft - über die Verarbeitungszwecke, die Kategorie der personenbezogenen - Daten, die Kategorien von Empfängern, gegenüber denen Ihre Daten - offengelegt wurden oder werden, die geplante Speicherdauer, das - Bestehen eines Rechts auf Berichtigung, Löschung, Einschränkung der - Verarbeitung oder Widerspruch, das Bestehen eines Beschwerderechts, - die Herkunft ihrer Daten, sofern diese nicht bei uns erhoben wurden - sowie über das Bestehen einer automatisierten Entscheidungsfindung - einschließlich Profiling und ggf. aussagekräftigen Informationen zu - deren Einzelheiten verlangen. - </li> - <li> - gemäß Art. 16 DS-GVO unverzüglich die Berichtigung unrichtiger oder - Vervollständigung Ihrer bei uns gespeicherten personenbezogenen Daten - zu verlangen. - </li> - <li> - gemäß Art. 17 DS-GVO die Löschung Ihrer bei uns gespeicherten - personenbezogenen Daten zu verlangen, soweit nicht die Verarbeitung - zur Ausübung des Rechts auf freie Meinungsäußerung und Information, - zur Erfüllung einer rechtlichen Verpflichtung, aus Gründen des - öffentlichen Interesses oder zur Geltendmachung, Ausübung oder - Verteidigung von Rechtsansprüchen erforderlich ist. - </li> - <li> - gemäß Art. 18 DS-GVO die Einschränkung der Verarbeitung Ihrer - personenbezogenen Daten zu verlangen, soweit die Richtigkeit der Daten - von Ihnen bestritten wird, die Verarbeitung unrechtmäßig ist, Sie aber - deren Löschung ablehnen und wir die Daten nicht mehr benötigen, Sie - jedoch diese zur Geltendmachung, Ausübung oder Verteidigung von - Rechtsansprüchen benötigen oder Sie gemäß Art. 21 DS-GVO Widerspruch - gegen die Verarbeitung eingelegt haben. - </li> - <li> - gemäß Art. 20 DS-GVO Ihre personenbezogenen Daten, die Sie uns - bereitgestellt haben, in einem strukturierten, gängigen und - maschinenlesebaren Format zu erhalten oder die Übermittlung an einen - anderen Verantwortlichen zu verlangen. - </li> - <li> - gemäß Art. 7 Abs. 3 DS-GVO Ihre einmal erteilte Einwilligung jederzeit - gegenüber uns zu widerrufen. Dies hat zur Folge, dass wir die - Datenverarbeitung, die auf dieser Einwilligung beruhte, für die - Zukunft nicht mehr fortführen dürfen und - </li> - <li> - gemäß Art. 77 DS-GVO sich bei der zuständigen Aufsichtsbehörde zu - beschweren. Die zuständige Aufsichtsbehörde ist: Der Hessische - Datenschutzbeauftragte, Postfach 3163, 65021 Wiesbaden, Telefon: - 0611/1408 - 0, poststelle@datenschutz-hessen.de. - </li> - </ul> - <Typography level="h4" component="h3"> - 8. Widerspruchsrecht - </Typography> - <Typography> - Sofern Ihre personenbezogenen Daten auf Grundlage von berechtigten - Interessen gemäß Art. 6 Abs. 1 S. 1 lit. f DS-GVO verarbeitet werden, - haben Sie das Recht, gemäß Art. 21 DS-GVO Widerspruch gegen die - Verarbeitung Ihrer personenbezogenen Daten einzulegen, soweit dafür - Gründe vorliegen, die sich aus Ihrer besonderen Situation ergeben. - Möchten Sie von Ihrem Widerrufs- oder Widerspruchsrecht Gebrauch machen, - genügt eine E-Mail an datenschutz.gesundheitsamt@stadt-frankfurt.de. - </Typography> - <Typography level="h4" component="h3"> - 9. Datensicherheit - </Typography> - <Typography> - Wir bedienen uns geeigneter technischer und organisatorischer - Sicherheitsmaßnahmen, um Ihre Daten gegen zufällige oder vorsätzliche - Manipulationen, teilweisen oder vollständigen Verlust, Zerstörung oder - gegen den unbefugten Zugriff Dritter zu schützen. Unsere - Sicherheitsmaßnahmen werden nach dem jeweiligen Stand der Technik gemäß - Art. 32 DS-GVO fortlaufend angepasst. - </Typography> - <Typography level="h4" component="h3"> - 10. Auftragsverarbeitung - </Typography> - <Typography> - Es findet keine Auftragsverarbeitung der erhobenen Daten statt. - </Typography> - <Typography level="h4" component="h3"> - 11. Aktualität und Änderung dieser Datenschutzerklärung - </Typography> - <Typography> - Diese Datenschutzerklärung ist aktuell gültig und hat den Stand - September 2024. - </Typography> - </TitleAndSheetContentLayout> - ); -} diff --git a/citizen-portal/src/app/[lang]/impressum/page.tsx b/citizen-portal/src/app/[lang]/impressum/page.tsx deleted file mode 100644 index 83dfdc5f9..000000000 --- a/citizen-portal/src/app/[lang]/impressum/page.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -"use client"; - -import { ExternalLink } from "@eshg/lib-portal/components/navigation/ExternalLink"; -import { InternalLink } from "@eshg/lib-portal/components/navigation/InternalLink"; -import { Typography } from "@mui/joy"; - -import { useRoutes } from "@/lib/baseModule/shared/routes"; -import { TitleAndSheetContentLayout } from "@/lib/shared/components/layout/TitleAndSheetContentLayout"; - -export default function ImprintPage() { - const routes = useRoutes(); - - return ( - <TitleAndSheetContentLayout pageTitle="Impressum"> - <Typography level="h4" component="h3"> - Gesamtverantwortung: - </Typography> - <Typography> - Stadt Frankfurt am Main - <br /> - DER MAGISTRAT - <br /> - Römerberg 23 - <br /> - 60311 Frankfurt am Main - <br /> - Website: www.frankfurt.de - <br /> - <br /> - USt-ID: DE 114 110 388 - </Typography> - - <Typography level="h4" component="h3"> - Verantwortung für das GA-Lotse Online-Portal: - </Typography> - <Typography> - Stadt Frankfurt am Main - <br /> - DER MAGISTRAT - <br /> - Gesundheitsamt Frankfurt am Main - <br /> - Abteilung Digitale Zukunft, IT und strategische Planung - <br /> - Breite Gasse 28 - <br /> - 60313 Frankfurt am Main - </Typography> - - <Typography> - GA-Lotse ist ein Kooperationsprojekt des Hessischen Ministeriums für - Familie, Senioren, Sport, Gesundheit und Pflege mit dem Gesundheitsamt - Frankfurt unter der EU-Förderung NextGenerationEU. - </Typography> - <Typography level="h4" component="h3"> - Telefonische Auskünfte: - </Typography> - <Typography> - Informationen erhalten Sie über die Rufnummer: +49 (0) 800 -4256873 - </Typography> - - <Typography level="h4" component="h3"> - Kontakt bei Presseanfragen: - </Typography> - <Typography> - <ExternalLink - href={`mailto:gesundheitsamt.einheitliche-software@stadt-frankfurt.de`} - > - gesundheitsamt.einheitliche-software@stadt-frankfurt.de - </ExternalLink> - </Typography> - - <Typography level="h4" component="h3"> - Kontakt bei Fragen zum GA-Lotse Online-Portal: - </Typography> - <Typography> - eMail: gesundheitsamt.einheitliche-software@stadt-frankfurt.de - <br /> - Die Abteilung Digitale Zukunft, IT und strategische Planung des - Gesundheitsamtes der Stadt Frankfurt am Main zeichnet für ihre Inhalte - auf www.ga-lotse.de redaktionell verantwortlich. - </Typography> - - <Typography level="h4" component="h3"> - Verantwortung: - </Typography> - <Typography> - Stefanie Kaulich, Abteilungsleitung Digitale Zukunft, IT und - strategische Planung. <br /> - Bei Fragen oder Anregungen zu konkreten Inhalten und Seiten können Sie - sich gerne an Frau Kaulich oder die unter „Kontakt“ benannte eMail - wenden. - </Typography> - - <Typography level="h4" component="h3"> - Technische Realisierung: - </Typography> - <Typography> - Gesundheitsamt der Stadt Frankfurt am Main - <br /> - Abteilung Digitale Zukunft, IT und strategische Planung - <br /> - Breite Gasse 28 - <br /> - 60313 Frankfurt am Main - </Typography> - - <Typography level="h4" component="h3"> - Bei Fragen oder Anmerkungen: - </Typography> - <Typography> - gesundheitsamt.einheitliche-software@stadt-frankfurt.de - </Typography> - - <Typography level="h4" component="h3"> - Hinweise zum Datenschutz: - </Typography> - <Typography> - Informationen zum Datenschutz finden Sie{" "} - <InternalLink href={routes.privacyPolicy}>hier</InternalLink>. - </Typography> - </TitleAndSheetContentLayout> - ); -} diff --git a/citizen-portal/src/app/[lang]/playground/alert/page.tsx b/citizen-portal/src/app/[lang]/playground/alert/page.tsx new file mode 100644 index 000000000..4f6c308dd --- /dev/null +++ b/citizen-portal/src/app/[lang]/playground/alert/page.tsx @@ -0,0 +1,106 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +"use client"; + +import { useAlert } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { isNonEmptyString } from "@eshg/lib-portal/helpers/guards"; +import { + Button, + Checkbox, + FormControl, + FormLabel, + Input, + Option, + Select, + Stack, +} from "@mui/joy"; +import { useState } from "react"; + +import { PageContent } from "@/lib/shared/components/layout/PageContent"; +import { ContentSheet } from "@/lib/shared/components/layout/contentSheet"; +import { PageLayout, PageTitle } from "@/lib/shared/components/layout/page"; + +const DEFAULT_TYPE = "error"; +const TYPES = ["error", "warning", "notification"] as const; + +export default function AlertPlaygroundPage() { + const alert = useAlert(); + const [type, setType] = useState<(typeof TYPES)[number]>(DEFAULT_TYPE); + const [title, setTitle] = useState("Title"); + const [message, setMessage] = useState("Message"); + const [closeable, setCloseable] = useState(false); + const [action, setAction] = useState(""); + + function openAlert() { + alert[type]({ + title, + message, + action: isNonEmptyString(action) + ? { text: action, onClick: () => window.alert("Action clicked") } + : undefined, + closeable, + }); + } + + return ( + <PageLayout> + <PageContent> + <PageTitle>Alert</PageTitle> + <ContentSheet> + <FormControl> + <FormLabel>Type</FormLabel> + <Select + value={type} + onChange={(_, value) => setType(value ?? DEFAULT_TYPE)} + > + {TYPES.map((type) => ( + <Option key={type} value={type}> + {type} + </Option> + ))} + </Select> + </FormControl> + <FormControl> + <FormLabel>Title</FormLabel> + <Input + value={title} + onChange={(event) => setTitle(event.target.value)} + /> + </FormControl> + <FormControl> + <FormLabel>Message</FormLabel> + <Input + value={message} + onChange={(event) => setMessage(event.target.value)} + /> + </FormControl> + <Checkbox + label="Closeable" + checked={closeable} + onChange={(event) => setCloseable(event.target.checked)} + /> + <FormControl> + <FormLabel>Action</FormLabel> + <Input + value={action} + onChange={(event) => setAction(event.target.value)} + /> + </FormControl> + <Stack direction="row" gap={3}> + <Button onClick={openAlert}>Open Alert</Button> + <Button + color="danger" + onClick={() => alert.close()} + disabled={!alert.isOpen} + > + Close Alert + </Button> + </Stack> + </ContentSheet> + </PageContent> + </PageLayout> + ); +} diff --git a/citizen-portal/src/app/[lang]/playground/page.tsx b/citizen-portal/src/app/[lang]/playground/page.tsx index 600f30155..c9a530575 100644 --- a/citizen-portal/src/app/[lang]/playground/page.tsx +++ b/citizen-portal/src/app/[lang]/playground/page.tsx @@ -9,6 +9,7 @@ import { InternalLink } from "@eshg/lib-portal/components/navigation/InternalLin import { List, ListItem } from "@mui/joy"; import { PageContent } from "@/lib/shared/components/layout/PageContent"; +import { ContentSheet } from "@/lib/shared/components/layout/contentSheet"; import { PageLayout, PageTitle } from "@/lib/shared/components/layout/page"; export default function CitizenSchoolEntryPage() { @@ -16,11 +17,16 @@ export default function CitizenSchoolEntryPage() { <PageLayout> <PageContent> <PageTitle>Playground</PageTitle> - <List marker="disc"> - <ListItem> - <InternalLink href="/playground/snackbar">Snackbar</InternalLink> - </ListItem> - </List> + <ContentSheet> + <List marker="disc"> + <ListItem> + <InternalLink href="/playground/snackbar">Snackbar</InternalLink> + </ListItem> + <ListItem> + <InternalLink href="/playground/alert">Alert</InternalLink> + </ListItem> + </List> + </ContentSheet> </PageContent> </PageLayout> ); diff --git a/citizen-portal/src/env/server.js b/citizen-portal/src/env/server.js index 396ed9042..fdd665cd6 100644 --- a/citizen-portal/src/env/server.js +++ b/citizen-portal/src/env/server.js @@ -24,6 +24,8 @@ const schema = object({ PUBLIC_MEASLES_PROTECTION_BACKEND_URL: pipe(string(), url()), PUBLIC_SCHOOL_ENTRY_BACKEND_URL: pipe(string(), url()), PUBLIC_TRAVEL_MEDICINE_BACKEND_URL: pipe(string(), url()), + + MARKDOWN_PAGE_DIRECTORY: string(), }); // eslint-disable-next-line no-restricted-properties diff --git a/citizen-portal/src/lib/baseModule/components/ContactInformation.tsx b/citizen-portal/src/lib/baseModule/components/ContactInformation.tsx index f070da4b3..dfc4d00d7 100644 --- a/citizen-portal/src/lib/baseModule/components/ContactInformation.tsx +++ b/citizen-portal/src/lib/baseModule/components/ContactInformation.tsx @@ -71,7 +71,7 @@ function OpeningHoursSection() { return ( <InfoSection icon={<AccessTimeOutlined />}> - <InfoSectionTitle>{t("sectionTitle.openig_hours")}</InfoSectionTitle> + <InfoSectionTitle>{t("sectionTitle.opening_hours")}</InfoSectionTitle> <Typography>{t("opening_hours_information")}</Typography> </InfoSection> ); diff --git a/citizen-portal/src/lib/baseModule/components/MarkdownPage.tsx b/citizen-portal/src/lib/baseModule/components/MarkdownPage.tsx new file mode 100644 index 000000000..0f731960d --- /dev/null +++ b/citizen-portal/src/lib/baseModule/components/MarkdownPage.tsx @@ -0,0 +1,66 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ExternalLink } from "@eshg/lib-portal/components/navigation/ExternalLink"; +import { evaluate } from "@mdx-js/mdx"; +import { List, ListItem, Typography } from "@mui/joy"; +import { promises as fs } from "fs"; +import path from "path"; +import * as runtime from "react/jsx-runtime"; +import "server-only"; + +import { env } from "@/env/server"; +import { TitleAndSheetContentLayout } from "@/lib/shared/components/layout/TitleAndSheetContentLayout"; + +export async function MarkdownPage({ + pageType, + title, +}: { + pageType: "imprint" | "accessibility" | "privacy"; + title: string; +}) { + const filePath = path.join( + "./public/markdown", + env.MARKDOWN_PAGE_DIRECTORY, + `${pageType}.md`, + ); + + const source = await fs.readFile(filePath, { encoding: "utf-8" }); + + const { default: MDXContent } = await evaluate(source, { + ...runtime, + format: "md", + }); + + return ( + <TitleAndSheetContentLayout pageTitle={title}> + <MDXContent + components={{ + h2: (props) => ( + <Typography component="h2" level="h3"> + {props.children} + </Typography> + ), + h3: (props) => ( + <Typography component="h3" level="title-md"> + {props.children} + </Typography> + ), + p: (props) => <Typography component="p">{props.children}</Typography>, + span: (props) => ( + <Typography component="span">{props.children}</Typography> + ), + a: (props) => ( + <ExternalLink href={props.href} target="_blank"> + {props.children} + </ExternalLink> + ), + ul: (props) => <List marker="disc">{props.children}</List>, + li: (props) => <ListItem>{props.children}</ListItem>, + }} + /> + </TitleAndSheetContentLayout> + ); +} diff --git a/citizen-portal/src/lib/baseModule/locales/de/contact.json b/citizen-portal/src/lib/baseModule/locales/de/contact.json index 7a912ce7b..d67017a3e 100644 --- a/citizen-portal/src/lib/baseModule/locales/de/contact.json +++ b/citizen-portal/src/lib/baseModule/locales/de/contact.json @@ -6,7 +6,7 @@ "sectionTitle": { "address": "Adresse", "contact": "Kontakt", - "openig_hours": "Öffnungszeiten", + "opening_hours": "Öffnungszeiten", "internet": "Internet" }, "opening_hours_information": "Für einzelne Bereiche und Beratungsstellen gelten unterschiedliche Sprechzeiten.", diff --git a/citizen-portal/src/lib/baseModule/locales/en/contact.json b/citizen-portal/src/lib/baseModule/locales/en/contact.json index 59b372a90..bf4cd5460 100644 --- a/citizen-portal/src/lib/baseModule/locales/en/contact.json +++ b/citizen-portal/src/lib/baseModule/locales/en/contact.json @@ -6,7 +6,7 @@ "sectionTitle": { "address": "Address", "contact": "Contact", - "openig_hours": "Opening hours", + "opening_hours": "Opening hours", "internet": "Internet" }, "opening_hours_information": "Different consultation hours apply for individual areas and advice centers.", diff --git a/citizen-portal/src/lib/businessModules/schoolEntry/pages/citizenAnamnesis/steps/CitizenAnamnesisStepFour.tsx b/citizen-portal/src/lib/businessModules/schoolEntry/pages/citizenAnamnesis/steps/CitizenAnamnesisStepFour.tsx index 415d3a76a..4802d6db7 100644 --- a/citizen-portal/src/lib/businessModules/schoolEntry/pages/citizenAnamnesis/steps/CitizenAnamnesisStepFour.tsx +++ b/citizen-portal/src/lib/businessModules/schoolEntry/pages/citizenAnamnesis/steps/CitizenAnamnesisStepFour.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +/* eslint-disable jsx-a11y/aria-props */ import { DateField } from "@eshg/lib-portal/components/formFields/DateField"; import { InputField } from "@eshg/lib-portal/components/formFields/InputField"; import { FormLabel, Grid, Typography } from "@mui/joy"; diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/api/mutations/citizenAuthApi.ts b/citizen-portal/src/lib/businessModules/travelMedicine/api/mutations/citizenAuthApi.ts index 1e373b13b..5480c3881 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/api/mutations/citizenAuthApi.ts +++ b/citizen-portal/src/lib/businessModules/travelMedicine/api/mutations/citizenAuthApi.ts @@ -3,7 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiMedicalHistoryContent } from "@eshg/citizen-portal-api/travelMedicine"; +import { + ApiAppointment, + ApiMedicalHistoryContent, +} from "@eshg/citizen-portal-api/travelMedicine"; import { useHandledMutation } from "@eshg/lib-portal/api/useHandledMutation"; import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; @@ -57,3 +60,28 @@ export function useDeleteAppointment() { }, }); } + +export interface PutAppointmentRequest { + procedureId: string; + procedureStepId: string; + appointment: ApiAppointment; +} + +export function usePutAppointment() { + const { t } = useTranslation(["travelMedicine/rebookAppointment"]); + const citizenAuthApi = useCitizenAuthApi(); + const snackbar = useSnackbar(); + + return useHandledMutation({ + mutationFn: (data: PutAppointmentRequest) => { + return citizenAuthApi.putAppointment( + data.procedureId, + data.procedureStepId, + data.appointment, + ); + }, + onSuccess: () => { + snackbar.confirmation(t("snackbar.putAppointmentConfirmation")); + }, + }); +} diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/AppointmentContent.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/AppointmentContent.tsx index c51815ad2..d0c419612 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/AppointmentContent.tsx +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/AppointmentContent.tsx @@ -13,10 +13,12 @@ import { NoAppointmentsContent } from "@/lib/businessModules/travelMedicine/comp import { AppointmentPicker } from "@/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/calendar/AppointmentPicker"; import { InitialAppointmentFormValues } from "@/lib/businessModules/travelMedicine/components/appointment/types"; import { useStepContext } from "@/lib/businessModules/travelMedicine/components/shared/contexts/StepContext"; +import { useCitizenRoutes } from "@/lib/businessModules/travelMedicine/shared/routes"; export function AppointmentContent() { const { values } = useFormikContext<InitialAppointmentFormValues>(); const { onShowOverviewChange } = useStepContext(); + const citizenRoutes = useCitizenRoutes(); const freeAppointments = useGetFreeAppointmentsForCitizen( values.initialStepAppointmentType, @@ -43,7 +45,7 @@ export function AppointmentContent() { <AppointmentPicker filteredAppointments={filteredAppointments} /> ) : ( <NoAppointments> - <NoAppointmentsContent /> + <NoAppointmentsContent backButtonLocation={citizenRoutes.overview} /> </NoAppointments> )} </> diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/NoAppointmentsContent.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/NoAppointmentsContent.tsx index afbfa8dca..0dd58448f 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/NoAppointmentsContent.tsx +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/NoAppointmentsContent.tsx @@ -7,13 +7,15 @@ import Button from "@mui/joy/Button"; import Typography from "@mui/joy/Typography"; import { useRouter } from "next/navigation"; -import { useCitizenRoutes } from "@/lib/businessModules/travelMedicine/shared/routes"; import { useTranslation } from "@/lib/i18n/client"; -export function NoAppointmentsContent() { +export function NoAppointmentsContent({ + backButtonLocation, +}: Readonly<{ + backButtonLocation: string; +}>) { const { t } = useTranslation(["travelMedicine/forms"]); const router = useRouter(); - const citizenRoutes = useCitizenRoutes(); return ( <> @@ -28,7 +30,7 @@ export function NoAppointmentsContent() { <Button sx={{ width: "20%" }} variant="solid" - onClick={() => router.push(citizenRoutes.overview)} + onClick={() => router.push(backButtonLocation)} > {t("appointmentSlotFormContent.backToOverview")} </Button> diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/InfoModal.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/InfoModal.tsx index dbf413d9b..d6bc287dc 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/InfoModal.tsx +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/components/InfoModal.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { DialogTitle, Modal, ModalClose, ModalDialog, Stack } from "@mui/joy"; import { DefaultColorPalette, SxProps } from "@mui/joy/styles/types"; import { ReactNode } from "react"; @@ -26,15 +26,13 @@ export function InfoModal({ onClose, sx, }: InfoModalProps) { - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); function handleClose() { if (onClose !== undefined) { onClose(); } - if (alertContext !== null) { - alertContext.setAlert(null); - } + resetAlertContext(); } return ( diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/contexts/IdContext.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/contexts/IdContext.tsx index 4104d05f1..3c9bdcd86 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/contexts/IdContext.tsx +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/shared/contexts/IdContext.tsx @@ -3,13 +3,17 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiGetAppointmentDetailsResponse } from "@eshg/citizen-portal-api/travelMedicine"; import { RequiresChildren } from "@eshg/lib-portal/types/react"; import { useSearchParams } from "next/navigation"; import { createContext, useContext } from "react"; +import { useGetProcedureStepAppointmentDetails } from "@/lib/businessModules/travelMedicine/api/queries/citizenAuthApi"; + interface IdContextProps { procedureId: string; procedureStepId: string; + appointmentDetails: ApiGetAppointmentDetailsResponse; } export const IdContext = createContext<IdContextProps | null>(null); @@ -20,9 +24,15 @@ export function IdContextProvider(props: Readonly<IdContextProviderProps>) { const searchParams = useSearchParams(); const procedureId = searchParams.get("procedureId")!; const procedureStepId = searchParams.get("procedureStepId")!; + const { data: appointmentDetails } = useGetProcedureStepAppointmentDetails( + procedureId, + procedureStepId, + ); return ( - <IdContext.Provider value={{ procedureId, procedureStepId }}> + <IdContext.Provider + value={{ procedureId, procedureStepId, appointmentDetails }} + > {props.children} </IdContext.Provider> ); diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentDetailsSidePanel.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentDetailsSidePanel.tsx index b2faebd51..1ed7a9729 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentDetailsSidePanel.tsx +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentDetailsSidePanel.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiAppointmentBookingType } from "@eshg/citizen-portal-api/travelMedicine"; import { Button, Stack } from "@mui/joy"; import { useRouter } from "next/navigation"; @@ -18,16 +19,14 @@ import { useAccessCodeParam } from "@/lib/shared/helpers/accessCode"; export function AppointmentDetailsSidePanel({ hasAccomplishedService, - isCancelled, }: Readonly<{ hasAccomplishedService: boolean; - isCancelled: boolean; }>) { const router = useRouter(); const citizenRoutes = useCitizenRoutes(); const accessCode = useAccessCodeParam(); const { t } = useTranslation(["travelMedicine/appointmentDetails"]); - const { procedureId, procedureStepId } = useIdContext(); + const { procedureId, procedureStepId, appointmentDetails } = useIdContext(); const deleteAppointment = useDeleteAppointment(); async function handleDeleteAppointment() { @@ -37,25 +36,54 @@ export function AppointmentDetailsSidePanel({ }); } + function navigateToRebookAppointment() { + const url = `${citizenRoutes.viewAppointment.details.rebook(accessCode)}?procedureId=${procedureId}&procedureStepId=${procedureStepId}`; + router.push(url); + } + + function bookingsRemaining() { + return appointmentDetails.bookingsRemaining > 0; + } + + function isBooked() { + return ( + appointmentDetails.summaryDto.appointmentBookingType === + ApiAppointmentBookingType.AppointmentBlock || + appointmentDetails.summaryDto.appointmentBookingType === + ApiAppointmentBookingType.UserDefined + ); + } + return ( <ContentSheet> <Stack gap={"16px"}> - {!hasAccomplishedService && !isCancelled && ( + {!hasAccomplishedService && ( <> <ContentSheetTitle sx={{ paddingBottom: "8px" }}> {t("sidePanel.title")} </ContentSheetTitle> - <Button color="primary" variant="outlined" type="submit"> - {t("sidePanel.postponeAppointment")} - </Button> - <Button - color="danger" - variant="outlined" - type="submit" - onClick={handleDeleteAppointment} - > - {t("sidePanel.cancelAppointment")} - </Button> + {bookingsRemaining() && ( + <Button + color="primary" + variant="outlined" + type="submit" + onClick={navigateToRebookAppointment} + > + {isBooked() + ? t("sidePanel.postponeAppointment") + : t("sidePanel.bookAppointment")} + </Button> + )} + {isBooked() && ( + <Button + color="danger" + variant="outlined" + type="submit" + onClick={handleDeleteAppointment} + > + {t("sidePanel.cancelAppointment")} + </Button> + )} </> )} <Button diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentPageContent.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentPageContent.tsx index 231ac1a0b..6445b1f65 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentPageContent.tsx +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentPageContent.tsx @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiAppointmentBookingType } from "@eshg/citizen-portal-api/travelMedicine"; - import { useGetProcedureStepAppointmentDetails } from "@/lib/businessModules/travelMedicine/api/queries/citizenAuthApi"; import { useIdContext } from "@/lib/businessModules/travelMedicine/components/shared/contexts/IdContext"; import { AppointmentDetails } from "@/lib/businessModules/travelMedicine/components/viewAppointment/AppointmentDetails"; @@ -31,10 +29,6 @@ export function AppointmentPageContent() { contentCenter={ <AppointmentDetailsSidePanel hasAccomplishedService={appointmentDetails.hasAccomplishedService} - isCancelled={ - appointmentDetails.summaryDto.appointmentBookingType === - ApiAppointmentBookingType.Cancelled - } /> } contentBottom={null} @@ -45,10 +39,6 @@ export function AppointmentPageContent() { sidePanel={ <AppointmentDetailsSidePanel hasAccomplishedService={appointmentDetails.hasAccomplishedService} - isCancelled={ - appointmentDetails.summaryDto.appointmentBookingType === - ApiAppointmentBookingType.Cancelled - } /> } /> diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointment.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointment.tsx new file mode 100644 index 000000000..1825664f5 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointment.tsx @@ -0,0 +1,118 @@ +/** + * Copyright 2024 SCOOP Software GmbH, cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiAppointment } from "@eshg/citizen-portal-api/travelMedicine"; +import { Alert } from "@eshg/lib-portal/components/Alert"; +import { durationBetweenDatesInMinutes } from "@eshg/lib-portal/helpers/dateTime"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import { Grid, Stack, Typography } from "@mui/joy"; +import { useFormikContext } from "formik"; +import { useCallback, useState } from "react"; + +import { AppointmentDayPicker } from "@/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/calendar/AppointmentDayPicker"; +import { AppointmentTimePicker } from "@/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/calendar/AppointmentTimePicker"; +import { FormSheetTitle } from "@/lib/businessModules/travelMedicine/components/shared/components/FormSheet"; +import { RebookAppointmentFormValues } from "@/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentPageContent"; +import { useTranslation } from "@/lib/i18n/client"; +import { byBreakpoint } from "@/lib/shared/breakpoints"; +import { ContentSheet } from "@/lib/shared/components/layout/contentSheet"; + +export function RebookAppointment({ + appointments, +}: Readonly<{ + appointments: ApiAppointment[]; +}>) { + const { t } = useTranslation(["travelMedicine/rebookAppointment"]); + const { values, setFieldValue, errors } = + useFormikContext<RebookAppointmentFormValues>(); + + const [isAppointmentDaySelected, setIsAppointmentDaySelected] = + useState(false); + const [availableAppointments, setAvailableAppointments] = useState< + ApiAppointment[] + >([]); + + const handelAvailableAppointmentsSelection = useCallback( + (appointments: ApiAppointment[]) => { + setAvailableAppointments(appointments); + setIsAppointmentDaySelected(true); + }, + [], + ); + + function handleAppointmentSelection(api: ApiAppointment) { + void setFieldValue( + "selectedAppointment", + `${api.start.toISOString()},${durationBetweenDatesInMinutes( + api.start, + api.end, + )}`, + ); + } + + function resetSelectedAppointment() { + void setFieldValue("selectedAppointment", ""); + } + + function onDisplayedMonthChanged() { + resetSelectedAppointment(); + setIsAppointmentDaySelected(false); + setAvailableAppointments([]); + } + + function getSelectedAppointmentFromContext() { + let selectedDate = ""; + const date = values.selectedAppointment?.split(",")[0]; + if (date) { + selectedDate = new Date(date).toString(); + } + return selectedDate; + } + + return ( + <ContentSheet data-testid="rebook-appointment-content-form"> + <FormSheetTitle requiredTitle={t("content.requiredTitle")}> + {t("content.title")} + </FormSheetTitle> + <Alert + title={t("content.infoPanelTitle")} + message={t("content.infoPanelText")} + color="primary" + /> + <Stack data-testid="appointment-picker"> + {errors.selectedAppointment && ( + <Typography + data-testid="appointment-picker-helper-text" + startDecorator={<InfoOutlinedIcon size="md" />} + color="danger" + sx={{ marginBottom: 2 }} + > + {errors.selectedAppointment} + </Typography> + )} + <Grid container spacing={2} sx={{ flexGrow: 1 }}> + <Grid {...byBreakpoint({ mobile: 12, desktop: 6 })}> + <AppointmentDayPicker + appointmentDayCandidates={appointments} + onAvailableAppointmentsSelected={ + handelAvailableAppointmentsSelection + } + onDisplayedMonthChanged={onDisplayedMonthChanged} + resetPreviousSelectedAppointment={resetSelectedAppointment} + /> + </Grid> + <Grid {...byBreakpoint({ mobile: 12, desktop: 6 })}> + <AppointmentTimePicker + onAppointmentDateAndTimeSelected={handleAppointmentSelection} + availableAppointments={availableAppointments} + isAppointmentDaySelected={isAppointmentDaySelected} + selectedAppointmentDayAndTime={getSelectedAppointmentFromContext()} + /> + </Grid> + </Grid> + </Stack> + </ContentSheet> + ); +} diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentPageContent.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentPageContent.tsx new file mode 100644 index 000000000..663542246 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentPageContent.tsx @@ -0,0 +1,126 @@ +/** + * Copyright 2024 SCOOP Software GmbH, cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { addMinutes, isAfter, isEqual } from "date-fns"; +import { Formik, FormikErrors } from "formik"; +import { useRouter } from "next/navigation"; + +import { + PutAppointmentRequest, + usePutAppointment, +} from "@/lib/businessModules/travelMedicine/api/mutations/citizenAuthApi"; +import { useGetFreeAppointmentsForCitizen } from "@/lib/businessModules/travelMedicine/api/queries/citizenPublicApi"; +import { NoAppointments } from "@/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/NoAppointments"; +import { NoAppointmentsContent } from "@/lib/businessModules/travelMedicine/components/appointment/steps/appointmentSlotStep/NoAppointmentsContent"; +import { useIdContext } from "@/lib/businessModules/travelMedicine/components/shared/contexts/IdContext"; +import { RebookAppointment } from "@/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointment"; +import { RebookAppointmentSidePanel } from "@/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentSidePanel"; +import { useCitizenRoutes } from "@/lib/businessModules/travelMedicine/shared/routes"; +import { useIsMobile } from "@/lib/businessModules/travelMedicine/shared/useIsMobile"; +import { useTranslation } from "@/lib/i18n/client"; +import { ContentSheet } from "@/lib/shared/components/layout/contentSheet"; +import { + OneColumnGrid, + TwoColumnGrid, +} from "@/lib/shared/components/layout/grid"; +import { useAccessCodeParam } from "@/lib/shared/helpers/accessCode"; + +export interface RebookAppointmentFormValues { + selectedAppointment: string; +} + +export function RebookAppointmentPageContent() { + const { t } = useTranslation(["travelMedicine/rebookAppointment"]); + const isMobile = useIsMobile(); + const idContext = useIdContext(); + const router = useRouter(); + const citizenRoutes = useCitizenRoutes(); + const accessCode = useAccessCodeParam(); + const putAppointment = usePutAppointment(); + + const freeAppointments = useGetFreeAppointmentsForCitizen( + idContext.appointmentDetails.summaryDto.appointmentType, + ).data; + + const filteredAppointments = freeAppointments.appointments.filter( + (appointment) => isDateAfterEarliestDate(appointment.start), + ); + + function isDateAfterEarliestDate(date: Date) { + const now = + idContext.appointmentDetails.summaryDto.earliestDate ?? new Date(); + return isEqual(date, now) || isAfter(date, now); + } + + async function handleSubmit(values: RebookAppointmentFormValues) { + const split = values.selectedAppointment.split(","); + const durationInMinutes = Number.parseInt(split.at(1)!); + const start = new Date(split.at(0)!); + const request: PutAppointmentRequest = { + procedureId: idContext.procedureId, + procedureStepId: idContext.procedureStepId, + appointment: { + start: start, + end: addMinutes(start, durationInMinutes), + }, + }; + await putAppointment.mutateAsync(request, { + onSuccess: () => routeBackToDetails(), + }); + } + + function routeBackToDetails() { + const url = `${citizenRoutes.viewAppointment.details.index(accessCode)}?procedureId=${idContext.procedureId}&procedureStepId=${idContext.procedureStepId}`; + router.push(url); + } + + function validateForm(values: RebookAppointmentFormValues) { + const errors: FormikErrors<RebookAppointmentFormValues> = {}; + + if (values.selectedAppointment === "") { + errors.selectedAppointment = t("content.errorMessage"); + } + + return errors; + } + + return ( + <Formik + initialValues={{ selectedAppointment: "" }} + onSubmit={handleSubmit} + validate={validateForm} + > + {filteredAppointments.length > 0 ? ( + isMobile ? ( + <OneColumnGrid + contentTop={null} + contentCenter={ + <> + <RebookAppointment appointments={filteredAppointments} /> + <RebookAppointmentSidePanel /> + </> + } + contentBottom={null} + /> + ) : ( + <TwoColumnGrid + content={<RebookAppointment appointments={filteredAppointments} />} + sidePanel={<RebookAppointmentSidePanel />} + /> + ) + ) : ( + <ContentSheet> + <NoAppointments> + <NoAppointmentsContent + backButtonLocation={`${citizenRoutes.viewAppointment.details.index( + accessCode, + )}?procedureId=${idContext.procedureId}&procedureStepId=${idContext.procedureStepId}`} + /> + </NoAppointments> + </ContentSheet> + )} + </Formik> + ); +} diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentSidePanel.tsx b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentSidePanel.tsx new file mode 100644 index 000000000..9a321ca05 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentSidePanel.tsx @@ -0,0 +1,79 @@ +/** + * Copyright 2024 SCOOP Software GmbH, cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiAppointmentBookingType } from "@eshg/citizen-portal-api/travelMedicine"; +import { formatTime } from "@eshg/lib-portal/formatters/dateTime"; +import { formatDateToFullReadableString } from "@eshg/lib-portal/helpers/dateTime"; +import { AccessTimeOutlined, DateRange } from "@mui/icons-material"; +import { Button, Stack } from "@mui/joy"; +import { useFormikContext } from "formik"; +import { useRouter } from "next/navigation"; + +import { DetailsField } from "@/lib/businessModules/travelMedicine/components/shared/components/DetailsField"; +import { useIdContext } from "@/lib/businessModules/travelMedicine/components/shared/contexts/IdContext"; +import { RebookAppointmentFormValues } from "@/lib/businessModules/travelMedicine/components/viewAppointment/rebook/RebookAppointmentPageContent"; +import { useCitizenRoutes } from "@/lib/businessModules/travelMedicine/shared/routes"; +import { useTranslation } from "@/lib/i18n/client"; +import { + ContentSheet, + ContentSheetTitle, +} from "@/lib/shared/components/layout/contentSheet"; +import { useAccessCodeParam } from "@/lib/shared/helpers/accessCode"; + +export function RebookAppointmentSidePanel() { + const router = useRouter(); + const citizenRoutes = useCitizenRoutes(); + const accessCode = useAccessCodeParam(); + const { t } = useTranslation(["travelMedicine/rebookAppointment"]); + const { procedureId, procedureStepId, appointmentDetails } = useIdContext(); + const { values, handleSubmit } = + useFormikContext<RebookAppointmentFormValues>(); + const splitArr = values.selectedAppointment?.split(","); + const split = splitArr?.at(0); + const appointmentStart = new Date(split!); + const durationInMinutes = splitArr?.at(1); + + function routeBackToDetails() { + const url = `${citizenRoutes.viewAppointment.details.index(accessCode)}?procedureId=${procedureId}&procedureStepId=${procedureStepId}`; + router.push(url); + } + + function isBooked() { + return ( + appointmentDetails.summaryDto.appointmentBookingType === + ApiAppointmentBookingType.AppointmentBlock || + appointmentDetails.summaryDto.appointmentBookingType === + ApiAppointmentBookingType.UserDefined + ); + } + + return ( + <ContentSheet data-testid="rebook-appointment-side-panel"> + <ContentSheetTitle>{t("sidePanel.title")}</ContentSheetTitle> + {values.selectedAppointment && ( + <> + <DetailsField + value={formatDateToFullReadableString(appointmentStart)} + icon={<DateRange />} + /> + <DetailsField + value={`${formatTime(appointmentStart)} ${t("sidePanel.appointmentDuration", { durationInMinutes: durationInMinutes })}`} + icon={<AccessTimeOutlined />} + /> + </> + )} + <Stack gap={2}> + <Button color="primary" variant="solid" onClick={() => handleSubmit()}> + {isBooked() + ? t("sidePanel.postponeAppointment") + : t("sidePanel.bookAppointment")} + </Button> + <Button color="neutral" variant="soft" onClick={routeBackToDetails}> + {t("sidePanel.back")} + </Button> + </Stack> + </ContentSheet> + ); +} diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/locales/de/appointmentDetails.json b/citizen-portal/src/lib/businessModules/travelMedicine/locales/de/appointmentDetails.json index 4b7eed0e9..995f7035b 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/locales/de/appointmentDetails.json +++ b/citizen-portal/src/lib/businessModules/travelMedicine/locales/de/appointmentDetails.json @@ -7,6 +7,7 @@ "sidePanel": { "title": "Sie können den Termin nicht wahrnehmen?", "postponeAppointment": "Termin verschieben", + "bookAppointment": "Termin buchen", "cancelAppointment": "Termin absagen", "back": "Zurück" }, diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/locales/de/rebookAppointment.json b/citizen-portal/src/lib/businessModules/travelMedicine/locales/de/rebookAppointment.json new file mode 100644 index 000000000..fa5c67395 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/travelMedicine/locales/de/rebookAppointment.json @@ -0,0 +1,24 @@ +{ + "header": { + "title": "Termin (um)buchen", + "logout": "Mein Bereich verlassen" + }, + "content": { + "title": "Verfügbare Termine", + "requiredTitle": "*Pflichtfeld", + "errorMessage": "Bitte einen Termin auswählen", + "infoPanelTitle": "Terminverschiebung", + "infoPanelText": "Sie können Ihren Termin maximal zwei Mal verschieben." + }, + "sidePanel": { + "title": "Übersicht", + "dateAndTime": "{{ appointmentStart }} Uhr", + "appointmentDuration": "(Dauer: {{ durationInMinutes }} Minuten)", + "postponeAppointment": "Termin verschieben", + "bookAppointment": "Termin buchen", + "back": "Zurück" + }, + "snackbar": { + "putAppointmentConfirmation": "Termin erfolgreich (um)gebucht" + } +} diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/locales/en/appointmentDetails.json b/citizen-portal/src/lib/businessModules/travelMedicine/locales/en/appointmentDetails.json index 4b7eed0e9..995f7035b 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/locales/en/appointmentDetails.json +++ b/citizen-portal/src/lib/businessModules/travelMedicine/locales/en/appointmentDetails.json @@ -7,6 +7,7 @@ "sidePanel": { "title": "Sie können den Termin nicht wahrnehmen?", "postponeAppointment": "Termin verschieben", + "bookAppointment": "Termin buchen", "cancelAppointment": "Termin absagen", "back": "Zurück" }, diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/locales/en/rebookAppointment.json b/citizen-portal/src/lib/businessModules/travelMedicine/locales/en/rebookAppointment.json new file mode 100644 index 000000000..fa5c67395 --- /dev/null +++ b/citizen-portal/src/lib/businessModules/travelMedicine/locales/en/rebookAppointment.json @@ -0,0 +1,24 @@ +{ + "header": { + "title": "Termin (um)buchen", + "logout": "Mein Bereich verlassen" + }, + "content": { + "title": "Verfügbare Termine", + "requiredTitle": "*Pflichtfeld", + "errorMessage": "Bitte einen Termin auswählen", + "infoPanelTitle": "Terminverschiebung", + "infoPanelText": "Sie können Ihren Termin maximal zwei Mal verschieben." + }, + "sidePanel": { + "title": "Übersicht", + "dateAndTime": "{{ appointmentStart }} Uhr", + "appointmentDuration": "(Dauer: {{ durationInMinutes }} Minuten)", + "postponeAppointment": "Termin verschieben", + "bookAppointment": "Termin buchen", + "back": "Zurück" + }, + "snackbar": { + "putAppointmentConfirmation": "Termin erfolgreich (um)gebucht" + } +} diff --git a/citizen-portal/src/lib/businessModules/travelMedicine/shared/routes.ts b/citizen-portal/src/lib/businessModules/travelMedicine/shared/routes.ts index c2256522f..7baa876f2 100644 --- a/citizen-portal/src/lib/businessModules/travelMedicine/shared/routes.ts +++ b/citizen-portal/src/lib/businessModules/travelMedicine/shared/routes.ts @@ -23,6 +23,7 @@ export function citizenRoutes(locale: SupportedLanguage | undefined) { details: defineRoutes(appointmentPath("/details"), (detailsPath) => ({ index: accessCodeRoute(detailsPath("/")), medicalHistory: accessCodeRoute(detailsPath("/anamnese")), + rebook: accessCodeRoute(detailsPath("/buchen")), })), }), ), diff --git a/citizen-portal/src/lib/shared/components/layout/TitleAndSheetContentLayout.tsx b/citizen-portal/src/lib/shared/components/layout/TitleAndSheetContentLayout.tsx index 9e7c9445f..f5bc15e5d 100644 --- a/citizen-portal/src/lib/shared/components/layout/TitleAndSheetContentLayout.tsx +++ b/citizen-portal/src/lib/shared/components/layout/TitleAndSheetContentLayout.tsx @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +"use client"; + import { PropsWithChildren } from "react"; import { ContentSheet } from "@/lib/shared/components/layout/contentSheet"; diff --git a/citizen-portal/src/lib/shared/components/layout/page.tsx b/citizen-portal/src/lib/shared/components/layout/page.tsx index 6bf5ff4c6..0bb82bcd1 100644 --- a/citizen-portal/src/lib/shared/components/layout/page.tsx +++ b/citizen-portal/src/lib/shared/components/layout/page.tsx @@ -5,10 +5,7 @@ "use client"; -import { - ScopedAlert, - useAlert, -} from "@eshg/lib-portal/errorHandling/AlertContext"; +import { AlertSlot } from "@eshg/lib-portal/errorHandling/AlertContext"; import { RequiresChildren } from "@eshg/lib-portal/types/react"; import { Sheet, Stack, Typography, styled } from "@mui/joy"; import { ReactNode } from "react"; @@ -24,22 +21,20 @@ const MainContents = styled("main")({ display: "contents", }); +const AlertContainer = styled(PageContent)({ + paddingBlockEnd: 0, +}); + interface PageLayoutProps extends RequiresChildren { banner?: BannerType; } export function PageLayout(props: PageLayoutProps) { - const alert = useAlert(); - return ( <> {isDefined(props.banner) && <PageBanner type={props.banner} />} <MainContents> - {alert !== null && ( - <PageContent> - <ScopedAlert /> - </PageContent> - )} + <AlertSlot container={AlertContainer} /> {props.children} </MainContents> </> diff --git a/docs/e2e-tests.adoc b/docs/e2e-tests.adoc index e6feb501b..b2fac1958 100644 --- a/docs/e2e-tests.adoc +++ b/docs/e2e-tests.adoc @@ -1,6 +1,8 @@ = E2E Tests :sectnums: +== Running e2e tests + e2e tests depend on docker containers of the required backend modules. At least the containers necessary for the actions performed in the test must be running. A full local setup / run of the test suite can be achieved like this: @@ -24,124 +26,32 @@ cd .. ./gradlew :e2e:test ---- -The `startLocal` task will start the backend modules based on the currently checkout submodule in `backend` and start the frontend based on your current code. +The `startLocal` task will start the backend modules based on the currently checked out submodule in `backend` and start the frontend based on your current code. The task will keep running as it starts the `:frontend:run` tasks which starts the next-server application to serve the frontend. If you like to have the fronted dockerized as well you can instead use the `startLocalDocker` task which will call `composeUp` to build a docker container -also based on your local code. Please note: This only works on Linux as this uses the docker host network feature. +also based on your local code. Refer to the link:./gradle.adoc#e2e-tasks[e2e tasks] for an overview of the common tasks. -== Multi-instance e2e test -=== Start setup - -[source,shell] ----- -cd e2e -./gradlew multiInstanceUp ----- - -This starts the central services (maildev, service-directory and central-repository) together with some GA-Lotse instances. - -The exact start setup can be configured via a link:../e2e/multi-instance/config.json[JSON file]. The default will start 2 "Gesundheitsamt"-like instances with all business modules and one "Landesamt"-like instance with selected modules. +== Smoke testing deployed environments -You can pass a different config file as `multiInstanceConfig` project parameter. +The playwright configuration for smoke testing actual environments is kept a bit separate from the normal configuration. +For each environment a `src/config/playwright.<env>.config.ts` file exists. -To reach the instances from your browser you either need a browser running as docker container inside the docker network or configure your browser to use the proxy server listening at localhost:3128. - -You must explicitly type http://employee-portal... otherwise (at least firefox) will present you a web search because it does not know the .docker TLD - -=== Access test instances method 1: Dockerized Firefox +In most environments the dummy user still exists but has a different password. The project is setup to check the `DUMMY_PASSWORD` environment variable +and use its value instead. In a CI pipeline the password could be read from a kubernetes secret: [source,shell] ---- -docker run -d \ ---name=firefox \ ---network eshg-default \ --p 5800:5800 \ -jlesage/firefox +DUMMY_PASSWORD=$(kubectl -n <namespace> get secrets/keycloak-test-user-secrets -o yaml | yq '.data["test-users-secret-override"]' | base64 -d) +export DUMMY_PASSWORD ---- -Mount a `/config` volume to persist history etc between runs (`-v $HOME/dockerized-firefox:/config:rw`) - -=== Access test instances method 2: Proxy - -Together with the central service an HTTP proxy server (3proxy) is started. It listens on localhost:3128. If you configure your browser to route `*.docker` domains through this proxy you can access all these docker containers directly. +All playwright tasks (`test`, `testUi` and `runDockerizedPlaywright`) check the project property `-PsmokeTestEnv` and try to match it to a configuration. -One way is to configure a \*.pac file for your browser (or extend it if you already use one). Below is a snippet how it might look. Another possibility is to use a browser extension like FoxyProxy and configure a corresponding rule. - -[source,js] +[source,shell] ---- -function FindProxyForURL(url, host) { - var useDockerProxy = "PROXY localhost:3128"; - - if (shExpMatch(host, "*.docker")) { - return useDockerProxy; - } - - return "DIRECT"; -} +./gradlew test -PsmokeTestEnv=playground ---- -=== URLs - -[cols="1,1,1"] -|=== -| *instance* | *service* | *URL* -.9+| central .2+| service-directory | http://service-directory.docker:8080 - | or http://localhost:8083 - .2+| admin-portal | http://admin-portal.docker:4002 - | or http://localhost:4002 - .2+| central-repository | http://central-repository.docker:8080 - | or http://localhost:8091 - .2+| maildev | http://maildev.docker:1080 - | or http://localhost:1080 - | proxy | http://localhost:3128 -.8+| eshg1 | keycloak | http://keycloak.eshg1.docker:8080 - | base | http://base.eshg1.docker:8080 - | employee-portal | http://employee-portal.eshg1.docker:3000 - | citizen-portal | http://citizen-portal.eshg1.docker:3001 - .4+| business module | http://<module>.eshg1.docker:8080 - | http://inspection.eshg1.docker:8080 - | http://school-entry.eshg1.docker:8080 - | ... -.8+| eshg2 | keycloak | http://keycloak.eshg2.docker:8080 - | base | http://base.eshg2.docker:8080 - | employee-portal | http://employee-portal.eshg2.docker:3000 - | citizen-portal | http://citizen-portal.eshg2.docker:3001 - .4+| business module | http://<module>.eshg2.docker:8080 - | http://inspection.eshg2.docker:8080 - | http://school-entry.eshg2.docker:8080 - | ... -.3+| eshg3-la | keycloak | http://keycloak.eshg3-la.docker:8080 - | base | http://base.eshg3-la.docker:8080 - | employee-portal | http://employee-portal.eshg3-la.docker:3000 -|=== - - -== Run tests - -=== Dockerized playwright - -[cols="1,1"] -|=== -| *task* *description* | -| `./gradlew :e2e:runDockerizedPlaywright -PmultiInstance=true` | run all tests -| `./gradlew :e2e:runDockerizedPlaywright -PmultiInstance=true -Pselector=@multi` | runs only the "multi" project -|=== - -Reports can be viewed "normally" via `./gradlew :e2e:testReport` as usual. - - -=== Through proxy - -The normal `e2e:test` or `e2e:testUi` tasks can be used to some extent as well -if you supply the `-Pproxy` gradle project property to the task. \ -Please note: Currently, not all existing tests work with this setup. - -== Stop instances - -[source,shell] ----- - ./gradlew multiInstanceDown ----- \ No newline at end of file diff --git a/docs/gradle.adoc b/docs/gradle.adoc index 63846d595..50393bbd4 100644 --- a/docs/gradle.adoc +++ b/docs/gradle.adoc @@ -60,7 +60,7 @@ All common tasks are also available within the e2e project. | `./gradlew e2e:startLocalEmployeePortal` | Start all backend containers and serve the employee portal locally in "production mode" | `./gradlew e2e:startLocalCitizenPortal` | Start all backend containers and serve the citizen portal locally in "production mode" | `./gradlew e2e:startLocalAdminPortal` | Start **only** a service-directory container and serve the admin portal locally in "production mode" -| `./gradlew e2e:startLocalDocker` | Start all backend containers and dockerized employee, citizen and admin portals (Linux only due to docker host network) +| `./gradlew e2e:startLocalDocker` | Start all backend containers and dockerized employee, citizen and admin portals | `./gradlew e2e:test` | Run all tests in headless mode | `./gradlew e2e:testUi` | Open Playwright in UI mode (enables running and debugging individual tests) | `./gradlew e2e:testReport` | Open HTML test report @@ -70,4 +70,4 @@ All common tasks are also available within the e2e project. *Notice:* `testUi` is recommended for local development, because tests can be run individually with a time travel experience. This simplifies the verification of the test runs and the analysis of errors. -There is a separate section in the link:./e2e-tests.adoc[e2e-tests README] concerning the multi instance e2e test setup. +There is a separate section in the link:./e2e-tests.adoc[e2e-tests README] concerning smoke testing deployed environments using the e2e test setup. diff --git a/docs/queries-and-mutations.adoc b/docs/queries-and-mutations.adoc index 375259a4d..e60a0db44 100644 --- a/docs/queries-and-mutations.adoc +++ b/docs/queries-and-mutations.adoc @@ -527,7 +527,7 @@ export default NextErrorBoundary; export default function Template(props: RequiresChildren) { return ( <QueryBoundary> - <ScopedAlert /> + <AlertSlot /> {props.children} </QueryBoundary> ); diff --git a/employee-portal/.env b/employee-portal/.env index 9ea559774..d7a05e620 100644 --- a/employee-portal/.env +++ b/employee-portal/.env @@ -9,6 +9,8 @@ PUBLIC_CHAT_MANAGEMENT_BACKEND_URL=http://localhost:4000/api/chat-management PUBLIC_AUDITLOG_BACKEND_URL=http://localhost:4000/api/auditlog PUBLIC_STI_PROTECTION_BACKEND_URL=http://localhost:4000/api/sti-protection +MARKDOWN_PAGE_DIRECTORY=frankfurt + MATRIX_SERVER_URL=http://localhost:4000/api/synapse NEXT_PUBLIC_IMAGE_COMPRESSION_DEFAULT_QUALITY=0.8 diff --git a/employee-portal/package.json b/employee-portal/package.json index acd6ecb94..dd37e8bc1 100644 --- a/employee-portal/package.json +++ b/employee-portal/package.json @@ -4,7 +4,7 @@ "type": "module", "private": true, "dependencies": { - "@ducanh2912/next-pwa": "10.2.8", + "@ducanh2912/next-pwa": "10.2.9", "@emotion/cache": "11.13.1", "@emotion/react": "11.13.3", "@emotion/styled": "11.13.0", @@ -21,7 +21,8 @@ "@mui/icons-material": "5.16.7", "@mui/joy": "5.0.0-beta.48", "@mui/material": "npm:@mui/joy@5.0.0-beta.48", - "@tanstack/react-query": "5.56.2", + "@mdx-js/mdx": "3.0.1", + "@tanstack/react-query": "5.59.10", "@tanstack/react-table": "8.20.5", "compressorjs": "1.2.1", "drauu": "0.4.1", @@ -29,29 +30,31 @@ "echarts-for-react": "3.0.2", "echarts-stat": "1.2.0", "formik": "2.4.6", - "hpke-js": "1.3.1", + "hpke-js": "1.4.3", "iso8601-duration": "2.1.2", "matrix-js-sdk": "34.3.1", - "next": "14.2.12", + "next": "14.2.14", "react": "18.3.1", "react-dom": "18.3.1", "react-error-boundary": "4.0.13", "react-transition-group": "4.4.5", + "server-only": "0.0.1", "use-debounce": "10.0.3", "uuid": "10.0.0", - "valibot": "0.42.0" + "valibot": "0.42.1" }, "devDependencies": { - "@next/bundle-analyzer": "14.2.12", - "@tanstack/eslint-plugin-query": "5.56.1", - "@types/react": "18.3.7", - "@types/react-dom": "18.3.0", + "@next/bundle-analyzer": "14.2.14", + "@tanstack/eslint-plugin-query": "5.59.7", + "@types/mdx": "2.0.13", + "@types/react": "18.3.11", + "@types/react-dom": "18.3.1", "@types/react-transition-group": "4.4.11", "@types/uuid": "10.0.0", - "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-istanbul": "2.1.1", - "eslint-config-next": "14.2.12", + "@vitejs/plugin-react": "4.3.2", + "@vitest/coverage-istanbul": "2.1.2", + "eslint-config-next": "14.2.14", "vite-tsconfig-paths": "5.0.1", - "vitest": "2.1.1" + "vitest": "2.1.2" } } diff --git a/employee-portal/public/markdown/common/release-notes.md b/employee-portal/public/markdown/common/release-notes.md new file mode 100644 index 000000000..a5e578274 --- /dev/null +++ b/employee-portal/public/markdown/common/release-notes.md @@ -0,0 +1,147 @@ +GA-Lotse ist ein Kooperationsprojekt des Gesundheitsamts Frankfurt am Main mit dem Hessisches Ministerium für Familie, Senioren, Sport, Gesundheit und Pflege. + +Finanziert von der Europäischen Union – NextGenerationEU + +## GA-Lotse 1.1 + +_21.10.2024_ + +Zweiter Release der Anwendung GA-Lotse. + +### Grundfunktionen: + +* Anmeldeprotokoll + * Historie von erfolgreichen und fehlgeschlagenen Loginversuchen +* Aktive Sitzungen + * Sitzungen einsehen und trennen + +### Einschulungsuntersuchungen: + +* Erstellen von Vorgängen + * Vorgangszusammenführung und Duplikateprüfung bei Listenimport + * Erfassung des Schuljahres an Vorgängen +* Planung + * Vorgangssuche nach Wissensfaktoren (Vorname, Nachname, Geburtstag) + * Löschung von leeren Vorgängen +* Untersuchungstag + * Übersicht über geplante heutige Untersuchungen im Wartezimmer + * Erstellung von Arztbriefen + * Erstellung des Schulinfobriefs + +## GA-Lotse 1.0 + +_26.09.2024_ + +Erster Release der neuen Anwendung GA-Lotse für Gesundheitsämter. + +### Einschulungsuntersuchungen: + +* Unterstützung der Mitarbeitenden des Gesundheitsamtes bei Planung und Durchführung von Einschulungsuntersuchungen +* Erstellen von Vorgängen + * Manuelles Anlegen von Vorgängen inklusive Kindern und Personensorgeberechtigten + * Import von Bürgeramtslisten und Schullisten mithilfe einer Excel-Tabelle, Prüfung auf Duplikate und fehlerhafte Datensätze + * Zuordnung der Untersuchungsart (Regeluntersuchung, Kann-Kind, Eingangsstufe, Besonderer Förderbedarf), Vorschläge anhand des Alters und Daten aus der Schulliste + * Anlegen und Zuordnen von Kennungen zu Vorgängen +* Planung + * Planen von Terminblöcken für die Schuleingangsuntersuchungen + * Zuordnung von Arzt:innen und MFA zu den Terminblöcken inklusive Verfügbarkeitsprüfung + * Berücksichtigung unterschiedlicher Untersuchungslängen für Kinder mit potenziell erhöhtem Förderbedarf + * Manuelle Terminvergabe durch die Mitarbeitenden anhand der zugeordneten Untersuchungsart + * Automatische Massen-Terminvergabe über die Vorgangsübersicht anhand der zugeordneten Untersuchungsart + * Erstellung von Einladungen mit QR-Code für den Zugang zum Bürgerportal + * Terminverschiebung und Selbst-Anamnese im Bürgerportal durch Personensorgeberechtigte +* Untersuchungstag + * Vervollständigung der von den Personensorgeberechtigten vorausgefüllte Anamnese + * Erfassung des Impfstatus + * Erfassung des Hörscreenings, Sehscreenings, der S1-SOPESS-2024-Untersuchung und S1-Befunds + * Bei körperlichen Untersuchungen und Feststellungen von Handicaps werden mögliche Befunde mithilfe von ICD-10 Codes festgehalten + * Gewicht, Größe und BMI des Kindes werden mit Referenzperzentilen bewertet + * Übermittlung der ESU-Kennzahlen an das Statistikmodul + +### Begehungen: + +* Unterstützung der Mitarbeitenden des Gesundheitsamtes bei der Hygieneüberwachung von Einrichtungen +* Erstellen von Vorgängen + * Erfassung von Einrichtungen: Name, Objekt-Typ, Adressen, Kontaktmöglichkeiten + * Manuelles Anlegen von Vorgängen für Einrichtungen + * Automatische Websuche nach neuen Einrichtungen (Quelle OpenSteetMap) und Hinzufügen zur Zentralkartei + * Anlegen von Vorgängen für neu gefundene Einrichtungen +* Planung + * Planung des Begehungstermins + * Auswahl der anzuwendenden Checklisten + * Reservierung von Inventar über die Inventarverwaltung + * Buchung von Ressourcen wie Fahrzeuge, Fahrräder, Räume + * Aufruf eines Routenplaners +* Ausführung + * Ausfüllen von Checklisten + * Hochladen von Bildern + * Erfassung weiterer Vorkommnisse + * Offline-Modus: Ausführung auch ohne Internetverbindung möglich + * Abschließen der Begehung, optional mit Erfassung der Unterschrift eines Teilnehmenden +* Erstellung eines Begehungsprotokolls + * Automatische Erstellung eines Begehungsprotokolls mit den ausgefüllten Checklisten und Vorkommnissen + * Möglichkeit zur Bearbeitung des Begehungsprotokolls + * Erstellung eines PDF-Dokuments für das Begehungsprotokoll + * Abschließen des Vorgangs mit Planung eines Nachfolgetermins +* Konfiguration + * Einstellungen für Objekt-Typen, z.B. Wiederholungsintervalle, Standarddauer, Anfahrtszeiten + * Definition von versionierbaren Checklisten + * Austausch von Checklisten mit Landesamt und anderen Gesundheitsämtern über die zentralen Dienste + +### Statistik: + +* Unterstützung der Gesundheitsberichterstattung durch Werkzeuge zum Erstellen statistischer Auswertungen und Diagramme sowie zur Bewertung und Verbesserung der Qualität von Vorgangsdaten +* Erstellung von Statistiken + * Aggregation von Vorgangsdaten der Einschulungsuntersuchung + * Auswahl der auszuwertenden Attribute + * Festlegen eines Betrachtungszeitraums + * Speichern und Anwenden von Vorlagen für die Erstellung von Statistiken +* Tabellenansicht + * Darstellung aller Daten in Tabellenform + * Filtern und sortieren der Tabelle + * Erstellen und anwenden von Filtervorlagen + * Verlinkung von Tabellenzeilen auf Vorgänge +* Erzeugen von Auswertungen und Diagrammen + * Sechs verschiedene Diagrammtypen (Balken-, Kreis-, Streu-, Linien-, Kartendiagramm und Histogramm) + * Jeweils verschiedene Konfigurationsoptionen für jeden Diagrammtyp + * Erzeugen mehrerer Diagrammversionen mit individuellen Filterkonfigurationen + * Auch hier: Erstellen und anwenden von Filtervorlagen + * Export von Diagrammen als png/svg-Datei + * Export von Diagrammdaten als xlsx-Datei +* Datenqualität + * Übersicht über Vollständigkeit der Daten + * Berücksichtigung expliziter Unbekannt-Werte (z.B. 'Weiß nicht') +* Geo Shape-Verwaltung + * Importieren von Geo Shapes für Kartendiagramme aus geojson-Files + * Löschen und archivieren (+ Archivierung wieder aufheben) von Geo Shapes + +### Reisemedizinische Impfberatung: + +* Unterstützung bei Impfstoffverwaltung, Terminplanung und Impfdokumentation +* Impfstoffverwaltung + * Krankheitenkategorien + * Impfstoffe mit Berücksichtigung von Mindestabständen + * Bestandsaktualisierung +* Terminplanung + * Terminkontingente pro Terminart + * Personalberücksichtigung + * Konfigurierbare Terminstandarddauer +* Impfdokumentation + * Planung aller Leistungen für eine anstehende Reise eines Patienten + * Aufteilung der Leistungen in Folgetermine + * Dokumentation der Durchführung mit Verlaufseinträgen + * Generieren von Bescheinigungen für die Krankenkasse + +### Masernschutz: + +* Unterstützung der Mitarbeitenden des Gesundheitsamtes bei der Bearbeitung von Meldungen zu fehlendem Masern-Impfschutz in Einrichtungen +* Erstellen und Bearbeiten von Vorgängen im Mitarbeitenden-Portal + * Manuelles Anlegen eines Vorgangs mit zentral verwalteten Personen und Einrichtungen + * Erstellen von Nachweisaufforderungen + * Erstellen von Terminblöcken für Nachweistermine + * Manuelles Buchen, Bearbeiten und Stornieren von Terminen + * Dokumentation von Betretungsverboten + * Dokumentation von Bußgeldern + * Vollständige Dokumentation des Vorgangsverlaufs +* Prozesse im Unternehmensportal + * Vorgangsmeldung durch Einrichtungen diff --git a/employee-portal/public/markdown/frankfurt/accessibility.md b/employee-portal/public/markdown/frankfurt/accessibility.md new file mode 100644 index 000000000..45b727218 --- /dev/null +++ b/employee-portal/public/markdown/frankfurt/accessibility.md @@ -0,0 +1,69 @@ +Diese Erklärung zur digitalen Barrierefreiheit gilt für die unter **ep.frankfurt.ga-lotse.de** veröffentlichte Webseite. + +Als öffentliche Stelle im Sinne der Richtlinie (EU) 2016/2102 sind wir +bemüht, unsere Websites und mobilen Anwendungen im Einklang mit den +Bestimmungen des Hessischen Behinderten-Gleichstellungsgesetzes +(HessBGG) sowie der Hessischen Verordnung über barrierefreie +Informationstechnik (BITV HE 2019) zur Umsetzung der Richtlinie (EU) +2016/2102 barrierefrei zugänglich zu machen. Frankfurt.ga-lotse.de ist +überwiegend mit den derzeit gültigen Vorschriften zur Barrierefreiheit +(BITV 2.0, 2019/WCAG 2.1) vereinbar. Inhalte und Funktionen, die dem +derzeit noch nicht vollständig entsprechen, sind nachfolgend aufgeführt. + +## Stand der Vereinbarkeit mit den Anforderungen + +Die Anforderungen der Barrierefreiheit ergeben sich aus § 3 Absätze 1 bis 4 und § 4 der BITV HE 2019, die auf Grundlage von § 14 des HessBGG erlassen wurde. + +Die Überprüfung der Einhaltung der Anforderungen beruht auf einer am 23.09.2024 durchgeführten Selbstbewertung. + +## Nicht barrierefreie Inhalte + +Aufgrund der Überprüfung ist die **Website** mit den zuvor genannten Anforderungen **nur teilweise** vereinbar. + +- **PDF-Dateien sind nicht vollständig barrierefrei** +- **Nicht alle Schaltflächen haben erkennbaren Text** +- **Es gibt Schaltflächen mit zu kleinen Klickflächen für Touch-Geräte** +- **Nicht alle Formelemente haben eine Beschriftung** + +**Die Stadt Frankfurt am Main arbeitet daran, die barrierefreien Angebote weiter auszubauen.** + +## Datum der Erstellung der Erklärung zur Barrierefreiheit + +Diese Erklärung wurde am **23.09.2024** erstellt und +zuletzt am **23.09.2024** überprüft und aktualisiert. + +## Feedback und Anfragen zur digitalen Barrierefreiheit + +Sie möchten uns noch bestehende Barrieren mitteilen oder nicht +barrierefreie Inhalte in einem barrierefreien Format anfordern? +Sprechen Sie unsere verantwortlichen Kontaktpersonen an: + +**Gesundheitsamt Frankfurt am Main +Digitale Zukunft, IT und strategische Planung ++49 (0) 800 -4256873** +[support@ga-lotse.de](mailto:support@ga-lotse.de) + +## Durchsetzungsverfahren + +Wenn auch nach Ihrem Feedback an den oben genannten Kontakt keine +zufriedenstellende Lösung gefunden wurde, können Sie die +Durchsetzungs- und Überwachungsstelle Barrierefreie +Informationstechnik einschalten. Sie haben nach Ablauf einer Frist von +sechs Wochen das Recht sich direkt an die Durchsetzungs- und +Überwachungsstelle zu wenden. Unter Einbeziehung aller Beteiligten +versucht die Durchsetzungsstelle, die Umstände der fehlenden +Barrierefreiheit zu ermitteln, damit der Träger diese beheben kann. + +## Durchsetzungs- und Überwachungsstelle Barrierefreie Informationstechnik Hessisches Ministerium für Soziales und Integration Sitz: Regierungspräsidium Gießen + +Prof. Dr. Erdmuthe Meyer zu Bexten +Landesbeauftragte für barrierefreie IT +Leiterin der Durchsetzungs- und Überwachungsstelle +Landgraf-Philipp-Platz 1-7 +35390 Gießen +Telefon: +49 641 303 - 2901 +E-Mail: [Durchsetzungsstelle-LBIT@rpgi.hessen.de](mailto:Durchsetzungsstelle-LBIT@rpgi.hessen.de) + + + +[Durchsetzung beantragen](https://lbit.hessen.de/Durchsetzungs-und-Ueberwachungsstelle/Durchsetzungsverfahren-beantragen/Formular-Durchsetzungsverfahren) diff --git a/employee-portal/public/markdown/frankfurt/contact.md b/employee-portal/public/markdown/frankfurt/contact.md new file mode 100644 index 000000000..4aec0e34a --- /dev/null +++ b/employee-portal/public/markdown/frankfurt/contact.md @@ -0,0 +1,12 @@ +**Telefonische Erreichbarkeit:** + +Telefon: +49 (0) 800 -4256873 + +Montag - Donnerstag: 07:30 Uhr - 16:00 Uhr +Freitag: 07:30 Uhr - 14:00 Uhr + +**Erreichbarkeit via Ticketsystem:** + +E-Mail: [support@ga-lotse.de](mailto:support@ga-lotse.de) + +Open Source: [OpenCoDE](https://gitlab.opencode.de/ga-lotse) diff --git a/employee-portal/public/markdown/frankfurt/privacy.md b/employee-portal/public/markdown/frankfurt/privacy.md new file mode 100644 index 000000000..7ec9ee67c --- /dev/null +++ b/employee-portal/public/markdown/frankfurt/privacy.md @@ -0,0 +1,195 @@ +Diese Datenschutzerklärung gilt für die Webseite „frankfurt.ga-lotse.de“ +(bzw. „https://frankfurt.ga-lotse.de“ sowie dazu zugehörige Subdomains) +des Gesundheitsamts der Stadt Frankfurt am Main. +Dieses Informationsportal bietet Informationen zu besonderen Ereignissen und wird ausschließlich zum dem Zwecke genutzt. + + +## 1. Name und Kontaktdaten des für die Verarbeitung Verantwortlichen sowie des behördlichen Datenschutzbeauftragten + +Diese Datenschutz-Information gilt für die Datenverarbeitung durch: + +Verantwortlicher: + +Verantwortlich für die Website „frankfurt.ga-lotse.de“ ist das Gesundheitsamt Frankfurt am Main: + +Gesundheitsamt Frankfurt am Main +Breite Gasse 28 +60313 Frankfurt am Main +E-Mail: [datenschutz.gesundheitsamt@stadt-frankfurt.de](mailto:datenschutz.gesundheitsamt@stadt-frankfurt.de) + +Behördlicher Datenschutzbeauftragter: + +Referat Datenschutz und IT-Sicherheit +Sandgasse 6, 60311 Frankfurt am Main + +## 2. Erhebung und Speicherung personenbezogener Daten sowie Art und Zweck von deren Verwendung" + +2.1 Beim Besuch der Website + +Beim Aufrufen unserer Website „frankfurt.ga-lotse.de“ +werden durch den auf Ihrem Endgerät zum Einsatz kommenden Browser +automatisch Informationen an den Server unserer Website gesendet. +Diese Informationen werden temporär in einem sog. Logfile gespeichert. +Folgende Informationen werden dabei ohne Ihr Zutun erfasst und bis zur +automatisierten Löschung gespeichert: + +- IP-Adresse des anfragenden Rechners +- Datum und Uhrzeit des Zugriffs +- Name und URL der abgerufenen Datei +- Website, von der aus der Zugriff erfolgt (Referrer-URL) +- verwendeter Browser und ggf. das Betriebssystem Ihres Rechners sowie der Name Ihres Access-Providers + +Die genannten Daten werden durch uns zu folgenden Zwecken verarbeitet: + +- Gewährleistung eines reibungslosen Verbindungsaufbaus der Website +- Gewährleistung einer komfortablen Nutzung unserer Website +- Auswertung der Systemsicherheit und -stabilität +- Rückverfolgung etwaiger DoS Attacken +- sowie zu weiteren administrativen Zwecken + +**2.2 Beim Verwenden des QR-Codes** + +Falls Ihnen für den Zugang zum Online-Portal ein individualisierter +QR-Code gegeben wurde, hat dieser den Zweck, Informationen oder +Nachrichten speziell für Sie zur Verfügung zu stellen, beispielsweise +als betroffener eines gesundheitsrelevanten Ereignisses. Nach +Einscannen des Codes wird man direkt auf die entsprechende +Informationsseite weitergeleitet. Im System wird dabei protokolliert, +zu welchem Zeitpunkt der QR-Code verwendet wurde, jedoch ohne weitere +Angaben wie IP-Adresse oder Namen o.ä. + +Die Rechtsgrundlage für die Datenverarbeitung ist Art. 6 Abs. 1 S. 1 lit. f DS-GVO. +Unser berechtigtes Interesse folgt aus oben aufgelisteten Zwecken zur Datenerhebung. +In keinem Fall verwenden wir die erhobenen Daten zu dem Zweck, +Rückschlüsse auf Ihre Person zu ziehen. + +Darüber hinaus setzen wir beim Besuch unserer Website Cookies ein. +Nähere Erläuterungen dazu erhalten Sie unter den Ziff. 4 dieser +Datenschutzerklärung. + +## 3. Weitergabe von Daten + +Es findet keine Weitergabe von Daten an Dritte statt. + +## 4. Cookies + +Wir setzen auf unserer Seite Cookies ein. Hierbei handelt es sich um +kleine Dateien, die Ihr Browser automatisch erstellt und die auf Ihrem +Endgerät (Laptop, Tablet, Smartphone o.ä.) gespeichert werden, wenn +Sie unsere Seite besuchen. Cookies richten auf Ihrem Endgerät keinen +Schaden an, enthalten keine Viren, Trojaner oder sonstige +Schadsoftware. + +In dem Cookie werden Informationen abgelegt, die sich jeweils im +Zusammenhang mit dem spezifisch eingesetzten Endgerät ergeben. Dies +bedeutet jedoch nicht, dass wir dadurch unmittelbar Kenntnis von Ihrer +Identität erhalten. + +Der Einsatz von Cookies dient einerseits dazu, die Nutzung unseres +Angebots für Sie angenehmer zu gestalten. So setzen wir sogenannte +Session-Cookies ein, um zu erkennen, dass Sie einzelne Seiten unserer +Website bereits besucht haben. Diese werden nach Verlassen unserer +Seite automatisch gelöscht. + +Darüber hinaus setzen wir ebenfalls zur Optimierung der +Benutzerfreundlichkeit temporäre Cookies ein, die für einen bestimmten +festgelegten Zeitraum auf Ihrem Endgerät gespeichert werden. Besuchen +Sie unsere Seite erneut, um unsere Dienste in Anspruch zu nehmen, wird +automatisch erkannt, dass Sie bereits bei uns waren und welche +Eingaben und Einstellungen sie getätigt haben, um diese nicht noch +einmal eingeben zu müssen. + +Die durch Cookies verarbeiteten Daten sind für die genannten Zwecke +zur Wahrung unserer berechtigten Interessen erforderlich. + +Die meisten Browser akzeptieren Cookies automatisch. Sie können Ihren +Browser jedoch so konfigurieren, dass keine Cookies auf Ihrem Computer +gespeichert werden oder stets ein Hinweis erscheint, bevor ein neuer +Cookie angelegt wird. Die vollständige Deaktivierung von Cookies kann +jedoch dazu führen, dass Sie nicht alle Funktionen unserer Website +nutzen können. + +## 5. Analyse-Tools + +Es werden keine Analyse-Tools verwendet. + +## 6. Social Media Plug-ins + +Es werden keine Social Media Plug-Ins verwendet. + +## 7. Betroffenenrechte + +Sie haben das Recht: +- gemäß Art. 15 DS-GVO Auskunft über Ihre von uns +verarbeiteten personenbezogenen Daten zu verlangen. Insbesondere +können Sie Auskunft über die Verarbeitungszwecke, die Kategorie +der personenbezogenen Daten, die Kategorien von Empfängern, +gegenüber denen Ihre Daten offengelegt wurden oder werden, die +geplante Speicherdauer, das Bestehen eines Rechts auf +Berichtigung, Löschung, Einschränkung der Verarbeitung oder +Widerspruch, das Bestehen eines Beschwerderechts, die Herkunft +ihrer Daten, sofern diese nicht bei uns erhoben wurden sowie über +das Bestehen einer automatisierten Entscheidungsfindung +einschließlich Profiling und ggf. aussagekräftigen Informationen +zu deren Einzelheiten verlangen. +- gemäß Art. 16 DS-GVO unverzüglich die +Berichtigung unrichtiger oder Vervollständigung Ihrer bei uns +gespeicherten personenbezogenen Daten zu verlangen. +- gemäß Art. 17 DS-GVO die Löschung Ihrer bei uns +gespeicherten personenbezogenen Daten zu verlangen, soweit nicht +die Verarbeitung zur Ausübung des Rechts auf freie +Meinungsäußerung und Information, zur Erfüllung einer rechtlichen +Verpflichtung, aus Gründen des öffentlichen Interesses oder zur +Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen +erforderlich ist. +- gemäß Art. 18 DS-GVO die Einschränkung der +Verarbeitung Ihrer personenbezogenen Daten zu verlangen, soweit +die Richtigkeit der Daten von Ihnen bestritten wird, die +Verarbeitung unrechtmäßig ist, Sie aber deren Löschung ablehnen +und wir die Daten nicht mehr benötigen, Sie jedoch diese zur +Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen +benötigen oder Sie gemäß Art. 21 DS-GVO +Widerspruch gegen die Verarbeitung eingelegt haben. +- gemäß Art. 20 DS-GVO Ihre personenbezogenen +Daten, die Sie uns bereitgestellt haben, in einem strukturierten, +gängigen und maschinenlesebaren Format zu erhalten oder die +Übermittlung an einen anderen Verantwortlichen zu verlangen. +- gemäß Art. 7 Abs. 3 DS-GVO Ihre einmal erteilte +Einwilligung jederzeit gegenüber uns zu widerrufen. Dies hat zur +Folge, dass wir die Datenverarbeitung, die auf dieser Einwilligung +beruhte, für die Zukunft nicht mehr fortführen dürfen und +- gemäß Art. 77 DS-GVO sich bei der zuständigen +Aufsichtsbehörde zu beschweren. Die zuständige Aufsichtsbehörde +ist: Der Hessische Datenschutzbeauftragte, Postfach 3163, 65021 +Wiesbaden, Telefon: 0611/1408 - 0, +poststelle@datenschutz-hessen.de. + +## 8. Widerspruchsrecht + +Sofern Ihre personenbezogenen Daten auf Grundlage von berechtigten +Interessen gemäß Art. 6 Abs. 1 S. 1 lit. f DS-GVO +verarbeitet werden, haben Sie das Recht, gemäß +Art. 21 DS-GVO Widerspruch gegen die Verarbeitung +Ihrer personenbezogenen Daten einzulegen, soweit dafür Gründe +vorliegen, die sich aus Ihrer besonderen Situation ergeben. Möchten +Sie von Ihrem Widerrufs- oder Widerspruchsrecht Gebrauch machen, +genügt eine E-Mail an +datenschutz.gesundheitsamt@stadt-frankfurt.de. + +## 9. Datensicherheit + +Wir bedienen uns geeigneter technischer und organisatorischer +Sicherheitsmaßnahmen, um Ihre Daten gegen zufällige oder vorsätzliche +Manipulationen, teilweisen oder vollständigen Verlust, Zerstörung oder +gegen den unbefugten Zugriff Dritter zu schützen. Unsere +Sicherheitsmaßnahmen werden nach dem jeweiligen Stand der Technik +gemäß Art. 32 DS-GVO fortlaufend angepasst. + +## 10. Auftragsverarbeitung + +Es findet keine Auftragsverarbeitung der erhobenen Daten statt. + +## 11. Aktualität und Änderung dieser Datenschutzerklärung + +Diese Datenschutzerklärung ist aktuell gültig und hat den Stand +September 2024. diff --git a/employee-portal/src/app/(baseModule)/(static)/[documentType]/page.tsx b/employee-portal/src/app/(baseModule)/(static)/[documentType]/page.tsx new file mode 100644 index 000000000..78e427607 --- /dev/null +++ b/employee-portal/src/app/(baseModule)/(static)/[documentType]/page.tsx @@ -0,0 +1,43 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import NotFound from "@/app/not-found"; +import { StaticTextDocumentPanel } from "@/lib/baseModule/components/StaticTextDocumentPanel"; +import { + MarkdownPage, + PageName, + isValidPageType, +} from "@/lib/baseModule/components/markdown/MarkdownPage"; +import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; +import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; +import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; + +const title = { + contact: "Kontakt", + accessibility: "Erklärung zur Barrierefreiheit", + privacy: "Datenschutzerklärung", + "release-notes": "Release Notes", +} as const satisfies Record<PageName, string>; + +export default function StaticDocumentPage({ + params, +}: Readonly<{ + params: { documentType: string }; +}>) { + const documentType = params.documentType; + if (!isValidPageType(documentType)) { + return <NotFound />; + } + + return ( + <StickyToolbarLayout toolbar={<Toolbar title={title[documentType]} />}> + <MainContentLayout> + <StaticTextDocumentPanel> + <MarkdownPage pageType={documentType} /> + </StaticTextDocumentPanel> + </MainContentLayout> + </StickyToolbarLayout> + ); +} diff --git a/employee-portal/src/app/(baseModule)/acknowledgements/page.tsx b/employee-portal/src/app/(baseModule)/(static)/acknowledgements/page.tsx similarity index 100% rename from employee-portal/src/app/(baseModule)/acknowledgements/page.tsx rename to employee-portal/src/app/(baseModule)/(static)/acknowledgements/page.tsx diff --git a/employee-portal/src/app/(baseModule)/usage-notes/page.tsx b/employee-portal/src/app/(baseModule)/(static)/usage-notes/page.tsx similarity index 100% rename from employee-portal/src/app/(baseModule)/usage-notes/page.tsx rename to employee-portal/src/app/(baseModule)/(static)/usage-notes/page.tsx diff --git a/employee-portal/src/app/(baseModule)/accessibility/page.tsx b/employee-portal/src/app/(baseModule)/accessibility/page.tsx deleted file mode 100644 index 0a0036656..000000000 --- a/employee-portal/src/app/(baseModule)/accessibility/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Accessibility } from "@/lib/baseModule/components/accessibility/Accessibility"; -import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; -import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; -import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; - -export default function AccessibilityPage() { - return ( - <StickyToolbarLayout - toolbar={<Toolbar title={"Erklärung zur Barrierefreiheit"} />} - > - <MainContentLayout> - <Accessibility /> - </MainContentLayout> - </StickyToolbarLayout> - ); -} diff --git a/employee-portal/src/app/(baseModule)/contact/page.tsx b/employee-portal/src/app/(baseModule)/contact/page.tsx deleted file mode 100644 index ea5acefb6..000000000 --- a/employee-portal/src/app/(baseModule)/contact/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Contact } from "@/lib/baseModule/components/contact/Contact"; -import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; -import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; -import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; - -export default function ContactPage() { - return ( - <StickyToolbarLayout toolbar={<Toolbar title={"Kontakt"} />}> - <MainContentLayout> - <Contact /> - </MainContentLayout> - </StickyToolbarLayout> - ); -} diff --git a/employee-portal/src/app/(baseModule)/privacy/page.tsx b/employee-portal/src/app/(baseModule)/privacy/page.tsx deleted file mode 100644 index f79018894..000000000 --- a/employee-portal/src/app/(baseModule)/privacy/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Privacy } from "@/lib/baseModule/components/privacy/Privacy"; -import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; -import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; -import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; - -export default function PrivacyPage() { - return ( - <StickyToolbarLayout toolbar={<Toolbar title={"Datenschutzerklärung"} />}> - <MainContentLayout> - <Privacy /> - </MainContentLayout> - </StickyToolbarLayout> - ); -} diff --git a/employee-portal/src/app/(baseModule)/release-notes/page.tsx b/employee-portal/src/app/(baseModule)/release-notes/page.tsx deleted file mode 100644 index bb5564d15..000000000 --- a/employee-portal/src/app/(baseModule)/release-notes/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ReleaseNotes } from "@/lib/baseModule/components/releaseNotes/ReleaseNotes"; -import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; -import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; -import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; - -export default function ReleaseNotesPage() { - return ( - <StickyToolbarLayout toolbar={<Toolbar title={"Release Notes"} />}> - <MainContentLayout> - <ReleaseNotes /> - </MainContentLayout> - </StickyToolbarLayout> - ); -} diff --git a/employee-portal/src/app/(businessModules)/chat/layout.tsx b/employee-portal/src/app/(businessModules)/chat/layout.tsx deleted file mode 100644 index 380825106..000000000 --- a/employee-portal/src/app/(businessModules)/chat/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { RequiresChildren } from "@eshg/lib-portal/types/react"; - -import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; -import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; -import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; - -export default function ChatLayout(props: Readonly<RequiresChildren>) { - return ( - <StickyToolbarLayout toolbar={<Toolbar title="Chat" />}> - <MainContentLayout fullViewportHeight>{props.children}</MainContentLayout> - </StickyToolbarLayout> - ); -} diff --git a/employee-portal/src/app/(businessModules)/chat/page.tsx b/employee-portal/src/app/(businessModules)/chat/page.tsx index 258d3f0ee..838fb81ee 100644 --- a/employee-portal/src/app/(businessModules)/chat/page.tsx +++ b/employee-portal/src/app/(businessModules)/chat/page.tsx @@ -13,6 +13,9 @@ import { ChatFeatureUnavailable } from "@/lib/businessModules/chat/components/Ch import { ChatNoAccessAlert } from "@/lib/businessModules/chat/components/ChatNoAccessAlert"; import { useChat } from "@/lib/businessModules/chat/shared/ChatProvider"; import { InfoPanelProvider } from "@/lib/businessModules/chat/shared/InfoPanelProvider"; +import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; +import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; +import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; export default function ChatPage() { const { @@ -30,13 +33,19 @@ export default function ChatPage() { return <ChatFeatureUnavailable />; } - return userSettings.chatUsageEnabled ? ( - <ChatErrorBoundary> - <InfoPanelProvider> - <Chat /> - </InfoPanelProvider> - </ChatErrorBoundary> - ) : ( - <ChatNoAccessAlert /> + return ( + <StickyToolbarLayout toolbar={<Toolbar title="Chat" />}> + <MainContentLayout fullViewportHeight> + {userSettings.chatUsageEnabled ? ( + <ChatErrorBoundary> + <InfoPanelProvider> + <Chat /> + </InfoPanelProvider> + </ChatErrorBoundary> + ) : ( + <ChatNoAccessAlert /> + )} + </MainContentLayout> + </StickyToolbarLayout> ); } diff --git a/employee-portal/src/app/(businessModules)/inspection/checklist/def/[defId]/versions/[versionId]/new/page.tsx b/employee-portal/src/app/(businessModules)/inspection/checklist/def/[defId]/versions/[versionId]/new/page.tsx index 00eb1b7fc..41a30757f 100644 --- a/employee-portal/src/app/(businessModules)/inspection/checklist/def/[defId]/versions/[versionId]/new/page.tsx +++ b/employee-portal/src/app/(businessModules)/inspection/checklist/def/[defId]/versions/[versionId]/new/page.tsx @@ -6,8 +6,14 @@ "use client"; import { ApiUserRole } from "@eshg/employee-portal-api/base"; +import { useSuspenseQueries } from "@tanstack/react-query"; -import { useGetChecklistDefinitionVersion } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; +import { + useChecklistDefinitionApi, + useObjectTypeApi, +} from "@/lib/businessModules/inspection/api/clients"; +import { getChecklistDefinitionVersionQuery } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; +import { getObjectTypesQuery } from "@/lib/businessModules/inspection/api/queries/objectTypes"; import { EditChecklistDefinition } from "@/lib/businessModules/inspection/components/checklistDefinition/EditChecklistDefinition"; import { routes } from "@/lib/businessModules/inspection/shared/routes"; import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; @@ -20,8 +26,16 @@ export default function NewChecklistVersion({ }: Readonly<{ params: { defId: string; versionId: string }; }>) { - const { data: checklistVersion } = - useGetChecklistDefinitionVersion(versionId); + const objectTypeApi = useObjectTypeApi(); + const checklistDefinitionApi = useChecklistDefinitionApi(); + + const [{ data: objectTypes }, { data: checklistVersion }] = + useSuspenseQueries({ + queries: [ + getObjectTypesQuery(objectTypeApi), + getChecklistDefinitionVersionQuery(checklistDefinitionApi, versionId), + ], + }); if (checklistVersion.context.defId !== defId) { throw new Error("defId does not match"); @@ -44,6 +58,7 @@ export default function NewChecklistVersion({ <EditChecklistDefinition cldVersion={checklistVersion} readonly={!canWrite} + objectTypes={objectTypes} /> </MainContentLayout> </StickyToolbarLayout> diff --git a/employee-portal/src/app/(businessModules)/inspection/checklist/def/[defId]/versions/[versionId]/page.tsx b/employee-portal/src/app/(businessModules)/inspection/checklist/def/[defId]/versions/[versionId]/page.tsx index b9059f0fb..0d06189f9 100644 --- a/employee-portal/src/app/(businessModules)/inspection/checklist/def/[defId]/versions/[versionId]/page.tsx +++ b/employee-portal/src/app/(businessModules)/inspection/checklist/def/[defId]/versions/[versionId]/page.tsx @@ -5,7 +5,14 @@ "use client"; -import { useGetChecklistDefinitionVersion } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; +import { useSuspenseQueries } from "@tanstack/react-query"; + +import { + useChecklistDefinitionApi, + useObjectTypeApi, +} from "@/lib/businessModules/inspection/api/clients"; +import { getChecklistDefinitionVersionQuery } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; +import { getObjectTypesQuery } from "@/lib/businessModules/inspection/api/queries/objectTypes"; import { EditChecklistDefinition } from "@/lib/businessModules/inspection/components/checklistDefinition/EditChecklistDefinition"; import { routes } from "@/lib/businessModules/inspection/shared/routes"; import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; @@ -17,8 +24,16 @@ export default function ViewChecklistVersion({ }: Readonly<{ params: { defId: string; versionId: string }; }>) { - const { data: checklistVersion } = - useGetChecklistDefinitionVersion(versionId); + const objectTypeApi = useObjectTypeApi(); + const checklistDefinitionApi = useChecklistDefinitionApi(); + + const [{ data: objectTypes }, { data: checklistVersion }] = + useSuspenseQueries({ + queries: [ + getObjectTypesQuery(objectTypeApi), + getChecklistDefinitionVersionQuery(checklistDefinitionApi, versionId), + ], + }); if (checklistVersion.context.defId !== defId) { throw new Error("defId does not match"); @@ -34,7 +49,11 @@ export default function ViewChecklistVersion({ } > <MainContentLayout> - <EditChecklistDefinition cldVersion={checklistVersion} readonly /> + <EditChecklistDefinition + cldVersion={checklistVersion} + readonly + objectTypes={objectTypes} + /> </MainContentLayout> </StickyToolbarLayout> ); diff --git a/employee-portal/src/app/(businessModules)/inspection/checklist/def/new/page.tsx b/employee-portal/src/app/(businessModules)/inspection/checklist/def/new/page.tsx index 3e0bbe87d..44a45bbd1 100644 --- a/employee-portal/src/app/(businessModules)/inspection/checklist/def/new/page.tsx +++ b/employee-portal/src/app/(businessModules)/inspection/checklist/def/new/page.tsx @@ -5,6 +5,7 @@ "use client"; +import { useGetObjectTypes } from "@/lib/businessModules/inspection/api/queries/objectTypes"; import { EditChecklistDefinition } from "@/lib/businessModules/inspection/components/checklistDefinition/EditChecklistDefinition"; import { routes } from "@/lib/businessModules/inspection/shared/routes"; import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; @@ -12,6 +13,8 @@ import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolba import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; export default function NewChecklist() { + const { data: objectTypes } = useGetObjectTypes(); + return ( <StickyToolbarLayout toolbar={ @@ -22,7 +25,7 @@ export default function NewChecklist() { } > <MainContentLayout fullViewportHeight> - <EditChecklistDefinition /> + <EditChecklistDefinition objectTypes={objectTypes} /> </MainContentLayout> </StickyToolbarLayout> ); diff --git a/employee-portal/src/app/(businessModules)/inspection/checklist/def/page.tsx b/employee-portal/src/app/(businessModules)/inspection/checklist/def/page.tsx index 5184b6678..1a20eca27 100644 --- a/employee-portal/src/app/(businessModules)/inspection/checklist/def/page.tsx +++ b/employee-portal/src/app/(businessModules)/inspection/checklist/def/page.tsx @@ -10,6 +10,7 @@ import { InternalLinkButton } from "@eshg/lib-portal/components/navigation/Inter import AddIcon from "@mui/icons-material/Add"; import { Box } from "@mui/joy"; +import { useGetChecklistDefinitions } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; import { ChecklistDefinitionOverviewTable } from "@/lib/businessModules/inspection/components/checklistDefinition/overview/ChecklistDefinitionOverviewTable"; import { routes } from "@/lib/businessModules/inspection/shared/routes"; import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; @@ -21,6 +22,7 @@ export default function ChecklistOverview() { const canWrite = useHasUserRoleCheck( ApiUserRole.InspectionChecklistdefinitionsWrite, ); + const { data: checklists, isFetching } = useGetChecklistDefinitions(); return ( <StickyToolbarLayout toolbar={<Toolbar title="Checklistendefinitionen" />}> @@ -35,7 +37,10 @@ export default function ChecklistOverview() { </InternalLinkButton> </Box> )} - <ChecklistDefinitionOverviewTable /> + <ChecklistDefinitionOverviewTable + checklists={checklists} + isFetching={isFetching} + /> </MainContentLayout> </StickyToolbarLayout> ); diff --git a/employee-portal/src/app/(businessModules)/inspection/procedures/new/[procedureId]/page.tsx b/employee-portal/src/app/(businessModules)/inspection/procedures/new/[procedureId]/page.tsx index 3e28869ed..b062c0f08 100644 --- a/employee-portal/src/app/(businessModules)/inspection/procedures/new/[procedureId]/page.tsx +++ b/employee-portal/src/app/(businessModules)/inspection/procedures/new/[procedureId]/page.tsx @@ -5,9 +5,21 @@ "use client"; -import { useGetInspection } from "@/lib/businessModules/inspection/api/queries/inspection"; -import { useGetObjectTypes } from "@/lib/businessModules/inspection/api/queries/objectTypes"; +import { useSuspenseQueries } from "@tanstack/react-query"; + +import { useUserApi } from "@/lib/baseModule/api/clients"; +import { + useInspectionApi, + useObjectTypeApi, +} from "@/lib/businessModules/inspection/api/clients"; +import { getInspectionQuery } from "@/lib/businessModules/inspection/api/queries/inspection"; +import { getObjectTypesQuery } from "@/lib/businessModules/inspection/api/queries/objectTypes"; +import { + getAllAssignableUsersQuery, + getSelfUserQuery, +} from "@/lib/businessModules/inspection/api/queries/users"; import { AddInspectionTiles } from "@/lib/businessModules/inspection/components/inspection/new/AddInspectionTiles"; +import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; import { routes } from "@/lib/businessModules/inspection/shared/routes"; import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; @@ -18,9 +30,30 @@ export default function NewInspectionProcedurePage({ }: Readonly<{ params: { procedureId: string }; }>) { - const { data: inspection } = useGetInspection(params.procedureId); + const inspectionApi = useInspectionApi(); + const objectTypeApi = useObjectTypeApi(); + const userApi = useUserApi(); + const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); + + const [ + { data: inspection }, + { data: objectTypes }, + { data: selfUser }, + { data: allAssignableUsers }, + ] = useSuspenseQueries({ + queries: [ + getInspectionQuery( + inspectionApi, + getPreCacheForOfflineModeHeaders, + params.procedureId, + ), + getObjectTypesQuery(objectTypeApi), + getSelfUserQuery(userApi), + getAllAssignableUsersQuery(userApi), + ], + }); + const facility = inspection.facility; - const { data: objectTypes } = useGetObjectTypes(); return ( <StickyToolbarLayout @@ -32,7 +65,12 @@ export default function NewInspectionProcedurePage({ } > <MainContentLayout> - <AddInspectionTiles inspection={inspection} objectTypes={objectTypes} /> + <AddInspectionTiles + inspection={inspection} + objectTypes={objectTypes} + selfUser={selfUser} + allAssignableUsers={allAssignableUsers} + /> </MainContentLayout> </StickyToolbarLayout> ); diff --git a/employee-portal/src/app/(businessModules)/inspection/repository/checklist/[repositoryChecklistDefinitionId]/versions/[version]/page.tsx b/employee-portal/src/app/(businessModules)/inspection/repository/checklist/[repositoryChecklistDefinitionId]/versions/[version]/page.tsx index 882e5ede9..dcf936723 100644 --- a/employee-portal/src/app/(businessModules)/inspection/repository/checklist/[repositoryChecklistDefinitionId]/versions/[version]/page.tsx +++ b/employee-portal/src/app/(businessModules)/inspection/repository/checklist/[repositoryChecklistDefinitionId]/versions/[version]/page.tsx @@ -5,7 +5,14 @@ "use client"; -import { useGetChecklistDefinitionFromCentralRepo } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; +import { useSuspenseQueries } from "@tanstack/react-query"; + +import { + useChecklistDefinitionCentralRepoApi, + useObjectTypeApi, +} from "@/lib/businessModules/inspection/api/clients"; +import { getChecklistDefinitionFromCentralRepoQuery } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; +import { getObjectTypesQuery } from "@/lib/businessModules/inspection/api/queries/objectTypes"; import { EditChecklistDefinition } from "@/lib/businessModules/inspection/components/checklistDefinition/EditChecklistDefinition"; import { RepoChecklistDefinitionHeaderRow } from "@/lib/businessModules/inspection/components/repository/RepoChecklistDefinitionHeaderRow"; import { routes } from "@/lib/businessModules/inspection/shared/routes"; @@ -21,9 +28,25 @@ export default function InspectionRepositoryPage({ const repoCldId = parseInt(params.repositoryChecklistDefinitionId); const repoVersion = parseInt(params.version); - const { - data: { checklistDefinition, ...metadata }, - } = useGetChecklistDefinitionFromCentralRepo(repoCldId, repoVersion, false); + const repoApi = useChecklistDefinitionCentralRepoApi(); + const objectTypeApi = useObjectTypeApi(); + + const [ + { + data: { checklistDefinition, ...metadata }, + }, + { data: objectTypes }, + ] = useSuspenseQueries({ + queries: [ + getChecklistDefinitionFromCentralRepoQuery( + repoApi, + repoCldId, + repoVersion, + false, + ), + getObjectTypesQuery(objectTypeApi), + ], + }); return ( <StickyToolbarLayout @@ -47,6 +70,7 @@ export default function InspectionRepositoryPage({ metadata={metadata} /> } + objectTypes={objectTypes} /> </MainContentLayout> </StickyToolbarLayout> diff --git a/employee-portal/src/app/(businessModules)/inspection/repository/core-checklist/[repositoryChecklistDefinitionId]/versions/[version]/page.tsx b/employee-portal/src/app/(businessModules)/inspection/repository/core-checklist/[repositoryChecklistDefinitionId]/versions/[version]/page.tsx index 2010fbb66..e4f213f26 100644 --- a/employee-portal/src/app/(businessModules)/inspection/repository/core-checklist/[repositoryChecklistDefinitionId]/versions/[version]/page.tsx +++ b/employee-portal/src/app/(businessModules)/inspection/repository/core-checklist/[repositoryChecklistDefinitionId]/versions/[version]/page.tsx @@ -5,7 +5,14 @@ "use client"; -import { useGetChecklistDefinitionFromCentralRepo } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; +import { useSuspenseQueries } from "@tanstack/react-query"; + +import { + useChecklistDefinitionCentralRepoApi, + useObjectTypeApi, +} from "@/lib/businessModules/inspection/api/clients"; +import { getChecklistDefinitionFromCentralRepoQuery } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; +import { getObjectTypesQuery } from "@/lib/businessModules/inspection/api/queries/objectTypes"; import { EditChecklistDefinition } from "@/lib/businessModules/inspection/components/checklistDefinition/EditChecklistDefinition"; import { RepoChecklistDefinitionHeaderRow } from "@/lib/businessModules/inspection/components/repository/RepoChecklistDefinitionHeaderRow"; import { routes } from "@/lib/businessModules/inspection/shared/routes"; @@ -21,9 +28,25 @@ export default function InspectionRepositoryPage({ const repoCldId = parseInt(params.repositoryChecklistDefinitionId); const repoVersion = parseInt(params.version); - const { - data: { checklistDefinition, ...metadata }, - } = useGetChecklistDefinitionFromCentralRepo(repoCldId, repoVersion, true); + const repoApi = useChecklistDefinitionCentralRepoApi(); + const objectTypeApi = useObjectTypeApi(); + + const [ + { + data: { checklistDefinition, ...metadata }, + }, + { data: objectTypes }, + ] = useSuspenseQueries({ + queries: [ + getChecklistDefinitionFromCentralRepoQuery( + repoApi, + repoCldId, + repoVersion, + true, + ), + getObjectTypesQuery(objectTypeApi), + ], + }); return ( <StickyToolbarLayout @@ -47,6 +70,7 @@ export default function InspectionRepositoryPage({ metadata={metadata} /> } + objectTypes={objectTypes} /> </MainContentLayout> </StickyToolbarLayout> diff --git a/employee-portal/src/app/(businessModules)/inspection/textblocks/page.tsx b/employee-portal/src/app/(businessModules)/inspection/textblocks/page.tsx index eafb27400..eb71f0636 100644 --- a/employee-portal/src/app/(businessModules)/inspection/textblocks/page.tsx +++ b/employee-portal/src/app/(businessModules)/inspection/textblocks/page.tsx @@ -3,8 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +"use client"; + import { GetTextBlocksRequest } from "@eshg/employee-portal-api/inspection"; +import { useGetTextBlocks } from "@/lib/businessModules/inspection/api/queries/textblocks"; import { TextBlocksTable } from "@/lib/businessModules/inspection/components/textBlock/TextBlocksTable"; import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; @@ -15,11 +18,19 @@ export default function TextBlocksOverviewPage(props: { searchParams: SearchParams; }) { const request: GetTextBlocksRequest = props.searchParams; + const { + data: { elements, totalNumberOfElements }, + isFetching, + } = useGetTextBlocks(request); return ( <StickyToolbarLayout toolbar={<Toolbar title="Textbausteine" />}> <MainContentLayout fullViewportHeight> - <TextBlocksTable params={request} /> + <TextBlocksTable + elements={elements} + totalNumberOfElements={totalNumberOfElements} + isFetching={isFetching} + /> </MainContentLayout> </StickyToolbarLayout> ); diff --git a/employee-portal/src/app/(businessModules)/school-entry/procedures/[id]/examinations/layout.tsx b/employee-portal/src/app/(businessModules)/school-entry/procedures/[id]/examinations/layout.tsx index d100f5e33..d0019f6d1 100644 --- a/employee-portal/src/app/(businessModules)/school-entry/procedures/[id]/examinations/layout.tsx +++ b/employee-portal/src/app/(businessModules)/school-entry/procedures/[id]/examinations/layout.tsx @@ -5,27 +5,22 @@ "use client"; -import { - ApiRequiredProcedureData, - ApiSchoolEntryFeature, -} from "@eshg/employee-portal-api/schoolEntry"; +import { ApiRequiredProcedureData } from "@eshg/employee-portal-api/schoolEntry"; import { Button, Grid, Stack } from "@mui/joy"; import { PropsWithChildren, useState } from "react"; import { SchoolEntryProcedurePageProps } from "@/app/(businessModules)/school-entry/procedures/[id]/layout"; import { useSchoolEntryApi } from "@/lib/businessModules/schoolEntry/api/clients"; -import { useIsNewFeatureEnabled } from "@/lib/businessModules/schoolEntry/api/queries/featureTogglesApi"; import { useGetProcedure } from "@/lib/businessModules/schoolEntry/api/queries/schoolEntryApi"; import { RequiredProcedureDataDialog } from "@/lib/businessModules/schoolEntry/features/procedures/examinations/RequiredProcedureDataModal"; -import { MedicalReportSidebar } from "@/lib/businessModules/schoolEntry/features/procedures/reports/MedicalReportSidebar"; -import { SchoolInfoLetterSidebar } from "@/lib/businessModules/schoolEntry/features/procedures/reports/SchoolInfoLetterSidebar"; +import { useMedicalReportSidebar } from "@/lib/businessModules/schoolEntry/features/procedures/reports/MedicalReportSidebar"; +import { useSchoolInfoLetterSidebar } from "@/lib/businessModules/schoolEntry/features/procedures/reports/SchoolInfoLetterSidebar"; import { routes } from "@/lib/businessModules/schoolEntry/shared/routes"; import { PageGrid } from "@/lib/shared/components/page/PageGrid"; import { SidePanel } from "@/lib/shared/components/sidePanel/SidePanel"; import { SidePanelNav } from "@/lib/shared/components/sidePanel/SidePanelNav"; import { SidePanelNavLink } from "@/lib/shared/components/sidePanel/SidePanelNavLink"; import { SidePanelTitle } from "@/lib/shared/components/sidePanel/SidePanelTitle"; -import { useToggle } from "@/lib/shared/hooks/useToggle"; interface NavItem { name: string; @@ -57,12 +52,6 @@ function buildNavItems(procedureId: string): NavItem[] { export default function SchoolEntryExaminationLayout( props: PropsWithChildren<SchoolEntryProcedurePageProps>, ) { - const medicalReportEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.MedicalReport, - ); - const schoolInfoLetterEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.SchoolInfoLetter, - ); const procedureId = props.params.id; const navItems = buildNavItems(procedureId); const procedureDetails = useGetProcedure(procedureId).data; @@ -82,14 +71,9 @@ export default function SchoolEntryExaminationLayout( ))} </SidePanelNav> </SidePanel> - {(medicalReportEnabled || schoolInfoLetterEnabled) && - !procedureDetails.isClosed && ( - <CreateReportsPanel - procedureId={procedureId} - showMedicalReportButton={medicalReportEnabled} - showSchoolInfoLetterButton={schoolInfoLetterEnabled} - /> - )} + {!procedureDetails.isClosed && ( + <CreateReportsPanel procedureId={procedureId} /> + )} </Stack> </Grid> </PageGrid> @@ -98,14 +82,11 @@ export default function SchoolEntryExaminationLayout( interface CreateReportsPanelProps { procedureId: string; - showMedicalReportButton: boolean; - showSchoolInfoLetterButton: boolean; } function CreateReportsPanel(props: CreateReportsPanelProps) { - const [medicalReportSidebarOpen, toggleMedicalReportSidebarOpen] = - useToggle(); - const [schoolInfoLetterSidebarOpen, toggleSchoolInfoSidebar] = useToggle(); + const medicalReportSidebar = useMedicalReportSidebar(); + const schoolInfoLetterSidebar = useSchoolInfoLetterSidebar(); const [requiredProcedureData, setRequiredProcedureData] = useState< ApiRequiredProcedureData[] >([]); @@ -115,7 +96,7 @@ function CreateReportsPanel(props: CreateReportsPanelProps) { const data = await schoolEntryApi.validateCompleteness(props.procedureId); const invalidAreas = data.invalidAreas; if (invalidAreas?.length === 0) { - toggleSchoolInfoSidebar(); + schoolInfoLetterSidebar.open({ procedureId: props.procedureId }); setRequiredProcedureData([]); } else { setRequiredProcedureData(invalidAreas); @@ -125,37 +106,22 @@ function CreateReportsPanel(props: CreateReportsPanelProps) { return ( <SidePanel data-testid="reportsPanel"> <SidePanelTitle>Berichte erstellen</SidePanelTitle> - {props.showSchoolInfoLetterButton && ( - <> - <Button variant="solid" onClick={handleSchoolInfoLetterClick}> - Schulinfobrief erstellen - </Button> - <RequiredProcedureDataDialog - open={requiredProcedureData.length > 0} - onClose={() => setRequiredProcedureData([])} - requiredProcedureData={requiredProcedureData} - /> - </> - )} - {props.showMedicalReportButton && ( - <Button variant="outlined" onClick={toggleMedicalReportSidebarOpen}> - Arztbrief erstellen - </Button> - )} - {medicalReportSidebarOpen && ( - <MedicalReportSidebar - open={medicalReportSidebarOpen} - procedureId={props.procedureId} - onClose={toggleMedicalReportSidebarOpen} - /> - )} - {schoolInfoLetterSidebarOpen && ( - <SchoolInfoLetterSidebar - procedureId={props.procedureId} - open={schoolInfoLetterSidebarOpen} - onClose={toggleSchoolInfoSidebar} - /> - )} + <Button variant="solid" onClick={handleSchoolInfoLetterClick}> + Schulinfobrief erstellen + </Button> + <RequiredProcedureDataDialog + open={requiredProcedureData.length > 0} + onClose={() => setRequiredProcedureData([])} + requiredProcedureData={requiredProcedureData} + /> + <Button + variant="outlined" + onClick={() => + medicalReportSidebar.open({ procedureId: props.procedureId }) + } + > + Arztbrief erstellen + </Button> </SidePanel> ); } diff --git a/employee-portal/src/app/(businessModules)/school-entry/procedures/page.tsx b/employee-portal/src/app/(businessModules)/school-entry/procedures/page.tsx index 4ffff3d27..775d3bdfb 100644 --- a/employee-portal/src/app/(businessModules)/school-entry/procedures/page.tsx +++ b/employee-portal/src/app/(businessModules)/school-entry/procedures/page.tsx @@ -3,28 +3,32 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { InternalLinkButton } from "@eshg/lib-portal/components/navigation/InternalLinkButton"; +"use client"; + import { Cached } from "@mui/icons-material"; +import { Button } from "@mui/joy"; +import { useImportDataSidebar } from "@/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataSidebar"; import { CreateProcedureSidebar } from "@/lib/businessModules/schoolEntry/features/procedures/new/CreateProcedureSidebar"; import { BUTTON_SIZE } from "@/lib/businessModules/schoolEntry/features/procedures/new/constants"; import { ProceduresTable } from "@/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProceduresTable"; -import { routes } from "@/lib/businessModules/schoolEntry/shared/routes"; import { OverlayBoundary } from "@/lib/shared/components/boundaries/OverlayBoundary"; import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; function ImportDataButton() { + const importDataSidebar = useImportDataSidebar(); + return ( - <InternalLinkButton - href={routes.procedures.importData} + <Button startDecorator={<Cached />} variant="outlined" size={BUTTON_SIZE} + onClick={importDataSidebar.open} > Daten importieren - </InternalLinkButton> + </Button> ); } diff --git a/employee-portal/src/app/(businessModules)/statistics/geo-shapes/template.tsx b/employee-portal/src/app/(businessModules)/statistics/geo-shapes/template.tsx new file mode 100644 index 000000000..36fdf6f55 --- /dev/null +++ b/employee-portal/src/app/(businessModules)/statistics/geo-shapes/template.tsx @@ -0,0 +1,8 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { QueryBoundary } from "@eshg/lib-portal/components/boundaries/QueryBoundary"; + +export default QueryBoundary; diff --git a/employee-portal/src/app/(businessModules)/statistics/reports/[id]/page.tsx b/employee-portal/src/app/(businessModules)/statistics/reports/[id]/page.tsx index 848ce1ee2..bde2095ce 100644 --- a/employee-portal/src/app/(businessModules)/statistics/reports/[id]/page.tsx +++ b/employee-portal/src/app/(businessModules)/statistics/reports/[id]/page.tsx @@ -5,8 +5,6 @@ "use client"; -import { isNonNullish } from "remeda"; - import { useGetReportDetails } from "@/lib/businessModules/statistics/api/queries/useGetReportDetails"; import { ReportDetails } from "@/lib/businessModules/statistics/components/reports/ReportDetails"; import { routes } from "@/lib/businessModules/statistics/shared/routes"; @@ -19,13 +17,11 @@ export default function ReportDetailsPage( ) { const reportDetails = useGetReportDetails(props.params.id); - const pageTitle = isNonNullish(reportDetails.series) - ? `${reportDetails.title} - Ausgabe #${reportDetails.series.index}` - : reportDetails.title; - return ( <StickyToolbarLayout - toolbar={<Toolbar title={pageTitle} backHref={routes.reports.index} />} + toolbar={ + <Toolbar title={reportDetails.title} backHref={routes.reports.index} /> + } > <MainContentLayout> <ReportDetails {...reportDetails} /> diff --git a/employee-portal/src/app/(businessModules)/statistics/reports/template.tsx b/employee-portal/src/app/(businessModules)/statistics/reports/template.tsx new file mode 100644 index 000000000..36fdf6f55 --- /dev/null +++ b/employee-portal/src/app/(businessModules)/statistics/reports/template.tsx @@ -0,0 +1,8 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { QueryBoundary } from "@eshg/lib-portal/components/boundaries/QueryBoundary"; + +export default QueryBoundary; diff --git a/employee-portal/src/app/(businessModules)/statistics/statistics/template.tsx b/employee-portal/src/app/(businessModules)/statistics/statistics/template.tsx new file mode 100644 index 000000000..36fdf6f55 --- /dev/null +++ b/employee-portal/src/app/(businessModules)/statistics/statistics/template.tsx @@ -0,0 +1,8 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { QueryBoundary } from "@eshg/lib-portal/components/boundaries/QueryBoundary"; + +export default QueryBoundary; diff --git a/employee-portal/src/app/(businessModules)/sti-protection/procedures/page.tsx b/employee-portal/src/app/(businessModules)/sti-protection/procedures/page.tsx index 7e3106273..ea2fe4629 100644 --- a/employee-portal/src/app/(businessModules)/sti-protection/procedures/page.tsx +++ b/employee-portal/src/app/(businessModules)/sti-protection/procedures/page.tsx @@ -10,7 +10,7 @@ import { Stack } from "@mui/joy"; import { StiProtectionProceduresSearchBar } from "@/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresSearchBar"; import { StiProtectionProceduresTable } from "@/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresTable"; -import { AddNewProcedureSidebar } from "@/lib/businessModules/stiProtection/procedures/addNewProcedure/AddNewProcedureSidebar"; +import { AddNewProcedureSidebar } from "@/lib/businessModules/stiProtection/features/procedures/addNewProcedure/AddNewProcedureSidebar"; import { ToggledPage } from "@/lib/shared/components/ToggledPage"; import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; diff --git a/employee-portal/src/app/@modal/(businessModules)/school-entry/procedures/import-data/page.tsx b/employee-portal/src/app/@modal/(businessModules)/school-entry/procedures/import-data/page.tsx deleted file mode 100644 index 40dbb66f2..000000000 --- a/employee-portal/src/app/@modal/(businessModules)/school-entry/procedures/import-data/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ImportDataSidebar } from "@/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataSidebar"; - -export default function SchoolEntryImportDataPage() { - return <ImportDataSidebar />; -} diff --git a/employee-portal/src/app/not-found.tsx b/employee-portal/src/app/not-found.tsx index 618e3997b..8766a7722 100644 --- a/employee-portal/src/app/not-found.tsx +++ b/employee-portal/src/app/not-found.tsx @@ -3,24 +3,45 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Alert } from "@eshg/lib-portal/components/Alert"; - -import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; -import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; -import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; -import { PAGE_ALERT_STYLE } from "@/lib/shared/styles"; +import { InternalLinkButton } from "@eshg/lib-portal/components/navigation/InternalLinkButton"; +import WebAssetOffOutlinedIcon from "@mui/icons-material/WebAssetOffOutlined"; +import { Box, Stack, SvgIcon, Typography } from "@mui/joy"; export default function NotFound() { return ( - <StickyToolbarLayout toolbar={<Toolbar title="404: Nicht gefunden" />}> - <MainContentLayout> - <Alert - title="Seite nicht gefunden" - message="Die aufgerufene Seite konnte leider nicht gefunden werden." - color="danger" - sx={PAGE_ALERT_STYLE} - /> - </MainContentLayout> - </StickyToolbarLayout> + <Box + sx={{ + display: "flex", + justifyContent: "center", + paddingTop: "5.625rem", + marginInline: 3, + // component: "alert", + }} + > + <Stack + sx={{ + maxWidth: "46.25rem", + width: "100%", + padding: 3, + backgroundColor: "background.body", + borderRadius: "lg", + alignItems: "center", + }} + > + <Stack spacing={2} sx={{ alignItems: "center", marginBottom: 3 }}> + <SvgIcon sx={{ width: "8.125rem", height: "8.125rem" }}> + <WebAssetOffOutlinedIcon /> + </SvgIcon> + <Typography level="h1" textAlign="center" data-testid="title"> + Seite nicht gefunden + </Typography> + <Typography textAlign="center" data-testid="message"> + Leider funktioniert der eingegebene Link nicht oder die Seite wurde + entfernt. + </Typography> + </Stack> + <InternalLinkButton href="/">Zur Startseit</InternalLinkButton> + </Stack> + </Box> ); } diff --git a/employee-portal/src/app/playground/alert/page.tsx b/employee-portal/src/app/playground/alert/page.tsx new file mode 100644 index 000000000..cc2e8164d --- /dev/null +++ b/employee-portal/src/app/playground/alert/page.tsx @@ -0,0 +1,107 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +"use client"; + +import { useAlert } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { isNonEmptyString } from "@eshg/lib-portal/helpers/guards"; +import { + Button, + Checkbox, + FormControl, + FormLabel, + Input, + Option, + Select, + Stack, +} from "@mui/joy"; +import { useState } from "react"; + +import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; +import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; +import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; + +const DEFAULT_TYPE = "error"; +const TYPES = ["error", "warning", "notification"] as const; + +export default function SnackbarPlaygroundPage() { + const alert = useAlert(); + const [type, setType] = useState<(typeof TYPES)[number]>(DEFAULT_TYPE); + const [title, setTitle] = useState("Title"); + const [message, setMessage] = useState("Message"); + const [closeable, setCloseable] = useState(false); + const [action, setAction] = useState(""); + + function openAlert() { + alert[type]({ + title, + message, + action: isNonEmptyString(action) + ? { text: action, onClick: () => window.alert("Action clicked") } + : undefined, + closeable, + }); + } + + return ( + <StickyToolbarLayout + toolbar={<Toolbar title="Alert" backHref="/playground" />} + > + <MainContentLayout> + <Stack gap={3}> + <FormControl> + <FormLabel>Type</FormLabel> + <Select + value={type} + onChange={(_, value) => setType(value ?? DEFAULT_TYPE)} + > + {TYPES.map((type) => ( + <Option key={type} value={type}> + {type} + </Option> + ))} + </Select> + </FormControl> + <FormControl> + <FormLabel>Title</FormLabel> + <Input + value={title} + onChange={(event) => setTitle(event.target.value)} + /> + </FormControl> + <FormControl> + <FormLabel>Message</FormLabel> + <Input + value={message} + onChange={(event) => setMessage(event.target.value)} + /> + </FormControl> + <Checkbox + label="Closeable" + checked={closeable} + onChange={(event) => setCloseable(event.target.checked)} + /> + <FormControl> + <FormLabel>Action</FormLabel> + <Input + value={action} + onChange={(event) => setAction(event.target.value)} + /> + </FormControl> + <Stack direction="row" gap={3}> + <Button onClick={openAlert}>Open Alert</Button> + <Button + color="danger" + onClick={() => alert.close()} + disabled={!alert.isOpen} + > + Close Alert + </Button> + </Stack> + </Stack> + </MainContentLayout> + </StickyToolbarLayout> + ); +} diff --git a/employee-portal/src/app/playground/appointment-picker/page.tsx b/employee-portal/src/app/playground/appointment-picker/page.tsx new file mode 100644 index 000000000..01e6250c2 --- /dev/null +++ b/employee-portal/src/app/playground/appointment-picker/page.tsx @@ -0,0 +1,177 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +"use client"; + +import { Row } from "@eshg/lib-portal/components/Row"; +import { SubmitButton } from "@eshg/lib-portal/components/buttons/SubmitButton"; +import { FormPlus } from "@eshg/lib-portal/components/form/FormPlus"; +import { AppointmentListProps } from "@eshg/lib-portal/components/formFields/appointmentPicker/AppointmentListForDate"; +import { + AppointmentPickerField, + AppointmentPickerLayoutProps, + FIELD_LABELS_DE, +} from "@eshg/lib-portal/components/formFields/appointmentPicker/AppointmentPickerField"; +import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; +import { CheckBox } from "@mui/icons-material"; +import { Box, Button, List, ListItem, Stack, Typography } from "@mui/joy"; +import { addMinutes } from "date-fns"; +import { Formik } from "formik"; +import { useState } from "react"; + +import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLayout"; +import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; +import { Toolbar } from "@/lib/shared/components/layout/Toolbar"; + +const now = new Date(); +interface Appointment { + start: Date; + end: Date; +} + +const appointments = new Array(100).fill(0).map((_, t): Appointment => { + const start = addMinutes(now, t * 15 + (t % 10) * 3600); + return { + start, + end: addMinutes(start, 10), + }; +}); + +const initialData: { appointment: Appointment | undefined } = { + appointment: undefined, +}; + +export default function AppointmentPickerPlaygroundPage() { + const [month, setMonth] = useState<Date>(new Date()); + const [appointment, setAppointment] = useState<Appointment | undefined>(); + const snackbar = useSnackbar(); + function handleSubmit(values: typeof initialData) { + setAppointment(values.appointment); + snackbar.confirmation("Termin genomen"); + return; + } + + return ( + <StickyToolbarLayout + toolbar={<Toolbar title="Appointment Picker" backHref="/playground" />} + > + <MainContentLayout> + <Formik initialValues={initialData} onSubmit={handleSubmit}> + <FormPlus> + <Stack gap={2}> + <Typography level="h2">Appointment Picker Field</Typography> + <p>Ausgewählte Termine: {appointment?.start.toLocaleString()}</p> + <AppointmentPickerField + name="appointment" + currentMonth={month} + setCurrentMonth={setMonth} + monthAppointments={appointments} + required={true} + labels={FIELD_LABELS_DE} + /> + <div> + <SubmitButton submitting={false}>Submit</SubmitButton> + </div> + </Stack> + </FormPlus> + </Formik> + <Box my={4} /> + <Formik initialValues={initialData} onSubmit={handleSubmit}> + <FormPlus> + <Stack gap={2}> + <Typography level="h2"> + Appointment Picker Field with alternative Layout + </Typography> + <p>Ausgewählte Termine: {appointment?.start.toLocaleString()}</p> + <AppointmentPickerField + name="appointment" + currentMonth={month} + setCurrentMonth={setMonth} + monthAppointments={appointments} + required={true} + labels={FIELD_LABELS_DE} + layout={AltLayout} + appointmentList={AltAppointmentList} + /> + </Stack> + </FormPlus> + </Formik> + </MainContentLayout> + </StickyToolbarLayout> + ); +} + +function AltLayout({ + sx, + className, + calendar, + appointmentList, +}: AppointmentPickerLayoutProps) { + return ( + <Row sx={sx} gap={3} className={className}> + {calendar} + {appointmentList} + </Row> + ); +} + +function AltAppointmentList<T extends Appointment>({ + date, + field, + appointments, + onAppointmentSelected, + label, + description, +}: 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 ( + <Box> + <Typography level="title-md" my={2}> + {label} + </Typography> + <List + orientation="vertical" + wrap + size="sm" + sx={{ marginBottom: "16px", gap: "8px", padding: 0 }} + // eslint-disable-next-line jsx-a11y/aria-props + aria-description={description} + > + {appointments.map((apt) => { + const isSelected = field.input.value === apt; + return ( + <ListItem + sx={{ padding: 0, minHeight: 0 }} + key={apt.start.getTime()} + > + <time dateTime={apt.start.toTimeString().slice(0, 5)}> + <Button + type="submit" + onClick={createOnSelected(apt)} + aria-selected={isSelected} + sx={{ display: "flex", justifyContent: "center", gap: 1 }} + > + {isSelected && <CheckBox />} + {apt.start.toLocaleTimeString()} + </Button> + </time> + </ListItem> + ); + })} + </List> + </Box> + ); +} diff --git a/employee-portal/src/app/playground/chat/chatPlaygroundContent.tsx b/employee-portal/src/app/playground/chat/chatPlaygroundContent.tsx index 65ae662d5..b3be46696 100644 --- a/employee-portal/src/app/playground/chat/chatPlaygroundContent.tsx +++ b/employee-portal/src/app/playground/chat/chatPlaygroundContent.tsx @@ -81,9 +81,7 @@ export function ChatPlaygroundContent() { } async function handleDeviceVerify() { - const deviceId = matrixClient.getDeviceId(); - if (!deviceId) return; - const isVerified = await isDeviceVerified(matrixClient, deviceId); + const isVerified = await isDeviceVerified(matrixClient); logger.debug({ isVerified }); } diff --git a/employee-portal/src/app/playground/page.tsx b/employee-portal/src/app/playground/page.tsx index 475c82c80..22ba0fd93 100644 --- a/employee-portal/src/app/playground/page.tsx +++ b/employee-portal/src/app/playground/page.tsx @@ -107,6 +107,14 @@ export default function PlaygroundIndexPage() { <li> <InternalLink href="/playground/sidebar">Sidebar</InternalLink> </li> + <li> + <InternalLink href="/playground/alert">Alert</InternalLink> + </li> + <li> + <InternalLink href="/playground/appointment-picker"> + Appointment Picker Field + </InternalLink> + </li> </ul> </MainContentLayout> </StickyToolbarLayout> diff --git a/employee-portal/src/env/server.js b/employee-portal/src/env/server.js index 446bb8ab4..48497f00e 100644 --- a/employee-portal/src/env/server.js +++ b/employee-portal/src/env/server.js @@ -30,6 +30,8 @@ const schema = object({ PUBLIC_AUDITLOG_BACKEND_URL: pipe(string(), url()), PUBLIC_STI_PROTECTION_BACKEND_URL: pipe(string(), url()), + MARKDOWN_PAGE_DIRECTORY: string(), + MATRIX_SERVER_URL: pipe(string(), url()), }); diff --git a/employee-portal/src/lib/baseModule/api/mapper/contacts.ts b/employee-portal/src/lib/baseModule/api/mapper/contacts.ts index b28341342..e60540b3e 100644 --- a/employee-portal/src/lib/baseModule/api/mapper/contacts.ts +++ b/employee-portal/src/lib/baseModule/api/mapper/contacts.ts @@ -63,6 +63,7 @@ export function mapAddContactRequest( export function mapImportMergeContactRequest( values: MergePersonContactFormValues | MergeInstitutionContactFormValues, + mergedFrom?: string, ): ApiUpdateContactRequest { switch (values.type) { case "UpdatePersonContactRequest": @@ -82,6 +83,7 @@ export function mapImportMergeContactRequest( differentBillingAddress: mapBaseAddressToApi( values.differentBillingAddress, ), + mergedFrom, }; case "UpdateInstitutionContactRequest": return { @@ -94,6 +96,7 @@ export function mapImportMergeContactRequest( differentBillingAddress: mapBaseAddressToApi( values.differentBillingAddress, ), + mergedFrom, }; } } diff --git a/employee-portal/src/lib/baseModule/api/mutations/gdpr.ts b/employee-portal/src/lib/baseModule/api/mutations/gdpr.ts index c8c015d45..ec38237af 100644 --- a/employee-portal/src/lib/baseModule/api/mutations/gdpr.ts +++ b/employee-portal/src/lib/baseModule/api/mutations/gdpr.ts @@ -6,6 +6,7 @@ import { ApiAddCentralFileIdToGdprProcedureRequest, ApiAddGdprProcedureRequest, + ApiGdprProcedureStatus, } from "@eshg/employee-portal-api/base"; import { useHandledMutation } from "@eshg/lib-portal/api/useHandledMutation"; import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; @@ -34,3 +35,28 @@ export function useAddCentralFileIdToGdprProcedure(id: string) { }, }); } + +export function useSetMatterOfConcern(id: string, version: number) { + const gdprProcedureApi = useGdprProcedureApi(); + const snackbar = useSnackbar(); + + return useHandledMutation({ + mutationFn: (matterOfConcern: string) => + gdprProcedureApi.setMatterOfConcern(id, { + version, + concern: matterOfConcern, + }), + onSuccess: () => { + snackbar.confirmation("Anliegen gespeichert"); + }, + }); +} + +export function useChangeProcedureStatus(id: string, version: number) { + const gdprProcedureApi = useGdprProcedureApi(); + + return useHandledMutation({ + mutationFn: (newStatus: ApiGdprProcedureStatus) => + gdprProcedureApi.changeStatus(id, { version, newStatus }), + }); +} diff --git a/employee-portal/src/lib/baseModule/api/mutations/users.ts b/employee-portal/src/lib/baseModule/api/mutations/users.ts index e50be2227..3a068eb90 100644 --- a/employee-portal/src/lib/baseModule/api/mutations/users.ts +++ b/employee-portal/src/lib/baseModule/api/mutations/users.ts @@ -10,6 +10,7 @@ import { } from "@eshg/employee-portal-api/base"; import { useHandledMutation } from "@eshg/lib-portal/api/useHandledMutation"; import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; +import { useMutation } from "@tanstack/react-query"; import { useUserApi } from "@/lib/baseModule/api/clients"; @@ -87,3 +88,26 @@ export function useInvalidateUserSessions() { onSuccess: () => snackbar.confirmation("Sitzungen wurden getrennt."), }); } + +export function useUpdateSelfUserChatUsername() { + const userApi = useUserApi(); + const snackbar = useSnackbar(); + + return useMutation({ + mutationFn: async ({ + externalChatUsername, + phoneNumber, + }: ApiUpdateSelfUserRequest) => { + await userApi.updateSelfUser({ + phoneNumber, + externalChatUsername, + }); + }, + onSuccess: () => { + snackbar.confirmation("Profil gespeichert"); + }, + onError: () => { + snackbar.error("Profil nicht gespeichert"); + }, + }); +} diff --git a/employee-portal/src/lib/baseModule/api/queries/config.ts b/employee-portal/src/lib/baseModule/api/queries/config.ts index 8721a9872..0244fb1a3 100644 --- a/employee-portal/src/lib/baseModule/api/queries/config.ts +++ b/employee-portal/src/lib/baseModule/api/queries/config.ts @@ -7,19 +7,14 @@ import { unwrapRawResponse } from "@eshg/lib-portal/api/unwrapRawResponse"; import { useSuspenseQuery } from "@tanstack/react-query"; import { useConfigApi } from "@/lib/baseModule/api/clients"; -import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; import { configApiQueryKey } from "./apiQueryKey"; export function useServerConfig() { const configApi = useConfigApi(); - const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); return useSuspenseQuery({ queryKey: configApiQueryKey(["getConfig"]), - queryFn: () => - configApi - .getConfigRaw(getPreCacheForOfflineModeHeaders()) - .then(unwrapRawResponse), + queryFn: () => configApi.getConfigRaw().then(unwrapRawResponse), select: (response) => response, // refresh only every 24h; config rarely changes staleTime: 86400_000, diff --git a/employee-portal/src/lib/baseModule/api/queries/users.ts b/employee-portal/src/lib/baseModule/api/queries/users.ts index f1430d1be..89a6bfedc 100644 --- a/employee-portal/src/lib/baseModule/api/queries/users.ts +++ b/employee-portal/src/lib/baseModule/api/queries/users.ts @@ -11,7 +11,6 @@ import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { useUserApi } from "@/lib/baseModule/api/clients"; import { userApiQueryKey } from "@/lib/baseModule/api/queries/apiQueryKey"; -import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; import { sortUsersByName } from "@/lib/shared/helpers/users"; export function useGetUserProfile(id: string) { @@ -58,10 +57,9 @@ export function useGetUserOverviewPageQuery() { export function useGetSelfUser() { const userApi = useUserApi(); - const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); return useSuspenseQuery({ queryKey: userApiQueryKey(["getSelfUser"]), - queryFn: () => userApi.getSelfUser(getPreCacheForOfflineModeHeaders()), + queryFn: () => userApi.getSelfUser(), staleTime: 60_000, }); } @@ -86,11 +84,9 @@ export function useGetSelfLeadersQueryOptions() { export function useGetSelfUserPermissions() { const userApi = useUserApi(); - const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); return useSuspenseQuery({ queryKey: userApiQueryKey(["getSelfUserPermissions"]), - queryFn: () => - userApi.getSelfUserPermissions(getPreCacheForOfflineModeHeaders()), + queryFn: () => userApi.getSelfUserPermissions(), select: (response) => response.permissions, staleTime: 60_000, }); diff --git a/employee-portal/src/lib/baseModule/components/StaticTextDocumentPanel.tsx b/employee-portal/src/lib/baseModule/components/StaticTextDocumentPanel.tsx index 1e7ccd34d..76b775015 100644 --- a/employee-portal/src/lib/baseModule/components/StaticTextDocumentPanel.tsx +++ b/employee-portal/src/lib/baseModule/components/StaticTextDocumentPanel.tsx @@ -34,14 +34,17 @@ export function StaticTextDocumentPanel(props: StackProps) { return ( <ContentPanel> <Stack - gap={4} + gap={2} sx={{ - "& :where(p, span)": { + "& :not(:where(h1, h2, h3))": { + margin: 0, + }, + "& :where(p, span, li)": { maxWidth: "750px", textWrap: "pretty", hyphens: "auto", }, - "& :where(h2, h3)": { + "& :where(h1, h2, h3)": { maxWidth: "750px", textWrap: "balance", hyphens: "auto", diff --git a/employee-portal/src/lib/baseModule/components/accessibility/Accessibility.tsx b/employee-portal/src/lib/baseModule/components/accessibility/Accessibility.tsx deleted file mode 100644 index 05771967a..000000000 --- a/employee-portal/src/lib/baseModule/components/accessibility/Accessibility.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { List, ListItem, Stack, Typography, TypographyProps } from "@mui/joy"; - -import { - LinkInNewTab, - StaticTextDocumentPanel, -} from "@/lib/baseModule/components/StaticTextDocumentPanel"; - -function Emphasis(props: TypographyProps) { - return <Typography fontWeight={600} {...props} />; -} - -export function Accessibility() { - return ( - <StaticTextDocumentPanel> - <Typography> - Diese Erklärung zur digitalen Barrierefreiheit gilt für die unter ep. - <Emphasis>frankfurt.ga-lotse.de</Emphasis> veröffentlichte Webseite. - <br /> - <br /> - Als öffentliche Stelle im Sinne der Richtlinie (EU) 2016/2102 sind wir - bemüht, unsere Websites und mobilen Anwendungen im Einklang mit den - Bestimmungen des Hessischen Behinderten-Gleichstellungsgesetzes - (HessBGG) sowie der Hessischen Verordnung über barrierefreie - Informationstechnik (BITV HE 2019) zur Umsetzung der Richtlinie (EU) - 2016/2102 barrierefrei zugänglich zu machen. Frankfurt.ga-lotse.de ist - überwiegend mit den derzeit gültigen Vorschriften zur Barrierefreiheit - (BITV 2.0, 2019/WCAG 2.1) vereinbar. Inhalte und Funktionen, die dem - derzeit noch nicht vollständig entsprechen, sind nachfolgend aufgeführt. - </Typography> - - <Stack - component={"section"} - aria-labelledby={"agreement-requirements"} - gap={1} - > - <Typography level={"h2"} id={"agreement-requirements"}> - Stand der Vereinbarkeit mit den Anforderungen - </Typography> - <Typography> - Die Anforderungen der Barrierefreiheit ergeben sich aus § 3 Absätze 1 - bis 4 und § 4 der BITV HE 2019, die auf Grundlage von § 14 des HessBGG - erlassen wurde. - <br /> - <br /> - Die Überprüfung der Einhaltung der Anforderungen beruht auf einer am - 23.09.2024 durchgeführten Selbstbewertung. - </Typography> - </Stack> - - <Stack component={"section"} aria-labelledby={"accessibility-exemptions"}> - <Typography level={"h2"} id={"accessibility-exemptions"}> - Nicht barrierefreie Inhalte{" "} - </Typography> - <Typography> - Aufgrund der Überprüfung ist die <Emphasis>Website</Emphasis> mit den - zuvor genannten Anforderungen <Emphasis>nur teilweise</Emphasis>{" "} - vereinbar. - </Typography> - <List marker={"disc"}> - <ListItem> - <Emphasis>PDF-Dateien sind nicht vollständig barrierefrei</Emphasis> - </ListItem> - <ListItem> - <Emphasis>Nicht alle Schaltflächen haben erkennbaren Text</Emphasis> - </ListItem> - <ListItem> - <Emphasis> - Es gibt Schaltflächen mit zu kleinen Klickflächen für Touch-Geräte - </Emphasis> - </ListItem> - <ListItem> - <Emphasis>Nicht alle Formelemente haben eine Beschriftung</Emphasis> - </ListItem> - </List> - <Emphasis> - Die Stadt Frankfurt am Main arbeitet daran, die barrierefreien - Angebote weiter auszubauen. - </Emphasis> - </Stack> - - <Stack - component={"section"} - aria-labelledby={"date-of-accessibility-verification"} - > - <Typography level={"h2"} id={"date-of-accessibility-verification"}> - Datum der Erstellung der Erklärung zur Barrierefreiheit - </Typography> - <Typography> - Diese Erklärung wurde am <Emphasis>23.09.2024</Emphasis> erstellt und - zuletzt am <Emphasis>23.09.2024</Emphasis> überprüft und aktualisiert. - </Typography> - </Stack> - - <Stack component={"section"} id={"feedback-and-suggestions"}> - <Typography level={"h2"} aria-labelledby={"feedback-and-suggestions"}> - Feedback und Anfragen zur digitalen Barrierefreiheit - </Typography> - <Typography> - Sie möchten uns noch bestehende Barrieren mitteilen oder nicht - barrierefreie Inhalte in einem barrierefreien Format anfordern? - Sprechen Sie unsere verantwortlichen Kontaktpersonen an: - </Typography> - <Emphasis> - Gesundheitsamt Frankfurt am Main - <br /> - Digitale Zukunft, IT und strategische Planung - <br /> - +49 (0) 800 -4256873 - </Emphasis> - <LinkInNewTab href={"mailto:support@ga-lotse.de"}> - support@ga-lotse.de - </LinkInNewTab> - </Stack> - - <Stack component={"section"} aria-labelledby={"feedback-procedure"}> - <Typography level={"h2"} id={"feedback-procedure"}> - Durchsetzungsverfahren - </Typography> - <Typography> - Wenn auch nach Ihrem Feedback an den oben genannten Kontakt keine - zufriedenstellende Lösung gefunden wurde, können Sie die - Durchsetzungs- und Überwachungsstelle Barrierefreie - Informationstechnik einschalten. Sie haben nach Ablauf einer Frist von - sechs Wochen das Recht sich direkt an die Durchsetzungs- und - Überwachungsstelle zu wenden. Unter Einbeziehung aller Beteiligten - versucht die Durchsetzungsstelle, die Umstände der fehlenden - Barrierefreiheit zu ermitteln, damit der Träger diese beheben kann. - </Typography> - </Stack> - - <Stack component={"section"} aria-labelledby={"contact"}> - <Typography level={"h2"} id={"contact"}> - Durchsetzungs- und Überwachungsstelle Barrierefreie - Informationstechnik Hessisches Ministerium für Soziales und - Integration Sitz: Regierungspräsidium Gießen - </Typography> - <Typography> - Prof. Dr. Erdmuthe Meyer zu Bexten - <br /> - Landesbeauftragte für barrierefreie IT - <br /> - Leiterin der Durchsetzungs- und Überwachungsstelle - <br /> - Landgraf-Philipp-Platz 1-7 - <br /> - 35390 Gießen - <br /> - Telefon: +49 641 303 - 2901 - <br /> - E-Mail:{" "} - <LinkInNewTab href={"mailto:Durchsetzungsstelle-LBIT@rpgi.hessen.de"}> - Durchsetzungsstelle-LBIT@rpgi.hessen.de - </LinkInNewTab> - </Typography> - </Stack> - - <LinkInNewTab - href={ - "https://lbit.hessen.de/Durchsetzungs-und-Ueberwachungsstelle/Durchsetzungsverfahren-beantragen/Formular-Durchsetzungsverfahren" - } - > - Durchsetzung beantragen - </LinkInNewTab> - </StaticTextDocumentPanel> - ); -} diff --git a/employee-portal/src/lib/baseModule/components/contact/Contact.tsx b/employee-portal/src/lib/baseModule/components/contact/Contact.tsx deleted file mode 100644 index b0e233f1c..000000000 --- a/employee-portal/src/lib/baseModule/components/contact/Contact.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ExternalLink } from "@eshg/lib-portal/components/navigation/ExternalLink"; -import { Stack, Typography } from "@mui/joy"; - -import { - LinkInNewTab, - StaticTextDocumentPanel, -} from "@/lib/baseModule/components/StaticTextDocumentPanel"; - -export function Contact() { - return ( - <StaticTextDocumentPanel> - <Stack component={"section"} aria-labelledby={"phone-contact"} gap={1}> - <Stack> - <Typography component={"h2"} level={"title-md"} id={"phone-contact"}> - Telefonische Erreichbarkeit: - </Typography> - <Typography>Telefon: +49 (0) 800 -4256873</Typography> - </Stack> - <Typography> - Montag - Donnerstag: 07:30 Uhr - 16:00 Uhr - <br /> - Freitag: 07:30 Uhr - 14:00 Uhr - </Typography> - </Stack> - <Stack component={"section"} aria-labelledby={"ticket-contact"}> - <Typography component={"h2"} level={"title-md"} id={"ticket-contact"}> - Erreichbarkeit via Ticketsystem: - </Typography> - <Typography> - E-Mail:{" "} - <ExternalLink href={"mailto:support@ga-lotse.de"}> - support@ga-lotse.de - </ExternalLink> - </Typography> - </Stack> - <Typography> - Open Source:{" "} - <LinkInNewTab href={"https://gitlab.opencode.de/ga-lotse"}> - OpenCoDE - </LinkInNewTab> - </Typography> - </StaticTextDocumentPanel> - ); -} diff --git a/employee-portal/src/lib/baseModule/components/contacts/ContactsTable.tsx b/employee-portal/src/lib/baseModule/components/contacts/ContactsTable.tsx index fb1a11c34..66d71351e 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/ContactsTable.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/ContactsTable.tsx @@ -3,7 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiContactType, ApiUserRole } from "@eshg/employee-portal-api/base"; +import { + ApiBaseFeature, + ApiContactType, + ApiUserRole, +} from "@eshg/employee-portal-api/base"; +import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; import { buildEnumOptions } from "@eshg/lib-portal/helpers/form"; import AddIcon from "@mui/icons-material/Add"; import BusinessIcon from "@mui/icons-material/Business"; @@ -21,8 +26,12 @@ import { ToggleButtonGroup, } from "@mui/joy"; import { useSearchParams } from "next/navigation"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useIsNewFeatureEnabled } from "@/lib/baseModule/api/queries/feature"; +import { ContactsTableTitle } from "@/lib/baseModule/components/contacts/ContactsTableTitle"; +import { useMergeInstitutionContactSidebar } from "@/lib/baseModule/components/contacts/modals/MergeInstitutionContactSidebar"; +import { useMergePersonContactSidebar } from "@/lib/baseModule/components/contacts/modals/MergePersonContactSidebar"; import { UpdateContactSidebar } from "@/lib/baseModule/components/contacts/modals/UpdateContactSidebar"; import { Contact } from "@/lib/baseModule/components/contacts/types"; import { routes } from "@/lib/baseModule/shared/routes"; @@ -37,6 +46,10 @@ import { UseTableControl, useTableControl, } from "@/lib/shared/hooks/searchParams/useTableControl"; +import { + mapToRowIds, + useRowSelection, +} from "@/lib/shared/hooks/table/useRowSelection"; import { useHasUserRoleCheck } from "@/lib/shared/hooks/useAccessControl"; import { contactTableColumns } from "./columns"; @@ -54,6 +67,35 @@ export interface ContactsTableProps { loading: boolean; } +// Persists the selected contacts between pages and filtering, +// to allow merging contacts from different pages +function usePersistentSelectionCache({ elements }: { elements: Contact[] }) { + const { rowSelection, rowSelectionProps } = useRowSelection<Contact>(); + + const [selectedContacts, setSelectedContacts] = useState< + Map<string, Contact> + >(new Map()); + + useEffect(() => { + setSelectedContacts((previous) => { + const contactIds = mapToRowIds(rowSelection); + const newSelected = new Map(); + + for (const contactId of contactIds) { + newSelected.set( + contactId, + previous.get(contactId) ?? + elements.find((contact) => contact.id === contactId), + ); + } + + return newSelected; + }); + }, [elements, rowSelection]); + + return { selectedContacts, rowSelection, rowSelectionProps }; +} + export function ContactsTable({ elements, totalNumberOfElements, @@ -61,17 +103,64 @@ export function ContactsTable({ onImport, loading, }: ContactsTableProps) { + const isContactMergeEnabled = useIsNewFeatureEnabled( + ApiBaseFeature.ContactMerge, + ); const hasWritePerms = useHasUserRoleCheck(ApiUserRole.BaseContactsWrite); + const snackbar = useSnackbar(); + const tableControl = useTableControl({ serverSideSorting: true, sortFieldName: "sortKey", }); + const institutionMergeSidebar = useMergeInstitutionContactSidebar(); + const personMergeSidebar = useMergePersonContactSidebar(); + const [editSidebar, setEditSidebar] = useState<{ open: boolean; contact?: Contact; }>({ open: false }); + const { selectedContacts, rowSelection, rowSelectionProps } = + usePersistentSelectionCache({ + elements, + }); + + function handleMerge(contactIds: string[]) { + const [first, second, ...rest] = contactIds; + + if (first === undefined || second === undefined || rest.length > 0) { + snackbar.error("Sie können nur zwei Kontakte zusammenführen."); + return; + } + + const firstContact = selectedContacts.get(first)!; + const secondContact = selectedContacts.get(second)!; + + if ( + firstContact.type === "PersonContact" && + secondContact.type === "PersonContact" + ) { + personMergeSidebar.open({ + firstContact: firstContact, + secondContact: secondContact, + }); + } else if ( + firstContact.type === "InstitutionContact" && + secondContact.type === "InstitutionContact" + ) { + institutionMergeSidebar.open({ + firstContact: firstContact, + secondContact: secondContact, + }); + } else { + snackbar.error( + "Sie können eine Person nicht mit einer Institution zusammenführen.", + ); + } + } + return ( <> <TablePage @@ -114,6 +203,15 @@ export function ContactsTable({ > <TableSheet loading={loading} + title={ + hasWritePerms && + isContactMergeEnabled && ( + <ContactsTableTitle + rowSelection={rowSelection} + onMerge={handleMerge} + /> + ) + } footer={ <Pagination totalCount={totalNumberOfElements} @@ -130,6 +228,9 @@ export function ContactsTable({ sorting={tableControl.tableSorting} rowNavRoute={(row) => routes.contacts.details(row.original.id)} focusColumnHeader="Name" + rowSelectionProps={ + isContactMergeEnabled ? rowSelectionProps : undefined + } /> </TableSheet> </TablePage> diff --git a/employee-portal/src/lib/baseModule/components/contacts/ContactsTableTitle.tsx b/employee-portal/src/lib/baseModule/components/contacts/ContactsTableTitle.tsx new file mode 100644 index 000000000..e25da6a43 --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/contacts/ContactsTableTitle.tsx @@ -0,0 +1,37 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MergeIcon from "@mui/icons-material/SchemaOutlined"; +import { Button } from "@mui/joy"; +import { RowSelectionState } from "@tanstack/react-table"; + +import { RowSelectionTableToolbar } from "@/lib/shared/components/table/RowSelectionTableToolbar"; +import { mapToRowIds } from "@/lib/shared/hooks/table/useRowSelection"; + +interface ContactsTableTitleProps { + rowSelection: RowSelectionState; + onMerge: (contactIds: string[]) => void; +} + +export function ContactsTableTitle(props: ContactsTableTitleProps) { + const rowIds = mapToRowIds(props.rowSelection); + return ( + <RowSelectionTableToolbar + rowSelection={props.rowSelection} + elementName={{ singular: "Kontakt", plural: "Kontakte" }} + > + <Button + variant="plain" + color="neutral" + size="sm" + onClick={() => props.onMerge(rowIds)} + startDecorator={<MergeIcon />} + disabled={rowIds.length !== 2} + > + Kontakte Zusammenführen + </Button> + </RowSelectionTableToolbar> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/contacts/columns.tsx b/employee-portal/src/lib/baseModule/components/contacts/columns.tsx index 85ed6511c..6a302f704 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/columns.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/columns.tsx @@ -11,11 +11,13 @@ import { Stack } from "@mui/joy"; import { createColumnHelper } from "@tanstack/react-table"; import { isDefined } from "remeda"; -import { fullContactName } from "@/lib/baseModule/components/contacts/helpers"; +import { + fullContactName, + getContactAddressLine, +} from "@/lib/baseModule/components/contacts/helpers"; import { routes } from "@/lib/baseModule/shared/routes"; import { contactCategoryNames } from "@/lib/baseModule/shared/translations"; import { ActionsMenu } from "@/lib/shared/components/buttons/ActionsMenu"; -import { BaseAddress } from "@/lib/shared/helpers/address"; import { Contact, isInstitutionContact } from "./types"; @@ -80,7 +82,7 @@ export function contactTableColumns({ columnHelper.accessor("contactAddress", { header: "Adresse", enableSorting: false, - cell: (props) => <AddressColumn address={props.getValue()} />, + cell: (props) => getContactAddressLine(props.getValue()), meta: { canNavigate: { parentRow: true, @@ -119,16 +121,6 @@ export function contactTableColumns({ ]; } -function AddressColumn({ address }: { address: BaseAddress | undefined }) { - if (address === undefined) { - return undefined; - } - - return address.type === "DomesticAddress" - ? `${[address.street, address.houseNumber].join(" ")}, ${address.postalCode} ${address.city}` - : `${address.postbox}, ${address.postalCode} ${address.city}`; -} - function ContactType({ type }: { type: string }) { return ( <Stack justifyContent={"center"}> diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressCardsField.tsx b/employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressCardsField.tsx deleted file mode 100644 index 8ab5c6005..000000000 --- a/employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressCardsField.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { - BaseField, - useBaseField, -} from "@eshg/lib-portal/components/formFields/BaseField"; -import { FieldProps } from "@eshg/lib-portal/types/form"; -import { RadioGroup, Stack, Typography } from "@mui/joy"; -import { ChangeEvent, useEffect, useState } from "react"; -import { isDefined } from "remeda"; - -import { SelectableCard } from "@/lib/shared/components/cards/SelectableCard"; -import { BaseAddressFormInputs } from "@/lib/shared/components/form/address/helpers"; -import { translateCountry } from "@/lib/shared/helpers/i18n"; -import { join } from "@/lib/shared/helpers/strings"; - -interface AddressCardsFieldProps extends FieldProps<BaseAddressFormInputs> { - options: BaseAddressFormInputs[]; -} - -function renderAddress(address: BaseAddressFormInputs) { - return join( - [ - address.type === "DomesticAddress" - ? [address.street, address.houseNumber].join(" ") - : address.postbox, - address.addressAddition, - [address.postalCode, address.city].join(" "), - address.country !== "DE" ? translateCountry(address.country) : undefined, - ], - "\n", - ); -} - -export function AddressCardsField({ - options, - ...props -}: AddressCardsFieldProps) { - const field = useBaseField(props); - const [selectedIndex, setSelectedIndex] = useState<number | undefined>( - options.length === 1 ? 0 : undefined, - ); - - const setValue = field.helpers.setValue; - - useEffect(() => { - if (isDefined(selectedIndex)) { - void setValue(options[selectedIndex]!, false); - } - }, [setValue, options, selectedIndex]); - - if (options.length === 0) { - return null; - } - - function handleChange(event: ChangeEvent<HTMLInputElement>) { - setSelectedIndex(parseInt(event.target.value)); - } - - return ( - <BaseField - helperText={field.helperText} - required={field.required} - error={field.error} - label={props.label} - > - <RadioGroup - name={field.input.name} - value={selectedIndex} - onChange={handleChange} - > - <Stack gap={2}> - {options.map((address, index) => ( - <SelectableCard - key={index} - value={index} - radioProps={{ - color: field.error ? "danger" : undefined, - }} - sx={{ - borderColor: field.error ? "danger.300" : undefined, - }} - > - <Typography whiteSpace={"preserve"}> - {renderAddress(address)} - </Typography> - </SelectableCard> - ))} - </Stack> - </RadioGroup> - </BaseField> - ); -} diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressLine.tsx b/employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressLine.tsx deleted file mode 100644 index bda7c04d4..000000000 --- a/employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressLine.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Typography } from "@mui/joy"; - -import { - TaggedDomesticAddress, - TaggedPostboxAddress, -} from "@/lib/shared/helpers/address"; -import { join } from "@/lib/shared/helpers/strings"; - -type AddressLineType = - | Pick< - TaggedDomesticAddress, - "type" | "street" | "houseNumber" | "postalCode" | "city" - > - | Pick<TaggedPostboxAddress, "type" | "postbox" | "postalCode" | "city">; - -export function AddressLine(props: { address: AddressLineType }) { - return ( - <Typography> - {props.address.type === "DomesticAddress" - ? join([props.address.street, props.address.houseNumber], " ")?.trim() - : props.address.postbox} - {", "} - {props.address.postalCode} {props.address.city} - </Typography> - ); -} diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressMergeField.tsx b/employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressMergeField.tsx new file mode 100644 index 000000000..ba701a73b --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/contacts/forms/card/AddressMergeField.tsx @@ -0,0 +1,159 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiCountryCode } from "@eshg/employee-portal-api/base"; +import { useBaseField } from "@eshg/lib-portal/components/formFields/BaseField"; +import { FieldProps } from "@eshg/lib-portal/types/form"; +import ErrorIcon from "@mui/icons-material/ErrorOutline"; +import { + FormControl, + FormHelperText, + FormLabel, + Radio, + RadioGroup, + Sheet, + Stack, + Typography, +} from "@mui/joy"; +import { ChangeEvent, useState } from "react"; +import { isDefined } from "remeda"; + +import { BaseAddressDetails } from "@/lib/shared/components/address/BaseAddressDetails"; +import { DetailsCell } from "@/lib/shared/components/detailsSection/DetailsCell"; +import { DetailsRow } from "@/lib/shared/components/detailsSection/DetailsRow"; +import { BaseAddressFormInputs } from "@/lib/shared/components/form/address/helpers"; +import { translateCountry } from "@/lib/shared/helpers/i18n"; +import { join } from "@/lib/shared/helpers/strings"; + +interface AddressSelectOption { + label: string; + value: BaseAddressFormInputs | undefined; +} + +interface AddressCardsFieldProps + extends FieldProps<BaseAddressFormInputs | undefined> { + options: AddressSelectOption[]; + value: BaseAddressFormInputs | undefined; + readOnly: boolean; +} + +function AddressOption({ address }: { address: BaseAddressFormInputs }) { + return ( + <Stack gap={1}> + <DetailsCell + name={"differentName"} + label={"Abweichender Empfänger"} + value={address.differentName} + /> + <DetailsCell + name={"postbox"} + label={"Postfachnummer"} + value={address.postbox} + /> + {address.street && ( + <DetailsCell + name={"street"} + label={"Straße und Haus-Nr."} + value={join([address.street, address.houseNumber], " ")} + /> + )} + + <DetailsCell + name={"addressAddition"} + label={"Adresszusatz"} + value={address.addressAddition} + /> + <DetailsRow> + <DetailsCell + name={"postalCode"} + label={"Postleitzahl"} + value={address.postalCode} + /> + <DetailsCell + name={"city"} + label={"Ort"} + value={address.city} + avoidWrap + /> + </DetailsRow> + + {address.country !== ApiCountryCode.De && ( + <DetailsCell + name={"country"} + label={"Land"} + value={translateCountry(address.country)} + /> + )} + </Stack> + ); +} + +export function AddressMergeField({ + options, + readOnly, + value, + ...fieldProps +}: AddressCardsFieldProps) { + const { input, error, required, helpers } = useBaseField(fieldProps); + + const [selectedLabel, setSelectedLabel] = useState(""); + + if (readOnly) { + const titleId = `${fieldProps.name}-title`; + return ( + isDefined(value) && ( + <Stack component={"section"} gap={"inherit"} aria-labelledby={titleId}> + <Typography component={"h2"} level={"title-md"} id={titleId}> + {fieldProps.label} + </Typography> + <BaseAddressDetails address={value} /> + </Stack> + ) + ); + } + + async function handleChange(event: ChangeEvent<HTMLInputElement>) { + const index = parseInt(event.target.value); + await helpers.setValue(options[index]?.value); + setSelectedLabel(event.target.value); + } + + return ( + <FormControl error={error} required={required}> + <FormLabel htmlFor={input.name}>{fieldProps.label}</FormLabel> + <RadioGroup + onChange={(event) => void handleChange(event)} + value={selectedLabel} + name={input.name} + > + <Stack gap={2}> + {options.map(({ label, value }, index) => ( + <Radio + key={index} + value={index} + label={ + <Stack gap={3}> + {label} + {isDefined(value) && ( + <Sheet variant={"soft"}> + <Typography whiteSpace={"preserve"}> + <AddressOption address={value} /> + </Typography> + </Sheet> + )} + </Stack> + } + /> + ))} + </Stack> + </RadioGroup> + {error && ( + <FormHelperText> + <ErrorIcon /> {fieldProps.required} + </FormHelperText> + )} + </FormControl> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/card/InstitutionContactCard.tsx b/employee-portal/src/lib/baseModule/components/contacts/forms/card/InstitutionContactCard.tsx index 26f696e56..20a5b75ad 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/forms/card/InstitutionContactCard.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/forms/card/InstitutionContactCard.tsx @@ -5,34 +5,24 @@ import { ApiInstitutionContact } from "@eshg/employee-portal-api/base"; import { Stack, Typography } from "@mui/joy"; -import { isNonNullish } from "remeda"; +import { isDefined } from "remeda"; -import { join } from "@/lib/shared/helpers/strings"; +import { getContactAddressLine } from "@/lib/baseModule/components/contacts/helpers"; export function InstitutionContactCard({ - contact: element, + contact, }: { contact: ApiInstitutionContact; }) { return ( - <Stack> - <Typography level={"title-md"}>{element.name}</Typography> - {isNonNullish(element.contactAddress) && ( - <> - <Typography> - {element.contactAddress.type === "DomesticAddress" - ? join( - [ - element.contactAddress.street, - element.contactAddress.houseNumber, - ], - " ", - )?.trim() - : element.contactAddress.postbox} - {", "} - {element.contactAddress.postalCode} {element.contactAddress.city} - </Typography> - </> + <Stack sx={{ minWidth: 0 }}> + <Typography level={"title-md"} noWrap> + {contact.name} + </Typography> + {isDefined(contact.contactAddress) && ( + <Typography noWrap> + {getContactAddressLine(contact.contactAddress)} + </Typography> )} </Stack> ); diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/card/PersonContactCard.tsx b/employee-portal/src/lib/baseModule/components/contacts/forms/card/PersonContactCard.tsx index 56dc9ca89..262e07a86 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/forms/card/PersonContactCard.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/forms/card/PersonContactCard.tsx @@ -4,37 +4,25 @@ */ import { ApiPersonContact } from "@eshg/employee-portal-api/base"; +import { formatPersonName } from "@eshg/lib-portal/formatters/person"; import { Stack, Typography } from "@mui/joy"; import { isNonNullish } from "remeda"; -import { join } from "@/lib/shared/helpers/strings"; +import { getContactAddressLine } from "@/lib/baseModule/components/contacts/helpers"; -export function PersonContactCard({ - contact: element, -}: { - contact: ApiPersonContact; -}) { +export function PersonContactCard({ contact }: { contact: ApiPersonContact }) { return ( - <Stack> - <Typography level={"title-md"}> - {element.firstName} {element.name} + <Stack sx={{ minWidth: 0 }}> + <Typography level={"title-md"} noWrap> + {formatPersonName({ + firstName: contact.firstName, + lastName: contact.name, + })} </Typography> - {isNonNullish(element.contactAddress) && ( - <> - <Typography> - {element.contactAddress.type === "DomesticAddress" - ? join( - [ - element.contactAddress.street, - element.contactAddress.houseNumber, - ], - " ", - )?.trim() - : element.contactAddress.postbox} - {", "} - {element.contactAddress.postalCode} {element.contactAddress.city} - </Typography> - </> + {isNonNullish(contact.contactAddress) && ( + <Typography noWrap> + {getContactAddressLine(contact.contactAddress)} + </Typography> )} </Stack> ); diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/helpers.ts b/employee-portal/src/lib/baseModule/components/contacts/forms/helpers.ts index 6b268dbd7..463878b6f 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/forms/helpers.ts +++ b/employee-portal/src/lib/baseModule/components/contacts/forms/helpers.ts @@ -4,12 +4,8 @@ */ import { ApiVCardAddress } from "@eshg/employee-portal-api/base"; -import { isDefined } from "remeda"; +import { isDeepEqual, isDefined } from "remeda"; -import { - InstitutionContactMergeSource, - PersonContactMergeSource, -} from "@/lib/baseModule/components/contacts/types"; import { BaseAddressFormInputs, createEmptyAddress, @@ -62,20 +58,6 @@ export function distinctConcat<T>(listA: T[], listB: T[]): T[] { return [...new Set(listA.concat(listB))]; } -export function getAddressOptions( - targetAddress: BaseAddress | undefined, - mergeSource: PersonContactMergeSource | InstitutionContactMergeSource, -) { - return [ - isDefined(targetAddress) ? mapApiAddressToForm(targetAddress) : undefined, - isValidAddress(mergeSource.data.contactAddress) - ? mergeSource.type === "Import" - ? mergeSource.data.contactAddress - : mapApiAddressToForm(mergeSource.data.contactAddress!) - : undefined, - ].filter(isDefined); -} - export function mapVCardAddressToForm(address: ApiVCardAddress | undefined) { return isDefined(address) ? ({ @@ -90,3 +72,41 @@ export function mapVCardAddressToForm(address: ApiVCardAddress | undefined) { } as const) : createEmptyAddress(); } + +function isAddressMerge(a?: BaseAddressFormInputs, b?: BaseAddressFormInputs) { + return isDefined(a) && isDefined(b) && !isDeepEqual(a, b); +} + +type AddressMergeSource = + | { + type: "Import"; + data: BaseAddressFormInputs | undefined; + } + | { + type: "Entity"; + data: BaseAddress | undefined; + }; + +export function getAddressOptions( + targetAddress: BaseAddress | undefined, + mergeSource: AddressMergeSource, +) { + const into: BaseAddressFormInputs | undefined = isDefined(targetAddress) + ? mapApiAddressToForm(targetAddress) + : undefined; + const from: BaseAddressFormInputs | undefined = + mergeSource.type === "Import" + ? mergeSource.data + : isDefined(mergeSource.data) + ? mapApiAddressToForm(mergeSource.data) + : undefined; + + const requiresMerge = isAddressMerge(from, into); + + return { + into, + from, + requiresMerge, + initialAddress: requiresMerge ? undefined : (into ?? from), + }; +} diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergeInstitutionContactForm.tsx b/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergeInstitutionContactForm.tsx index 3f031f3e4..843462fe7 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergeInstitutionContactForm.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergeInstitutionContactForm.tsx @@ -13,11 +13,10 @@ import { isDefined } from "remeda"; import { mapImportMergeContactRequest } from "@/lib/baseModule/api/mapper/contacts"; import { useUpdateContactMutation } from "@/lib/baseModule/api/mutations/contacts"; -import { AddressCardsField } from "@/lib/baseModule/components/contacts/forms/card/AddressCardsField"; +import { AddressMergeField } from "@/lib/baseModule/components/contacts/forms/card/AddressMergeField"; import { distinctConcat, getAddressOptions, - isValidAddress, mapMergeValue, } from "@/lib/baseModule/components/contacts/forms/helpers"; import { MergeStringField } from "@/lib/baseModule/components/contacts/forms/merge/MergeStringField"; @@ -32,8 +31,8 @@ import { SidebarFormHandle, } from "@/lib/shared/components/form/SidebarForm"; import { + BaseAddressFormInputs, createEmptyAddress, - mapApiAddressToForm, } from "@/lib/shared/components/form/address/helpers"; import { SidebarActions } from "@/lib/shared/components/sidebar/SidebarActions"; import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; @@ -41,6 +40,8 @@ import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; function initialValues( into: ApiInstitutionContact, from: InstitutionContactMergeSource, + contactAddress: BaseAddressFormInputs | undefined, + billingAddress: BaseAddressFormInputs | undefined, ): MergeInstitutionContactFormValues { return { type: "UpdateInstitutionContactRequest", @@ -51,14 +52,8 @@ function initialValues( into.emailAddresses, from.data.emailAddresses, ), - contactAddress: - isValidAddress(from.data.contactAddress) || - into.contactAddress === undefined - ? createEmptyAddress() - : mapApiAddressToForm(into.contactAddress), - differentBillingAddress: isDefined(into.differentBillingAddress) - ? mapApiAddressToForm(into.differentBillingAddress) - : undefined, + contactAddress: contactAddress ?? createEmptyAddress(), + differentBillingAddress: billingAddress, }; } @@ -68,6 +63,9 @@ interface MergeInstitutionContactFormProps { sidebarFormRef: Ref<SidebarFormHandle>; onCancel: () => void; onSuccess: () => void; + onBack?: () => void; + intoLabel: string; + fromLabel: string; } export function MergeInstitutionContactForm({ @@ -76,75 +74,145 @@ export function MergeInstitutionContactForm({ sidebarFormRef, onCancel, onSuccess, + onBack, + intoLabel, + fromLabel, }: MergeInstitutionContactFormProps) { const fieldName = createFieldNameMapper<MergeInstitutionContactFormValues>(); - const contactAddressChoices = getAddressOptions(into.contactAddress, from); + const { + from: fromContactAddress, + into: intoContactAddress, + requiresMerge: requiresContactAddressMerge, + initialAddress: initialContactAddress, + } = getAddressOptions( + into.contactAddress, + from.type === "Entity" + ? { type: "Entity", data: from.data.contactAddress } + : { type: "Import", data: from.data.contactAddress }, + ); + + const { + from: fromBillingAddress, + into: intoBillingAddress, + requiresMerge: requiresBillingAddressMerge, + initialAddress: initialBillingAddress, + } = getAddressOptions( + into.differentBillingAddress, + from.type === "Entity" + ? { type: "Entity", data: from.data.differentBillingAddress } + : { type: "Import", data: from.data.differentBillingAddress }, + ); const updateContact = useUpdateContactMutation(into.id); async function handleSubmit(values: MergeInstitutionContactFormValues) { await updateContact - .mutateAsync(mapImportMergeContactRequest(values), { - onSuccess: () => { - onSuccess(); + .mutateAsync( + mapImportMergeContactRequest( + values, + from.type === "Entity" ? from.data.id : undefined, + ), + { + onSuccess, }, - }) + ) .catch(); } return ( <Formik - initialValues={initialValues(into, from)} + initialValues={initialValues( + into, + from, + initialContactAddress, + initialBillingAddress, + )} onSubmit={async (values) => await handleSubmit(values)} > - {({ isSubmitting }) => ( + {({ isSubmitting, values }) => ( <SidebarForm ref={sidebarFormRef}> - <SidebarContent title={"Institution zusammenführen"}> - <Stack gap={3}> - <MergeStringField - target={into.name} - source={from.data.name} - name={fieldName("name")} - label={"Name"} - /> - <MergeStringField - target={into.category} - source={from.data.category} - name={fieldName("category")} - label={"Objekttyp"} - getOptionLabel={(value) => - contactCategoryNames[ - value as keyof typeof contactCategoryNames - ] - } - /> - {contactAddressChoices.length > 0 && ( - <> - <Divider /> - <AddressCardsField - options={contactAddressChoices} - name={fieldName("contactAddress")} - label={"Kontaktadresse"} - required={"Bitte auswählen"} - /> - </> - )} - <Divider /> - <Box component={"section"} aria-label={"E-Mail-Adressen"}> - <InputArrayField - name={fieldName("emailAddresses")} - label={"E-Mail-Adresse"} - addMoreLabel={"E-Mail-Adresse hinzufügen"} + <SidebarContent title="Institution zusammenführen"> + <Stack gap={3} divider={<Divider />}> + <Stack gap={"inherit"}> + <MergeStringField + target={into.name} + source={from.data.name} + name={fieldName("name")} + label={"Name"} + sourceValueLabel={fromLabel} + targetValueLabel={intoLabel} /> - </Box> - <Box component={"section"} aria-label={"Telefonnummern"}> - <InputArrayField - name={fieldName("phoneNumbers")} - label={"Telefonnummer"} - addMoreLabel={"Telefonnummer hinzufügen"} + <MergeStringField + target={into.category} + source={from.data.category} + name={fieldName("category")} + label={"Objekttyp"} + getOptionLabel={(value) => + contactCategoryNames[ + value as keyof typeof contactCategoryNames + ] + } + sourceValueLabel={fromLabel} + targetValueLabel={intoLabel} /> - </Box> + </Stack> + {(isDefined(values.contactAddress) || + requiresContactAddressMerge) && ( + <AddressMergeField + options={[ + { + label: `Übernehmen von ${intoLabel}`, + value: intoContactAddress, + }, + { + label: `Übernehmen von ${fromLabel}`, + value: fromContactAddress, + }, + ]} + name={fieldName("contactAddress")} + label={"Kontaktadresse"} + required={"Bitte auswählen"} + value={values.contactAddress} + readOnly={!requiresContactAddressMerge} + /> + )} + {(isDefined(values.differentBillingAddress) || + requiresBillingAddressMerge) && ( + <AddressMergeField + options={[ + { + label: `Übernehmen von ${intoLabel}`, + value: intoBillingAddress, + }, + { + label: `Übernehmen von ${fromLabel}`, + value: fromBillingAddress, + }, + ]} + name={fieldName("differentBillingAddress")} + label={"Abweichende Rechnungsadresse"} + required={"Bitte auswählen"} + value={values.differentBillingAddress} + readOnly={!requiresBillingAddressMerge} + /> + )} + <Stack gap={"inherit"}> + <Box component={"section"} aria-label={"E-Mail-Adressen"}> + <InputArrayField + name={fieldName("emailAddresses")} + label={"E-Mail-Adresse"} + addMoreLabel={"E-Mail-Adresse hinzufügen"} + /> + </Box> + <Box component={"section"} aria-label={"Telefonnummern"}> + <InputArrayField + name={fieldName("phoneNumbers")} + label={"Telefonnummer"} + addMoreLabel={"Telefonnummer hinzufügen"} + /> + </Box> + </Stack> </Stack> </SidebarContent> <SidebarActions> @@ -152,6 +220,7 @@ export function MergeInstitutionContactForm({ submitting={isSubmitting} submitLabel={"Bestätigen"} onCancel={onCancel} + onBack={onBack} /> </SidebarActions> </SidebarForm> diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergePersonContactForm.tsx b/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergePersonContactForm.tsx index ea45910ee..34d377712 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergePersonContactForm.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergePersonContactForm.tsx @@ -17,11 +17,10 @@ import { isDefined } from "remeda"; import { mapImportMergeContactRequest } from "@/lib/baseModule/api/mapper/contacts"; import { useUpdateContactMutation } from "@/lib/baseModule/api/mutations/contacts"; -import { AddressCardsField } from "@/lib/baseModule/components/contacts/forms/card/AddressCardsField"; +import { AddressMergeField } from "@/lib/baseModule/components/contacts/forms/card/AddressMergeField"; import { distinctConcat, getAddressOptions, - isValidAddress, mapMergeValue, } from "@/lib/baseModule/components/contacts/forms/helpers"; import { MergeStringField } from "@/lib/baseModule/components/contacts/forms/merge/MergeStringField"; @@ -35,7 +34,7 @@ import { SidebarForm, SidebarFormHandle, } from "@/lib/shared/components/form/SidebarForm"; -import { mapApiAddressToForm } from "@/lib/shared/components/form/address/helpers"; +import { BaseAddressFormInputs } from "@/lib/shared/components/form/address/helpers"; import { GENDER_VALUES, SALUTATION_VALUES, @@ -47,6 +46,8 @@ import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; function initialValues( into: ApiPersonContact, from: PersonContactMergeSource, + contactAddress: BaseAddressFormInputs | undefined, + billingAddress: BaseAddressFormInputs | undefined, ): MergePersonContactFormValues { return { type: "UpdatePersonContactRequest", @@ -64,14 +65,8 @@ function initialValues( into.emailAddresses, from.data.emailAddresses, ), - contactAddress: - isValidAddress(from.data.contactAddress) || - into.contactAddress === undefined - ? undefined - : mapApiAddressToForm(into.contactAddress), - differentBillingAddress: isDefined(into.differentBillingAddress) - ? mapApiAddressToForm(into.differentBillingAddress) - : undefined, + contactAddress: contactAddress, + differentBillingAddress: billingAddress, }; } @@ -81,6 +76,9 @@ interface MergePersonContactFormProps { sidebarFormRef: Ref<SidebarFormHandle>; onCancel: () => void; onSuccess: () => void; + onBack?: () => void; + fromLabel: string; + intoLabel: string; } export function MergePersonContactForm({ @@ -89,103 +87,181 @@ export function MergePersonContactForm({ sidebarFormRef, onCancel, onSuccess, + onBack, + fromLabel, + intoLabel, }: MergePersonContactFormProps) { const fieldName = createFieldNameMapper<PersonContactFormValues>(); - const contactAddressChoices = getAddressOptions(into.contactAddress, from); + const { + from: fromContactAddress, + into: intoContactAddress, + requiresMerge: requiresContactAddressMerge, + initialAddress: initialContactAddress, + } = getAddressOptions( + into.contactAddress, + from.type === "Entity" + ? { type: "Entity", data: from.data.contactAddress } + : { type: "Import", data: from.data.contactAddress }, + ); + + const { + from: fromBillingAddress, + into: intoBillingAddress, + requiresMerge: requiresBillingAddressMerge, + initialAddress: initialBillingAddress, + } = getAddressOptions( + into.differentBillingAddress, + from.type === "Entity" + ? { type: "Entity", data: from.data.differentBillingAddress } + : { type: "Import", data: from.data.differentBillingAddress }, + ); const updateContact = useUpdateContactMutation(into.id); async function handleSubmit(values: MergePersonContactFormValues) { await updateContact - .mutateAsync(mapImportMergeContactRequest(values), { - onSuccess: () => { - onSuccess(); + .mutateAsync( + mapImportMergeContactRequest( + values, + from.type === "Entity" ? from.data.id : undefined, + ), + { + onSuccess, }, - }) + ) .catch(); } return ( <Formik - initialValues={initialValues(into, from)} + initialValues={initialValues( + into, + from, + initialContactAddress, + initialBillingAddress, + )} onSubmit={async (values) => await handleSubmit(values)} > - {({ isSubmitting }) => ( + {({ isSubmitting, values }) => ( <SidebarForm ref={sidebarFormRef}> <SidebarContent title={"Person zusammenführen"}> - <Stack gap={3}> - <Grid container spacing={2}> - <Grid xxs={6}> - <MergeStringField - target={into.salutation} - source={from.data.salutation} - name={fieldName("salutation")} - label={"Anrede"} - emptyValue={ApiSalutation.NotSpecified} - getOptionLabel={(value) => - SALUTATION_VALUES[value as keyof typeof SALUTATION_VALUES] - } - /> - </Grid> - <Grid xxs> - <MergeStringField - target={into.title} - source={from.data.title} - name={fieldName("title")} - label={"Titel"} - emptyValue={TITLE_VALUES.NotSpecified} - /> + <Stack gap={3} divider={<Divider />}> + <Stack gap={"inherit"}> + <Grid container spacing={2}> + <Grid xxs={6}> + <MergeStringField + target={into.salutation} + source={from.data.salutation} + name={fieldName("salutation")} + label={"Anrede"} + emptyValue={ApiSalutation.NotSpecified} + getOptionLabel={(value) => + SALUTATION_VALUES[ + value as keyof typeof SALUTATION_VALUES + ] + } + sourceValueLabel={fromLabel} + targetValueLabel={intoLabel} + /> + </Grid> + <Grid xxs> + <MergeStringField + target={into.title} + source={from.data.title} + name={fieldName("title")} + label={"Titel"} + emptyValue={TITLE_VALUES.NotSpecified} + sourceValueLabel={fromLabel} + targetValueLabel={intoLabel} + /> + </Grid> </Grid> - </Grid> - <MergeStringField - target={into.firstName} - source={from.data.firstName} - name={fieldName("firstName")} - label={"Vorname"} - /> - <MergeStringField - target={into.name} - source={from.data.name} - name={fieldName("name")} - label={"Name"} - /> - <MergeStringField - target={into.gender} - source={from.data.gender} - name={fieldName("gender")} - label={"Geschlecht"} - emptyValue={ApiGender.NotSpecified} - getOptionLabel={(value) => - GENDER_VALUES[value as keyof typeof GENDER_VALUES] - } - /> - {contactAddressChoices.length > 0 && ( - <> - <Divider /> - <AddressCardsField - options={contactAddressChoices} - name={fieldName("contactAddress")} - label={"Kontaktadresse"} - required={"Bitte auswählen"} - /> - </> - )} - <Divider /> - <Box component={"section"} aria-label={"E-Mail-Adressen"}> - <InputArrayField - name={fieldName("emailAddresses")} - label={"E-Mail-Adresse"} - addMoreLabel={"E-Mail-Adresse hinzufügen"} + <MergeStringField + target={into.firstName} + source={from.data.firstName} + name={fieldName("firstName")} + label={"Vorname"} + sourceValueLabel={fromLabel} + targetValueLabel={intoLabel} + /> + <MergeStringField + target={into.name} + source={from.data.name} + name={fieldName("name")} + label={"Name"} + sourceValueLabel={fromLabel} + targetValueLabel={intoLabel} + /> + <MergeStringField + target={into.gender} + source={from.data.gender} + name={fieldName("gender")} + label={"Geschlecht"} + emptyValue={ApiGender.NotSpecified} + getOptionLabel={(value) => + GENDER_VALUES[value as keyof typeof GENDER_VALUES] + } + sourceValueLabel={fromLabel} + targetValueLabel={intoLabel} + /> + </Stack> + {(isDefined(values.contactAddress) || + requiresContactAddressMerge) && ( + <AddressMergeField + options={[ + { + label: `Übernehmen von ${intoLabel}`, + value: intoContactAddress, + }, + { + label: `Übernehmen von ${fromLabel}`, + value: fromContactAddress, + }, + ]} + name={fieldName("contactAddress")} + label={"Kontaktadresse"} + required={"Bitte auswählen"} + value={values.contactAddress} + readOnly={!requiresContactAddressMerge} /> - </Box> - <Box component={"section"} aria-label={"Telefonnummern"}> - <InputArrayField - name={fieldName("phoneNumbers")} - label={"Telefonnummer"} - addMoreLabel={"Telefonnummer hinzufügen"} + )} + {(isDefined(values.differentBillingAddress) || + requiresBillingAddressMerge) && ( + <AddressMergeField + options={[ + { + label: `Übernehmen von ${intoLabel}`, + value: intoBillingAddress, + }, + { + label: `Übernehmen von ${fromLabel}`, + value: fromBillingAddress, + }, + ]} + name={fieldName("differentBillingAddress")} + label={"Abweichende Rechnungsadresse"} + required={"Bitte auswählen"} + value={values.differentBillingAddress} + readOnly={!requiresBillingAddressMerge} /> - </Box> + )} + <Stack gap={"inherit"}> + <Box component={"section"} aria-label={"E-Mail-Adressen"}> + <InputArrayField + name={fieldName("emailAddresses")} + label={"E-Mail-Adresse"} + addMoreLabel={"E-Mail-Adresse hinzufügen"} + /> + </Box> + <Box component={"section"} aria-label={"Telefonnummern"}> + <InputArrayField + name={fieldName("phoneNumbers")} + label={"Telefonnummer"} + addMoreLabel={"Telefonnummer hinzufügen"} + /> + </Box> + </Stack> </Stack> </SidebarContent> <SidebarActions> @@ -193,6 +269,7 @@ export function MergePersonContactForm({ submitting={isSubmitting} submitLabel={"Bestätigen"} onCancel={onCancel} + onBack={onBack} /> </SidebarActions> </SidebarForm> diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergeStringField.tsx b/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergeStringField.tsx index 26a48db8b..b89de1e9e 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergeStringField.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/forms/merge/MergeStringField.tsx @@ -9,11 +9,13 @@ import { Stack, Typography } from "@mui/joy"; import { DetailsCell } from "@/lib/shared/components/detailsSection/DetailsCell"; -interface MergeStringFieldProps extends FieldProps<string> { +export interface MergeStringFieldProps extends FieldProps<string> { target: string | undefined; source: string | undefined; getOptionLabel?: (value: string) => string | undefined; emptyValue?: string; + targetValueLabel: string; + sourceValueLabel: string; } export function MergeStringField({ @@ -21,6 +23,8 @@ export function MergeStringField({ source, getOptionLabel, emptyValue, + targetValueLabel, + sourceValueLabel, ...fieldProps }: MergeStringFieldProps) { function getLabel(value: string) { @@ -54,7 +58,7 @@ export function MergeStringField({ label: ( <AnnotatedSelectOption label={getLabel(normalizedTarget)} - title={"Aktuell"} + title={targetValueLabel ?? "Aktuell"} /> ), }, @@ -63,7 +67,7 @@ export function MergeStringField({ label: ( <AnnotatedSelectOption label={getLabel(normalizedSource)} - title={"Importiert"} + title={sourceValueLabel ?? "Importiert"} /> ), }, diff --git a/employee-portal/src/lib/baseModule/components/contacts/forms/merge/SelectMergeTargetForm.tsx b/employee-portal/src/lib/baseModule/components/contacts/forms/merge/SelectMergeTargetForm.tsx new file mode 100644 index 000000000..4be9b3879 --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/contacts/forms/merge/SelectMergeTargetForm.tsx @@ -0,0 +1,133 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import SwapVertIcon from "@mui/icons-material/SwapVert"; +import { Button, Card, IconButton, Stack, Tooltip, Typography } from "@mui/joy"; +import { Formik } from "formik"; +import { ReactNode } from "react"; + +import { routes } from "@/lib/baseModule/shared/routes"; +import { MultiFormButtonBar } from "@/lib/shared/components/form/MultiFormButtonBar"; +import { SidebarForm } from "@/lib/shared/components/form/SidebarForm"; +import { SidebarActions } from "@/lib/shared/components/sidebar/SidebarActions"; +import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; + +interface EntityWithId { + id: string; +} + +interface SelectMergeTargetFormProps<T> { + firstContact: T; + secondContact: T; + onClose: () => void; + onSubmit: (selected: "first" | "second") => void; + renderCard: (data: T) => ReactNode; +} + +export function SelectMergeTargetForm<T extends EntityWithId>({ + firstContact, + secondContact, + onClose, + onSubmit, + renderCard, +}: SelectMergeTargetFormProps<T>) { + return ( + <Formik + initialValues={{ swapped: false }} + onSubmit={(values: { swapped: boolean }) => + onSubmit(values.swapped ? "second" : "first") + } + > + {({ isSubmitting, values, setFieldValue }) => ( + <SidebarForm> + <SidebarContent title={"Kontakt zusammenführen"}> + <Stack gap={2} sx={{ height: "100%" }}> + <Typography> + Die Kontakte werden zusammengeführt. Dabei bleibt die + Änderungshistorie von diesem Kontakt bestehen: + </Typography> + <ContactCard + contact={values.swapped ? firstContact : secondContact} + renderCard={renderCard} + /> + <Typography sx={{ textWrap: "pretty" }}> + Alle Referenzen auf den folgenden Kontakt werden{" "} + <Typography component="strong" fontWeight="bold"> + unwiderruflich + </Typography>{" "} + ersetzt, und dessen Änderungshistorie geht verloren: + </Typography> + <ContactCard + contact={values.swapped ? secondContact : firstContact} + renderCard={renderCard} + /> + <Button + variant={"plain"} + size={"sm"} + sx={{ + alignSelf: "end", + }} + startDecorator={<SwapVertIcon />} + onClick={() => setFieldValue("swapped", !values.swapped)} + > + Tauschen + </Button> + <Typography sx={{ marginBlockStart: "auto" }}> + <Typography component="strong" fontWeight="bold"> + Hinweis:{" "} + </Typography> + Vor dem Zusammenführen können Sie im nächsten Schritt noch + Änderungen vornehmen. + </Typography> + </Stack> + </SidebarContent> + <SidebarActions> + <MultiFormButtonBar + submitting={isSubmitting} + submitLabel={"Weiter"} + onCancel={onClose} + /> + </SidebarActions> + </SidebarForm> + )} + </Formik> + ); +} + +function ContactCard<T extends EntityWithId>({ + contact, + renderCard, +}: { + contact: T; + renderCard: (data: T) => ReactNode; +}) { + return ( + <Card color={"primary"}> + <Stack + direction={"row"} + gap={1} + justifyContent={"space-between"} + alignItems={"center"} + > + {renderCard(contact)} + <Tooltip title={"In neuem Tab anzeigen"} placement={"left"}> + <IconButton + component={"a"} + color={"primary"} + size={"sm"} + href={routes.contacts.details(contact.id)} + target={"_blank"} + sx={{ + alignSelf: "start", + }} + > + <OpenInNewIcon /> + </IconButton> + </Tooltip> + </Stack> + </Card> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/contacts/helpers.ts b/employee-portal/src/lib/baseModule/components/contacts/helpers.ts index b498b6c0e..83d5959fc 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/helpers.ts +++ b/employee-portal/src/lib/baseModule/components/contacts/helpers.ts @@ -6,9 +6,25 @@ import { formatPersonName } from "@eshg/lib-portal/formatters/person"; import { Contact } from "@/lib/baseModule/components/contacts/types"; +import { BaseAddress, isDomesticAddress } from "@/lib/shared/helpers/address"; +import { join } from "@/lib/shared/helpers/strings"; export function fullContactName(contact: Contact) { return contact.type === "PersonContact" ? formatPersonName({ firstName: contact.firstName, lastName: contact.name }) : contact.name; } + +export function getContactAddressLine(address: BaseAddress | undefined) { + if (address === undefined) { + return undefined; + } + + const streetOrPostbox = isDomesticAddress(address) + ? join([address.street, address.houseNumber], " ") + : `PO Box ${address.postbox}`; + + const cityAndPostalCode = `${address.postalCode} ${address.city}`; + + return `${streetOrPostbox}, ${cityAndPostalCode}`; +} diff --git a/employee-portal/src/lib/baseModule/components/contacts/history/ContactHistory.tsx b/employee-portal/src/lib/baseModule/components/contacts/history/ContactHistory.tsx index 61848e4c1..692a36f75 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/history/ContactHistory.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/history/ContactHistory.tsx @@ -104,6 +104,9 @@ function mapEntryToTimelineEntries( } else { changes = ["Kontaktdetails geändert"]; } + if (entry.changes.mergedFrom.isChanged) { + changes.push("Kontakt zusammengeführt"); + } } break; } diff --git a/employee-portal/src/lib/baseModule/components/contacts/modals/AddInstitutionContactSidebar.tsx b/employee-portal/src/lib/baseModule/components/contacts/modals/AddInstitutionContactSidebar.tsx index 0a7812e7a..bab05d4f5 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/modals/AddInstitutionContactSidebar.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/modals/AddInstitutionContactSidebar.tsx @@ -97,6 +97,8 @@ export function AddInstitutionContactSidebar({ <MergeInstitutionContactForm into={formState.into} from={formState.from} + intoLabel={"Aktuell"} + fromLabel={"Importiert"} onCancel={onClose} onSuccess={onSuccess} sidebarFormRef={sidebarFormRef} diff --git a/employee-portal/src/lib/baseModule/components/contacts/modals/AddPersonContactSidebar.tsx b/employee-portal/src/lib/baseModule/components/contacts/modals/AddPersonContactSidebar.tsx index 383309b19..371e06d8c 100644 --- a/employee-portal/src/lib/baseModule/components/contacts/modals/AddPersonContactSidebar.tsx +++ b/employee-portal/src/lib/baseModule/components/contacts/modals/AddPersonContactSidebar.tsx @@ -98,6 +98,8 @@ export function AddPersonContactSidebar({ <MergePersonContactForm into={formState.into} from={formState.from} + intoLabel={"Aktuell"} + fromLabel={"Importiert"} onCancel={onClose} onSuccess={onSuccess} sidebarFormRef={sidebarFormRef} diff --git a/employee-portal/src/lib/baseModule/components/contacts/modals/MergeInstitutionContactSidebar.tsx b/employee-portal/src/lib/baseModule/components/contacts/modals/MergeInstitutionContactSidebar.tsx new file mode 100644 index 000000000..3667e8cf5 --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/contacts/modals/MergeInstitutionContactSidebar.tsx @@ -0,0 +1,76 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiInstitutionContact } from "@eshg/employee-portal-api/base"; +import { useState } from "react"; + +import { InstitutionContactCard } from "@/lib/baseModule/components/contacts/forms/card/InstitutionContactCard"; +import { MergeInstitutionContactForm } from "@/lib/baseModule/components/contacts/forms/merge/MergeInstitutionContactForm"; +import { SelectMergeTargetForm } from "@/lib/baseModule/components/contacts/forms/merge/SelectMergeTargetForm"; +import { + SidebarWithFormRefProps, + useSidebarWithFormRef, +} from "@/lib/shared/hooks/useSidebarWithFormRef"; + +interface SelectStage { + stage: "select"; +} + +interface MergeStage { + stage: "merge"; + selected: "first" | "second"; +} + +type SidebarState = SelectStage | MergeStage; + +interface MergeInstitutionContactSidebarProps extends SidebarWithFormRefProps { + firstContact: ApiInstitutionContact; + secondContact: ApiInstitutionContact; +} + +export function useMergeInstitutionContactSidebar() { + return useSidebarWithFormRef({ + component: MergeInstitutionContactSidebar, + }); +} + +function MergeInstitutionContactSidebar( + props: MergeInstitutionContactSidebarProps, +) { + const [state, setState] = useState<SidebarState>({ + stage: "select", + }); + + if (state.stage === "select") { + return ( + <SelectMergeTargetForm + onSubmit={(selected) => setState({ stage: "merge", selected })} + renderCard={(contact) => <InstitutionContactCard contact={contact} />} + firstContact={props.firstContact} + secondContact={props.secondContact} + onClose={() => props.onClose(true)} + /> + ); + } + + return ( + <MergeInstitutionContactForm + into={ + state.selected === "first" ? props.firstContact : props.secondContact + } + from={{ + type: "Entity", + data: + state.selected === "first" ? props.secondContact : props.firstContact, + }} + sidebarFormRef={props.formRef} + onCancel={() => props.onClose(false)} + onSuccess={() => props.onClose(true)} + onBack={() => setState({ stage: "select" })} + intoLabel={"Kontakt A"} + fromLabel={"Kontakt B"} + /> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/contacts/modals/MergePersonContactSidebar.tsx b/employee-portal/src/lib/baseModule/components/contacts/modals/MergePersonContactSidebar.tsx new file mode 100644 index 000000000..c2e41e9b1 --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/contacts/modals/MergePersonContactSidebar.tsx @@ -0,0 +1,76 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiPersonContact } from "@eshg/employee-portal-api/base"; +import { useState } from "react"; + +import { PersonContactCard } from "@/lib/baseModule/components/contacts/forms/card/PersonContactCard"; +import { MergePersonContactForm } from "@/lib/baseModule/components/contacts/forms/merge/MergePersonContactForm"; +import { SelectMergeTargetForm } from "@/lib/baseModule/components/contacts/forms/merge/SelectMergeTargetForm"; +import { + SidebarWithFormRefProps, + useSidebarWithFormRef, +} from "@/lib/shared/hooks/useSidebarWithFormRef"; + +interface SelectStage { + stage: "select"; +} + +interface MergeStage { + stage: "merge"; + selected: "first" | "second"; +} + +type SidebarState = SelectStage | MergeStage; + +interface MergePersonContactSidebarProps extends SidebarWithFormRefProps { + firstContact: ApiPersonContact; + secondContact: ApiPersonContact; +} + +export function useMergePersonContactSidebar() { + return useSidebarWithFormRef({ + component: MergePersonContactSidebar, + }); +} + +export function MergePersonContactSidebar( + props: MergePersonContactSidebarProps, +) { + const [state, setState] = useState<SidebarState>({ + stage: "select", + }); + + if (state.stage === "select") { + return ( + <SelectMergeTargetForm + onSubmit={(selected) => setState({ stage: "merge", selected })} + renderCard={(contact) => <PersonContactCard contact={contact} />} + firstContact={props.firstContact} + secondContact={props.secondContact} + onClose={() => props.onClose(true)} + /> + ); + } + + return ( + <MergePersonContactForm + into={ + state.selected === "first" ? props.firstContact : props.secondContact + } + from={{ + type: "Entity", + data: + state.selected === "first" ? props.secondContact : props.firstContact, + }} + sidebarFormRef={props.formRef} + onCancel={() => props.onClose(false)} + onSuccess={() => props.onClose(true)} + onBack={() => setState({ stage: "select" })} + intoLabel={"Kontakt A"} + fromLabel={"Kontakt B"} + /> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/gdpr/procedure/DownloadReportButton.tsx b/employee-portal/src/lib/baseModule/components/gdpr/procedure/DownloadReportButton.tsx new file mode 100644 index 000000000..c98c0628a --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/gdpr/procedure/DownloadReportButton.tsx @@ -0,0 +1,49 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiGetGdprProcedureResponse } from "@eshg/employee-portal-api/base"; +import { useFileDownload } from "@eshg/lib-portal/api/files/download"; +import { HiddenContainer } from "@eshg/lib-portal/components/HiddenContainer"; +import { Button, Sheet } from "@mui/joy"; +import { useState } from "react"; + +import { useGdprProcedureApi } from "@/lib/baseModule/api/clients"; + +export function DownloadReportButton({ + procedure, +}: { + procedure: ApiGetGdprProcedureResponse; +}) { + const gdprApi = useGdprProcedureApi(); + const fileDownload = useFileDownload(() => + gdprApi.getReportDocumentRaw({ + id: procedure.id, + }), + ); + + const [loading, setLoading] = useState(false); + + function openPreview() { + setLoading(true); + void fileDownload.preview().finally(() => setLoading(false)); + } + + return ( + <Sheet> + <Button + loading={loading} + loadingPosition={"start"} + onClick={() => openPreview()} + sx={{ + width: "100%", + minWidth: "fit-content", + }} + > + Antrag Dokument ansehen + </Button> + <HiddenContainer ref={fileDownload.downloadContainerRef} /> + </Sheet> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/gdpr/procedure/GDPRProcedureDetails.tsx b/employee-portal/src/lib/baseModule/components/gdpr/procedure/GDPRProcedureDetails.tsx index f09cc1ebb..8f16798e7 100644 --- a/employee-portal/src/lib/baseModule/components/gdpr/procedure/GDPRProcedureDetails.tsx +++ b/employee-portal/src/lib/baseModule/components/gdpr/procedure/GDPRProcedureDetails.tsx @@ -6,6 +6,7 @@ "use client"; import { + ApiGdprProcedureStatus, ApiGetGdprProcedureResponse, ApiGetReferenceFacilityResponse, ApiGetReferencePersonResponse, @@ -26,6 +27,7 @@ import { SectionTile, SectionTitle, } from "@/lib/baseModule/components/gdpr/procedure/tiles/SectionTile"; +import { OverlayBoundary } from "@/lib/shared/components/boundaries/OverlayBoundary"; import { CentralFileFacilityDetails } from "@/lib/shared/components/centralFile/display/CentralFileFacilityDetails"; import { CentralFilePersonDetails } from "@/lib/shared/components/centralFile/display/CentralFilePersonDetails"; import { useSidebar } from "@/lib/shared/components/drawer/useSidebar"; @@ -82,46 +84,53 @@ export function GDPRProcedureDetails({ } return ( - <> - <Stack direction={{ xxs: "column", md: "row" }} gap={3}> - <Stack sx={{ flex: 5, minWidth: "fit-content" }} gap={3}> + <Stack + direction={{ xxs: "column", md: "row" }} + gap={3} + sx={{ + alignItems: { + md: "start", + }, + }} + > + <Stack gap={3} flex={1}> + {isGdprPerson(identity) ? ( + <GdprPersonDataTile identity={identity} columnSx={COLUMN_STYLE} /> + ) : ( + <GdprFacilityDataTile identity={identity} columnSx={COLUMN_STYLE} /> + )} + {linkedPersons.map((person, index) => ( + <SectionTile key={person.id} id={person.id}> + <SectionTitle id={person.id}> + {index + 1}. Datensatz aus der Zentralkartei + </SectionTitle> + <CentralFilePersonDetails person={person} columnSx={COLUMN_STYLE} /> + </SectionTile> + ))} + {linkedFacilities.map((facility, index) => ( + <SectionTile key={facility.id} id={facility.id}> + <SectionTitle id={facility.id}> + {index + 1}. Datensatz aus der Zentralkartei + </SectionTitle> + <CentralFileFacilityDetails + facility={facility} + columnSx={COLUMN_STYLE} + /> + </SectionTile> + ))} + </Stack> + <Stack gap={3} flexBasis={"50ch"}> + <OverlayBoundary> <ProcedureDetailsTile procedure={procedure} /> + </OverlayBoundary> + {procedure.status === ApiGdprProcedureStatus.Draft && ( <CentralFileLinkTile centralFileId={procedure.centralFileId} numMatches={personMatches.length + facilityMatches.length} onAddLink={hasWritePerms && (() => openLinkSidebar())} /> - </Stack> - <Stack sx={{ flex: 20 }} gap={3}> - {isGdprPerson(identity) ? ( - <GdprPersonDataTile identity={identity} columnSx={COLUMN_STYLE} /> - ) : ( - <GdprFacilityDataTile identity={identity} columnSx={COLUMN_STYLE} /> - )} - {linkedPersons.map((person, index) => ( - <SectionTile key={person.id} id={person.id}> - <SectionTitle id={person.id}> - {index + 1}. Datensatz aus der Zentralkartei - </SectionTitle> - <CentralFilePersonDetails - person={person} - columnSx={COLUMN_STYLE} - /> - </SectionTile> - ))} - {linkedFacilities.map((facility, index) => ( - <SectionTile key={facility.id} id={facility.id}> - <SectionTitle id={facility.id}> - {index + 1}. Datensatz aus der Zentralkartei - </SectionTitle> - <CentralFileFacilityDetails - facility={facility} - columnSx={COLUMN_STYLE} - /> - </SectionTile> - ))} - </Stack> + )} </Stack> - </> + </Stack> ); } diff --git a/employee-portal/src/lib/baseModule/components/gdpr/procedure/sidebars/EditMatterOfConcernSidebar.tsx b/employee-portal/src/lib/baseModule/components/gdpr/procedure/sidebars/EditMatterOfConcernSidebar.tsx new file mode 100644 index 000000000..fabbc82b8 --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/gdpr/procedure/sidebars/EditMatterOfConcernSidebar.tsx @@ -0,0 +1,102 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ApiGetGdprProcedureResponse } from "@eshg/employee-portal-api/base"; +import { InputField } from "@eshg/lib-portal/components/formFields/InputField"; +import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; +import { createFieldNameMapper } from "@eshg/lib-portal/helpers/form"; +import { Stack } from "@mui/joy"; +import { Formik } from "formik"; + +import { useSetMatterOfConcern } from "@/lib/baseModule/api/mutations/gdpr"; +import { statusTranslation } from "@/lib/baseModule/components/gdpr/i18n"; +import { MultiFormButtonBar } from "@/lib/shared/components/form/MultiFormButtonBar"; +import { SidebarForm } from "@/lib/shared/components/form/SidebarForm"; +import { TextareaField } from "@/lib/shared/components/formFields/TextareaField"; +import { SidebarActions } from "@/lib/shared/components/sidebar/SidebarActions"; +import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; +import { + SidebarWithFormRefProps, + useSidebarWithFormRef, +} from "@/lib/shared/hooks/useSidebarWithFormRef"; + +interface EditMatterOfConcernSidebarProps extends SidebarWithFormRefProps { + procedure: ApiGetGdprProcedureResponse; +} + +interface EditMatterOfConcernFormValues { + matterOfConcern: string; + status: string; + date: string; +} + +export function useEditMatterOfConcernSidebar() { + return useSidebarWithFormRef({ + component: EditMatterOfConcernSidebar, + }); +} + +function EditMatterOfConcernSidebar({ + procedure, + formRef, + onClose, +}: EditMatterOfConcernSidebarProps) { + const fieldName = createFieldNameMapper<EditMatterOfConcernFormValues>(); + const setMatterOfConcern = useSetMatterOfConcern( + procedure.id, + procedure.version, + ); + + return ( + <Formik + initialValues={{ + matterOfConcern: procedure.matterOfConcern ?? "", + status: statusTranslation[procedure.status], + date: formatDate(procedure.createdAt), + }} + onSubmit={async (values: EditMatterOfConcernFormValues) => { + await setMatterOfConcern + .mutateAsync(values.matterOfConcern, { + onSuccess: () => onClose(true), + }) + .catch(); + }} + > + {({ isSubmitting }) => ( + <SidebarForm ref={formRef}> + <SidebarContent title={"Vorgang bearbeiten"}> + <Stack gap={2}> + <InputField + label={"Erstellt"} + name={fieldName("date")} + readOnly + /> + <InputField + label={"Status"} + name={fieldName("status")} + readOnly + /> + <TextareaField + label={"Anliegen"} + name={fieldName("matterOfConcern")} + required={"Bitte ein Anliegen angeben."} + sxTextarea={{ + minHeight: "6rem", + }} + /> + </Stack> + </SidebarContent> + <SidebarActions> + <MultiFormButtonBar + submitting={isSubmitting} + submitLabel={"Speichern"} + onCancel={() => onClose(false)} + /> + </SidebarActions> + </SidebarForm> + )} + </Formik> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/gdpr/procedure/tiles/ProcedureDetailsTile.tsx b/employee-portal/src/lib/baseModule/components/gdpr/procedure/tiles/ProcedureDetailsTile.tsx index c34381d21..5e0c07680 100644 --- a/employee-portal/src/lib/baseModule/components/gdpr/procedure/tiles/ProcedureDetailsTile.tsx +++ b/employee-portal/src/lib/baseModule/components/gdpr/procedure/tiles/ProcedureDetailsTile.tsx @@ -3,17 +3,34 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiGetGdprProcedureResponse } from "@eshg/employee-portal-api/base"; +import { + ApiGdprProcedureStatus, + ApiGdprProcedureType, + ApiGetGdprProcedureResponse, +} from "@eshg/employee-portal-api/base"; +import { + AlertSlot, + useAlert, +} from "@eshg/lib-portal/errorHandling/AlertContext"; import { formatDateTime } from "@eshg/lib-portal/formatters/dateTime"; +import EditIcon from "@mui/icons-material/EditOutlined"; +import InfoIcon from "@mui/icons-material/InfoOutlined"; +import { Button, Divider, IconButton, Stack, Typography } from "@mui/joy"; +import { isNullish } from "remeda"; +import { useChangeProcedureStatus } from "@/lib/baseModule/api/mutations/gdpr"; import { statusTranslation, typeTranslation, } from "@/lib/baseModule/components/gdpr/i18n"; +import { DownloadReportButton } from "@/lib/baseModule/components/gdpr/procedure/DownloadReportButton"; +import { useEditMatterOfConcernSidebar } from "@/lib/baseModule/components/gdpr/procedure/sidebars/EditMatterOfConcernSidebar"; import { SectionTile, SectionTitle, } from "@/lib/baseModule/components/gdpr/procedure/tiles/SectionTile"; +import { multiLineEllipsis } from "@/lib/baseModule/theme/theme"; +import { ButtonBar } from "@/lib/shared/components/buttons/ButtonBar"; import { DetailsCell } from "@/lib/shared/components/detailsSection/DetailsCell"; export function ProcedureDetailsTile({ @@ -21,25 +38,116 @@ export function ProcedureDetailsTile({ }: { procedure: ApiGetGdprProcedureResponse; }) { + const alert = useAlert(); + + const editMatterOfConcernSidebar = useEditMatterOfConcernSidebar(); + + const changeProcedureStatus = useChangeProcedureStatus( + procedure.id, + procedure.version, + ); + + async function startProcedure() { + if (isNullish(procedure.matterOfConcern)) { + alert?.warning({ + message: "Sie müssen ein Anliegen angeben.", + closeable: true, + }); + } else { + await changeProcedureStatus + .mutateAsync(ApiGdprProcedureStatus.InProgress, { + onSuccess: () => alert?.close(), + }) + .catch(); + } + } + + const isObjection = procedure.type === ApiGdprProcedureType.ToObject; + const isDraft = procedure.status === ApiGdprProcedureStatus.Draft; + const isEditable = + procedure.status === ApiGdprProcedureStatus.Draft || + procedure.status === ApiGdprProcedureStatus.InProgress; + return ( - <SectionTile id={"procedure-details"}> - <SectionTitle id={"procedure-details"}>Vorgangsdaten</SectionTitle> - <DetailsCell - name={"createdAt"} - label={"Erstellt"} - value={formatDateTime(procedure.createdAt)} - /> - <DetailsCell - name={"type"} - label={"Vorgangsart"} - value={typeTranslation[procedure.type]} - avoidWrap - /> - <DetailsCell - name={"status"} - label={"Status"} - value={statusTranslation[procedure.status]} - /> - </SectionTile> + <> + <SectionTile id={"procedure-details"}> + <SectionTitle id={"procedure-details"}> + <Stack + component={"span"} + direction={"row"} + justifyContent={"space-between"} + > + <Typography component={"span"}>Zusatzinfos</Typography> + {isObjection && isEditable && ( + <IconButton + size={"sm"} + color={"primary"} + variant={"outlined"} + aria-label={"Editieren"} + onClick={() => editMatterOfConcernSidebar.open({ procedure })} + > + <EditIcon /> + </IconButton> + )} + </Stack> + </SectionTitle> + + <AlertSlot /> + + <DetailsCell + name={"createdAt"} + label={"Erstellt"} + value={formatDateTime(procedure.createdAt)} + /> + <DetailsCell + name={"type"} + label={"Vorgangsart"} + value={typeTranslation[procedure.type]} + avoidWrap + /> + <DetailsCell + name={"status"} + label={"Status"} + value={statusTranslation[procedure.status]} + /> + {isObjection && ( + <DetailsCell + name={"matterOfConcern"} + label={"Anliegen"} + value={ + procedure.matterOfConcern ?? ( + <Typography + startDecorator={<InfoIcon color={"danger"} size={"md"} />} + > + Bitte Anliegen eintragen. + </Typography> + ) + } + valueSx={{ + ...multiLineEllipsis(3), + maxWidth: "100%", + }} + /> + )} + + <Divider /> + <ButtonBar + right={ + <> + <Button variant={"plain"} disabled> + Abbrechen + </Button> + {isDraft && ( + <Button onClick={() => startProcedure()}>Starten</Button> + )} + </> + } + /> + </SectionTile> + + {isObjection && procedure.status !== ApiGdprProcedureStatus.Draft && ( + <DownloadReportButton procedure={procedure} /> + )} + </> ); } diff --git a/employee-portal/src/lib/baseModule/components/layout/SelfUserSidebar.tsx b/employee-portal/src/lib/baseModule/components/layout/SelfUserSidebar.tsx index eaf002296..a3a63f3e6 100644 --- a/employee-portal/src/lib/baseModule/components/layout/SelfUserSidebar.tsx +++ b/employee-portal/src/lib/baseModule/components/layout/SelfUserSidebar.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiBaseFeature } from "@eshg/employee-portal-api/base"; import { InternalLink } from "@eshg/lib-portal/components/navigation/InternalLink"; import { InternalLinkButton } from "@eshg/lib-portal/components/navigation/InternalLinkButton"; import ProfileIcon from "@mui/icons-material/AccountCircle"; @@ -13,7 +12,6 @@ import ManageSearchIcon from "@mui/icons-material/ManageSearch"; import { Button, Divider, Stack } from "@mui/joy"; import { ReactNode } from "react"; -import { useIsNewFeatureEnabled } from "@/lib/baseModule/api/queries/feature"; import { useGetSelfUser } from "@/lib/baseModule/api/queries/users"; import { UserSidebarHeader } from "@/lib/baseModule/components/users/userSidebar/UserSidebarHeader"; import { routes } from "@/lib/baseModule/shared/routes"; @@ -70,12 +68,6 @@ function MiscLinkButton({ href, label }: { href: string; label: string }) { } function SelfUserSidebar() { - const isActiveSessionsEnabled = useIsNewFeatureEnabled( - ApiBaseFeature.AccountActiveSessions, - ); - const isLoginProtocolEnabled = useIsNewFeatureEnabled( - ApiBaseFeature.LoginProtocol, - ); const { data: selfUser } = useGetSelfUser(); return ( @@ -91,25 +83,18 @@ function SelfUserSidebar() { > Profil </NavLinkButton> - - {isActiveSessionsEnabled && ( - <NavLinkButton - decorator={<DevicesIcon />} - href={routes.account.sessions} - > - Aktive Sitzungen - </NavLinkButton> - )} - - {isLoginProtocolEnabled && ( - <NavLinkButton - href={routes.account.loginProtocol} - decorator={<ManageSearchIcon />} - > - Anmeldeprotokoll - </NavLinkButton> - )} - + <NavLinkButton + decorator={<DevicesIcon />} + href={routes.account.sessions} + > + Aktive Sitzungen + </NavLinkButton> + <NavLinkButton + href={routes.account.loginProtocol} + decorator={<ManageSearchIcon />} + > + Anmeldeprotokoll + </NavLinkButton> <Divider orientation="horizontal" /> </Stack> diff --git a/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessageInformation.tsx b/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessageInformation.tsx new file mode 100644 index 000000000..a9efff564 --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessageInformation.tsx @@ -0,0 +1,49 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { WarningAmberOutlined } from "@mui/icons-material"; +import { Alert, Box, Typography } from "@mui/joy"; + +import { ClientState } from "@/lib/businessModules/chat/shared/enums"; + +interface MessageInformationProps { + clientState: ClientState.CreateBackupKey | ClientState.RestoreBackupKey; +} + +export function MessageInformation({ clientState }: MessageInformationProps) { + const text = + clientState === ClientState.CreateBackupKey + ? "Richten Sie ein Sicherheitsbackup ein um die Chatfunktion zu nutzen" + : "Bestätigen sie dieses Endgerät um die Chatfunktion zu nutzen"; + + return ( + <Alert + variant="outlined" + color="primary" + invertedColors + sx={{ alignItems: "flex-start" }} + startDecorator={<WarningAmberOutlined fontSize="xl2" />} + > + <Box> + <Typography level="title-md" color="primary" data-testid="title"> + Chat + </Typography> + <Typography + level="body-sm" + color="primary" + sx={{ + fontSize: { + xs: "sm", + sm: "md", + }, + }} + data-testid="message" + > + {text} + </Typography> + </Box> + </Alert> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessagesSidebar.tsx b/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessagesSidebar.tsx index 48d0573e9..106522a1f 100644 --- a/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessagesSidebar.tsx +++ b/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessagesSidebar.tsx @@ -7,6 +7,7 @@ import { useNavigation } from "@eshg/lib-portal/components/navigation/Navigation import OpenInNew from "@mui/icons-material/OpenInNew"; import { Button, Divider, Stack } from "@mui/joy"; +import { MessagesSidebarContent } from "@/lib/baseModule/components/layout/messagesSidebar/MessagesSidebarContent"; import { routes } from "@/lib/baseModule/shared/routes"; import { DrawerProps } from "@/lib/shared/components/drawer/drawerContext"; import { @@ -14,8 +15,6 @@ import { useSidebar, } from "@/lib/shared/components/drawer/useSidebar"; -import { MessagesSidebarContent } from "./MessagesSidebarContent"; - export function useMessagesSidebar(): UseSidebarResult { return useSidebar({ component: MessagesSidebar, diff --git a/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessagesSidebarContent.tsx b/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessagesSidebarContent.tsx index 524b64bd0..d33486310 100644 --- a/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessagesSidebarContent.tsx +++ b/employee-portal/src/lib/baseModule/components/layout/messagesSidebar/MessagesSidebarContent.tsx @@ -6,10 +6,12 @@ import { ButtonLink } from "@eshg/lib-portal/components/buttons/ButtonLink"; import { Divider, Stack, Typography } from "@mui/joy"; +import { MessageInformation } from "@/lib/baseModule/components/layout/messagesSidebar/MessageInformation"; import { MessageNotification } from "@/lib/baseModule/components/layout/messagesSidebar/MessageNotification"; import { NoMessagesIllustration } from "@/lib/businessModules/chat/assets/NoMessagesIllustration"; import { ChatNoAccessAlert } from "@/lib/businessModules/chat/components/ChatNoAccessAlert"; import { useChat } from "@/lib/businessModules/chat/shared/ChatProvider"; +import { ClientState } from "@/lib/businessModules/chat/shared/enums"; import { useMatrixClient } from "@/lib/businessModules/chat/shared/hooks/useMatrixClient"; import { useNewMessages } from "@/lib/businessModules/chat/shared/hooks/useNewMessages"; import { allMessagesRead } from "@/lib/businessModules/chat/shared/utils"; @@ -29,10 +31,27 @@ const title = "Ungelesene Chats"; export function MessagesSidebarContent() { const { userSettings } = useChat(); const { newMessages } = useNewMessages(); - const matrixClient = useMatrixClient(); + const matrix = useMatrixClient(); + if (!userSettings.chatUsageEnabled) { - return <ChatNoAccessAlert />; + return ( + <SidebarContent title={title}> + <ChatNoAccessAlert /> + </SidebarContent> + ); } + + if ( + matrix?.state === ClientState.CreateBackupKey || + matrix?.state === ClientState.RestoreBackupKey + ) { + return ( + <SidebarContent title={title}> + <MessageInformation clientState={matrix.state} /> + </SidebarContent> + ); + } + if (!newMessages.length) { return ( <SidebarContent title={title}> @@ -40,6 +59,7 @@ export function MessagesSidebarContent() { </SidebarContent> ); } + return ( <SidebarContent title={title} @@ -48,8 +68,8 @@ export function MessagesSidebarContent() { <ButtonLink level="title-md" onClick={() => { - if (matrixClient) { - allMessagesRead(matrixClient, newMessages); + if (matrix) { + allMessagesRead(matrix.client, newMessages); } }} > diff --git a/employee-portal/src/lib/baseModule/components/markdown/MarkdownPage.tsx b/employee-portal/src/lib/baseModule/components/markdown/MarkdownPage.tsx new file mode 100644 index 000000000..0c34bca0b --- /dev/null +++ b/employee-portal/src/lib/baseModule/components/markdown/MarkdownPage.tsx @@ -0,0 +1,79 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ExternalLink } from "@eshg/lib-portal/components/navigation/ExternalLink"; +import { evaluate } from "@mdx-js/mdx"; +import { List, ListItem, Typography } from "@mui/joy"; +import { promises as fs } from "fs"; +import * as path from "path"; +import { HTMLProps } from "react"; +import * as runtime from "react/jsx-runtime"; +import "server-only"; + +import { env } from "@/env/server"; + +const mdxComponents = { + a: (props: HTMLProps<HTMLAnchorElement>) => ( + <ExternalLink href={props.href} target="_blank"> + {props.children} + </ExternalLink> + ), + p: (props: HTMLProps<HTMLParagraphElement>) => ( + <Typography component="p">{props.children}</Typography> + ), + span: (props: HTMLProps<HTMLSpanElement>) => ( + <Typography component="span">{props.children}</Typography> + ), + h2: (props: HTMLProps<HTMLHeadingElement>) => ( + <Typography level="h2">{props.children}</Typography> + ), + h3: (props: HTMLProps<HTMLHeadingElement>) => ( + <Typography level="h3">{props.children}</Typography> + ), + h4: (props: HTMLProps<HTMLHeadingElement>) => ( + <Typography component="p" level="title-md"> + {props.children} + </Typography> + ), + ul: (props: HTMLProps<HTMLUListElement>) => ( + <List marker="disc">{props.children}</List> + ), + li: (props: HTMLProps<HTMLLIElement>) => ( + <ListItem>{props.children}</ListItem> + ), +}; + +export type PageName = + | "contact" + | "accessibility" + | "privacy" + | "release-notes"; +const validPageTypes: string[] = [ + "contact", + "accessibility", + "privacy", + "release-notes", +] as const satisfies PageName[]; + +export function isValidPageType(type: string): type is PageName { + return validPageTypes.includes(type); +} + +export async function MarkdownPage({ pageType }: { pageType: PageName }) { + const filePath = path.join( + "./public/markdown", + pageType === "release-notes" ? "common" : env.MARKDOWN_PAGE_DIRECTORY, + `${pageType}.md`, + ); + + const source = await fs.readFile(filePath, { encoding: "utf-8" }); + + const { default: MDXContent } = await evaluate(source, { + ...runtime, + format: "md", + }); + + return <MDXContent components={mdxComponents} />; +} diff --git a/employee-portal/src/lib/baseModule/components/privacy/Privacy.tsx b/employee-portal/src/lib/baseModule/components/privacy/Privacy.tsx deleted file mode 100644 index 0bfb6eef2..000000000 --- a/employee-portal/src/lib/baseModule/components/privacy/Privacy.tsx +++ /dev/null @@ -1,371 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ExternalLink } from "@eshg/lib-portal/components/navigation/ExternalLink"; -import { List, ListItem, Stack, Typography } from "@mui/joy"; -import { PropsWithChildren } from "react"; - -import { - NoWrap, - StaticTextDocumentPanel, -} from "@/lib/baseModule/components/StaticTextDocumentPanel"; - -function Section({ - id, - title, - children, -}: PropsWithChildren<{ id: string; title: string }>) { - return ( - <Stack - component={"section"} - aria-labelledby={id} - alignItems={"start"} - gap={1} - > - <Typography level={"h2"} id={id}> - {title} - </Typography> - {children} - </Stack> - ); -} - -export function Privacy() { - return ( - <StaticTextDocumentPanel> - <Typography> - Diese Datenschutzerklärung gilt für die Webseite{" "} - <NoWrap>„frankfurt.ga-lotse.de“</NoWrap> (bzw.{" "} - <NoWrap>„https://frankfurt.ga-lotse.de“</NoWrap> sowie dazu zugehörige - Subdomains) des Gesundheitsamts der Stadt Frankfurt am Main. Dieses - Informationsportal bietet Informationen zu besonderen Ereignissen und - wird ausschließlich zum dem Zwecke genutzt. - </Typography> - - <Section - id={"section-1"} - title={ - "1. Name und Kontaktdaten des für die Verarbeitung Verantwortlichen sowie des behördlichen Datenschutzbeauftragten" - } - > - <Typography> - Diese Datenschutz-Information gilt für die Datenverarbeitung durch: - <br /> - <br /> - Verantwortlicher: - <br /> - <br /> - Verantwortlich für die Website{" "} - <NoWrap>„frankfurt.ga-lotse.de“</NoWrap> ist das Gesundheitsamt - Frankfurt am Main: - <br /> - <br /> - Gesundheitsamt Frankfurt am Main - <br /> - Breite Gasse 28 - <br /> - 60313 Frankfurt am Main - <br /> - E-Mail:{" "} - <ExternalLink - href={"mailto:datenschutz.gesundheitsamt@stadt-frankfurt.de"} - > - datenschutz.gesundheitsamt@stadt-frankfurt.de - </ExternalLink> - <br /> - <br /> - Behördlicher Datenschutzbeauftragter: - <br /> - <br /> - Referat Datenschutz und IT-Sicherheit - <br /> - Sandgasse 6, 60311 Frankfurt am Main - </Typography> - </Section> - - <Section - id={"section-2"} - title={ - "2. Erhebung und Speicherung personenbezogener Daten sowie Art und Zweck von deren Verwendung" - } - > - <Typography> - 2.1 Beim Besuch der Website - <br /> - <br /> - Beim Aufrufen unserer Website <NoWrap> - „frankfurt.ga-lotse.de“ - </NoWrap>{" "} - werden durch den auf Ihrem Endgerät zum Einsatz kommenden Browser - automatisch Informationen an den Server unserer Website gesendet. - Diese Informationen werden temporär in einem sog. Logfile gespeichert. - Folgende Informationen werden dabei ohne Ihr Zutun erfasst und bis zur - automatisierten Löschung gespeichert: - </Typography> - - <List marker={"disc"}> - <ListItem> - <Typography>IP-Adresse des anfragenden Rechners</Typography> - </ListItem> - <ListItem> - <Typography>Datum und Uhrzeit des Zugriffs</Typography> - </ListItem> - <ListItem> - <Typography>Name und URL der abgerufenen Datei</Typography> - </ListItem> - <ListItem> - <Typography> - Website, von der aus der Zugriff erfolgt (Referrer-URL) - </Typography> - </ListItem> - <ListItem> - <Typography> - verwendeter Browser und ggf. das Betriebssystem Ihres Rechners - sowie der Name Ihres Access-Providers - </Typography> - </ListItem> - </List> - - <Typography> - Die genannten Daten werden durch uns zu folgenden Zwecken verarbeitet: - </Typography> - - <List marker={"disc"}> - <ListItem> - <Typography> - Gewährleistung eines reibungslosen Verbindungsaufbaus der Website - </Typography> - </ListItem> - <ListItem> - <Typography> - Gewährleistung einer komfortablen Nutzung unserer Website - </Typography> - </ListItem> - <ListItem> - <Typography> - Auswertung der Systemsicherheit und -stabilität - </Typography> - </ListItem> - <ListItem> - <Typography>Rückverfolgung etwaiger DoS Attacken</Typography> - </ListItem> - <ListItem> - <Typography>sowie zu weiteren administrativen Zwecken</Typography> - </ListItem> - </List> - - <Typography level={"title-md"}> - 2.2 Beim Verwenden des QR-Codes - </Typography> - <Typography> - Falls Ihnen für den Zugang zum Online-Portal ein individualisierter - QR-Code gegeben wurde, hat dieser den Zweck, Informationen oder - Nachrichten speziell für Sie zur Verfügung zu stellen, beispielsweise - als betroffener eines gesundheitsrelevanten Ereignisses. Nach - Einscannen des Codes wird man direkt auf die entsprechende - Informationsseite weitergeleitet. Im System wird dabei protokolliert, - zu welchem Zeitpunkt der QR-Code verwendet wurde, jedoch ohne weitere - Angaben wie IP-Adresse oder Namen o.ä. - <br /> - <br /> - Die Rechtsgrundlage für die Datenverarbeitung ist{" "} - <NoWrap>Art. 6 Abs. 1 S. 1 lit. f DS-GVO</NoWrap>. Unser berechtigtes - Interesse folgt aus oben aufgelisteten Zwecken zur Datenerhebung. In - keinem Fall verwenden wir die erhobenen Daten zu dem Zweck, - Rückschlüsse auf Ihre Person zu ziehen. - <br /> - <br /> - Darüber hinaus setzen wir beim Besuch unserer Website Cookies ein. - Nähere Erläuterungen dazu erhalten Sie unter den Ziff. 4 dieser - Datenschutzerklärung. - <br /> - <br /> - </Typography> - </Section> - - <Section - id={"sharing-with-third-parties"} - title={"3. Weitergabe von Daten"} - > - <Typography> - Es findet keine Weitergabe von Daten an Dritte statt. - </Typography> - </Section> - - <Section id={"cookies"} title={"4. Cookies"}> - <Typography> - Wir setzen auf unserer Seite Cookies ein. Hierbei handelt es sich um - kleine Dateien, die Ihr Browser automatisch erstellt und die auf Ihrem - Endgerät (Laptop, Tablet, Smartphone o.ä.) gespeichert werden, wenn - Sie unsere Seite besuchen. Cookies richten auf Ihrem Endgerät keinen - Schaden an, enthalten keine Viren, Trojaner oder sonstige - Schadsoftware. - <br /> - <br /> - In dem Cookie werden Informationen abgelegt, die sich jeweils im - Zusammenhang mit dem spezifisch eingesetzten Endgerät ergeben. Dies - bedeutet jedoch nicht, dass wir dadurch unmittelbar Kenntnis von Ihrer - Identität erhalten. - <br /> - <br /> - Der Einsatz von Cookies dient einerseits dazu, die Nutzung unseres - Angebots für Sie angenehmer zu gestalten. So setzen wir sogenannte - Session-Cookies ein, um zu erkennen, dass Sie einzelne Seiten unserer - Website bereits besucht haben. Diese werden nach Verlassen unserer - Seite automatisch gelöscht. - <br /> - <br /> - Darüber hinaus setzen wir ebenfalls zur Optimierung der - Benutzerfreundlichkeit temporäre Cookies ein, die für einen bestimmten - festgelegten Zeitraum auf Ihrem Endgerät gespeichert werden. Besuchen - Sie unsere Seite erneut, um unsere Dienste in Anspruch zu nehmen, wird - automatisch erkannt, dass Sie bereits bei uns waren und welche - Eingaben und Einstellungen sie getätigt haben, um diese nicht noch - einmal eingeben zu müssen. - <br /> - <br /> - Die durch Cookies verarbeiteten Daten sind für die genannten Zwecke - zur Wahrung unserer berechtigten Interessen erforderlich. - <br /> - <br /> - Die meisten Browser akzeptieren Cookies automatisch. Sie können Ihren - Browser jedoch so konfigurieren, dass keine Cookies auf Ihrem Computer - gespeichert werden oder stets ein Hinweis erscheint, bevor ein neuer - Cookie angelegt wird. Die vollständige Deaktivierung von Cookies kann - jedoch dazu führen, dass Sie nicht alle Funktionen unserer Website - nutzen können. - </Typography> - </Section> - - <Section id={"analytic-tools"} title={"5. Analyse-Tools"}> - <Typography>Es werden keine Analyse-Tools verwendet.</Typography> - </Section> - - <Section id={"social-media-plugins"} title={"6. Social Media Plug-ins"}> - <Typography> - Es werden keine Social Media Plug-Ins verwendet. - </Typography> - </Section> - - <Section id={"rights-of-affected"} title={"7. Betroffenenrechte"}> - <Typography>Sie haben das Recht:</Typography> - <List marker={"disc"}> - <ListItem> - <Typography> - gemäß <NoWrap>Art. 15 DS-GVO</NoWrap> Auskunft über Ihre von uns - verarbeiteten personenbezogenen Daten zu verlangen. Insbesondere - können Sie Auskunft über die Verarbeitungszwecke, die Kategorie - der personenbezogenen Daten, die Kategorien von Empfängern, - gegenüber denen Ihre Daten offengelegt wurden oder werden, die - geplante Speicherdauer, das Bestehen eines Rechts auf - Berichtigung, Löschung, Einschränkung der Verarbeitung oder - Widerspruch, das Bestehen eines Beschwerderechts, die Herkunft - ihrer Daten, sofern diese nicht bei uns erhoben wurden sowie über - das Bestehen einer automatisierten Entscheidungsfindung - einschließlich Profiling und ggf. aussagekräftigen Informationen - zu deren Einzelheiten verlangen. - </Typography> - </ListItem> - <ListItem> - <Typography> - gemäß <NoWrap>Art. 16 DS-GVO</NoWrap> unverzüglich die - Berichtigung unrichtiger oder Vervollständigung Ihrer bei uns - gespeicherten personenbezogenen Daten zu verlangen. - </Typography> - </ListItem> - <ListItem> - <Typography> - gemäß <NoWrap>Art. 17 DS-GVO</NoWrap> die Löschung Ihrer bei uns - gespeicherten personenbezogenen Daten zu verlangen, soweit nicht - die Verarbeitung zur Ausübung des Rechts auf freie - Meinungsäußerung und Information, zur Erfüllung einer rechtlichen - Verpflichtung, aus Gründen des öffentlichen Interesses oder zur - Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen - erforderlich ist. - </Typography> - </ListItem> - <ListItem> - <Typography> - gemäß <NoWrap>Art. 18 DS-GVO</NoWrap> die Einschränkung der - Verarbeitung Ihrer personenbezogenen Daten zu verlangen, soweit - die Richtigkeit der Daten von Ihnen bestritten wird, die - Verarbeitung unrechtmäßig ist, Sie aber deren Löschung ablehnen - und wir die Daten nicht mehr benötigen, Sie jedoch diese zur - Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen - benötigen oder Sie gemäß <NoWrap>Art. 21 DS-GVO</NoWrap>{" "} - Widerspruch gegen die Verarbeitung eingelegt haben. - </Typography> - </ListItem> - <ListItem> - <Typography> - gemäß <NoWrap>Art. 20 DS-GVO</NoWrap> Ihre personenbezogenen - Daten, die Sie uns bereitgestellt haben, in einem strukturierten, - gängigen und maschinenlesebaren Format zu erhalten oder die - Übermittlung an einen anderen Verantwortlichen zu verlangen. - </Typography> - </ListItem> - <ListItem> - <Typography> - gemäß <NoWrap>Art. 7 Abs. 3 DS-GVO</NoWrap> Ihre einmal erteilte - Einwilligung jederzeit gegenüber uns zu widerrufen. Dies hat zur - Folge, dass wir die Datenverarbeitung, die auf dieser Einwilligung - beruhte, für die Zukunft nicht mehr fortführen dürfen und - </Typography> - </ListItem> - <ListItem> - <Typography> - gemäß <NoWrap>Art. 77 DS-GVO</NoWrap> sich bei der zuständigen - Aufsichtsbehörde zu beschweren. Die zuständige Aufsichtsbehörde - ist: Der Hessische Datenschutzbeauftragte, Postfach 3163, 65021 - Wiesbaden, Telefon: 0611/1408 - 0, - poststelle@datenschutz-hessen.de. - </Typography> - </ListItem> - </List> - </Section> - - <Section id={"right-to-refute"} title={"8. Widerspruchsrecht"}> - <Typography> - Sofern Ihre personenbezogenen Daten auf Grundlage von berechtigten - Interessen gemäß <NoWrap>Art. 6 Abs. 1 S. 1 lit. f DS-GVO</NoWrap>{" "} - verarbeitet werden, haben Sie das Recht, gemäß{" "} - <NoWrap>Art. 21 DS-GVO</NoWrap> Widerspruch gegen die Verarbeitung - Ihrer personenbezogenen Daten einzulegen, soweit dafür Gründe - vorliegen, die sich aus Ihrer besonderen Situation ergeben. Möchten - Sie von Ihrem Widerrufs- oder Widerspruchsrecht Gebrauch machen, - genügt eine E-Mail an - <NoWrap>datenschutz.gesundheitsamt@stadt-frankfurt.de</NoWrap> . - </Typography> - </Section> - - <Section id={"data-safety"} title={"9. Datensicherheit"}> - <Typography> - Wir bedienen uns geeigneter technischer und organisatorischer - Sicherheitsmaßnahmen, um Ihre Daten gegen zufällige oder vorsätzliche - Manipulationen, teilweisen oder vollständigen Verlust, Zerstörung oder - gegen den unbefugten Zugriff Dritter zu schützen. Unsere - Sicherheitsmaßnahmen werden nach dem jeweiligen Stand der Technik - gemäß <NoWrap>Art. 32 DS-GVO</NoWrap> fortlaufend angepasst. - </Typography> - </Section> - - <Section id={"request-proceeding"} title={"10. Auftragsverarbeitung"}> - <Typography> - Es findet keine Auftragsverarbeitung der erhobenen Daten statt. - </Typography> - </Section> - - <Section - id={"recency-and-updates"} - title={"11. Aktualität und Änderung dieser Datenschutzerklärung"} - > - <Typography> - Diese Datenschutzerklärung ist aktuell gültig und hat den Stand - September 2024. - </Typography> - </Section> - </StaticTextDocumentPanel> - ); -} diff --git a/employee-portal/src/lib/baseModule/components/procedureMetrics/ProcedureMetricsDisplay.tsx b/employee-portal/src/lib/baseModule/components/procedureMetrics/ProcedureMetricsDisplay.tsx index 13d359b56..80535ddb3 100644 --- a/employee-portal/src/lib/baseModule/components/procedureMetrics/ProcedureMetricsDisplay.tsx +++ b/employee-portal/src/lib/baseModule/components/procedureMetrics/ProcedureMetricsDisplay.tsx @@ -133,6 +133,7 @@ export function ProcedureMetricsDisplay() { ) : undefined } + focusColumnHeader="Typ" /> </TableSheet> </Stack> diff --git a/employee-portal/src/lib/baseModule/components/procedureMetrics/taskMetrics/TaskMetricsDisplay.tsx b/employee-portal/src/lib/baseModule/components/procedureMetrics/taskMetrics/TaskMetricsDisplay.tsx index 8fae0bda7..5fa2a3a3b 100644 --- a/employee-portal/src/lib/baseModule/components/procedureMetrics/taskMetrics/TaskMetricsDisplay.tsx +++ b/employee-portal/src/lib/baseModule/components/procedureMetrics/taskMetrics/TaskMetricsDisplay.tsx @@ -5,7 +5,12 @@ "use client"; -import { ApiProcedureType } from "@eshg/employee-portal-api/base"; +import { + ApiBusinessModule, + ApiProcedureStatus, + ApiProcedureType, + ApiProcedureWithDuration, +} from "@eshg/employee-portal-api/base"; import { CheckOutlined, HourglassEmptyOutlined, @@ -18,6 +23,7 @@ import { startTransition, useState } from "react"; import { useTaskMetricsQuery } from "@/lib/baseModule/api/queries/taskMetrics"; import { TimeRangeSelect } from "@/lib/baseModule/components/procedureMetrics/TimeRangeSelect"; import { lastXMonthsInDate } from "@/lib/baseModule/components/procedureMetrics/rangeSelectHelper"; +import { resolveProcedureDetailsRoute } from "@/lib/baseModule/moduleRegister/routeResolver"; import { FlashCard } from "@/lib/shared/components/cards/FlashCard"; import { DataTable } from "@/lib/shared/components/table/DataTable"; import { TableSheet } from "@/lib/shared/components/table/TableSheet"; @@ -96,42 +102,55 @@ export function TaskMetricsDisplay(props: { /> </TableSheet> - <TableSheet - title={ - <Stack marginBottom={1}> - <Typography level="h3" component="h2"> - Schnellste Vorgänge - </Typography> - </Stack> - } - > - <DataTable - data={taskMetrics.fastestProcedures} - columns={slowestAndFastestTasksColumns} - sorting={{ - manualSorting: false, - }} - /> - </TableSheet> - - <TableSheet - title={ - <Stack marginBottom={1}> - <Typography level="h3" component="h2"> - Langsamste Vorgänge - </Typography> - </Stack> - } - > - <DataTable - data={taskMetrics.slowestProcedures} - columns={slowestAndFastestTasksColumns} - sorting={{ - manualSorting: false, - }} - /> - </TableSheet> + <SlowestAndFastestTable + title="Schnellste Vorgänge" + data={taskMetrics.fastestProcedures} + businessModuleName={props.businessModuleName} + /> + <SlowestAndFastestTable + title="Langsamste Vorgänge" + data={taskMetrics.slowestProcedures} + businessModuleName={props.businessModuleName} + /> </Stack> </Stack> ); } + +function SlowestAndFastestTable({ + title, + data, + businessModuleName, +}: { + title: string; + data: ApiProcedureWithDuration[]; + businessModuleName: string; +}) { + return ( + <TableSheet + title={ + <Stack marginBottom={1}> + <Typography level="h3" component="h2"> + {title} + </Typography> + </Stack> + } + > + <DataTable + data={data} + columns={slowestAndFastestTasksColumns} + sorting={{ + manualSorting: false, + }} + rowNavRoute={(row) => + resolveProcedureDetailsRoute({ + businessModule: businessModuleName as ApiBusinessModule, + procedureId: row.original.id, + status: ApiProcedureStatus.Closed, + }) + } + focusColumnHeader="Erstellt am" + /> + </TableSheet> + ); +} diff --git a/employee-portal/src/lib/baseModule/components/procedureMetrics/taskMetrics/slowestAndFastestColumns.tsx b/employee-portal/src/lib/baseModule/components/procedureMetrics/taskMetrics/slowestAndFastestColumns.tsx index 719e2b755..d00e4cd86 100644 --- a/employee-portal/src/lib/baseModule/components/procedureMetrics/taskMetrics/slowestAndFastestColumns.tsx +++ b/employee-portal/src/lib/baseModule/components/procedureMetrics/taskMetrics/slowestAndFastestColumns.tsx @@ -12,6 +12,9 @@ import { formatOptionalDuration } from "./formatOptionalDuration"; const columnHelper = createColumnHelper<ApiProcedureWithDuration>(); const meta = { + canNavigate: { + parentRow: true, + }, width: "6rem", }; diff --git a/employee-portal/src/lib/baseModule/components/releaseNotes/ReleaseNotes.tsx b/employee-portal/src/lib/baseModule/components/releaseNotes/ReleaseNotes.tsx deleted file mode 100644 index dd1c0e82e..000000000 --- a/employee-portal/src/lib/baseModule/components/releaseNotes/ReleaseNotes.tsx +++ /dev/null @@ -1,263 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Divider, List, ListItem, Stack, Typography } from "@mui/joy"; - -import { StaticTextDocumentPanel } from "@/lib/baseModule/components/StaticTextDocumentPanel"; - -function BulletPointPlain(props: { text: string }) { - return ( - <ListItem> - <Typography component="span" level="body-md"> - {props.text} - </Typography> - </ListItem> - ); -} - -function Module(props: { moduleName: string }) { - return ( - <Typography level="h4" component="h3"> - {props.moduleName}: - </Typography> - ); -} - -export function ReleaseNotes() { - return ( - <StaticTextDocumentPanel> - <Stack gap={2}> - <Typography level="h2">GA-Lotse 1.0</Typography> - <Divider /> - <Typography level="title-md">26.09.2024</Typography> - <Typography level="body-md"> - Erster Release der neuen Anwendung GA-Lotse für Gesundheitsämter. - <br /> - <br /> - GA-Lotse ist ein Kooperationsprojekt des Gesundheitsamt Frankfurt am - Main mit dem Hessisches Ministerium für Familie, Senioren, Sport, - Gesundheit und Pflege. - <br /> - <br /> - Finanziert von der Europäischen Union – NextGenerationEU - </Typography> - </Stack> - <Stack> - <Module moduleName="Einschulungsuntersuchungen" /> - <List> - <ListItem nested> - <List marker="disc"> - <BulletPointPlain text="Unterstützung der Mitarbeitenden des Gesundheitsamtes bei Planung und Durchführung von Einschulungsuntersuchungen" /> - <ListItem nested> - <BulletPointPlain text="Erstellen von Vorgängen" /> - <List marker="circle"> - <BulletPointPlain text="Manuelles Anlegen von Vorgängen inklusive Kindern und Personensorgeberechtigten" /> - <BulletPointPlain text="Import von Bürgeramtslisten und Schullisten mithilfe einer Excel-Tabelle, Prüfung auf Duplikate und fehlerhafte Datensätze" /> - <BulletPointPlain text="Zuordnung der Untersuchungsart (Regeluntersuchung, Kann-Kind, Eingangsstufe, Besonderer Förderbedarf), Vorschläge anhand des Alters und Daten aus der Schulliste" /> - <BulletPointPlain text="Anlegen und Zuordnen von Kennungen zu Vorgängen" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Planung" /> - <List marker="circle"> - <BulletPointPlain text="Planen von Terminblöcken für die Schuleingangsuntersuchungen" /> - <BulletPointPlain text="Zuordnung von Artz:innen und MFA zu den Terminblöcken inklusive Verfügbarkeitsprüfung" /> - <BulletPointPlain text="Berücksichtigung unterschiedlicher Untersuchungslängen für Kinder mit potentiell erhöhtem Förderbedarf" /> - <BulletPointPlain text="Manuelle Terminvergabe durch die Mitarbeitenden anhand der zugeordneten Untersuchungsart" /> - <BulletPointPlain text="Automatische Massen-Terminvergabe über die Vorgangsübersicht anhand der zugeordneten Untersuchungsart" /> - <BulletPointPlain text="Erstellung von Einladungen mit QR-Code für den Zugang zum Bürgerportal" /> - <BulletPointPlain text="Terminverschiebung und Selbst-Anamnese im Bürgerportal durch Personensorgeberechtigte" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Untersuchungstag" /> - <List marker="circle"> - <BulletPointPlain text="Vervollständigung der von den Personensorgeberechtigten vorausgefüllte Anamnese" /> - <BulletPointPlain text="Erfassung des Impfstatus" /> - <BulletPointPlain text="Erfassung des Hörscreenings, Sehscreenings, der S1-SOPESS-2024-Untersuchung und S1-Befunds" /> - <BulletPointPlain text="Bei körperlichen Untersuchungen und Feststellungen von Handicaps werden mögliche Befunde mithilfe von ICD-10 Codes festgehalten" /> - <BulletPointPlain text="Gewicht, Größe und BMI des Kindes werden mit Referenzperzentilen bewertet" /> - <BulletPointPlain text="Übermittlung der ESU-Kennzahlen an das Statistikmodul" /> - </List> - </ListItem> - </List> - </ListItem> - </List> - </Stack> - <Stack> - <Module moduleName="Begehungen" /> - <List> - <ListItem nested> - <List marker="disc"> - <BulletPointPlain text="Unterstützung der Mitarbeitenden des Gesundheitsamtes bei der Hygieneüberwachung von Einrichtungen" /> - <ListItem nested> - <BulletPointPlain text="Erstellen von Vorgängen" /> - <List marker="circle"> - <BulletPointPlain text="Erfassung von Einrichtungen: Name, Objekt-Typ, Adressen, Kontaktmöglichkeiten" /> - <BulletPointPlain text="Manuelles Anlegen von Vorgängen für Einrichtungen" /> - <BulletPointPlain text="Automatische Websuche nach neuen Einrichtungen (Quelle OpenSteetMap) und Hinzufügen zur Zentralkartei" /> - <BulletPointPlain text="Anlegen von Vorgängen für neu gefundene Einrichtungen" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Planung" /> - <List marker="circle"> - <BulletPointPlain text="Planung des Begehungstermins" /> - <BulletPointPlain text="Auswahl der anzuwendenden Checklisten" /> - <BulletPointPlain text="Reservierung von Inventar über die Inventarverwaltung" /> - <BulletPointPlain text="Buchung von Ressourcen wie Fahrzeuge, Fahrräder, Räume" /> - <BulletPointPlain text="Aufruf eines Routenplaners" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Ausführung" /> - <List marker="circle"> - <BulletPointPlain text="Ausfüllen von Checklisten" /> - <BulletPointPlain text="Hochladen von Bildern" /> - <BulletPointPlain text="Erfassung weiterer Vorkommnisse" /> - <BulletPointPlain text="Offline-Modus: Ausführung auch ohne Internet-Verbindung möglich" /> - <BulletPointPlain text="Abschließen der Begehung, optional mit Erfassung der Unterschrift eines Teilnehmenden" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Erstellung eines Begehungsprotokolls" /> - <List marker="circle"> - <BulletPointPlain text="Automatische Erstellung eines Begehungsprotokolls mit den ausgefüllten Checklisten und Vorkommnissen" /> - <BulletPointPlain text="Möglichkeit zur Bearbeitung des Begehungsprotokolls" /> - <BulletPointPlain text="Erstellung eines PDF-Dokuments für das Begehungsprotokoll" /> - <BulletPointPlain text="Abschließen der Vorgangs mit Planung eines Nachfolgetermins" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Konfiguration" /> - <List marker="circle"> - <BulletPointPlain text="Einstellungen für Objekt-Typen, z.B. Wiederholungsintervalle, Standarddauer, Anfahrtszeiten" /> - <BulletPointPlain text="Definition von versionierbaren Checklisten" /> - <BulletPointPlain text="Austausch von Checklisten mit Landesamt und anderen Gesundheitsämtern über die zentralen Dienste" /> - </List> - </ListItem> - </List> - </ListItem> - </List> - </Stack> - <Stack> - <Module moduleName="Statistik" /> - <List> - <ListItem nested> - <List marker="disc"> - <BulletPointPlain text="Unterstützung der Gesundheitsberichterstattung durch Werkzeuge zum Erstellen statistischer Auswertungen und Diagramme sowie zur Bewertung und Verbesserung der Qualität von Vorgangsdaten" /> - <ListItem nested> - <BulletPointPlain text="Erstellung von Statistiken" /> - <List marker="circle"> - <BulletPointPlain text="Aggregation von Vorgangsdaten der Einschulungsuntersuchung" /> - <BulletPointPlain text="Auswahl der auszuwertenden Attribute" /> - <BulletPointPlain text="Festlegen eines Betrachtungszeitraums" /> - <BulletPointPlain text="Speichern und Anwenden von Vorlagen für die Erstellung von Statistiken" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Tabellenansicht" /> - <List marker="circle"> - <BulletPointPlain text="Darstellung aller Daten in Tabellenform" /> - <BulletPointPlain text="Filtern und sortieren der Tabelle" /> - <BulletPointPlain text="Erstellen und anwenden von Filtervorlagen" /> - <BulletPointPlain text="Verlinkung von Tabellenzeilen auf Vorgänge" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Erzeugen von Auswertungen und Diagrammen" /> - <List marker="circle"> - <BulletPointPlain text="Sechs verschiedene Diagrammtypen (Balken-, Kreis-, Streu-, Linien-, Kartendiagramm und Histogramm)" /> - <BulletPointPlain text="Jeweils verschiedene Konfigurationsoptionen für jeden Diagrammtyp" /> - <BulletPointPlain text="Erzeugen mehrerer Diagrammversionen mit individuellen Filterkonfigurationen" /> - <BulletPointPlain text="Auch hier: Erstellen und anwenden von Filtervorlagen" /> - <BulletPointPlain text="Export von Diagrammen als png/svg-Datei" /> - <BulletPointPlain text="Export von Diagrammdaten als xlsx-Datei" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Datenqualität" /> - <List marker="circle"> - <BulletPointPlain text="Übersicht über Vollständigkeit der Daten" /> - <BulletPointPlain text="Berücksichtigung expliziter Unbekannt-Werte (z.B. 'Weiß nicht')" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Geo Shape-Verwaltung" /> - <List marker="circle"> - <BulletPointPlain text="Importieren von Geo Shapes für Kartendiagramme aus geojson-Files" /> - <BulletPointPlain text="Löschen und archivieren (+ Archivierung wieder aufheben) von Geo Shapes" /> - </List> - </ListItem> - </List> - </ListItem> - </List> - </Stack> - <Stack> - <Module moduleName="Reisemedizinische Impfberatung" /> - <List> - <ListItem nested> - <List marker="disc"> - <BulletPointPlain text="Unterstützung bei Impfstoffverwaltung, Terminplanung und Impfdokumentation" /> - <ListItem nested> - <BulletPointPlain text="Impfstoffverwaltung" /> - <List marker="circle"> - <BulletPointPlain text="Krankheitenkategorien" /> - <BulletPointPlain text="Impfstoffe mit Berücksichtigung von Mindestabständen" /> - <BulletPointPlain text="Bestandsaktualisierung" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Terminplanung" /> - <List marker="circle"> - <BulletPointPlain text="Terminkontingente pro Terminart" /> - <BulletPointPlain text="Personalberücksichtigung" /> - <BulletPointPlain text="Konfigurierbare Terminstandarddauer" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Impfdokumentation" /> - <List marker="circle"> - <BulletPointPlain text="Planung aller Leistungen für eine anstehende Reise eines Patienten" /> - <BulletPointPlain text="Aufteilung der Leistungen in Folgetermine" /> - <BulletPointPlain text="Dokumentation der Durchführung mit Verlaufseinträgen" /> - <BulletPointPlain text="Generieren von Bescheinigungen für die Krankenkasse" /> - </List> - </ListItem> - </List> - </ListItem> - </List> - </Stack> - <Stack> - <Module moduleName="Masernschutz" /> - <List> - <ListItem nested> - <List marker="disc"> - <BulletPointPlain text="Unterstützung der Mitarbeitenden des Gesundheitsamtes bei der Bearbeitung von Meldungen zu fehlendem Masern-Impfschutz in Einrichtungen" /> - <ListItem nested> - <BulletPointPlain text="Erstellen und Bearbeiten von Vorgängen im Mitarbeitenden-Portal" /> - <List marker="circle"> - <BulletPointPlain text="Manuelles Anlegen eines Vorgangs mit zentral verwalteten Personen und Einrichtungen" /> - <BulletPointPlain text="Erstellen von Nachweisaufforderungen" /> - <BulletPointPlain text="Erstellen von Terminblöcken für Nachweistermine" /> - <BulletPointPlain text="Manuelles Buchen, Bearbeiten und Stornieren von Terminen" /> - <BulletPointPlain text="Dokumentation von Betretungsverboten" /> - <BulletPointPlain text="Dokumentation von Bußgeldern" /> - <BulletPointPlain text="Vollständige Dokumentation des Vorgangsverlaufs" /> - </List> - </ListItem> - <ListItem nested> - <BulletPointPlain text="Prozesse im Unternehmensportal" /> - <List marker="circle"> - <BulletPointPlain text="Vorgangsmeldung durch Einrichtungen" /> - </List> - </ListItem> - </List> - </ListItem> - </List> - </Stack> - </StaticTextDocumentPanel> - ); -} diff --git a/employee-portal/src/lib/businessModules/chat/components/ChatConsentModal.tsx b/employee-portal/src/lib/businessModules/chat/components/ChatConsentModal.tsx index 5b73fe36b..2d9bd1cc1 100644 --- a/employee-portal/src/lib/businessModules/chat/components/ChatConsentModal.tsx +++ b/employee-portal/src/lib/businessModules/chat/components/ChatConsentModal.tsx @@ -10,7 +10,11 @@ import { clearCachedCredentials } from "@/lib/businessModules/chat/matrix/tokens import { useUserSettings } from "@/lib/businessModules/chat/shared/hooks/useUserSettings"; import { BaseModal, BaseModalProps } from "@/lib/shared/components/BaseModal"; -export function ChatConsentModal(props: Omit<BaseModalProps, "children">) { +type ChatConsentModalProps = Omit<BaseModalProps, "children"> & { + chatUsageEnabled: boolean; +}; + +export function ChatConsentModal(props: ChatConsentModalProps) { const { updateChatUserConsents } = useUserSettings(); async function handleAcceptClick() { @@ -30,12 +34,20 @@ export function ChatConsentModal(props: Omit<BaseModalProps, "children">) { props.onClose(); } + function handleCloseClick() { + updateChatUserConsents({ + isChatConsentAsked: true, + isChatUsageEnabled: props.chatUsageEnabled ?? false, + }); + props.onClose(); + } + return ( <BaseModal modalTitle="Hier koennten Ihre Nutzungsbedingungen stehen." key="chat-consent-modal" {...props} - onClose={handleRejectClick} + onClose={handleCloseClick} data-testid="chat-consent-modal" > <Stack direction="column" alignItems="center" spacing={2} marginTop={2}> diff --git a/employee-portal/src/lib/businessModules/chat/components/ChatFeatureUnavailable.tsx b/employee-portal/src/lib/businessModules/chat/components/ChatFeatureUnavailable.tsx index 55d58dfc4..b75ec1bbb 100644 --- a/employee-portal/src/lib/businessModules/chat/components/ChatFeatureUnavailable.tsx +++ b/employee-portal/src/lib/businessModules/chat/components/ChatFeatureUnavailable.tsx @@ -3,23 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { WarningAmberOutlined } from "@mui/icons-material"; -import { Alert, Box, Typography } from "@mui/joy"; +import { notFound } from "next/navigation"; +import { useEffect } from "react"; export function ChatFeatureUnavailable() { - return ( - <Alert - variant="outlined" - color="danger" - invertedColors - sx={{ alignItems: "flex-start" }} - startDecorator={<WarningAmberOutlined fontSize="xl2" />} - > - <Box> - <Typography color="danger"> - Der Chat-Dienst ist nicht verfügbar. - </Typography> - </Box> - </Alert> - ); + useEffect(() => { + notFound(); + }, []); + + return null; } diff --git a/employee-portal/src/lib/businessModules/chat/components/ChatNoAccessAlert.tsx b/employee-portal/src/lib/businessModules/chat/components/ChatNoAccessAlert.tsx index 1ba0367b3..0bb1d30be 100644 --- a/employee-portal/src/lib/businessModules/chat/components/ChatNoAccessAlert.tsx +++ b/employee-portal/src/lib/businessModules/chat/components/ChatNoAccessAlert.tsx @@ -24,7 +24,7 @@ export function ChatNoAccessAlert({ withButton = true, }: ChatNoAccessProps) { const { - userSettings: { chatConsentAsked }, + userSettings: { chatConsentAsked, chatUsageEnabled }, } = useChat(); return ( @@ -59,7 +59,12 @@ export function ChatNoAccessAlert({ size="sm" color={color} sx={{ textTransform: "uppercase" }} - renderModal={(props) => <ChatConsentModal {...props} />} + renderModal={(props) => ( + <ChatConsentModal + {...props} + chatUsageEnabled={chatUsageEnabled} + /> + )} // If consent to use the chat has never been requested, the initial modal value should be true initialModalValue={chatConsentAsked === false} > diff --git a/employee-portal/src/lib/businessModules/chat/components/messageTeaser/MessageTeaser.tsx b/employee-portal/src/lib/businessModules/chat/components/messageTeaser/MessageTeaser.tsx new file mode 100644 index 000000000..a1fd7b384 --- /dev/null +++ b/employee-portal/src/lib/businessModules/chat/components/messageTeaser/MessageTeaser.tsx @@ -0,0 +1,173 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { InternalLinkButton } from "@eshg/lib-portal/components/navigation/InternalLinkButton"; +import CloseIcon from "@mui/icons-material/Close"; +import { Box, Button, IconButton, Snackbar, Stack, Typography } from "@mui/joy"; +import { usePathname } from "next/navigation"; +import { useEffect } from "react"; + +import { ChatSnackbarValues } from "@/lib/businessModules/chat/components/messageTeaser/MessageTeaserProvider"; +import { useChat } from "@/lib/businessModules/chat/shared/ChatProvider"; +import { routes } from "@/lib/businessModules/chat/shared/routes"; +import { Presence } from "@/lib/businessModules/chat/shared/types"; +import { getStatusColor } from "@/lib/businessModules/chat/shared/utils"; + +export interface BaseSnackbarProps { + snackbar: ChatSnackbarValues | undefined; + onClose: () => void; +} + +export function MessageTeaser({ + snackbar, + onClose, +}: Readonly<BaseSnackbarProps>) { + const pathname = usePathname(); + const { userSettings, messagesSidebar } = useChat(); + + const isInfoType = snackbar?.type === "info"; + const link = snackbar?.link ?? routes.index; + + useEffect(() => { + if (pathname === routes.index || messagesSidebar.isOpen) { + onClose(); + } + }, [onClose, pathname, messagesSidebar.isOpen]); + + function toggleMessagesSidebar(): void { + if (messagesSidebar.isOpen) { + messagesSidebar.close(); + } else { + messagesSidebar.open(); + } + } + + return ( + <Snackbar + open={!!snackbar} + variant="outlined" + size="md" + anchorOrigin={{ vertical: "top", horizontal: "right" }} + autoHideDuration={5000} + key={snackbar?.key} + onClose={(_event, reason) => { + if (reason !== "clickaway") { + onClose(); + } + }} + slotProps={{ + root: { + sx: { + borderRadius: "md", + backgroundColor: "common.white", + top: { + xxs: "4rem", + sm: "5rem", + }, + }, + }, + }} + > + {snackbar && ( + <Stack sx={{ maxWidth: "21.5rem", maxHeight: "11.375rem" }}> + <Stack direction="row"> + <Stack + direction="row" + sx={{ + width: "100%", + boxSizing: "content-box", + alignItems: "center", + }} + > + {userSettings.sharePresence && snackbar.userPresence && ( + <Box + sx={{ + width: "0.625rem", + height: "0.625rem", + borderRadius: "100%", + backgroundColor: getStatusColor( + snackbar.userPresence as Presence, + ), + marginRight: 0.8, + }} + ></Box> + )} + <Typography + level="title-md" + fontWeight={600} + noWrap + sx={{ + fontWeight: "bold", + maxWidth: "15rem", + textOverflow: "ellipsis", + }} + > + {snackbar.title} + </Typography> + </Stack> + <IconButton + aria-label="Schließen" + onClick={onClose} + color="primary" + > + <CloseIcon /> + </IconButton> + </Stack> + <Typography + level="body-md" + maxWidth="18rem" + textColor="common.black" + noWrap={true} + sx={{ + display: "-webkit-box", + overflow: "hidden", + WebkitBoxOrient: "vertical", + WebkitLineClamp: 3, + whiteSpace: "normal", + }} + > + {snackbar.text} + </Typography> + <Stack + spacing={2} + display="flex" + flexDirection="row" + marginTop="1rem" + justifyContent="space-between" + sx={{ width: "100%" }} + > + <InternalLinkButton + href={link} + variant="outlined" + size="sm" + sx={{ + borderRadius: "sm", + }} + > + Zum Chatbereich + </InternalLinkButton> + {!isInfoType && ( + <Button + variant="soft" + size="sm" + color="primary" + sx={{ + border: "1px", + borderRadius: "sm", + }} + onClick={() => { + toggleMessagesSidebar(); + onClose(); + }} + > + Antworten + </Button> + )} + </Stack> + </Stack> + )} + </Snackbar> + ); +} diff --git a/employee-portal/src/lib/businessModules/chat/components/messageTeaser/MessageTeaserProvider.tsx b/employee-portal/src/lib/businessModules/chat/components/messageTeaser/MessageTeaserProvider.tsx new file mode 100644 index 000000000..e5cb9e56f --- /dev/null +++ b/employee-portal/src/lib/businessModules/chat/components/messageTeaser/MessageTeaserProvider.tsx @@ -0,0 +1,80 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +"use client"; + +import { + Dispatch, + ReactNode, + SetStateAction, + createContext, + useCallback, + useContext, + useMemo, + useState, +} from "react"; +import { v4 as uuidv4 } from "uuid"; + +import { MessageTeaser } from "@/lib/businessModules/chat/components/messageTeaser/MessageTeaser"; + +type ChatSnackbarType = "message" | "info"; + +export interface ChatSnackbarValues { + title: string; + text: string; + link?: string; + userPresence?: string; + key: string; + type: ChatSnackbarType; +} + +type SnackbarValuesWithoutKey = Omit<ChatSnackbarValues, "key" | "type"> & { + type?: ChatSnackbarType; +}; + +const SnackbarContext = createContext<{ + snackbarValues: ChatSnackbarValues | undefined; + setSnackbar: Dispatch<SetStateAction<ChatSnackbarValues | undefined>>; +}>(null!); + +export function MessageTeaserProvider({ + children, +}: Readonly<{ children: ReactNode }>) { + const [snackbarValues, setSnackbar] = useState< + ChatSnackbarValues | undefined + >(); + const contextValues = useMemo( + () => ({ snackbarValues, setSnackbar }), + [snackbarValues], + ); + return ( + <SnackbarContext.Provider value={contextValues}> + <MessageTeaser + snackbar={snackbarValues} + onClose={() => setSnackbar(undefined)} + /> + {children} + </SnackbarContext.Provider> + ); +} + +export function useMessageTeaser() { + const context = useContext(SnackbarContext); + if (context === null) { + throw new Error("useSnackbar was called outside SnackbarProvider"); + } + const { setSnackbar } = context; + + return useCallback( + (values: SnackbarValuesWithoutKey | undefined) => { + setSnackbar( + values + ? { ...values, key: uuidv4(), type: values.type ?? "message" } + : undefined, + ); + }, + [setSnackbar], + ); +} diff --git a/employee-portal/src/lib/businessModules/chat/components/roomList/ReceiptStatus.tsx b/employee-portal/src/lib/businessModules/chat/components/roomList/ReceiptStatus.tsx index 6c4152b84..520d8394d 100644 --- a/employee-portal/src/lib/businessModules/chat/components/roomList/ReceiptStatus.tsx +++ b/employee-portal/src/lib/businessModules/chat/components/roomList/ReceiptStatus.tsx @@ -3,15 +3,26 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import DoneIcon from "@mui/icons-material/Done"; import { Box, useTheme } from "@mui/joy"; +import { ReadingReceipt } from "@/lib/businessModules/chat/components/chatPanel/ReadingReceipt"; +import { useChat } from "@/lib/businessModules/chat/shared/ChatProvider"; + interface ReceiptStatusProps { unreadNotifications?: number; + isRead?: boolean; + isMessageMine?: boolean; } -export function ReceiptStatus({ unreadNotifications }: ReceiptStatusProps) { +export function ReceiptStatus({ + unreadNotifications, + isRead, + isMessageMine, +}: ReceiptStatusProps) { const theme = useTheme(); + const { + userSettings: { showReadConfirmation }, + } = useChat(); if (!!unreadNotifications) { return ( @@ -34,10 +45,12 @@ export function ReceiptStatus({ unreadNotifications }: ReceiptStatusProps) { ); } - return ( - <DoneIcon - color="primary" - sx={{ color: theme.palette.neutral.outlinedDisabledColor }} - /> - ); + if (!isMessageMine) + return ( + <ReadingReceipt + isReadReceiptEnabled={showReadConfirmation} + isRead={isRead} + /> + ); + else return null; } diff --git a/employee-portal/src/lib/businessModules/chat/components/roomList/RoomListItem.tsx b/employee-portal/src/lib/businessModules/chat/components/roomList/RoomListItem.tsx index 200da818c..6d5c92778 100644 --- a/employee-portal/src/lib/businessModules/chat/components/roomList/RoomListItem.tsx +++ b/employee-portal/src/lib/businessModules/chat/components/roomList/RoomListItem.tsx @@ -7,12 +7,14 @@ import NotificationsOffOutlinedIcon from "@mui/icons-material/NotificationsOffOu import { Box, Stack, Typography, useTheme } from "@mui/joy"; import { Room } from "matrix-js-sdk"; import { useMemo } from "react"; +import { isEmpty } from "remeda"; import { ChatAvatar } from "@/lib/businessModules/chat/components/ChatAvatar"; import { HighlightedText } from "@/lib/businessModules/chat/components/roomList/HighlightedText"; import { ReceiptStatus } from "@/lib/businessModules/chat/components/roomList/ReceiptStatus"; import { useChatClientContext } from "@/lib/businessModules/chat/shared/ChatClientProvider"; import { CommunicationType } from "@/lib/businessModules/chat/shared/enums"; +import { useReadConfirmation } from "@/lib/businessModules/chat/shared/hooks/useReadConfirmation"; import { Message } from "@/lib/businessModules/chat/shared/types"; import { formatChatDate, @@ -39,6 +41,7 @@ export function RoomListItem({ const parsedDate = formatChatDate(latestMessage?.timestamp); const unreadNotifications = unreadNotificationsPerRoom[room.roomId]; + const { messageReadsPerRoom } = useReadConfirmation(true); // TO DO - finish notification feature const disableNotifications = false; @@ -56,6 +59,16 @@ export function RoomListItem({ [dmMember, matrixClient, room], ); + const isLatestMessageRead = messageReadsPerRoom[room.roomId]?.some( + (id) => id === latestMessage?.id, + ); + + const latestMessageRead = + latestMessage?.readReceipts && !isEmpty(latestMessage?.readReceipts); + + const isMessageMine = + latestMessage?.sender?.userId !== matrixClient.getUserId(); + return ( <Stack direction="row" @@ -113,7 +126,11 @@ export function RoomListItem({ placeItems: "center end", }} > - <ReceiptStatus unreadNotifications={unreadNotifications} /> + <ReceiptStatus + unreadNotifications={unreadNotifications} + isRead={isLatestMessageRead ?? latestMessageRead} + isMessageMine={isMessageMine} + /> </Box> </Stack> </Stack> diff --git a/employee-portal/src/lib/businessModules/chat/components/secureBackup/SSOAuthModal.tsx b/employee-portal/src/lib/businessModules/chat/components/secureBackup/SSOAuthModal.tsx index 40825644d..17f0db164 100644 --- a/employee-portal/src/lib/businessModules/chat/components/secureBackup/SSOAuthModal.tsx +++ b/employee-portal/src/lib/businessModules/chat/components/secureBackup/SSOAuthModal.tsx @@ -75,11 +75,6 @@ export function SSOAuthModal({ values }: SSOAuthModalProps) { .catch((e) => { logger.error("AttemptAuth Error", e); }); - // .finally(() => { - // void authLogic.submitAuthDict({}).then((response) => { - // logger.debug("submitAuthDict", response); - // }); - // }); } }, [authLogic, values]); @@ -106,14 +101,15 @@ export function SSOAuthModal({ values }: SSOAuthModalProps) { return ( <BaseModal - modalTitle="Use Single Sign On to continue" + modalTitle="Nutzen Sie Single Sign On um fortzufahren" key="sso-auth-modal" onClose={handleCancel} open={!!values} > <> <Typography textColor="text.secondary"> - To continue, use Single Sign On to prove your identity. + Um fortzufahren und Ihre Identität zu bestätigen, nutzen Sie Single + Sign On. </Typography> <Stack direction="row" @@ -127,7 +123,7 @@ export function SSOAuthModal({ values }: SSOAuthModalProps) { onClick={handleCancel} data-testid="ssoAuthDialogCancel" > - Cancel + Abbrechen </Button> <Button size="sm" @@ -136,7 +132,7 @@ export function SSOAuthModal({ values }: SSOAuthModalProps) { onClick={handleSSOClick} data-testid="ssoAuthDialogStart" > - Single Sign On + Fortfahren </Button> </Stack> </> diff --git a/employee-portal/src/lib/businessModules/chat/matrix/crypto.ts b/employee-portal/src/lib/businessModules/chat/matrix/crypto.ts index 30063933f..3009d1f4d 100644 --- a/employee-portal/src/lib/businessModules/chat/matrix/crypto.ts +++ b/employee-portal/src/lib/businessModules/chat/matrix/crypto.ts @@ -93,13 +93,11 @@ export async function getCrossSigningStatus(matrixClient: MatrixClient) { }; } -export async function isDeviceVerified(client: MatrixClient, deviceId: string) { +export async function isDeviceVerified(client: MatrixClient) { + const deviceId = client.getDeviceId(); const trustLevel = await client .getCrypto() - ?.getDeviceVerificationStatus(client.getSafeUserId(), deviceId); - if (!trustLevel) { - // either no crypto, or an unknown/no-e2e device - return null; - } - return trustLevel.crossSigningVerified; + ?.getDeviceVerificationStatus(client.getSafeUserId(), deviceId ?? ""); + + return trustLevel?.crossSigningVerified ?? null; } diff --git a/employee-portal/src/lib/businessModules/chat/shared/ChatClientProvider.tsx b/employee-portal/src/lib/businessModules/chat/shared/ChatClientProvider.tsx index 35362c96b..ab8553b04 100644 --- a/employee-portal/src/lib/businessModules/chat/shared/ChatClientProvider.tsx +++ b/employee-portal/src/lib/businessModules/chat/shared/ChatClientProvider.tsx @@ -26,6 +26,7 @@ import { } from "react"; import { isNullish, omit } from "remeda"; +import { useMessageTeaser } from "@/lib/businessModules/chat/components/messageTeaser/MessageTeaserProvider"; import { useChat } from "@/lib/businessModules/chat/shared/ChatProvider"; import { ClientState } from "@/lib/businessModules/chat/shared/enums"; import { useChatLifecycle } from "@/lib/businessModules/chat/shared/hooks/useChatLifecycle"; @@ -37,7 +38,6 @@ import { isMessageTypeWithBody, } from "@/lib/businessModules/chat/shared/types"; import { shouldShowMessageTeaser } from "@/lib/businessModules/chat/shared/utils"; -import { useMessageTeaser } from "@/lib/shared/components/chat/MessageTeaserProvider"; export interface ChatClientContextType { matrixClient: MatrixClient; @@ -170,7 +170,7 @@ export function ChatClientProvider({ children }: Readonly<RequiresChildren>) { sender?.displayName ) { showMessageTeaser({ - username: room.name, + title: room.name, text: guestCount > 1 ? `${sender.displayName}: ${messageContent.body}` @@ -198,6 +198,22 @@ export function ChatClientProvider({ children }: Readonly<RequiresChildren>) { }; }, [clientState, showMessageTeaser]); + useEffect(() => { + if ( + clientState === ClientState.CreateBackupKey || + clientState === ClientState.RestoreBackupKey + ) { + showMessageTeaser({ + title: "Chat", + text: + clientState === ClientState.CreateBackupKey + ? "Richten Sie ein Sicherheitsbackup ein um die Chatfunktion zu nutzen" + : "Bestätigen sie dieses Endgerät um die Chatfunktion zu nutzen", + type: "info", + }); + } + }, [clientState, showMessageTeaser]); + const contextValues = useMemo<ChatClientContextType>( () => ({ clientState, diff --git a/employee-portal/src/lib/businessModules/chat/shared/ChatProvider.tsx b/employee-portal/src/lib/businessModules/chat/shared/ChatProvider.tsx index ed410b237..8f2aa906f 100644 --- a/employee-portal/src/lib/businessModules/chat/shared/ChatProvider.tsx +++ b/employee-portal/src/lib/businessModules/chat/shared/ChatProvider.tsx @@ -15,9 +15,9 @@ import { useGetSelfUser } from "@/lib/baseModule/api/queries/users"; import { useMessagesSidebar } from "@/lib/baseModule/components/layout/messagesSidebar/MessagesSidebar"; import { useIsNewFeatureEnabledUnsuspended } from "@/lib/businessModules/chat/api/queries/featureTogglesApi"; import { useGetUserSettings } from "@/lib/businessModules/chat/api/queries/userSettingsApi"; +import { MessageTeaserProvider } from "@/lib/businessModules/chat/components/messageTeaser/MessageTeaserProvider"; import { ChatClientProvider } from "@/lib/businessModules/chat/shared/ChatClientProvider"; import { ChatConfiguration } from "@/lib/businessModules/chat/shared/config"; -import { MessageTeaserProvider } from "@/lib/shared/components/chat/MessageTeaserProvider"; import { useHasUserRoleCheck } from "@/lib/shared/hooks/useAccessControl"; import { useIsOffline } from "@/lib/shared/hooks/useIsOffline"; @@ -55,8 +55,7 @@ function InnerChatProvider({ children, configuration }: ChatProviderProps) { const { data: selfUser } = useGetSelfUser(); const canAccessChat = useHasUserRoleCheck(ApiUserRole.ChatManagementWrite) && - !!featureToggleChatEnabled && - selfUser.username !== "dummy"; //TODO: Don't allow dummy user to use chat until it becomes more stable in demo environment + !!featureToggleChatEnabled; const messagesSidebar = useMessagesSidebar(); const { data: userSettingsData, isLoading } = useGetUserSettings( selfUser.userId, diff --git a/employee-portal/src/lib/businessModules/chat/shared/hooks/useChatLifecycle.tsx b/employee-portal/src/lib/businessModules/chat/shared/hooks/useChatLifecycle.tsx index 4ee917078..c622e5eba 100644 --- a/employee-portal/src/lib/businessModules/chat/shared/hooks/useChatLifecycle.tsx +++ b/employee-portal/src/lib/businessModules/chat/shared/hooks/useChatLifecycle.tsx @@ -15,11 +15,12 @@ import { useState, } from "react"; -import { useUpdateSelfUser } from "@/lib/baseModule/api/mutations/users"; +import { useUpdateSelfUserChatUsername } from "@/lib/baseModule/api/mutations/users"; import { useGetSelfUser } from "@/lib/baseModule/api/queries/users"; import { fetchBackupInfo, getRustCryptoStoreArgs, + isDeviceVerified, } from "@/lib/businessModules/chat/matrix/crypto"; import { cacheSecretStorageKey, @@ -44,7 +45,7 @@ export function useChatLifecycle( setClientState: Dispatch<SetStateAction<ClientState>>, ) { const { data: selfUser } = useGetSelfUser(); - const updateSelfUser = useUpdateSelfUser(); + const updateSelfUser = useUpdateSelfUserChatUsername(); const { configuration } = useChat(); @@ -162,7 +163,9 @@ export function useChatLifecycle( res.backupInfo, ); - if (!restored) { + const isVerified = await isDeviceVerified(matrixClient.current); + + if (!restored || !isVerified) { setClientState(ClientState.RestoreBackupKey); } else { setClientState(ClientState.Prepared); diff --git a/employee-portal/src/lib/businessModules/chat/shared/hooks/useMatrixClient.tsx b/employee-portal/src/lib/businessModules/chat/shared/hooks/useMatrixClient.tsx index 51dc0a95e..903080ad7 100644 --- a/employee-portal/src/lib/businessModules/chat/shared/hooks/useMatrixClient.tsx +++ b/employee-portal/src/lib/businessModules/chat/shared/hooks/useMatrixClient.tsx @@ -15,5 +15,10 @@ export function useMatrixClient() { const isChatEnabled = canAccessChat && userSettings.chatUsageEnabled && chatContext?.matrixClient; - return isChatEnabled ? chatContext.matrixClient : null; + return isChatEnabled + ? { + client: chatContext.matrixClient, + state: chatContext.clientState, + } + : null; } diff --git a/employee-portal/src/lib/businessModules/inspection/api/mutations/inventory.ts b/employee-portal/src/lib/businessModules/inspection/api/mutations/inventory.ts index f9e18c1d0..01d693864 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/mutations/inventory.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/mutations/inventory.ts @@ -8,19 +8,20 @@ import { ModifyInventoryRequest } from "@eshg/employee-portal-api/inspection"; import { unwrapRawResponse } from "@eshg/lib-portal/api/unwrapRawResponse"; import { useHandledMutation } from "@eshg/lib-portal/api/useHandledMutation"; import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useAlert } from "@eshg/lib-portal/errorHandling/AlertContext"; import { resolveError } from "@eshg/lib-portal/errorHandling/errorResolvers"; import { useInspectionApi } from "@/lib/businessModules/inspection/api/clients"; export function useModifyInventory() { const inspectionApi = useInspectionApi(); - const alertContext = useAlertContext(); + const alert = useAlert(); const snackbar = useSnackbar(); return useHandledMutation({ mutationFn: (req: ModifyInventoryRequest) => inspectionApi.modifyInventoryRaw(req).then(unwrapRawResponse), onSuccess: () => { + alert.close(); snackbar.confirmation("Änderung gespeichert."); }, onError: (error) => { @@ -30,12 +31,7 @@ export function useModifyInventory() { return; } - if (alertContext === null) { - throw new Error("No alert context available."); - } - - alertContext.setAlert({ - color: "danger", + alert.error({ title: "Inventar nicht verfügbar", message: "Das gewählte Inventar ist in der gewünschten Anzahl nicht mehr verfügbar.", diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/checklist.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/checklist.ts index b4ee2ba7b..a68765f9b 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/checklist.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/checklist.ts @@ -3,20 +3,21 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { useSuspenseQuery } from "@tanstack/react-query"; +import { ChecklistApi } from "@eshg/employee-portal-api/inspection"; +import { queryOptions } from "@tanstack/react-query"; -import { useChecklistApi } from "@/lib/businessModules/inspection/api/clients"; import { checklistApiQueryKey } from "@/lib/businessModules/inspection/api/queries/apiQueryKeys"; -import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; export function getChecklistsQueryKey(inspectionId: string) { return checklistApiQueryKey(["getChecklists", { inspectionId }]); } -export function useGetChecklists(inspectionId: string) { - const checklistApi = useChecklistApi(); - const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); - return useSuspenseQuery({ +export function getChecklistsQuery( + checklistApi: ChecklistApi, + getPreCacheForOfflineModeHeaders: (inspectionId?: string) => RequestInit, + inspectionId: string, +) { + return queryOptions({ queryKey: getChecklistsQueryKey(inspectionId), queryFn: ({ signal }) => { return checklistApi.getChecklists(inspectionId, { diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/checklistDefinition.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/checklistDefinition.ts index 22edceb95..3fc13118b 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/checklistDefinition.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/checklistDefinition.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { useSuspenseQuery } from "@tanstack/react-query"; +import { + ChecklistDefinitionApi, + ChecklistDefinitionCentralRepoApi, +} from "@eshg/employee-portal-api/inspection"; +import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { useChecklistDefinitionApi, @@ -23,9 +27,11 @@ export function useGetChecklistDefinitions() { }); } -export function useGetChecklistDefinitionVersion(versionId: string) { - const checklistDefinitionApi = useChecklistDefinitionApi(); - return useSuspenseQuery({ +export function getChecklistDefinitionVersionQuery( + checklistDefinitionApi: ChecklistDefinitionApi, + versionId: string, +) { + return queryOptions({ queryKey: checklistDefinitionApiQueryKey([ "getChecklistDefinitionVersion", { versionId }, @@ -47,13 +53,13 @@ export function useGetChecklistDefinitionVersions(defId: string) { }); } -export function useGetChecklistDefinitionFromCentralRepo( +export function getChecklistDefinitionFromCentralRepoQuery( + repoApi: ChecklistDefinitionCentralRepoApi, repositoryID: number, repositoryVersion: number, isCoreChecklist: boolean, ) { - const repoApi = useChecklistDefinitionCentralRepoApi(); - return useSuspenseQuery({ + return queryOptions({ queryKey: checklistDefinitionCentralRepoApiQueryKey([ "getChecklistDefinitionFromCentralRepo", { repositoryID, repositoryVersion, isCoreChecklist }, diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/department.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/department.ts index 4441d2421..38a53fa73 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/department.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/department.ts @@ -7,7 +7,6 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { useDepartmentApi } from "@/lib/baseModule/api/clients"; import { departmentApiQueryKey } from "@/lib/businessModules/inspection/api/queries/apiQueryKeys"; -import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; export function getDepartmentQueryKey() { return departmentApiQueryKey(["getDepartment"]); @@ -15,10 +14,8 @@ export function getDepartmentQueryKey() { export function useGetDepartment() { const departmentApi = useDepartmentApi(); - const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); return useSuspenseQuery({ queryKey: getDepartmentQueryKey(), - queryFn: () => - departmentApi.getDepartmentInfo(getPreCacheForOfflineModeHeaders()), + queryFn: () => departmentApi.getDepartmentInfo(), }); } diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/facility.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/facility.ts index 212cbe236..aa68e7cfa 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/facility.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/facility.ts @@ -3,9 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { GetPendingFacilitiesRequest } from "@eshg/employee-portal-api/inspection"; +import { + FacilityApi, + GetPendingFacilitiesRequest, +} from "@eshg/employee-portal-api/inspection"; import { unwrapRawResponse } from "@eshg/lib-portal/api/unwrapRawResponse"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { useFacilityApi } from "@/lib/businessModules/inspection/api/clients"; import { facilityApiQueryKey } from "@/lib/businessModules/inspection/api/queries/apiQueryKeys"; @@ -31,6 +34,19 @@ export function useGetPendingFacilities(filters: PendingFacilitiesFilters) { }); } +export function getPendingFacilitiesQuery( + filters: PendingFacilitiesFilters, + facilityApi: FacilityApi, +) { + const req = facilitiesFiltersToApi(filters); + + return queryOptions({ + queryKey: facilityApiQueryKey(["getPendingFacilities", { req }]), + queryFn: () => + facilityApi.getPendingFacilitiesRaw(req).then(unwrapRawResponse), + }); +} + function facilitiesFiltersToApi( filters: PendingFacilitiesFilters, ): GetPendingFacilitiesRequest { diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/incidents.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/incidents.ts index 43b976636..987fb224d 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/incidents.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/incidents.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { useSuspenseQuery } from "@tanstack/react-query"; +import { InspectionIncidentApi } from "@eshg/employee-portal-api/inspection"; +import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { useIncidentApi } from "@/lib/businessModules/inspection/api/clients"; import { incidentsApiQueryKey } from "@/lib/businessModules/inspection/api/queries/apiQueryKeys"; @@ -12,7 +13,21 @@ import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection export function useGetIncidents(inspectionId: string) { const incidentApi = useIncidentApi(); const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); - return useSuspenseQuery({ + return useSuspenseQuery( + getIncidentsQuery( + incidentApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + ); +} + +export function getIncidentsQuery( + incidentApi: InspectionIncidentApi, + getPreCacheForOfflineModeHeaders: (inspectionId?: string) => RequestInit, + inspectionId: string, +) { + return queryOptions({ queryKey: incidentsApiQueryKey(["getIncidents", { inspectionId }]), queryFn: () => incidentApi.getIncidents( diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/inspection.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/inspection.ts index 343f27746..865f400f8 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/inspection.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/inspection.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { useSuspenseQuery } from "@tanstack/react-query"; +import { InspectionApi } from "@eshg/employee-portal-api/inspection"; +import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { useInspectionApi } from "@/lib/businessModules/inspection/api/clients"; import { inspectionApiQueryKey } from "@/lib/businessModules/inspection/api/queries/apiQueryKeys"; @@ -37,7 +38,21 @@ export function getAvailablePLDRsQueryKey(inspectionId: string) { export function useGetInspection(procedureId: string) { const inspectionApi = useInspectionApi(); const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); - return useSuspenseQuery({ + return useSuspenseQuery( + getInspectionQuery( + inspectionApi, + getPreCacheForOfflineModeHeaders, + procedureId, + ), + ); +} + +export function getInspectionQuery( + inspectionApi: InspectionApi, + getPreCacheForOfflineModeHeaders: (inspectionId?: string) => RequestInit, + procedureId: string, +) { + return queryOptions({ queryKey: getInspectionQueryKey(procedureId), queryFn: () => inspectionApi.getInspection( @@ -50,7 +65,21 @@ export function useGetInspection(procedureId: string) { export function useGetAvailableCLDVs(inspectionId: string) { const inspectionApi = useInspectionApi(); const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); - return useSuspenseQuery({ + return useSuspenseQuery( + getAvailableCLDVsQuery( + inspectionApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + ); +} + +export function getAvailableCLDVsQuery( + inspectionApi: InspectionApi, + getPreCacheForOfflineModeHeaders: (inspectionId?: string) => RequestInit, + inspectionId: string, +) { + return queryOptions({ queryKey: getAvailableCLDVsQueryKey(inspectionId), queryFn: () => inspectionApi.getAvailableCLDs( diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/inspectionReport.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/inspectionReport.ts index 670401e43..0f69394c2 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/inspectionReport.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/inspectionReport.ts @@ -6,20 +6,26 @@ import { ApiSortDirection, ApiTextBlockSortKey, + EditorApi, + InspectionApi, + TextBlockApi, } from "@eshg/employee-portal-api/inspection"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { useEditorApi, - useTextBlockApi, + useInspectionApi, } from "@/lib/businessModules/inspection/api/clients"; import { editorApiQueryKey } from "@/lib/businessModules/inspection/api/queries/apiQueryKeys"; import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; -export function useLoadEditor(reportId: string, inspectionId: string) { - const editorApi = useEditorApi(); - const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); - return useSuspenseQuery({ +export function loadEditorQuery( + editorApi: EditorApi, + getPreCacheForOfflineModeHeaders: (inspectionId?: string) => RequestInit, + reportId: string, + inspectionId: string, +) { + return queryOptions({ queryKey: editorApiQueryKey(["loadEditor", { reportId, inspectionId }]), queryFn: () => editorApi.loadEditor( @@ -29,9 +35,49 @@ export function useLoadEditor(reportId: string, inspectionId: string) { }); } -export function useGetTextBlocks() { - const textBlockApi = useTextBlockApi(); - return useSuspenseQuery({ +export function useGetInspectionAndLoadEditor(inspectionId: string) { + const inspectionApi = useInspectionApi(); + const editorApi = useEditorApi(); + const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); + return useSuspenseQuery( + getInspectionAndLoadEditorQuery( + inspectionApi, + editorApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + ); +} + +export function getInspectionAndLoadEditorQuery( + inspectionApi: InspectionApi, + editorApi: EditorApi, + getPreCacheForOfflineModeHeaders: (inspectionId?: string) => RequestInit, + inspectionId: string, +) { + return queryOptions({ + queryKey: editorApiQueryKey([ + "getInspectionAndLoadEditor", + { inspectionId }, + ]), + queryFn: async () => { + const inspection = await inspectionApi.getInspection( + inspectionId, + getPreCacheForOfflineModeHeaders(inspectionId), + ); + + const editorData = await editorApi.loadEditor( + inspection.reportId!, + getPreCacheForOfflineModeHeaders(inspectionId), + ); + + return { inspection, editorData }; + }, + }); +} + +export function getTextBlocksQuery(textBlockApi: TextBlockApi) { + return queryOptions({ queryKey: editorApiQueryKey(["getTextBlocksTemplate"]), queryFn: () => textBlockApi.getTextBlocks( diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/objectTypes.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/objectTypes.ts index 94873b284..221f17644 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/objectTypes.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/objectTypes.ts @@ -3,14 +3,19 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { useSuspenseQuery } from "@tanstack/react-query"; +import { ObjectTypeApi } from "@eshg/employee-portal-api/inspection"; +import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { useObjectTypeApi } from "@/lib/businessModules/inspection/api/clients"; import { objectTypeApiQueryKey } from "@/lib/businessModules/inspection/api/queries/apiQueryKeys"; export function useGetObjectTypes() { const objectTypeApi = useObjectTypeApi(); - return useSuspenseQuery({ + return useSuspenseQuery(getObjectTypesQuery(objectTypeApi)); +} + +export function getObjectTypesQuery(objectTypeApi: ObjectTypeApi) { + return queryOptions({ queryKey: objectTypeApiQueryKey(["getObjectTypes"]), queryFn: () => objectTypeApi.getObjectTypes(), select: (response) => response.objectTypes ?? [], diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/packlistDefinition.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/packlistDefinition.ts index d932a92f3..67024f95e 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/packlistDefinition.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/packlistDefinition.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { useSuspenseQuery } from "@tanstack/react-query"; +import { PacklistDefinitionApi } from "@eshg/employee-portal-api/inspection"; +import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import { usePacklistDefinitionApi } from "@/lib/businessModules/inspection/api/clients"; import { packlistDefinitionApiQueryKey } from "@/lib/businessModules/inspection/api/queries/apiQueryKeys"; @@ -17,9 +18,11 @@ export function useGetPacklistDefinitions() { }); } -export function useGetPacklistDefinitionRevision(versionId: string) { - const packlistDefinitionApi = usePacklistDefinitionApi(); - return useSuspenseQuery({ +export function getPacklistDefinitionRevisionQuery( + packlistDefinitionApi: PacklistDefinitionApi, + versionId: string, +) { + return queryOptions({ queryKey: packlistDefinitionApiQueryKey([ "getPacklistDefinitionRevision", { versionId }, diff --git a/employee-portal/src/lib/businessModules/inspection/api/queries/users.ts b/employee-portal/src/lib/businessModules/inspection/api/queries/users.ts index 9ae332423..f4bf02481 100644 --- a/employee-portal/src/lib/businessModules/inspection/api/queries/users.ts +++ b/employee-portal/src/lib/businessModules/inspection/api/queries/users.ts @@ -3,24 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { useSuspenseQuery } from "@tanstack/react-query"; +import { UserApi } from "@eshg/employee-portal-api/base"; +import { queryOptions } from "@tanstack/react-query"; -import { useUserApi } from "@/lib/baseModule/api/clients"; import { userApiQueryKey } from "@/lib/businessModules/inspection/api/queries/apiQueryKeys"; -export function useGetAllAssignableUsers() { - const userApi = useUserApi(); - return useSuspenseQuery({ - queryKey: userApiQueryKey(["useGetAllAssignableUsers"]), +export function getAllAssignableUsersQuery(userApi: UserApi) { + return queryOptions({ + queryKey: userApiQueryKey(["getAllAssignableUsers"]), queryFn: () => userApi.getUsersByGroup("[System] Begehung"), select: (response) => response.users ?? [], }); } -export function useGetSelfUser() { - const userApi = useUserApi(); - return useSuspenseQuery({ - queryKey: userApiQueryKey(["useGetSelfUser"]), +export function getSelfUserQuery(userApi: UserApi) { + return queryOptions({ + queryKey: userApiQueryKey(["getSelfUser"]), queryFn: () => userApi.getSelfUser(), select: (response) => response, }); diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/EditChecklistDefinition.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/EditChecklistDefinition.tsx index c1405be42..5e40d90be 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/EditChecklistDefinition.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/EditChecklistDefinition.tsx @@ -6,7 +6,10 @@ "use client"; import { ApiUserRole } from "@eshg/employee-portal-api/base"; -import { ApiChecklistDefinitionVersion } from "@eshg/employee-portal-api/inspection"; +import { + ApiChecklistDefinitionVersion, + ApiObjectType, +} from "@eshg/employee-portal-api/inspection"; import { FormPlus } from "@eshg/lib-portal/components/form/FormPlus"; import { Stack } from "@mui/joy"; import { Formik } from "formik"; @@ -27,6 +30,7 @@ import { } from "@/lib/businessModules/inspection/components/checklistDefinition/header/ChecklistDefinitionHeaderRow"; import { routes } from "@/lib/businessModules/inspection/shared/routes"; import { ButtonBar } from "@/lib/shared/components/buttons/ButtonBar"; +import { ConfirmLeaveDirtyFormEffect } from "@/lib/shared/components/form/ConfirmLeaveDirtyFormEffect"; import { useHasUserRolesCheck } from "@/lib/shared/hooks/useAccessControl"; import { ChecklistDefinitionSectionsList } from "./sections/ChecklistDefinitionSectionsList"; @@ -35,12 +39,14 @@ interface EditChecklistDefinitionProps { headerRow?: ReactNode; cldVersion?: ApiChecklistDefinitionVersion; // unset when this is a completely new cld readonly?: boolean; + objectTypes: ApiObjectType[]; } export function EditChecklistDefinition({ headerRow, cldVersion, readonly, + objectTypes, }: Readonly<EditChecklistDefinitionProps>) { const router = useRouter(); @@ -129,6 +135,7 @@ export function EditChecklistDefinition({ > {({ isSubmitting }) => ( <FormPlus> + <ConfirmLeaveDirtyFormEffect /> <Stack spacing={2}> {headerRow ?? ( <ChecklistDefinitionHeaderRow @@ -147,6 +154,7 @@ export function EditChecklistDefinition({ <ChecklistDefinitionHeaderCard readOnlyMode={readOnlyMode} version={cldVersion?.context.version} + objectTypes={objectTypes} /> <ChecklistDefinitionSectionsList readOnlyMode={readOnlyMode} /> {canSeeSaveActions && !readOnlyMode && ( diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/ChecklistDefinitionElement.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/ChecklistDefinitionElement.tsx index d882f505e..bf7531e2d 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/ChecklistDefinitionElement.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/ChecklistDefinitionElement.tsx @@ -7,32 +7,27 @@ import { ApiCLSectionContextElementsInner, ApiInspectionFeature, } from "@eshg/employee-portal-api/inspection"; +import { InputField } from "@eshg/lib-portal/components/formFields/InputField"; +import { SelectField } from "@eshg/lib-portal/components/formFields/SelectField"; +import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd"; import { - AudiotrackOutlined, + Audiotrack, + CheckBox, DeleteOutlined, - ExpandMore, - FormatColorTextOutlined, - PhotoCameraOutlined, - QuestionAnswerOutlined, - RadioButtonCheckedOutlined, + DragIndicatorOutlined, + FormatColorText, + PhotoCamera, + QuestionAnswer, + RadioButtonChecked, } from "@mui/icons-material"; -import { - Box, - Checkbox, - Divider, - IconButton, - Input, - Option, - Select, - Stack, -} from "@mui/joy"; -import { ChangeEvent } from "react"; +import { Box, Chip, Divider, Grid, IconButton, Stack } from "@mui/joy"; import { useIsNewFeatureEnabled } from "@/lib/businessModules/inspection/api/queries/feature"; import { NoteAndHelpTextInput } from "@/lib/businessModules/inspection/components/checklistDefinition/elements/NoteAndHelpTextInput"; import { ChecklistDefinitionElementInner } from "@/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementInner"; import { CopyDeleteDropdown } from "@/lib/businessModules/inspection/components/checklistDefinition/helpers/CopyDeleteDropdown"; import { createChecklistElement } from "@/lib/businessModules/inspection/components/checklistDefinition/helpers/helpers"; +import { CheckboxField } from "@/lib/shared/components/formFields/CheckboxField"; interface ChecklistDefinitionElementProps { element: ApiCLSectionContextElementsInner; @@ -42,6 +37,7 @@ interface ChecklistDefinitionElementProps { sectionIndex: number; elementIndex: number; readOnlyMode?: boolean; + dragHandleProps?: DraggableProvidedDragHandleProps | null; } export function ChecklistDefinitionElement({ @@ -52,6 +48,7 @@ export function ChecklistDefinitionElement({ sectionIndex, elementIndex, readOnlyMode, + dragHandleProps, }: Readonly<ChecklistDefinitionElementProps>) { const isChecklistAudioFeatureEnabled = useIsNewFeatureEnabled( ApiInspectionFeature.ChecklistAudios, @@ -85,7 +82,6 @@ export function ChecklistDefinitionElement({ return !isSeparator ? ( <Box - boxShadow="sm" border="1px solid var(--neutral-outlined-border, #CDD7E1)" borderRadius={12} component="section" @@ -95,96 +91,169 @@ export function ChecklistDefinitionElement({ background: "var(--background-level-1, #F0F4F8)", }} > - <Stack - direction="row" - spacing={2} - display="flex" - flex={1} - alignContent="center" - justifyContent="center" - alignItems="center" - > - <Input - aria-label="Fragenummer" - value={sectionIndex + 1 + "." + (elementIndex + 1)} - style={{ maxWidth: 60, height: 40 }} - disabled - /> - <Input - disabled={readOnlyMode} - style={{ flex: 1, height: 40 }} - placeholder="Frage eingeben" - defaultValue={element.text ?? ""} - onBlur={(event) => updateElement({ text: event.target.value })} - /> - <Select - aria-label="Antworttyp" - disabled={readOnlyMode} - placeholder="Funktion auswählen" - defaultValue={element.type} - onChange={(_, newValue) => { - changeType(newValue); - }} - > - <Option value="CHECKBOX"> - <ExpandMore /> - Einfachauswahl (Ja/Nein) - </Option> - <Option value="MULTI_SELECT"> - <QuestionAnswerOutlined /> - Mehrfachauswahl - </Option> - <Option value="TEXT"> - <FormatColorTextOutlined /> - Textfeld - </Option> - <Option value="SINGLE_SELECT"> - <RadioButtonCheckedOutlined /> - Optionsauswahl - </Option> - <Option value="IMAGE"> - <PhotoCameraOutlined /> - Bilder hinzufügen - </Option> - {isChecklistAudioFeatureEnabled && ( - <Option value="AUDIO"> - <AudiotrackOutlined /> - Audio hinzufügen - </Option> - )} - </Select> - {!isImage && !isAudio && ( - /* image and audio elements cannot be marked as mandatory */ - <Checkbox - disabled={readOnlyMode} - label="Pflichtfeld" - checked={element.mandatory} - onChange={(event: ChangeEvent<HTMLInputElement>) => { - updateElement({ mandatory: event.target.checked }); - }} + <Grid container spacing={2}> + <Grid xs="auto"> + <div + {...dragHandleProps} + style={{ marginTop: "1.7rem" }} + aria-label={`Element ${elementIndex + 1} der Sektion ${sectionIndex + 1} ziehen und verschieben`} + role="button" + > + <DragIndicatorOutlined + style={{ + backgroundColor: "white", + borderRadius: 50, + padding: 4, + width: 32, + height: 32, + }} + /> + </div> + </Grid> + <Grid xs> + <Stack + direction="row" + spacing={2} + display="flex" + flex={1} + alignContent="center" + justifyContent="center" + alignItems="flex-start" + > + <Chip + aria-label="Fragenummer" + style={{ marginTop: "2.1rem" }} + color="success" + > + {sectionIndex + 1 + "." + (elementIndex + 1)} + </Chip> + <InputField + name={`context.sections.${sectionIndex}.elements.${elementIndex}.text`} + label="Frage" + disabled={readOnlyMode} + sx={{ flex: 1 }} + placeholder="Frage eingeben" + required="Bitte geben Sie eine Frage ein." + onBlur={(event) => updateElement({ text: event.target.value })} + /> + <SelectField + name={`context.sections.${sectionIndex}.elements.${elementIndex}.type`} + label="Antworttyp" + aria-label="Antworttyp" + disabled={readOnlyMode} + placeholder="Funktion auswählen" + required="Bitte wählen Sie einen Antworttyp aus." + sx={{ width: "17rem" }} + onChange={(newValue) => { + changeType( + newValue as + | "CHECKBOX" + | "MULTI_SELECT" + | "TEXT" + | "SINGLE_SELECT" + | "IMAGE" + | "AUDIO" + | null, + ); + }} + options={[ + { + value: "CHECKBOX", + label: ( + <> + <CheckBox /> + Einfachauswahl (Ja/Nein) + </> + ), + }, + { + value: "MULTI_SELECT", + label: ( + <> + <QuestionAnswer /> + Mehrfachauswahl + </> + ), + }, + { + value: "TEXT", + label: ( + <> + <FormatColorText /> + Textfeld + </> + ), + }, + { + value: "SINGLE_SELECT", + label: ( + <> + <RadioButtonChecked /> + Optionsauswahl + </> + ), + }, + { + value: "IMAGE", + label: ( + <> + <PhotoCamera /> + Bilder hinzufügen + </> + ), + }, + ...(isChecklistAudioFeatureEnabled + ? [ + { + value: "AUDIO", + label: ( + <> + <Audiotrack /> + Audio hinzufügen + </> + ), + }, + ] + : []), + ]} + /> + {!isImage && !isAudio && ( + /* image and audio elements cannot be marked as mandatory */ + <CheckboxField + name={`context.sections.${sectionIndex}.elements.${elementIndex}.mandatory`} + label="Pflichtfeld" + disabled={readOnlyMode} + sx={{ mt: "2.1rem" }} + /> + )} + {!readOnlyMode && ( + <Box mt="1.7rem"> + <CopyDeleteDropdown + onDelete={deleteElement} + onCopy={() => copyElement()} + /> + </Box> + )} + </Stack> + <ChecklistDefinitionElementInner + sectionIndex={sectionIndex} + elementIndex={elementIndex} + readOnlyMode={readOnlyMode} + element={element} + setElement={setElement} /> - )} - {!readOnlyMode && ( - <CopyDeleteDropdown - onDelete={deleteElement} - onCopy={() => copyElement()} + <NoteAndHelpTextInput + sectionIndex={sectionIndex} + elementIndex={elementIndex} + element={element} + setElement={setElement} + readOnlyMode={readOnlyMode} /> - )} - </Stack> - <ChecklistDefinitionElementInner - readOnlyMode={readOnlyMode} - element={element} - setElement={setElement} - /> - <NoteAndHelpTextInput - element={element} - setElement={setElement} - readOnlyMode={readOnlyMode} - /> + </Grid> + </Grid> </Box> ) : ( <Box - boxShadow="sm" border="1px solid var(--neutral-outlined-border, #CDD7E1)" borderRadius={12} component="section" diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/ChecklistDefinitionElementsList.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/ChecklistDefinitionElementsList.tsx index 76e518c14..bf31ea0af 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/ChecklistDefinitionElementsList.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/ChecklistDefinitionElementsList.tsx @@ -3,67 +3,112 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiCLSectionContextElementsInner } from "@eshg/employee-portal-api/inspection"; +import { useNonce } from "@eshg/lib-portal/components/NonceProvider"; +import { + DragDropContext, + Draggable, + DraggingStyle, + Droppable, + NotDraggingStyle, +} from "@hello-pangea/dnd"; import { Stack } from "@mui/joy"; +import { FieldArray, useFormikContext } from "formik"; +import { FormChecklistDefinitionVersion } from "@/lib/businessModules/inspection/api/mutations/checklistDefinition"; import { ChecklistDefinitionElement } from "@/lib/businessModules/inspection/components/checklistDefinition/elements/ChecklistDefinitionElement"; interface ChecklistDefinitionElementsListProps { readOnlyMode?: boolean; - elements: ApiCLSectionContextElementsInner[]; - setElements: (elements: ApiCLSectionContextElementsInner[]) => void; sectionIndex: number; } export function ChecklistDefinitionElementsList({ readOnlyMode, - elements, - setElements, sectionIndex, }: Readonly<ChecklistDefinitionElementsListProps>) { - function setElement( - index: number, - element: ApiCLSectionContextElementsInner, - ) { - const newElements = [...elements]; - newElements[index] = element; - setElements(newElements); - } - - function deleteElement(index: number) { - const newElements = [...elements]; - newElements.splice(index, 1); - setElements(newElements); - } - - function addElement(element: ApiCLSectionContextElementsInner) { - setElements([...elements, element]); - } + const nonce = useNonce(); + const { values } = useFormikContext<FormChecklistDefinitionVersion>(); return ( - <> - {elements.map((element, elementIndex) => { - return ( - <Stack spacing={2} key={element.id}> - <ChecklistDefinitionElement - readOnlyMode={readOnlyMode} - key={element.id} - element={element} - setElement={(element) => { - setElement(elementIndex, element); - }} - deleteElement={() => { - deleteElement(elementIndex); - }} - addElement={(element) => { - addElement(element); - }} - sectionIndex={sectionIndex} - elementIndex={elementIndex} - /> - </Stack> - ); - })} - </> + <FieldArray name={`context.sections.${sectionIndex}.elements`}> + {({ push, remove, replace, move }) => ( + <> + <DragDropContext + nonce={nonce} + onDragEnd={(result) => { + if (result.reason !== "DROP") return; + if (result.destination === null) return; + if (result.destination.index === result.source.index) return; + move(result.source.index, result.destination.index); + }} + > + <Droppable droppableId="droppable"> + {(provided, snapshot) => ( + <Stack + spacing={2} + {...provided.droppableProps} + ref={provided.innerRef} + style={getListStyle(snapshot.isDraggingOver)} + > + {values.context.sections[sectionIndex]?.elements.map( + (element, elementIndex) => ( + <Draggable + key={element.id} + draggableId={element.id} + index={elementIndex} + > + {(provided, snapshot) => ( + <div + ref={provided.innerRef} + {...provided.draggableProps} + style={getItemStyle( + snapshot.isDragging, + provided.draggableProps.style, + )} + > + <ChecklistDefinitionElement + dragHandleProps={provided.dragHandleProps} + readOnlyMode={readOnlyMode} + key={element.id} + element={element} + setElement={(element) => + replace(elementIndex, element) + } + deleteElement={() => remove(elementIndex)} + addElement={(element) => push(element)} + sectionIndex={sectionIndex} + elementIndex={elementIndex} + /> + </div> + )} + </Draggable> + ), + )} + {provided.placeholder} + </Stack> + )} + </Droppable> + </DragDropContext> + </> + )} + </FieldArray> ); } + +function getItemStyle( + isDragging: boolean, + draggableStyle: DraggingStyle | NotDraggingStyle | undefined, +) { + return { + background: isDragging ? "#F0F4F8" : "white", + borderRadius: 12, + ...draggableStyle, + }; +} + +function getListStyle(isDraggingOver: boolean) { + return { + background: isDraggingOver ? "#F0F4F8" : "white", + borderRadius: 12, + }; +} diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/NoteAndHelpTextInput.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/NoteAndHelpTextInput.tsx index cd6a396ea..99eecc411 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/NoteAndHelpTextInput.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/NoteAndHelpTextInput.tsx @@ -13,11 +13,15 @@ interface NoteAndHelpTextInputProps { readOnlyMode?: boolean; element: ApiCLSectionContextElementsInner; setElement: (element: ApiCLSectionContextElementsInner) => void; + sectionIndex: number; + elementIndex: number; } export function NoteAndHelpTextInput({ element, setElement, + sectionIndex, + elementIndex, readOnlyMode, }: Readonly<NoteAndHelpTextInputProps>) { const [showNote, setShowNote] = useState( @@ -51,42 +55,30 @@ export function NoteAndHelpTextInput({ direction={showNote || showHelpText ? "column" : "row"} > <InputWithDeleteButton + name={`context.sections.${sectionIndex}.elements.${elementIndex}.help`} disabled={readOnlyMode} placeholder="Hilfetext eingeben" multiline defaultValue={element.help} - title="Hilfetext" - onBlur={(value) => - updateElement({ - help: value, - }) - } + label="Hilfetext" onDelete={() => { setShowHelpText(false); - updateElement({ - help: "", - }); + updateElement({ help: "" }); }} addButtonTitle="Hilfetext verfassen" onAddItem={() => setShowHelpText(true)} /> <InputWithDeleteButton + name={`context.sections.${sectionIndex}.elements.${elementIndex}.note`} disabled={readOnlyMode} placeholder="Bemerkung eingeben" multiline defaultValue={element.note} - title="Bemerkungsfeld" - onBlur={(value) => - updateElement({ - note: value, - }) - } + label="Bemerkungsfeld" onDelete={() => { setShowNote(false); - updateElement({ - note: "", - }); + updateElement({ note: "" }); }} addButtonTitle="Bemerkungsfeld hinzufügen" onAddItem={() => setShowNote(true)} diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionAnswerItem.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionAnswerItem.tsx index d591d57b0..5d135635a 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionAnswerItem.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionAnswerItem.tsx @@ -4,12 +4,18 @@ */ import { ApiCLFieldOptionContext } from "@eshg/employee-portal-api/inspection"; +import { OptionalFieldValue } from "@eshg/lib-portal/types/form"; +import { SubdirectoryArrowRight } from "@mui/icons-material"; import { Stack } from "@mui/joy"; +import { useFormikContext } from "formik"; import { useState } from "react"; +import { FormChecklistDefinitionVersion } from "@/lib/businessModules/inspection/api/mutations/checklistDefinition"; import { InputWithDeleteButton } from "@/lib/businessModules/inspection/components/checklistDefinition/helpers/InputWithDeleteButton"; interface ChecklistDefinitionAnswerItemProps { + sectionIndex: number; + elementIndex: number; itemIndex: number; item: ApiCLFieldOptionContext; setItem: (item: ApiCLFieldOptionContext) => void; @@ -19,6 +25,8 @@ interface ChecklistDefinitionAnswerItemProps { } export function ChecklistDefinitionAnswerItem({ + sectionIndex, + elementIndex, itemIndex, item, setItem, @@ -26,12 +34,14 @@ export function ChecklistDefinitionAnswerItem({ hideDeleteButton = false, readOnlyMode = false, }: Readonly<ChecklistDefinitionAnswerItemProps>) { - const [showTextModuleInputTrue, setShowTextModuleInputTrue] = useState( + const { values } = useFormikContext<FormChecklistDefinitionVersion>(); + const [showTextModuleTrue, setShowTextModuleTrue] = useState( !!item?.textModuleTrue, ); - const [showTextModuleInputFalse, setShowTextModuleInputFalse] = useState( + const [showTextModuleFalse, setShowTextModuleFalse] = useState( !!item?.textModuleFalse, ); + const showTextModules = showTextModuleTrue || showTextModuleFalse; function updateItem(partialItem: Partial<ApiCLFieldOptionContext>) { setItem({ @@ -40,10 +50,6 @@ export function ChecklistDefinitionAnswerItem({ }); } - function setText(text: string) { - updateItem({ text }); - } - function setTextModuleTrue(textModuleTrue: string) { updateItem({ textModuleTrue }); } @@ -52,61 +58,79 @@ export function ChecklistDefinitionAnswerItem({ updateItem({ textModuleFalse }); } + function validateMultipleAnswers(value: OptionalFieldValue<string>) { + if (value === "") { + return undefined; + } + + switch ( + values.context.sections[sectionIndex]?.elements[elementIndex]?.type + ) { + case "MULTI_SELECT": + case "CLMultiSelectContext": + case "SINGLE_SELECT": + case "CLSingleSelectContext": { + const occurances = values.context.sections[sectionIndex]?.elements[ + elementIndex + ].items!.filter((answer) => answer.text === value); + if (occurances.length > 1) { + return "Bitte unterschiedliche Werte eingeben."; + } else { + return undefined; + } + } + } + } + return ( <Stack spacing={1}> <InputWithDeleteButton + name={`context.sections.${sectionIndex}.elements.${elementIndex}.items.${itemIndex}.text`} disabled={readOnlyMode} - title={`Antwort ${itemIndex + 1}`} + label={`Antwort ${itemIndex + 1}`} placeholder="Antwort eingeben" defaultValue={item?.text} - onBlur={setText} onDelete={onDelete} + validate={validateMultipleAnswers} + required="Bitte geben Sie eine Antwort ein." onToggleTextModule={(show) => { - setShowTextModuleInputTrue(show); - setShowTextModuleInputFalse(show); - if (!show) { - setTextModuleFalse(""); - setTextModuleTrue(""); - } + setShowTextModuleTrue(show); + setShowTextModuleFalse(show); }} - toggleTextModuleActive={ - showTextModuleInputTrue || showTextModuleInputFalse - } + toggleTextModuleActive={showTextModules} showTextModule hideAddButton hideDeleteButton={hideDeleteButton} /> - {showTextModuleInputTrue && ( - <InputWithDeleteButton - disabled={readOnlyMode} - style={{ marginLeft: 16 }} - multiline - title={`Textbaustein ${itemIndex + 1} Ja`} - placeholder="Textbaustein eingeben" - defaultValue={item?.textModuleTrue} - onBlur={setTextModuleTrue} - onDelete={() => { - setShowTextModuleInputTrue(false); - setTextModuleTrue(""); - }} - hideAddButton - /> + {showTextModules && ( + <Stack direction="row" spacing={2} alignItems="flex-start"> + <SubdirectoryArrowRight sx={{ mt: "1.8rem" }} /> + <InputWithDeleteButton + name={`context.sections.${sectionIndex}.elements.${elementIndex}.items.${itemIndex}.textModuleTrue`} + disabled={readOnlyMode} + multiline + label={`Textbaustein für Antwort ${itemIndex + 1} (ausgewählt)`} + placeholder="Textbaustein eingeben" + defaultValue={item?.textModuleTrue} + onDelete={() => setTextModuleTrue("")} + hideAddButton + /> + </Stack> )} - {showTextModuleInputFalse && ( - <InputWithDeleteButton - disabled={readOnlyMode} - style={{ marginLeft: 16 }} - multiline - title={`Textbaustein ${itemIndex + 1} Nein`} - placeholder="Textbaustein eingeben" - defaultValue={item?.textModuleFalse} - onBlur={setTextModuleFalse} - onDelete={() => { - setShowTextModuleInputFalse(false); - setTextModuleFalse(""); - }} - hideAddButton - /> + {showTextModules && ( + <Stack direction="row" spacing={2} alignItems="flex-start"> + <SubdirectoryArrowRight sx={{ mt: "1.8rem" }} /> + <InputWithDeleteButton + name={`context.sections.${sectionIndex}.elements.${elementIndex}.items.${itemIndex}.textModuleFalse`} + disabled={readOnlyMode} + multiline + label={`Textbaustein für Antwort ${itemIndex + 1} (nicht ausgewählt)`} + placeholder="Textbaustein eingeben" + defaultValue={item?.textModuleFalse} + onDelete={() => setTextModuleFalse("")} + hideAddButton + /> + </Stack> )} </Stack> ); diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementCheckboxInner.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementCheckboxInner.tsx index 42d6d3c91..89a9e6da1 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementCheckboxInner.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementCheckboxInner.tsx @@ -4,8 +4,11 @@ */ import { ApiCLCheckboxContext } from "@eshg/employee-portal-api/inspection"; -import { DeveloperModeRounded } from "@mui/icons-material"; -import { Box, Radio, Stack, Typography } from "@mui/joy"; +import { + DeveloperModeRounded, + SubdirectoryArrowRight, +} from "@mui/icons-material"; +import { Radio, Stack } from "@mui/joy"; import { useState } from "react"; import { ChecklistDefinitionElementInnerProps } from "@/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementInner"; @@ -14,6 +17,8 @@ import { ToggleButton } from "@/lib/shared/components/buttons/ToggleButton"; export function ChecklistDefinitionElementCheckboxInner({ readOnlyMode = true, + sectionIndex, + elementIndex, element, setElement, }: Readonly<ChecklistDefinitionElementInnerProps<ApiCLCheckboxContext>>) { @@ -23,6 +28,7 @@ export function ChecklistDefinitionElementCheckboxInner({ const [showTextModuleFalse, setShowTextModuleFalse] = useState( !!element.textModuleFalse, ); + const showTextModules = showTextModuleTrue || showTextModuleFalse; function setTextModuleTrue(textModuleTrue: string) { setElement({ @@ -40,88 +46,54 @@ export function ChecklistDefinitionElementCheckboxInner({ return ( <Stack spacing={2}> - <Stack - spacing={2} - direction="row" - style={{ marginTop: 12 }} - alignItems={"center"} - > - <Typography>Antwortmöglichkeiten: </Typography> - <Box - style={{ - backgroundColor: "white", - padding: 16, - borderRadius: 12, - - flex: 1, - }} - display={"flex"} - alignItems={"center"} - > - <Radio disabled label="Ja" /> - </Box> - <Box - style={{ - backgroundColor: "white", - padding: 16, - borderRadius: 12, - flex: 1, - }} - display={"flex"} - alignItems={"center"} - > - <Radio disabled label="Nein" /> - </Box> + <Stack direction="row" spacing={2} mt={1} alignItems={"center"}> + <Radio disabled label="Ja" /> + <Radio disabled label="Nein" /> {!readOnlyMode && ( <ToggleButton asIcon={true} title="Textbausteine" aria-label="Textbausteine" - aria-pressed={showTextModuleTrue || showTextModuleFalse} + aria-pressed={showTextModules} + defaultChecked={showTextModules} onToggle={(pressed) => { setShowTextModuleTrue(pressed); setShowTextModuleFalse(pressed); - if (!pressed) { - setTextModuleTrue(""); - setTextModuleFalse(""); - } }} > <DeveloperModeRounded /> </ToggleButton> )} </Stack> - {showTextModuleTrue && ( - <InputWithDeleteButton - disabled={readOnlyMode} - style={{ marginLeft: 16 }} - title={`Textbaustein Ja`} - multiline - placeholder="Textbaustein eingeben" - defaultValue={element.textModuleTrue} - onBlur={setTextModuleTrue} - onDelete={() => { - setShowTextModuleTrue(false); - setTextModuleTrue(""); - }} - hideAddButton - /> + {showTextModules && ( + <Stack direction="row" spacing={2} alignItems="flex-start"> + <SubdirectoryArrowRight sx={{ mt: "1.8rem" }} /> + <InputWithDeleteButton + name={`context.sections.${sectionIndex}.elements.${elementIndex}.textModuleTrue`} + disabled={readOnlyMode} + label="Textbaustein für Antwort Ja" + multiline + placeholder="Textbaustein eingeben" + defaultValue={element.textModuleTrue} + onDelete={() => setTextModuleTrue("")} + hideAddButton + /> + </Stack> )} - {showTextModuleFalse && ( - <InputWithDeleteButton - disabled={readOnlyMode} - style={{ marginLeft: 16 }} - title={`Textbaustein Nein`} - multiline - placeholder="Textbaustein eingeben" - defaultValue={element.textModuleFalse} - onBlur={setTextModuleFalse} - onDelete={() => { - setShowTextModuleFalse(false); - setTextModuleFalse(""); - }} - hideAddButton - /> + {showTextModules && ( + <Stack direction="row" spacing={2} alignItems="flex-start"> + <SubdirectoryArrowRight sx={{ mt: "1.8rem" }} /> + <InputWithDeleteButton + name={`context.sections.${sectionIndex}.elements.${elementIndex}.textModuleFalse`} + disabled={readOnlyMode} + label="Textbaustein für Antwort Nein" + multiline + placeholder="Textbaustein eingeben" + defaultValue={element.textModuleFalse} + onDelete={() => setTextModuleFalse("")} + hideAddButton + /> + </Stack> )} </Stack> ); diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementInner.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementInner.tsx index fb19048ff..7e6cc4126 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementInner.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementInner.tsx @@ -13,6 +13,8 @@ export interface ChecklistDefinitionElementInnerProps< TElement = ApiCLSectionContextElementsInner, > { readOnlyMode?: boolean; + sectionIndex: number; + elementIndex: number; element: { type: ApiCLSectionContextElementsInner["type"] } & TElement; setElement: ( element: { type: ApiCLSectionContextElementsInner["type"] } & TElement, diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementMultiInner.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementMultiInner.tsx index 945fe637d..a4585d514 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementMultiInner.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/elements/inner/ChecklistDefinitionElementMultiInner.tsx @@ -17,6 +17,8 @@ import { ChecklistDefinitionElementInnerProps } from "@/lib/businessModules/insp export function ChecklistDefinitionElementMultiInner({ readOnlyMode = false, + sectionIndex, + elementIndex, element, setElement, }: Readonly<ChecklistDefinitionElementInnerProps<ApiCLMultiSelectContext>>) { @@ -69,6 +71,8 @@ export function ChecklistDefinitionElementMultiInner({ <ChecklistDefinitionAnswerItem key={key} readOnlyMode={readOnlyMode} + sectionIndex={sectionIndex} + elementIndex={elementIndex} itemIndex={index} item={item} setItem={(item) => setItem(index, item)} diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/header/ChecklistDefinitionHeaderCard.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/header/ChecklistDefinitionHeaderCard.tsx index 0330d3952..47329ca18 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/header/ChecklistDefinitionHeaderCard.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/header/ChecklistDefinitionHeaderCard.tsx @@ -4,6 +4,7 @@ */ import { ApiUserRole } from "@eshg/employee-portal-api/base"; +import { ApiObjectType } from "@eshg/employee-portal-api/inspection"; import { InputField } from "@eshg/lib-portal/components/formFields/InputField"; import { SelectField } from "@eshg/lib-portal/components/formFields/SelectField"; import { Stack } from "@mui/joy"; @@ -12,7 +13,6 @@ import { useMemo } from "react"; import { isDefined } from "remeda"; import { FormChecklistDefinitionVersion } from "@/lib/businessModules/inspection/api/mutations/checklistDefinition"; -import { useGetObjectTypes } from "@/lib/businessModules/inspection/api/queries/objectTypes"; import { CheckboxField } from "@/lib/shared/components/formFields/CheckboxField"; import { TextareaField } from "@/lib/shared/components/formFields/TextareaField"; import { InformationSheet } from "@/lib/shared/components/infoTile/InformationSheet"; @@ -21,14 +21,14 @@ import { useHasUserRoleCheck } from "@/lib/shared/hooks/useAccessControl"; interface ChecklistDefinitionHeaderCardProps { readOnlyMode: boolean; version: number | undefined; + objectTypes: ApiObjectType[]; } export function ChecklistDefinitionHeaderCard({ readOnlyMode, version, + objectTypes, }: Readonly<ChecklistDefinitionHeaderCardProps>) { - const { data: objectTypes } = useGetObjectTypes(); - const objectTypeOptions = useMemo( () => objectTypes.map((item) => ({ diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/helpers/InputWithDeleteButton.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/helpers/InputWithDeleteButton.tsx index 5c2e2a1c4..e710615b4 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/helpers/InputWithDeleteButton.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/helpers/InputWithDeleteButton.tsx @@ -5,26 +5,19 @@ "use client"; -import { Add, DeleteOutlined, DeveloperModeRounded } from "@mui/icons-material"; -import { - Button, - Grid, - IconButton, - Input, - Stack, - Textarea, - Typography, -} from "@mui/joy"; -import { CSSProperties, useId, useState } from "react"; +import { InputField } from "@eshg/lib-portal/components/formFields/InputField"; +import { Validator } from "@eshg/lib-portal/types/form"; +import { Add, Delete, DeveloperModeRounded } from "@mui/icons-material"; +import { Button, IconButton, Stack } from "@mui/joy"; +import { useState } from "react"; import { ToggleButton } from "@/lib/shared/components/buttons/ToggleButton"; +import { TextareaField } from "@/lib/shared/components/formFields/TextareaField"; export function InputWithDeleteButton({ - title, + label, placeholder, defaultValue, - onChange, - onBlur, onDelete, onAddItem, addButtonTitle, @@ -33,15 +26,15 @@ export function InputWithDeleteButton({ hideDeleteButton = false, toggleTextModuleActive = false, onToggleTextModule, - style, + name, + validate, + required, multiline = false, disabled = false, }: Readonly<{ - title: string; + label: string; placeholder: string; defaultValue?: string; - onChange?: (value: string) => void; - onBlur?: (value: string) => void; onDelete: () => void; onAddItem?: () => void; addButtonTitle?: string; @@ -50,79 +43,73 @@ export function InputWithDeleteButton({ hideDeleteButton?: boolean; toggleTextModuleActive?: boolean; onToggleTextModule?: (show: boolean) => void; - style?: CSSProperties; + name: string; + validate?: Validator<string>; + required?: string; multiline?: boolean; disabled?: boolean; }>) { const [showInput, setShowInput] = useState(!!defaultValue); - const id = useId(); return ( - <Stack spacing={1} style={style}> + <Stack spacing={1} style={{ flex: 1 }}> {showInput || hideAddButton ? ( - <Grid container columnSpacing={2} alignItems={"center"}> - <Grid xs={2}> - <Typography component="label" htmlFor={id}> - {title + ": "} - </Typography> - </Grid> - <Grid xs={8.7}> - {multiline ? ( - <Textarea - id={id} - aria-label={title} - disabled={disabled} - minRows={2} - placeholder={placeholder} - defaultValue={defaultValue} - onChange={(event) => onChange && onChange(event.target.value)} - onBlur={(event) => onBlur && onBlur(event.target.value)} - style={{ display: "flex", flex: 1 }} - /> - ) : ( - <Input - id={id} - aria-label={title} - disabled={disabled} - placeholder={placeholder} - defaultValue={defaultValue} - onChange={(event) => onChange && onChange(event.target.value)} - onBlur={(event) => onBlur && onBlur(event.target.value)} - style={{ display: "flex", flex: 1 }} - /> - )} - </Grid> - <Grid xs={1}> - <Stack direction="row" spacing={2}> - {!disabled && !hideDeleteButton && ( - <IconButton - title="Löschen" - aria-label="Löschen" - color="danger" - onClick={() => { - setShowInput(false); - onDelete(); - }} - > - <DeleteOutlined /> - </IconButton> - )} - {!disabled && showTextModule && ( - <ToggleButton - asIcon={true} - title="Textbausteine" - aria-label="Textbausteine" - aria-pressed={toggleTextModuleActive} - onToggle={(pressed) => { - onToggleTextModule?.(pressed); - }} - > - <DeveloperModeRounded /> - </ToggleButton> - )} - </Stack> - </Grid> - </Grid> + <Stack direction="row" spacing={2} alignItems="flex-start"> + {multiline ? ( + <TextareaField + name={name} + label={label} + aria-label={label} + disabled={disabled} + minRows={2} + placeholder={placeholder} + validate={validate} + required={required} + sx={{ flex: 1 }} + /> + ) : ( + <InputField + name={name} + label={label} + aria-label={label} + disabled={disabled} + placeholder={placeholder} + validate={validate} + required={required} + sx={{ flex: 1 }} + /> + )} + {!disabled && showTextModule && ( + <ToggleButton + title="Textbausteine" + aria-label="Textbausteine" + asIcon={true} + sx={{ mt: "1.7rem" }} + aria-pressed={toggleTextModuleActive} + defaultChecked={toggleTextModuleActive} + onToggle={(pressed) => { + onToggleTextModule?.(pressed); + }} + > + <DeveloperModeRounded /> + </ToggleButton> + )} + {!disabled && !hideDeleteButton && ( + <IconButton + title="Löschen" + aria-label="Löschen" + variant="outlined" + color="danger" + sx={{ mt: "1.7rem" }} + onClick={() => { + setShowInput(false); + onDelete(); + }} + > + <Delete /> + </IconButton> + )} + </Stack> ) : ( <Button disabled={disabled} diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/helpers/helpers.ts b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/helpers/helpers.ts index 0769d172d..0530c22b4 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/helpers/helpers.ts +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/helpers/helpers.ts @@ -50,6 +50,10 @@ function createCheckboxElement( keepId = false, ): { type: "CHECKBOX" } & ApiCLCheckboxContext { return { + text: "", + mandatory: false, + help: "", + note: "", ...partial, type: "CHECKBOX", id: getId(partial, keepId), @@ -61,6 +65,10 @@ function createMultiselectElement( keepId = false, ): { type: "MULTI_SELECT" } & ApiCLMultiSelectContext { return { + text: "", + mandatory: false, + help: "", + note: "", ...partial, type: "MULTI_SELECT", id: getId(partial, keepId), @@ -73,6 +81,10 @@ function createSingleSelectElement( keepId = false, ): { type: "SINGLE_SELECT" } & ApiCLSingleSelectContext { return { + text: "", + mandatory: false, + help: "", + note: "", ...partial, type: "SINGLE_SELECT", id: getId(partial, keepId), @@ -85,6 +97,10 @@ function createTextElement( keepId = false, ): { type: "TEXT" } & ApiCLTextElementContext { return { + text: "", + mandatory: false, + help: "", + note: "", ...partial, type: "TEXT", id: getId(partial, keepId), @@ -96,6 +112,9 @@ function createImageElement( keepId = false, ): { type: "IMAGE" } & ApiCLImageContext { return { + text: "", + help: "", + note: "", ...partial, type: "IMAGE", id: getId(partial, keepId), @@ -107,6 +126,9 @@ function createAudioElement( keepId = false, ): { type: "AUDIO" } & ApiCLAudioContext { return { + text: "", + help: "", + note: "", ...partial, type: "AUDIO", id: getId(partial, keepId), diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/overview/ChecklistDefinitionOverviewTable.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/overview/ChecklistDefinitionOverviewTable.tsx index 2324da84c..861cf02b4 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/overview/ChecklistDefinitionOverviewTable.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/overview/ChecklistDefinitionOverviewTable.tsx @@ -16,7 +16,6 @@ import { useAddChecklistDefinitionVersion, useEditDraftChecklistDefinitionVersion, } from "@/lib/businessModules/inspection/api/mutations/checklistDefinition"; -import { useGetChecklistDefinitions } from "@/lib/businessModules/inspection/api/queries/checklistDefinition"; import { generateChecklistDefinitionOverviewTableColumns } from "@/lib/businessModules/inspection/components/checklistDefinition/overview/columns"; import { UploadChecklistToRepoSidebar } from "@/lib/businessModules/inspection/components/checklistDefinition/sidebars/UploadChecklistToRepoSidebar"; import { ChecklistVersionsSidebar } from "@/lib/businessModules/inspection/components/checklistDefinition/sidebars/history/ChecklistVersionsSidebar"; @@ -41,7 +40,15 @@ type UserActivityState = const initialUserActivity: UserActivityState = { type: "view-table" }; -export function ChecklistDefinitionOverviewTable() { +interface ChecklistDefinitionOverviewTableProps { + checklists: ApiChecklistDefinition[]; + isFetching: boolean; +} + +export function ChecklistDefinitionOverviewTable({ + checklists, + isFetching, +}: Readonly<ChecklistDefinitionOverviewTableProps>) { const { openConfirmationDialog } = useConfirmationDialog(); const snackbar = useSnackbar(); const [ @@ -56,7 +63,6 @@ export function ChecklistDefinitionOverviewTable() { ApiUserRole.InspectionChecklistdefinitionsWrite, ]); - const { data: checklists, isFetching } = useGetChecklistDefinitions(); const { mutateAsync: addCldVersion } = useAddChecklistDefinitionVersion(); const { mutateAsync: editDraftCldVersion } = useEditDraftChecklistDefinitionVersion(); diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/overview/columns.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/overview/columns.tsx index 6fb1ed283..85fe93720 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/overview/columns.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/overview/columns.tsx @@ -128,6 +128,9 @@ export function generateChecklistDefinitionOverviewTableColumns( columnHelper.accessor("objectType.name", { header: "Objekttyp", meta: { + canNavigate: { + parentRow: true, + }, width: 366, }, }), @@ -186,6 +189,9 @@ export function generateChecklistDefinitionOverviewTableColumns( <Typography>{formatDateTime(info.getValue())}</Typography> ), meta: { + canNavigate: { + parentRow: true, + }, width: 186, }, }), diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sections/ChecklistDefinitionSection.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sections/ChecklistDefinitionSection.tsx index cc3633aec..691d5ca27 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sections/ChecklistDefinitionSection.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sections/ChecklistDefinitionSection.tsx @@ -9,6 +9,7 @@ import { ApiCLSectionContext, ApiCLSectionContextElementsInner, } from "@eshg/employee-portal-api/inspection"; +import { InputField } from "@eshg/lib-portal/components/formFields/InputField"; import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd"; import { Add, DragIndicatorOutlined } from "@mui/icons-material"; import { @@ -16,8 +17,9 @@ import { AccordionDetails, AccordionGroup, AccordionSummary, + Box, Button, - Input, + Chip, Stack, } from "@mui/joy"; import { doNothing } from "remeda"; @@ -72,12 +74,6 @@ export function ChecklistDefinitionSection({ }); } - function setElements(elements: ApiCLSectionContextElementsInner[]) { - updateSection({ - elements, - }); - } - const defaultIndex = sectionIndex + 1; return ( <AccordionGroup @@ -100,11 +96,12 @@ export function ChecklistDefinitionSection({ flex={1} alignContent="center" justifyContent="center" - alignItems="center" + alignItems="flex-start" > <div {...dragHandleProps} - aria-label={`Section ${defaultIndex} Ziehen und Verschieben`} + style={{ marginTop: "1.7rem" }} + aria-label={`Sektion ${defaultIndex} ziehen und verschieben`} role="button" > <DragIndicatorOutlined @@ -117,35 +114,43 @@ export function ChecklistDefinitionSection({ }} /> </div> - <Input - aria-label="Abschnittsnummer" - value={defaultIndex.toString()} - disabled - style={{ width: 54, height: 51 }} - /> - <Input + <Chip + aria-label="Sektionsnummer" + style={{ marginTop: "2.1rem" }} + color="primary" + > + {defaultIndex.toString()} + </Chip> + <InputField + name={`context.sections.${sectionIndex}.title`} + label="Sektionstitel" disabled={readOnlyMode} - style={{ flex: 1, height: 51 }} - defaultValue={section?.title ?? ""} + sx={{ flex: 1 }} placeholder={`Titel Sektion ${defaultIndex} eingeben`} + required="Bitte geben Sie einen Titel ein." onBlur={(event) => setTitle(event.target.value)} /> {!readOnlyMode && ( - <CopyDeleteDropdown - onDelete={() => deleteSection()} - onCopy={() => copySection()} - /> + <Box mt="1.7rem"> + <CopyDeleteDropdown + onDelete={() => deleteSection()} + onCopy={() => copySection()} + /> + </Box> )} <AccordionSummary + sx={{ + mt: "1.5rem", + }} slotProps={{ button: { - "aria-label": "Abschnitt erweitern/einklappen", + "aria-label": "Sektion erweitern/einklappen", }, }} /> </Stack> <AccordionDetails - aria-label={`Fragen zu Abschnitt ${sectionIndex}`} + aria-label={`Fragen zu Sektion ${sectionIndex}`} slotProps={{ root: { "aria-labelledby": undefined, @@ -155,10 +160,6 @@ export function ChecklistDefinitionSection({ <Stack spacing={2} style={{ marginTop: 12, marginLeft: 48 }}> <ChecklistDefinitionElementsList readOnlyMode={readOnlyMode} - elements={section.elements} - setElements={(elements) => { - setElements(elements); - }} sectionIndex={sectionIndex} /> <Stack spacing={2} direction={"row"}> diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sections/ChecklistDefinitionSectionsList.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sections/ChecklistDefinitionSectionsList.tsx index fca6e1248..80b7177bc 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sections/ChecklistDefinitionSectionsList.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sections/ChecklistDefinitionSectionsList.tsx @@ -19,6 +19,7 @@ import { FieldArray, useFormikContext } from "formik"; import { v4 as uuidv4 } from "uuid"; import { FormChecklistDefinitionVersion } from "@/lib/businessModules/inspection/api/mutations/checklistDefinition"; +import { createChecklistElement } from "@/lib/businessModules/inspection/components/checklistDefinition/helpers/helpers"; import { ChecklistDefinitionSection } from "./ChecklistDefinitionSection"; @@ -119,12 +120,7 @@ function createNewSection() { return { id: uuidv4(), title: "", - elements: [ - { - id: uuidv4(), - type: "CHECKBOX", - }, - ], + elements: [createChecklistElement("CHECKBOX")], }; } diff --git a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sidebars/history/ChecklistVersionsSidebar.tsx b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sidebars/history/ChecklistVersionsSidebar.tsx index 0eaf1798c..4c719042d 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sidebars/history/ChecklistVersionsSidebar.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/checklistDefinition/sidebars/history/ChecklistVersionsSidebar.tsx @@ -49,6 +49,7 @@ function ChecklistVersionsSidebarWithQuery({ const { data: versions } = useGetChecklistDefinitionVersions( checklistDefinition.id, ); + const nameChangeMap = computeNameChangeMap(versions); const [canEditChecklists, canEditCoreChecklists] = useHasUserRolesCheck([ ApiUserRole.InspectionChecklistdefinitionsWrite, diff --git a/employee-portal/src/lib/businessModules/inspection/components/facility/search/results/columns.tsx b/employee-portal/src/lib/businessModules/inspection/components/facility/search/results/columns.tsx index a03c89153..b2d2b9337 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/facility/search/results/columns.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/facility/search/results/columns.tsx @@ -100,7 +100,7 @@ function DropdownMenu(props: { onClickAdd: () => void }) { <ActionsMenu actionItems={[ { - label: "Zu Zentralkartei hinzufügen...", + label: "Zu Stammdaten hinzufügen...", onClick: props.onClickAdd, startDecorator: <Add />, }, diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/InspectionTabNavigationToolbar.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/InspectionTabNavigationToolbar.tsx index e8ad3311f..dd9303dca 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/InspectionTabNavigationToolbar.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/InspectionTabNavigationToolbar.tsx @@ -5,6 +5,7 @@ "use client"; +import { ApiUserRole } from "@eshg/employee-portal-api/base"; import { ApiInspectionPhase } from "@eshg/employee-portal-api/inspection"; import { OtherHousesOutlined, @@ -21,19 +22,23 @@ import { inspectionIsBeforePhase } from "@/lib/businessModules/inspection/shared import { routes } from "@/lib/businessModules/inspection/shared/routes"; import { TabNavigationItem } from "@/lib/shared/components/tabNavigation/types"; import { TabNavigationToolbar } from "@/lib/shared/components/tabNavigationToolbar/TabNavigationToolbar"; +import { useHasUserRoleCheck } from "@/lib/shared/hooks/useAccessControl"; export function InspectionTabNavigationToolbar({ inspectionId, }: Readonly<{ inspectionId: string; }>) { + const hasProcedureEditRole = useHasUserRoleCheck( + ApiUserRole.InspectionProcedureEdit, + ); const { data: inspection } = useGetInspection(inspectionId); const tabItems = createTabItems(inspectionId, inspection.phase); return ( <TabNavigationToolbar items={tabItems} - routeBack={routes.procedures.index} + routeBack={hasProcedureEditRole ? routes.procedures.index : undefined} header={<InspectionTabHeader inspection={inspection} />} afterTabs={ <OfflineSwitch diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/assignee/InspectionAssigneeSelection.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/assignee/InspectionAssigneeSelection.tsx index af0868016..8abf13f73 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/assignee/InspectionAssigneeSelection.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/assignee/InspectionAssigneeSelection.tsx @@ -6,27 +6,25 @@ import { ApiUser } from "@eshg/employee-portal-api/base"; import { ButtonLink } from "@eshg/lib-portal/components/buttons/ButtonLink"; -import { useGetAllAssignableUsers } from "@/lib/businessModules/inspection/api/queries/users"; import { AssigneeAutocompleteField } from "@/lib/businessModules/inspection/components/inspection/assignee/AssigneeAutocompleteField"; import { AssigneeInfo } from "@/lib/businessModules/inspection/components/inspection/assignee/AssigneeInfo"; import { AutocompleteSelectOption } from "@/lib/shared/components/AutocompleteSelectOptions"; import { fullName } from "@/lib/shared/components/users/userFormatter"; -export interface InspectionStaffSelectionProps { +export interface InspectionAssigneeSelectionProps { selfUser: ApiUser; onSelfAssign: () => void; currentAssigneeName: string | null; currentAssigneeId: string | null; onlySelfAssignable?: boolean; assigneeIdFieldValueName: string; + allAssignableUsers: ApiUser[]; } export function InspectionAssigneeSelection( - props: Readonly<InspectionStaffSelectionProps>, + props: Readonly<InspectionAssigneeSelectionProps>, ) { - const allAssignableUsersQuery = useGetAllAssignableUsers(); - - const assignableUsersOptions = allAssignableUsersQuery.data.map((option) => ({ + const assignableUsersOptions = props.allAssignableUsers.map((option) => ({ value: option.userId, label: fullName(option), })); diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/basedata/InspectionTabBasedata.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/basedata/InspectionTabBasedata.tsx index d321233d4..3e00345f6 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/basedata/InspectionTabBasedata.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/basedata/InspectionTabBasedata.tsx @@ -7,16 +7,20 @@ import { ApiInspectionPhase } from "@eshg/employee-portal-api/inspection"; import { Grid } from "@mui/joy"; +import { useSuspenseQueries } from "@tanstack/react-query"; import { useState } from "react"; +import { useUserApi } from "@/lib/baseModule/api/clients"; +import { useInspectionApi } from "@/lib/businessModules/inspection/api/clients"; import { useUpdateInspectionFacility } from "@/lib/businessModules/inspection/api/mutations/facility"; -import { useGetInspection } from "@/lib/businessModules/inspection/api/queries/inspection"; -import { useGetSelfUser } from "@/lib/businessModules/inspection/api/queries/users"; +import { getInspectionQuery } from "@/lib/businessModules/inspection/api/queries/inspection"; +import { getSelfUserQuery } from "@/lib/businessModules/inspection/api/queries/users"; import { InspectionTypeCard } from "@/lib/businessModules/inspection/components/inspection/basedata/InspectionTypeCard"; import { BillingAddressTile } from "@/lib/businessModules/inspection/components/inspection/basedata/billingaddress/BillingAddressTile"; import { ContactPersonTile } from "@/lib/businessModules/inspection/components/inspection/basedata/contactperson/ContactPersonTile"; import { FacilityTile } from "@/lib/businessModules/inspection/components/inspection/common/facility/FacilityTile"; import { inspectionIsBeforePhase } from "@/lib/businessModules/inspection/shared/enums"; +import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; import { LegacyFacilitySidebar, Mode, @@ -32,9 +36,22 @@ interface InspectionTabBasedataProps { export function InspectionTabBasedata({ inspectionId, }: Readonly<InspectionTabBasedataProps>) { - const { data: inspection } = useGetInspection(inspectionId); + const inspectionApi = useInspectionApi(); + const userApi = useUserApi(); + const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); + + const [{ data: inspection }, { data: selfUser }] = useSuspenseQueries({ + queries: [ + getInspectionQuery( + inspectionApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + getSelfUserQuery(userApi), + ], + }); + const { mutateAsync: updateFacility } = useUpdateInspectionFacility(); - const { data: selfUser } = useGetSelfUser(); const isOffline = useIsOffline(); const lockedByDifferentUser = diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/basedata/InspectionTypeCard.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/basedata/InspectionTypeCard.tsx index 9e442a9f3..b54df04bb 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/basedata/InspectionTypeCard.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/basedata/InspectionTypeCard.tsx @@ -10,11 +10,16 @@ import { ApiInspection } from "@eshg/employee-portal-api/inspection"; import { formatPersonName } from "@eshg/lib-portal/formatters/person"; import { SetFieldValueHelper } from "@eshg/lib-portal/types/form"; import { Divider, Grid, Stack } from "@mui/joy"; +import { useSuspenseQueries } from "@tanstack/react-query"; import { Formik } from "formik"; import { useRef } from "react"; +import { useUserApi } from "@/lib/baseModule/api/clients"; import { useUpdateInspection } from "@/lib/businessModules/inspection/api/mutations/inspection"; -import { useGetSelfUser } from "@/lib/businessModules/inspection/api/queries/users"; +import { + getAllAssignableUsersQuery, + getSelfUserQuery, +} from "@/lib/businessModules/inspection/api/queries/users"; import { InspectionAssigneeSelection } from "@/lib/businessModules/inspection/components/inspection/assignee/InspectionAssigneeSelection"; import { translateInspectionType } from "@/lib/businessModules/inspection/shared/enums"; import { OverlayBoundary } from "@/lib/shared/components/boundaries/OverlayBoundary"; @@ -92,7 +97,14 @@ function EditInspectionTypeSidebar({ inspection: ApiInspection; modalProps: SimplifiedModalProps; }>) { - const { data: selfUser } = useGetSelfUser(); + const userApi = useUserApi(); + + const [{ data: selfUser }, { data: allAssignableUsers }] = useSuspenseQueries( + { + queries: [getSelfUserQuery(userApi), getAllAssignableUsersQuery(userApi)], + }, + ); + const initialValues: { challenging: boolean; assigneeId?: string; @@ -171,6 +183,7 @@ function EditInspectionTypeSidebar({ currentAssigneeId={values.assigneeId ?? selfUser.userId} onlySelfAssignable={onlySelfAssignable} assigneeIdFieldValueName="assigneeId" + allAssignableUsers={allAssignableUsers} /> </Stack> </SidebarContent> diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/ExecutionSidePanel.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/ExecutionSidePanel.tsx index 7a0af8c4e..76bbd72fc 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/ExecutionSidePanel.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/ExecutionSidePanel.tsx @@ -7,6 +7,7 @@ import { ApiChecklist, + ApiInspection, ApiInspectionPhase, } from "@eshg/employee-portal-api/inspection"; import { scrollToFirstFormError } from "@eshg/lib-portal/components/form/FormPlus"; @@ -15,7 +16,6 @@ import EditIcon from "@mui/icons-material/EditOutlined"; import { Button, Divider, IconButton, Stack } from "@mui/joy"; import { useState } from "react"; -import { useGetInspection } from "@/lib/businessModules/inspection/api/queries/inspection"; import { AppointmentSidebar } from "@/lib/businessModules/inspection/components/inspection/common/appointment/AppointmentSidebar"; import { getFormattedAppointmentParts } from "@/lib/businessModules/inspection/components/inspection/common/appointment/appointmentUtils"; import { FinalizeInspectionModal } from "@/lib/businessModules/inspection/components/inspection/execution/FinalizeInspectionModal"; @@ -36,7 +36,7 @@ import { useIsOffline } from "@/lib/shared/hooks/useIsOffline"; interface ExecutionSidePanelProps { tabs: SidePanelNavigationTab[]; activeTabId: string; - inspectionId: string; + inspection: ApiInspection; checklists: ApiChecklist[]; onActiveTabChange: (tab: SidePanelEvent) => void; onDeleteClick?: (tab: SidePanelEvent) => void; @@ -47,15 +47,13 @@ interface ExecutionSidePanelProps { export function ExecutionSidePanel({ tabs, activeTabId, - inspectionId, + inspection, onActiveTabChange, onDeleteClick, onAddButtonClick, checklists, readOnly, }: Readonly<ExecutionSidePanelProps>) { - const { data: inspection } = useGetInspection(inspectionId); - const [approvalSidebarOpen, setApprovalSidebarOpen] = useState(false); const [approveModalOpen, setApproveModalOpen] = useState(false); diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/InspectionTabExecution.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/InspectionTabExecution.tsx index f6d7c189d..ae5fff613 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/InspectionTabExecution.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/InspectionTabExecution.tsx @@ -15,17 +15,24 @@ import { Checklist as ChecklistIcon, } from "@mui/icons-material"; import { Grid } from "@mui/joy"; -import { useQueryClient } from "@tanstack/react-query"; +import { useQueryClient, useSuspenseQueries } from "@tanstack/react-query"; import { useCallback, useEffect, useState } from "react"; +import { useUserApi } from "@/lib/baseModule/api/clients"; +import { + useChecklistApi, + useIncidentApi, + useInspectionApi, +} from "@/lib/businessModules/inspection/api/clients"; import { useUpdateInspection } from "@/lib/businessModules/inspection/api/mutations/inspection"; -import { useGetChecklists } from "@/lib/businessModules/inspection/api/queries/checklist"; -import { useGetIncidents } from "@/lib/businessModules/inspection/api/queries/incidents"; +import { getChecklistsQuery } from "@/lib/businessModules/inspection/api/queries/checklist"; +import { getIncidentsQuery } from "@/lib/businessModules/inspection/api/queries/incidents"; import { + getAvailableCLDVsQuery, + getInspectionQuery, inspectionGettersQueryKey, - useGetInspection, } from "@/lib/businessModules/inspection/api/queries/inspection"; -import { useGetSelfUser } from "@/lib/businessModules/inspection/api/queries/users"; +import { getSelfUserQuery } from "@/lib/businessModules/inspection/api/queries/users"; import { ExecutionSidePanel } from "@/lib/businessModules/inspection/components/inspection/execution/ExecutionSidePanel"; import { SidePanelEvent, @@ -36,6 +43,7 @@ import { ChecklistValidationProvider } from "@/lib/businessModules/inspection/co import { IncidentsPanel } from "@/lib/businessModules/inspection/components/inspection/execution/incident/IncidentsPanel"; import { ChecklistSelectSidebar } from "@/lib/businessModules/inspection/components/inspection/planning/checklist/ChecklistSelectSidebar"; import { inspectionIsBeforePhase } from "@/lib/businessModules/inspection/shared/enums"; +import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; import { useConfirmationDialog } from "@/lib/shared/components/confirmationDialog/ConfirmationDialogProvider"; export enum InspectionExecutionTabType { @@ -75,10 +83,42 @@ export function InspectionTabExecution({ }: Readonly<{ inspectionId: string }>) { const queryClient = useQueryClient(); - const { data: checklists } = useGetChecklists(inspectionId); - const { data: inspection } = useGetInspection(inspectionId); - const { data: incidents } = useGetIncidents(inspectionId); - const { data: selfUser } = useGetSelfUser(); + const checklistApi = useChecklistApi(); + const inspectionApi = useInspectionApi(); + const incidentApi = useIncidentApi(); + const userApi = useUserApi(); + const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); + + const [ + { data: checklists }, + { data: inspection }, + { data: incidents }, + { data: selfUser }, + ] = useSuspenseQueries({ + queries: [ + getChecklistsQuery( + checklistApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + getInspectionQuery( + inspectionApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + getIncidentsQuery( + incidentApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + getSelfUserQuery(userApi), + getAvailableCLDVsQuery( + inspectionApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + ], + }); const { mutateAsync: updateInspection } = useUpdateInspection(); const { openCancelDialog } = useConfirmationDialog(); const currentSelectedNonCoreVersions = @@ -235,7 +275,7 @@ export function InspectionTabExecution({ <ExecutionSidePanel tabs={tabsList.map((tab) => tab.SidePanelProps)} activeTabId={tabState.tabId} - inspectionId={inspectionId} + inspection={inspection} checklists={checklists} onActiveTabChange={handleActiveTabChange} onAddButtonClick={handleAddClick} diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/checklist/form/ChecklistFileElement.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/checklist/form/ChecklistFileElement.tsx index fc6e92db0..ed2413273 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/checklist/form/ChecklistFileElement.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/checklist/form/ChecklistFileElement.tsx @@ -7,8 +7,9 @@ import { ApiFileType } from "@eshg/employee-portal-api/inspection"; import { FormPlus } from "@eshg/lib-portal/components/form/FormPlus"; -import { DeleteOutlined, InfoOutlined, OpenInNew } from "@mui/icons-material"; -import { Stack, Tooltip, Typography } from "@mui/joy"; +import { DeleteOutlined, OpenInNew } from "@mui/icons-material"; +import { Stack, Typography } from "@mui/joy"; +import { useId } from "react"; import { useServerConfig } from "@/lib/baseModule/api/queries/config"; import { useConfiguration } from "@/lib/businessModules/inspection/api/clients"; @@ -20,6 +21,7 @@ import { FileCard, FileCardActionProps, } from "@/lib/shared/components/FileCard"; +import { InfoIconTooltipButton } from "@/lib/shared/components/buttons/IconTooltipButton"; import { FileField } from "@/lib/shared/components/formFields/file/FileField"; import { FileType } from "@/lib/shared/components/formFields/file/FileType"; import { FileLike } from "@/lib/shared/components/formFields/file/validators"; @@ -52,6 +54,7 @@ export function ChecklistFileElement({ readOnly = false, }: Readonly<ChecklistFileElementProps>) { const { data: config } = useServerConfig(); + const uploadTooltipTitleId = useId(); if (element.type !== "IMAGE" && element.type !== "AUDIO") { return; } @@ -112,14 +115,12 @@ export function ChecklistFileElement({ {!readOnly && ( <> <Typography level="body-sm"> - {uploadTooltipTitle} - <Tooltip title={uploadTooltipText}> - <InfoOutlined - style={{ marginRight: 8 }} - size="sm" - color="primary" - /> - </Tooltip> + <label id={uploadTooltipTitleId}>{uploadTooltipTitle}</label> + <InfoIconTooltipButton + size="sm" + iconLabelledBy={uploadTooltipTitleId} + title={uploadTooltipText} + /> </Typography> <FileField name={name} diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/checklist/form/ChecklistLabel.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/checklist/form/ChecklistLabel.tsx index c173f0e0f..a3354394b 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/checklist/form/ChecklistLabel.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/execution/checklist/form/ChecklistLabel.tsx @@ -4,12 +4,12 @@ */ import { RequiresChildren } from "@eshg/lib-portal/types/react"; -import { InfoOutlined } from "@mui/icons-material"; -import { FormHelperText, FormLabel, Stack, Tooltip } from "@mui/joy"; +import { FormHelperText, FormLabel, Stack } from "@mui/joy"; import { ReactNode } from "react"; import { isDefined, isNonNullish } from "remeda"; import { theme } from "@/lib/baseModule/theme/theme"; +import { InfoIconTooltipButton } from "@/lib/shared/components/buttons/IconTooltipButton"; interface ChecklistLabelProps extends RequiresChildren { incident?: boolean; @@ -49,13 +49,7 @@ export function ChecklistLabel(props: Readonly<ChecklistLabelProps>) { {props.children} </FormLabel> {isDefined(props.tooltipText) && ( - <Tooltip title={props.tooltipText}> - <InfoOutlined - style={{ marginRight: 8 }} - size="md" - color="primary" - /> - </Tooltip> + <InfoIconTooltipButton size="md" title={props.tooltipText} /> )} </Stack> {props.endDecorator} diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/new/AddInspectionTiles.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/new/AddInspectionTiles.tsx index 4d9b927c4..e969bdb7e 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/new/AddInspectionTiles.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/new/AddInspectionTiles.tsx @@ -5,6 +5,7 @@ "use client"; +import { ApiUser } from "@eshg/employee-portal-api/base"; import { ApiInspection, ApiObjectType, @@ -25,9 +26,13 @@ import { mapApiFacilityStateToBaseFacility } from "@/lib/shared/helpers/facility export function AddInspectionTiles({ inspection, objectTypes, + selfUser, + allAssignableUsers, }: Readonly<{ inspection: ApiInspection; objectTypes: ApiObjectType[]; + allAssignableUsers: ApiUser[]; + selfUser: ApiUser; }>) { const { mutateAsync: updateFacility } = useUpdateInspectionFacility(); const facility = inspection.facility; @@ -59,6 +64,8 @@ export function AddInspectionTiles({ procedureId={inspection.externalId} objectTypes={objectTypes} facility={facility} + selfUser={selfUser} + allAssignableUsers={allAssignableUsers} /> </Grid> </Grid> diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/new/AdditionalInfoTile.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/new/AdditionalInfoTile.tsx index 52e2e6fba..14b4f1f12 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/new/AdditionalInfoTile.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/new/AdditionalInfoTile.tsx @@ -5,7 +5,7 @@ "use client"; -import { ApiUserRole } from "@eshg/employee-portal-api/base"; +import { ApiUser, ApiUserRole } from "@eshg/employee-portal-api/base"; import { ApiInspFacility, ApiInspection, @@ -26,7 +26,6 @@ import { useRouter } from "next/navigation"; import { isNonNullish, isNullish } from "remeda"; import { useStartInspection } from "@/lib/businessModules/inspection/api/mutations/inspection"; -import { useGetSelfUser } from "@/lib/businessModules/inspection/api/queries/users"; import { InspectionAssigneeSelection } from "@/lib/businessModules/inspection/components/inspection/assignee/InspectionAssigneeSelection"; import { inspectionTypeNames } from "@/lib/businessModules/inspection/shared/enums"; import { routes } from "@/lib/businessModules/inspection/shared/routes"; @@ -47,17 +46,20 @@ interface AdditionalInfoTileProps { procedureId: string; objectTypes: ApiObjectType[]; facility: ApiInspFacility; + selfUser: ApiUser; + allAssignableUsers: ApiUser[]; } export function AdditionalInfoTile({ procedureId, objectTypes, facility, + selfUser, + allAssignableUsers, }: Readonly<AdditionalInfoTileProps>) { const onlySelfAssignable = !useHasUserRoleCheck( ApiUserRole.InspectionProcedureAssign, ); - const { data: selfUser } = useGetSelfUser(); const router = useRouter(); const TYPE_OPTIONS = buildEnumOptions(inspectionTypeNames); @@ -149,6 +151,7 @@ export function AdditionalInfoTile({ currentAssigneeId={values.assigneeId} onlySelfAssignable={onlySelfAssignable} assigneeIdFieldValueName="assigneeId" + allAssignableUsers={allAssignableUsers} /> <Divider sx={{ marginBottom: 2 }} /> <ButtonBar isSubmitting={isSubmitting} /> diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/planning/InspectionTabPlanning.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/planning/InspectionTabPlanning.tsx index c45cae8f6..c870fb242 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/planning/InspectionTabPlanning.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/planning/InspectionTabPlanning.tsx @@ -5,16 +5,23 @@ import { ApiInspection, + ApiInspectionAvailableCLDVersionsResponse, ApiInspectionFeature, ApiInspectionPhase, } from "@eshg/employee-portal-api/inspection"; import { useWindowDimensions } from "@eshg/lib-portal/hooks/useWindowDimension"; import { Box, useTheme } from "@mui/joy"; +import { useSuspenseQueries } from "@tanstack/react-query"; +import { useUserApi } from "@/lib/baseModule/api/clients"; import { headerHeightDesktop } from "@/lib/baseModule/components/layout/sizes"; +import { useInspectionApi } from "@/lib/businessModules/inspection/api/clients"; import { useIsNewFeatureEnabled } from "@/lib/businessModules/inspection/api/queries/feature"; -import { useGetInspection } from "@/lib/businessModules/inspection/api/queries/inspection"; -import { useGetSelfUser } from "@/lib/businessModules/inspection/api/queries/users"; +import { + getAvailableCLDVsQuery, + getInspectionQuery, +} from "@/lib/businessModules/inspection/api/queries/inspection"; +import { getSelfUserQuery } from "@/lib/businessModules/inspection/api/queries/users"; import { AnnouncementTile } from "@/lib/businessModules/inspection/components/inspection/planning/announcement/AnnouncementTile"; import { AppointmentTile } from "@/lib/businessModules/inspection/components/inspection/planning/appointment/AppointmentTile"; import { ChecklistTile } from "@/lib/businessModules/inspection/components/inspection/planning/checklist/ChecklistTile"; @@ -23,6 +30,7 @@ import { PacklistTile } from "@/lib/businessModules/inspection/components/inspec import { ResourceTile } from "@/lib/businessModules/inspection/components/inspection/planning/resource/ResourceTile"; import { TravelTimeTile } from "@/lib/businessModules/inspection/components/inspection/planning/traveltime/TravelTimeTile"; import { inspectionIsBeforePhase } from "@/lib/businessModules/inspection/shared/enums"; +import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; import { useIsOffline } from "@/lib/shared/hooks/useIsOffline"; interface InspectionTabPlanningProps { @@ -32,8 +40,26 @@ interface InspectionTabPlanningProps { export function InspectionTabPlanning({ inspectionId, }: Readonly<InspectionTabPlanningProps>) { - const { data: inspection } = useGetInspection(inspectionId); - const { data: selfUser } = useGetSelfUser(); + const inspectionApi = useInspectionApi(); + const userApi = useUserApi(); + const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); + + const [{ data: inspection }, { data: selfUser }, { data: availableCldvs }] = + useSuspenseQueries({ + queries: [ + getInspectionQuery( + inspectionApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + getSelfUserQuery(userApi), + getAvailableCLDVsQuery( + inspectionApi, + getPreCacheForOfflineModeHeaders, + inspectionId, + ), + ], + }); const isPacklistsEnabled = useIsNewFeatureEnabled( ApiInspectionFeature.Packlists, ); @@ -93,6 +119,7 @@ export function InspectionTabPlanning({ lockedByDifferentUser={lockedByDifferentUser} hasReachedExecuting={hasReachedExecuting} inspection={inspection} + availableCldvs={availableCldvs} /> </Box> <Box @@ -148,6 +175,7 @@ export function InspectionTabPlanning({ lockedByDifferentUser={lockedByDifferentUser} hasReachedExecuting={hasReachedExecuting} inspection={inspection} + availableCldvs={availableCldvs} /> <RightColumnElements isOffline={isOffline} @@ -172,11 +200,13 @@ function TopTwinElements({ lockedByDifferentUser, hasReachedExecuting, inspection, + availableCldvs, }: { isOffline: boolean; lockedByDifferentUser: boolean; hasReachedExecuting: boolean; inspection: ApiInspection; + availableCldvs: ApiInspectionAvailableCLDVersionsResponse; }) { return ( <> @@ -191,6 +221,7 @@ function TopTwinElements({ <ChecklistTile readonly={isOffline || lockedByDifferentUser || hasReachedExecuting} inspection={inspection} + availableCldvs={availableCldvs} /> </Box> </> diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/planning/checklist/ChecklistTile.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/planning/checklist/ChecklistTile.tsx index db21a021e..8bf36a6bf 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/planning/checklist/ChecklistTile.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/planning/checklist/ChecklistTile.tsx @@ -8,6 +8,7 @@ import { ApiFollowupType, ApiInspection, + ApiInspectionAvailableCLDVersionsResponse, ApiInspectionCLDVersion, } from "@eshg/employee-portal-api/inspection"; import { DeleteOutlined } from "@mui/icons-material"; @@ -16,10 +17,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { useUpdateInspection } from "@/lib/businessModules/inspection/api/mutations/inspection"; -import { - inspectionGettersQueryKey, - useGetAvailableCLDVs, -} from "@/lib/businessModules/inspection/api/queries/inspection"; +import { inspectionGettersQueryKey } from "@/lib/businessModules/inspection/api/queries/inspection"; import { ChecklistSelectSidebar } from "@/lib/businessModules/inspection/components/inspection/planning/checklist/ChecklistSelectSidebar"; import { useConfirmationDialog } from "@/lib/shared/components/confirmationDialog/ConfirmationDialogProvider"; import { InfoTile } from "@/lib/shared/components/infoTile/InfoTile"; @@ -28,11 +26,13 @@ import { InfoTileAddButton } from "@/lib/shared/components/infoTile/InfoTileAddB export interface ChecklistTileProps { readonly?: boolean; inspection: ApiInspection; + availableCldvs: ApiInspectionAvailableCLDVersionsResponse; } export function ChecklistTile({ readonly, inspection, + availableCldvs, }: Readonly<ChecklistTileProps>) { const queryClient = useQueryClient(); @@ -41,7 +41,6 @@ export function ChecklistTile({ .filter((version) => !version.isCoreChecklist) .map((version) => version); - const { data: availableCldvs } = useGetAvailableCLDVs(inspection.externalId); const { mutateAsync: updateInspection } = useUpdateInspection(); const { openCancelDialog } = useConfirmationDialog(); diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/reportresult/InspectionTabReportResult.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/reportresult/InspectionTabReportResult.tsx index b065e0d58..3dded7772 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/reportresult/InspectionTabReportResult.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/reportresult/InspectionTabReportResult.tsx @@ -8,8 +8,7 @@ import { Grid } from "@mui/joy"; import { useConfiguration } from "@/lib/businessModules/inspection/api/clients"; -import { useGetInspection } from "@/lib/businessModules/inspection/api/queries/inspection"; -import { useLoadEditor } from "@/lib/businessModules/inspection/api/queries/inspectionReport"; +import { useGetInspectionAndLoadEditor } from "@/lib/businessModules/inspection/api/queries/inspectionReport"; import { InspectionResultSidePanel } from "@/lib/businessModules/inspection/components/inspection/reportresult/InspectionResultSidePanel"; import { ReportApprovalButtons } from "@/lib/businessModules/inspection/components/inspection/reportresult/ReportApprovalButtons"; import { ReportDownloadButtons } from "@/lib/businessModules/inspection/components/inspection/reportresult/ReportDownloadButtons"; @@ -23,11 +22,10 @@ interface InspectionTabReportResultProps { export function InspectionTabReportResult({ inspectionId, }: Readonly<InspectionTabReportResultProps>) { - const { data: inspection } = useGetInspection(inspectionId); - const { data: editorData } = useLoadEditor( - inspection.reportId!, - inspectionId, - ); + const { + data: { inspection, editorData }, + } = useGetInspectionAndLoadEditor(inspectionId); + const elements = editorData.editorBody.elements; const { basePath } = useConfiguration(); diff --git a/employee-portal/src/lib/businessModules/inspection/components/inspection/reportresult/editor/InspectionReportEditor.tsx b/employee-portal/src/lib/businessModules/inspection/components/inspection/reportresult/editor/InspectionReportEditor.tsx index bae70aa54..4fcd89772 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/inspection/reportresult/editor/InspectionReportEditor.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/inspection/reportresult/editor/InspectionReportEditor.tsx @@ -6,17 +6,20 @@ "use client"; import { ApiEditorBodyElementsInner } from "@eshg/employee-portal-api/inspection"; +import { useSuspenseQueries } from "@tanstack/react-query"; import { v4 as uuidv4 } from "uuid"; import { useConfiguration, useEditorApi, + useTextBlockApi, } from "@/lib/businessModules/inspection/api/clients"; import { - useGetTextBlocks, - useLoadEditor, + getTextBlocksQuery, + loadEditorQuery, } from "@/lib/businessModules/inspection/api/queries/inspectionReport"; import { ReportDownloadButtons } from "@/lib/businessModules/inspection/components/inspection/reportresult/ReportDownloadButtons"; +import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; import { StickyBottomButtonBar } from "@/lib/shared/components/buttons/StickyBottomButtonBar"; import { ContentEditor } from "@/lib/shared/components/contentEditor/ContentEditor"; import { @@ -31,9 +34,22 @@ export function InspectionReportEditor({ reportId: string; inspectionId: string; }>) { - const { data: editorData } = useLoadEditor(reportId, inspectionId); - const { data: textBlocks } = useGetTextBlocks(); const editorApi = useEditorApi(); + const textBlockApi = useTextBlockApi(); + const getPreCacheForOfflineModeHeaders = useGetHeadersForOfflineCaching(); + + const [{ data: editorData }, { data: textBlocks }] = useSuspenseQueries({ + queries: [ + loadEditorQuery( + editorApi, + getPreCacheForOfflineModeHeaders, + reportId, + inspectionId, + ), + getTextBlocksQuery(textBlockApi), + ], + }); + const { basePath } = useConfiguration(); const palette: PaletteItem[] = textBlocks.map((textBlock) => { diff --git a/employee-portal/src/lib/businessModules/inspection/components/objectType/ObjectTypesTable.tsx b/employee-portal/src/lib/businessModules/inspection/components/objectType/ObjectTypesTable.tsx index 8e93103d6..88dc91f05 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/objectType/ObjectTypesTable.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/objectType/ObjectTypesTable.tsx @@ -70,7 +70,7 @@ export function ObjectTypesTable() { </IconButton> ), meta: { - width: "max-content", + cellStyle: "button", }, }), ]; diff --git a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/CreateOrEditPacklistDefinitionSidebar.tsx b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/CreateOrEditPacklistDefinitionSidebar.tsx index e708c279c..d9c68267d 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/CreateOrEditPacklistDefinitionSidebar.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/CreateOrEditPacklistDefinitionSidebar.tsx @@ -5,7 +5,10 @@ "use client"; -import { ApiPacklistDefinitionRevision } from "@eshg/employee-portal-api/inspection"; +import { + ApiObjectType, + ApiPacklistDefinitionRevision, +} from "@eshg/employee-portal-api/inspection"; import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; import { Stack } from "@mui/joy"; import { Formik } from "formik"; @@ -44,6 +47,7 @@ interface CreateOrEditPacklistDefinitionSidebarProps { version: number, revisionId: string, ) => void; + objectTypes: ApiObjectType[]; } export function CreateOrEditPacklistDefinitionSidebar( @@ -66,6 +70,7 @@ export function CreateOrEditPacklistDefinitionSidebarWithQueriesAndMutations({ readonly, title, onClickNewRevision, + objectTypes, }: Readonly<CreateOrEditPacklistDefinitionSidebarProps>) { const router = useRouter(); const sidebarFormRef = useRef<SidebarFormHandle>(null); @@ -146,6 +151,7 @@ export function CreateOrEditPacklistDefinitionSidebarWithQueriesAndMutations({ <PacklistDefinitionHeaderCard readOnlyMode={readOnlyMode} revision={pldRevision?.revision} + objectTypes={objectTypes} /> <PacklistDefinitionElementsList readOnlyMode={readOnlyMode} /> </Stack> diff --git a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/CreatePacklistDefinitionSidebar.tsx b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/CreatePacklistDefinitionSidebar.tsx index d61773489..58dc8b902 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/CreatePacklistDefinitionSidebar.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/CreatePacklistDefinitionSidebar.tsx @@ -5,6 +5,7 @@ "use client"; +import { useGetObjectTypes } from "@/lib/businessModules/inspection/api/queries/objectTypes"; import { CreateOrEditPacklistDefinitionSidebar } from "@/lib/businessModules/inspection/components/packlistDefinition/CreateOrEditPacklistDefinitionSidebar"; interface CreatePacklistDefinitionSidebarProps { @@ -14,11 +15,13 @@ interface CreatePacklistDefinitionSidebarProps { export function CreatePacklistDefinitionSidebar({ onClose, }: Readonly<CreatePacklistDefinitionSidebarProps>) { + const { data: objectTypes } = useGetObjectTypes(); return ( <CreateOrEditPacklistDefinitionSidebar open onClose={onClose} title={"Packliste erstellen"} + objectTypes={objectTypes} /> ); } diff --git a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/EditPacklistDefinitionSidebar.tsx b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/EditPacklistDefinitionSidebar.tsx index bdbe2dd55..48523f5af 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/EditPacklistDefinitionSidebar.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/EditPacklistDefinitionSidebar.tsx @@ -5,7 +5,14 @@ "use client"; -import { useGetPacklistDefinitionRevision } from "@/lib/businessModules/inspection/api/queries/packlistDefinition"; +import { useSuspenseQueries } from "@tanstack/react-query"; + +import { + useObjectTypeApi, + usePacklistDefinitionApi, +} from "@/lib/businessModules/inspection/api/clients"; +import { getObjectTypesQuery } from "@/lib/businessModules/inspection/api/queries/objectTypes"; +import { getPacklistDefinitionRevisionQuery } from "@/lib/businessModules/inspection/api/queries/packlistDefinition"; import { CreateOrEditPacklistDefinitionSidebar } from "@/lib/businessModules/inspection/components/packlistDefinition/CreateOrEditPacklistDefinitionSidebar"; interface EditPacklistDefinitionSidebarProps { @@ -27,8 +34,17 @@ export function EditPacklistDefinitionSidebar({ version, onClickNewRevision, }: Readonly<EditPacklistDefinitionSidebarProps>) { - const { data: packlistRevision } = - useGetPacklistDefinitionRevision(revisionId); + const objectTypeApi = useObjectTypeApi(); + const packlistDefinitionApi = usePacklistDefinitionApi(); + + const [{ data: packlistRevision }, { data: objectTypes }] = + useSuspenseQueries({ + queries: [ + getPacklistDefinitionRevisionQuery(packlistDefinitionApi, revisionId), + getObjectTypesQuery(objectTypeApi), + ], + }); + return ( <CreateOrEditPacklistDefinitionSidebar open @@ -42,6 +58,7 @@ export function EditPacklistDefinitionSidebar({ } onClickNewRevision={onClickNewRevision} version={version} + objectTypes={objectTypes} /> ); } diff --git a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/elements/PacklistDefinitionElement.tsx b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/elements/PacklistDefinitionElement.tsx index 41da112e5..852283d1f 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/elements/PacklistDefinitionElement.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/elements/PacklistDefinitionElement.tsx @@ -51,7 +51,7 @@ export function PacklistDefinitionElement({ > <div {...dragHandleProps} - aria-label={`Element ${defaultIndex} Ziehen und Verschieben`} + aria-label={`Element ${defaultIndex} ziehen und verschieben`} role="button" style={{ display: "flex", diff --git a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/header/PacklistDefinitionHeaderCard.tsx b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/header/PacklistDefinitionHeaderCard.tsx index 37cc82add..3dae8a272 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/header/PacklistDefinitionHeaderCard.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/packlistDefinition/header/PacklistDefinitionHeaderCard.tsx @@ -3,26 +3,26 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiObjectType } from "@eshg/employee-portal-api/inspection"; import { InputField } from "@eshg/lib-portal/components/formFields/InputField"; import { SelectField } from "@eshg/lib-portal/components/formFields/SelectField"; import { Stack } from "@mui/joy"; import { useMemo } from "react"; import { isDefined } from "remeda"; -import { useGetObjectTypes } from "@/lib/businessModules/inspection/api/queries/objectTypes"; import { TextareaField } from "@/lib/shared/components/formFields/TextareaField"; interface PacklistDefinitionHeaderCardProps { readOnlyMode: boolean; revision: number | undefined; + objectTypes: ApiObjectType[]; } export function PacklistDefinitionHeaderCard({ readOnlyMode, revision, + objectTypes, }: Readonly<PacklistDefinitionHeaderCardProps>) { - const { data: objectTypes } = useGetObjectTypes(); - const objectTypeOptions = useMemo( () => objectTypes.map((item) => ({ diff --git a/employee-portal/src/lib/businessModules/inspection/components/repository/ChecklistDefinitionRepoOverviewTable.tsx b/employee-portal/src/lib/businessModules/inspection/components/repository/ChecklistDefinitionRepoOverviewTable.tsx index 50b4bb017..1ff025d99 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/repository/ChecklistDefinitionRepoOverviewTable.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/repository/ChecklistDefinitionRepoOverviewTable.tsx @@ -38,6 +38,7 @@ const INITIAL_USER_ACTIVITY: UserActivityState = { type: "view-table" }; export function ChecklistDefinitionRepoOverviewTable() { const { data: repoMetadataList, isFetching } = useGetNewestChecklistDefinitionsFromCentralRepo(); + const { openCancelDialog } = useConfirmationDialog(); const [canEditCoreCld, canEditCld, canDeleteCld] = useHasUserRolesCheck([ ApiUserRole.InspectionCorechecklistdefinitionsEdit, diff --git a/employee-portal/src/lib/businessModules/inspection/components/textBlock/TextBlocksTable.tsx b/employee-portal/src/lib/businessModules/inspection/components/textBlock/TextBlocksTable.tsx index 66bccd6ee..dd723a6b9 100644 --- a/employee-portal/src/lib/businessModules/inspection/components/textBlock/TextBlocksTable.tsx +++ b/employee-portal/src/lib/businessModules/inspection/components/textBlock/TextBlocksTable.tsx @@ -5,17 +5,13 @@ "use client"; -import { - ApiTextBlock, - GetTextBlocksRequest, -} from "@eshg/employee-portal-api/inspection"; +import { ApiTextBlock } from "@eshg/employee-portal-api/inspection"; import { Add, DeleteOutlined, Edit } from "@mui/icons-material"; import { Button } from "@mui/joy"; import { createColumnHelper } from "@tanstack/react-table"; import { useState } from "react"; import { useDeleteTextBlock } from "@/lib/businessModules/inspection/api/mutations/textblocks"; -import { useGetTextBlocks } from "@/lib/businessModules/inspection/api/queries/textblocks"; import { EditTextBlockSidebar } from "@/lib/businessModules/inspection/components/textBlock/EditTextBlockSidebar"; import { ActionsMenu } from "@/lib/shared/components/buttons/ActionsMenu"; import { ButtonBar } from "@/lib/shared/components/buttons/ButtonBar"; @@ -30,7 +26,9 @@ import { useTableControl } from "@/lib/shared/hooks/searchParams/useTableControl const columnHelper = createColumnHelper<ApiTextBlock>(); interface TextBlocksTableProps { - params: GetTextBlocksRequest; + elements: ApiTextBlock[]; + totalNumberOfElements: number; + isFetching: boolean; } interface TextBlockTableState { @@ -38,11 +36,11 @@ interface TextBlockTableState { textBlock: ApiTextBlock; } -export function TextBlocksTable({ params }: TextBlocksTableProps) { - const { - data: { elements, totalNumberOfElements }, - isFetching, - } = useGetTextBlocks(params); +export function TextBlocksTable({ + elements, + totalNumberOfElements, + isFetching, +}: TextBlocksTableProps) { const deleteTextBlock = useDeleteTextBlock(); const { openConfirmationDialog } = useConfirmationDialog(); diff --git a/employee-portal/src/lib/businessModules/inspection/shared/offline/ServiceWorkerProvider.tsx b/employee-portal/src/lib/businessModules/inspection/shared/offline/ServiceWorkerProvider.tsx index 21bafb924..d25b9450e 100644 --- a/employee-portal/src/lib/businessModules/inspection/shared/offline/ServiceWorkerProvider.tsx +++ b/employee-portal/src/lib/businessModules/inspection/shared/offline/ServiceWorkerProvider.tsx @@ -14,9 +14,13 @@ import { useMemo, useState, } from "react"; +import { isEmpty } from "remeda"; import { RegisterServiceWorker } from "@/lib/businessModules/inspection/shared/offline/RegisterServiceWorker"; -import { deleteInspectionFromAllCaches } from "@/lib/businessModules/inspection/shared/offline/deleteInspectionFromAllCaches"; +import { + deleteAllEncryptedCaches, + deleteInspectionFromAllCaches, +} from "@/lib/businessModules/inspection/shared/offline/deleteInspectionFromAllCaches"; import { getInspectionIdsOfProcedureBaseDataRequests, useGetPrecachedInspections, @@ -94,12 +98,11 @@ function ServiceWorkerProviderInner({ ); if (remove.length > 0) { setDeleting(true); - void Promise.all(remove.map(deleteInspectionFromAllCaches)).then( - () => setDeleting(false), - (reason) => { + deleteFromCache(remove, isEmpty(desiredPrecachedInspectionIds)) + .catch((reason) => { throw reason; - }, - ); + }) + .finally(() => setDeleting(false)); } }, [actualPrecachedInspectionIds, desiredPrecachedInspectionIds]); @@ -180,3 +183,10 @@ const EMPTY_CONTEXT: ServiceWorker = { async function sendMessageToServiceWorker(message: object) { return (await window.workbox?.messageSW(message)) as unknown; } + +async function deleteFromCache(remove: string[], removeAll: boolean) { + await Promise.all(remove.map(deleteInspectionFromAllCaches)); + if (removeAll) { + await deleteAllEncryptedCaches(); + } +} diff --git a/employee-portal/src/lib/businessModules/inspection/shared/offline/usePrecacheInspections.ts b/employee-portal/src/lib/businessModules/inspection/shared/offline/usePrecacheInspections.ts index 7d916ca31..f0708afcc 100644 --- a/employee-portal/src/lib/businessModules/inspection/shared/offline/usePrecacheInspections.ts +++ b/employee-portal/src/lib/businessModules/inspection/shared/offline/usePrecacheInspections.ts @@ -6,6 +6,7 @@ "use client"; import { + ApiErrorCode, ApiUserRole, BaseFeatureTogglesApi, ConfigApi, @@ -13,6 +14,7 @@ import { UserApi, } from "@eshg/employee-portal-api/base"; import { + ApiInspection, ApiInspectionFeature, ChecklistApi, EditorApi, @@ -25,7 +27,10 @@ import { ProgressEntryApi, } from "@eshg/employee-portal-api/inspection"; import { queryKeyFactory } from "@eshg/lib-portal/api/queryKeyFactory"; +import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; +import { resolveError } from "@eshg/lib-portal/errorHandling/errorResolvers"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; +import { useMemo } from "react"; import { isNonNullish } from "remeda"; import { @@ -66,6 +71,7 @@ import { } from "@/lib/businessModules/inspection/api/queries/inspection"; import { getPacklistsQueryKey } from "@/lib/businessModules/inspection/api/queries/packlist"; import { moduleUserGroup } from "@/lib/businessModules/inspection/shared/moduleUserGroup"; +import { useServiceWorker } from "@/lib/businessModules/inspection/shared/offline/ServiceWorkerProvider"; import { chunkArray } from "@/lib/businessModules/inspection/shared/offline/password/chunkArray"; import { useGetHeadersForOfflineCaching } from "@/lib/businessModules/inspection/shared/offline/useGetHeadersForOfflineCaching"; import { routes as inspectionRoutes } from "@/lib/businessModules/inspection/shared/routes"; @@ -113,31 +119,68 @@ export function usePrecacheInspections(inspectionIds: string[]) { ApiInspectionFeature.Packlists, ); + const { removeFromPrecache } = useServiceWorker(); + const snackbar = useSnackbar(); + // execute async call in this synchronous hook - prefetchAll({ - inspectionIds, + useMemo(() => { + prefetchAll({ + inspectionIds, + cacheHeaders, + queryClient, + configApi, + departmentApi, + userApi, + baseFeatureTogglesApi, + inspectionFeatureTogglesApi, + inspectionApi, + checklistApi, + incidentApi, + editorApi, + fileApi, + progressEntryApi, + procedureApi, + packlistApi, + fetchApprovalRequests, + isPacklistsEnabled, + }) + .then((inspectionIdToRemove) => { + if (inspectionIdToRemove) { + snackbar.error( + "Begehung " + + inspectionIdToRemove + + " existiert nicht. Wird aus dem Offline-Cache entfernt.", + ); + removeFromPrecache(inspectionIdToRemove); + } + }) + .catch((reason) => { + // eslint-disable-next-line no-console + console.error("error pre-fetching offline data", reason); + throw reason; // will be caught by the ErrorBoundary in RegisterServiceWorker.tsx + }); + }, [ + baseFeatureTogglesApi, cacheHeaders, - queryClient, + checklistApi, configApi, departmentApi, - userApi, - baseFeatureTogglesApi, - inspectionFeatureTogglesApi, - inspectionApi, - checklistApi, - incidentApi, editorApi, - fileApi, - progressEntryApi, - procedureApi, - packlistApi, fetchApprovalRequests, + fileApi, + incidentApi, + inspectionApi, + inspectionFeatureTogglesApi, isPacklistsEnabled, - }).catch((reason) => { - // eslint-disable-next-line no-console - console.error("error pre-fetching offline data", reason); - throw reason; // will be caught by the ErrorBoundary in RegisterServiceWorker.tsx - }); + packlistApi, + procedureApi, + progressEntryApi, + queryClient, + removeFromPrecache, + snackbar, + userApi, + inspectionIds, + ]); } async function prefetchAll({ @@ -179,75 +222,35 @@ async function prefetchAll({ fetchApprovalRequests: boolean; isPacklistsEnabled: boolean; }) { - const promises: Promise<unknown>[] = []; - - // 1. pre-fetch api requests - // 1.1 pre-fetch useServerConfig() - promises.push( - queryClient.fetchQuery({ - queryKey: configApiQueryKey(["getConfig"]), - queryFn: () => configApi.getConfigRaw(cacheHeaders()), - }), - ); - // 1.2 pre-fetch useGetDepartment() - promises.push( - queryClient.fetchQuery({ - queryKey: getDepartmentQueryKey(), - queryFn: () => departmentApi.getDepartmentInfo(cacheHeaders()), - }), - ); - // 1.3 pre-fetch useGetUsersByGroupQuery(moduleUserGroup.group) - promises.push( - queryClient.fetchQuery({ - queryKey: userApiQueryKey(["getUsersByGroup", moduleUserGroup.group]), - queryFn: () => - userApi.getUsersByGroup(moduleUserGroup.group, cacheHeaders()), - }), - ); - // 1.4 pre-fetch useGetBaseFeatureToggle - promises.push( - queryClient.fetchQuery({ - queryKey: baseFeatureTogglesApiQueryKey(["getFeatureToggles"]), - queryFn: () => baseFeatureTogglesApi.getFeatureToggles(cacheHeaders()), - }), - ); - // 1.5 pre-fetch inspection's useFeatureToggleQuery - promises.push( - queryClient.fetchQuery({ - queryKey: inspectionFeatureTogglesApiQueryKey(["getFeatureToggles"]), - queryFn: () => - inspectionFeatureTogglesApi.getFeatureToggles(cacheHeaders()), - }), - ); - - // 2. pre-fetch general pages - const procedureIndexUrl = inspectionRoutes.procedures.index; - promises.push(...precachePage(procedureIndexUrl, cacheHeaders())); - promises.push(...precachePage("/~offline", cacheHeaders())); - - // 3. execute all promises gathered so far - await executeInChunks(promises); - - // 4. pre-fetch inspection procedure related queries + // 1. pre-fetch inspection procedure related queries for (const inspectionId of inspectionIds) { const inspPromises: Promise<unknown>[] = []; const headers = cacheHeaders(inspectionId); - // 4.1 pre-fetch useGetInspection() + // 1.1 pre-fetch useGetInspection() // this gets executed immediately because we need the response - const inspection = await queryClient.fetchQuery({ - queryKey: getInspectionQueryKey(inspectionId), - queryFn: () => inspectionApi.getInspection(inspectionId, headers), - }); + let inspection: ApiInspection; + try { + inspection = await queryClient.fetchQuery({ + queryKey: getInspectionQueryKey(inspectionId), + queryFn: () => inspectionApi.getInspection(inspectionId, headers), + }); + } catch (error) { + const { originalErrorCode } = resolveError(error); + if (originalErrorCode === ApiErrorCode.NotFound) { + return inspectionId; + } + throw error; + } - // 4.2 pre-fetch useGetChecklists() + // 1.2 pre-fetch useGetChecklists() // this gets executed immediately because we need the response const checklists = await queryClient.fetchQuery({ queryKey: getChecklistsQueryKey(inspectionId), queryFn: () => checklistApi.getChecklists(inspectionId, headers), }); - // 4.3 pre-fetch useGetAvailableCLDVs() + // 1.3 pre-fetch useGetAvailableCLDVs() inspPromises.push( queryClient.fetchQuery({ queryKey: getAvailableCLDVsQueryKey(inspectionId), @@ -255,7 +258,7 @@ async function prefetchAll({ }), ); - // 4.4 pre-fetch useGetIncidents() + // 1.4 pre-fetch useGetIncidents() inspPromises.push( queryClient.fetchQuery({ queryKey: incidentsApiQueryKey(["getIncidents", { inspectionId }]), @@ -263,7 +266,7 @@ async function prefetchAll({ }), ); - // 4.5 pre-fetch useLoadEditor(reportId, inspectionId) and downloadReport + // 1.5 pre-fetch useLoadEditor(reportId, inspectionId) and downloadReport if (isNonNullish(inspection.reportId)) { inspPromises.push( queryClient.fetchQuery({ @@ -277,7 +280,7 @@ async function prefetchAll({ ); } - // 4.6 pre-fetch download report file + // 1.6 pre-fetch download report file if (isNonNullish(inspection.reportInfo)) { inspPromises.push( fileApi.downloadFileRaw( @@ -287,7 +290,7 @@ async function prefetchAll({ ); } - // 4.7 pre-fetch checklist image and audio files + // 1.7 pre-fetch checklist image and audio files checklists.checklists .flatMap(({ sections }) => sections) .flatMap(({ elements }) => elements) @@ -305,7 +308,7 @@ async function prefetchAll({ inspPromises.push(checklistApi.checklistGetFile(audioID, headers)); }); - // 4.8 pre-fetch useFetchProgressEntries() aka useFetchProgressEntriesTemplate() + // 1.8 pre-fetch useFetchProgressEntries() aka useFetchProgressEntriesTemplate() const pgQueryKey = queryKeyFactory( progressEntryApiQueryKey([ "fetchProgressEntries", @@ -314,7 +317,7 @@ async function prefetchAll({ `${fetchApprovalRequests}`, ]), ); - // 4.8.1 pre-fetch progress entries + // 1.8.1 pre-fetch progress entries // this gets executed immediately because we need the response const pgResponse = await queryClient.fetchQuery({ queryKey: pgQueryKey(["progressEntries"]), @@ -332,7 +335,7 @@ async function prefetchAll({ headers, ), }); - // 4.8.2 pre-fetch useFetchProgressEntryDetailsTemplate() + // 1.8.2 pre-fetch useFetchProgressEntryDetailsTemplate() for (const { progressEntryId: entryId } of pgResponse.progressEntries) { inspPromises.push( queryClient.fetchQuery({ @@ -356,7 +359,7 @@ async function prefetchAll({ ), ); } - // 4.8.3 pre-fetch progress entries file details + // 1.8.3 pre-fetch progress entries file details inspPromises.push( queryClient.fetchQuery({ queryKey: pgQueryKey(["procedureFileDetails"]), @@ -364,14 +367,14 @@ async function prefetchAll({ procedureApi.getProcedureFileDetails(inspectionId, headers), }), ); - // 4.8.4 pre-fetch progress entries procedure details + // 1.8.4 pre-fetch progress entries procedure details inspPromises.push( queryClient.fetchQuery({ queryKey: pgQueryKey(["detailedProcedure"]), queryFn: () => procedureApi.getDetailedProcedure(inspectionId, headers), }), ); - // 4.8.5 fetch progress entries approval requests if needed + // 1.8.5 fetch progress entries approval requests if needed if (fetchApprovalRequests) { inspPromises.push( queryClient.fetchQuery({ @@ -382,7 +385,7 @@ async function prefetchAll({ ); } - // 4.9 pre-fetch useGetPacklists() + // 1.9 pre-fetch useGetPacklists() if (isPacklistsEnabled) { inspPromises.push( queryClient.fetchQuery({ @@ -390,7 +393,7 @@ async function prefetchAll({ queryFn: () => packlistApi.getPacklists(inspectionId, headers), }), ); - // 4.9.1 pre-fetch useGetAvailablePLDRs() + // 1.9.1 pre-fetch useGetAvailablePLDRs() inspPromises.push( queryClient.fetchQuery({ queryKey: getAvailablePLDRsQueryKey(inspectionId), @@ -399,7 +402,7 @@ async function prefetchAll({ ); } - // 4.10 pre-fetch inspection related pages + // 1.10 pre-fetch inspection related pages const pages = [ inspectionRoutes.procedures.basedata, inspectionRoutes.procedures.planning, @@ -412,9 +415,74 @@ async function prefetchAll({ inspPromises.push(...precachePage(page(inspectionId), headers)); }); - // 4.11 execute all inspection related promises + // 1.11 execute all inspection related promises await executeInChunks(inspPromises); } + + const promises: Promise<unknown>[] = []; + + // 2. pre-fetch general api requests + // 2.1 pre-fetch useServerConfig() + promises.push( + queryClient.fetchQuery({ + queryKey: configApiQueryKey(["getConfig"]), + queryFn: () => configApi.getConfigRaw(cacheHeaders()), + }), + ); + // 2.2 pre-fetch useGetDepartment() + promises.push( + queryClient.fetchQuery({ + queryKey: getDepartmentQueryKey(), + queryFn: () => departmentApi.getDepartmentInfo(cacheHeaders()), + }), + ); + // 2.3 pre-fetch useGetUsersByGroupQuery(moduleUserGroup.group) + promises.push( + queryClient.fetchQuery({ + queryKey: userApiQueryKey(["getUsersByGroup", moduleUserGroup.group]), + queryFn: () => + userApi.getUsersByGroup(moduleUserGroup.group, cacheHeaders()), + }), + ); + // 2.4 pre-fetch useGetSelfUser + promises.push( + queryClient.fetchQuery({ + queryKey: userApiQueryKey(["getSelfUser"]), + queryFn: () => userApi.getSelfUser(cacheHeaders()), + }), + ); + // 2.5 pre-fetch getSelfUserPermissions + promises.push( + queryClient.fetchQuery({ + queryKey: userApiQueryKey(["getSelfUserPermissions"]), + queryFn: () => userApi.getSelfUserPermissions(cacheHeaders()), + }), + ); + // 2.6 pre-fetch useGetBaseFeatureToggle + promises.push( + queryClient.fetchQuery({ + queryKey: baseFeatureTogglesApiQueryKey(["getFeatureToggles"]), + queryFn: () => baseFeatureTogglesApi.getFeatureToggles(cacheHeaders()), + }), + ); + // 2.7 pre-fetch inspection's useFeatureToggleQuery + promises.push( + queryClient.fetchQuery({ + queryKey: inspectionFeatureTogglesApiQueryKey(["getFeatureToggles"]), + queryFn: () => + inspectionFeatureTogglesApi.getFeatureToggles(cacheHeaders()), + }), + ); + + // 3. pre-fetch general pages + const procedureIndexUrl = inspectionRoutes.procedures.index; + promises.push(...precachePage(procedureIndexUrl, cacheHeaders())); + promises.push(...precachePage("/~offline", cacheHeaders())); + + // 4. execute all promises for general requests + await executeInChunks(promises); + + return undefined; } function precachePage(url: string, preCacheForOfflineModeHeaders: RequestInit) { diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/AppointmentBlockGroupForm.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/AppointmentBlockGroupForm.tsx index 33765daf8..47a6fca24 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/AppointmentBlockGroupForm.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/AppointmentBlockGroupForm.tsx @@ -14,11 +14,11 @@ import { AppointmentBlockGroupValues, AppointmentCountWithDays, } from "@/lib/shared/components/appointmentBlocks/AppointmentCountWithDays"; +import { validateAppointmentBlock } from "@/lib/shared/components/appointmentBlocks/validateAppointmentBlock"; import { FormButtonBar } from "@/lib/shared/components/form/FormButtonBar"; import { FormSheet } from "@/lib/shared/components/form/FormSheet"; import { validateFieldArray } from "@/lib/shared/helpers/validators"; -import { validateAppointmentBlock } from "./ValidateAppointmentBlock"; import { APPOINTMENT_TYPE_OPTIONS } from "./options"; function validateForm( @@ -60,8 +60,8 @@ export function AppointmentBlockGroupForm( } > {({ values, isSubmitting, handleSubmit }) => ( - <FormSheet onSubmit={handleSubmit}> - <Stack gap={4}> + <FormSheet gap={5} onSubmit={handleSubmit}> + <Stack gap={5}> <AppointmentBlockGroupFields appointmentBlocksWithDays={values.appointmentBlocks} options={APPOINTMENT_TYPE_OPTIONS} diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/AppointmentBlockGroupsTable.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/AppointmentBlockGroupsTable.tsx index 76c4275aa..cf0fde3b1 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/AppointmentBlockGroupsTable.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/AppointmentBlockGroupsTable.tsx @@ -251,7 +251,11 @@ export function AppointmentBlockGroupsTable( toAggregatedAppointmentBlockRow, ); return ( - <TablePage fullHeight controls={props.controls}> + <TablePage + fullHeight + controls={props.controls} + data-testid="appointmentBlockGroupsTable" + > <TableSheet footer={ <Pagination diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/ValidateAppointmentBlock.ts b/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/ValidateAppointmentBlock.ts deleted file mode 100644 index d9454a60b..000000000 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/appointmentBlocks/ValidateAppointmentBlock.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ApiAppointmentType } from "@eshg/employee-portal-api/schoolEntry"; -import { isEmptyString } from "@eshg/lib-portal/helpers/guards"; -import { OptionalFieldValue } from "@eshg/lib-portal/types/form"; -import { differenceInCalendarDays, isBefore, isEqual, isPast } from "date-fns"; -import { FormikErrors } from "formik"; -import { isEmpty } from "remeda"; - -import { AppointmentDurationsMeasles } from "@/lib/businessModules/measlesProtection/api/models/AppointmentBlockGroup"; -import { AppointmentBlockGroupValuesWithDays } from "@/lib/shared/components/appointmentBlocks/AppointmentBlockFormWithDays"; -import { - calculateAppointmentsPerBlock, - getAppointmentDurationInMinutes, -} from "@/lib/shared/components/appointmentBlocks/AppointmentCountWithDays"; -import { toLocalDateTime } from "@/lib/shared/helpers/dateTime"; - -const MAX_DAYS_IN_APPOINTMENT_BLOCK = 31; - -export function validateAppointmentBlock( - type: OptionalFieldValue<ApiAppointmentType>, - appointmentBlock: AppointmentBlockGroupValuesWithDays, - appointmentDurationsMeasles: AppointmentDurationsMeasles, -) { - const { startDate, endDate, startTime, endTime, daysOfWeek } = - appointmentBlock; - const errors: FormikErrors<AppointmentBlockGroupValuesWithDays> = {}; - if ( - isEmpty(startDate) || - isEmpty(endDate) || - isEmpty(startTime) || - isEmpty(endTime) || - !daysOfWeek.length - ) { - return errors; - } - - const start = toLocalDateTime(startDate, startTime); - - if (isPast(start)) { - errors.startTime = "Die Startzeit liegt in der Vergangenheit."; - } - - const end = toLocalDateTime(endDate, endTime); - const dailyStartTime = toLocalDateTime(startDate, startTime); - const dailyEndTime = toLocalDateTime(startDate, endTime); - - if (isEqual(dailyStartTime, dailyEndTime)) { - errors.endTime = "Die Endzeit ist identisch zur Startzeit."; - } else if (isBefore(dailyEndTime, dailyStartTime)) { - errors.endTime = "Die Endzeit liegt vor der Startzeit."; - } else if (isBefore(end, start)) { - errors.endDate = "Das Enddatum liegt vor dem Startdatum."; - } else if ( - differenceInCalendarDays(endDate, startDate) > MAX_DAYS_IN_APPOINTMENT_BLOCK - ) { - errors.endDate = `Der Datumsbereich für einen Terminblock ist auf ${MAX_DAYS_IN_APPOINTMENT_BLOCK} Tage begrenzt.`; - } else if ( - !isEmptyString(type) && - calculateAppointmentsPerBlock( - ApiAppointmentType.ProofSubmission, - start, - end, - appointmentDurationsMeasles, - ) === 0 - ) { - const appointmentDurationInMinutes = getAppointmentDurationInMinutes( - type, - appointmentDurationsMeasles, - ); - errors.endTime = `Die Dauer ist nicht teilbar durch die Terminlänge von ${appointmentDurationInMinutes} Minuten.`; - } - - return errors; -} diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AdditionalInfoSection.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AdditionalInfoSection.tsx index c5a4b0e5b..c2970814a 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AdditionalInfoSection.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AdditionalInfoSection.tsx @@ -21,11 +21,14 @@ import { import { ReopenProcedureModal } from "@/lib/businessModules/measlesProtection/components/procedures/proceduresTable/ReopenProcedureModal"; import { useProceduresContext } from "@/lib/businessModules/measlesProtection/shared/ProceduresContext"; import { ConfirmationDialog } from "@/lib/shared/components/confirmationDialog/ConfirmationDialog"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; import { useSearchParam } from "@/lib/shared/hooks/searchParams/useSearchParam"; import { AdditionalInfoUpdateSidebar } from "./AdditionalInfoUpdateSidebar"; -import { DetailCard } from "./DetailCard"; -import { LabeledValue, ValueList } from "./LabeledValue"; import { CLOSE_PROCEDURE_SUCCESS_MESSAGE } from "./helpers"; type AdditionalInfoSectionProps = Readonly<{ @@ -70,7 +73,7 @@ export function AdditionalInfoSection({ return ( <Stack rowGap={2}> - <DetailCard title={"Zusatzinfos"} actionButton={editAction}> + <DetailsCard title={"Zusatzinfos"} actionButton={editAction}> <ValueList> <LabeledValue label="Personenstatus" @@ -99,7 +102,7 @@ export function AdditionalInfoSection({ /> ) : null} </ValueList> - </DetailCard> + </DetailsCard> <Sheet component="section"> {!procedure.isOpen ? ( <Button color="danger" onClick={handleReopenProcedure} fullWidth> diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AddressDetails.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AddressDetails.tsx index 494e04f21..f924631cd 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AddressDetails.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AddressDetails.tsx @@ -7,13 +7,15 @@ import { ApiDomesticAddress, ApiPostboxAddress, } from "@eshg/employee-portal-api/measlesProtection"; +import { Row } from "@eshg/lib-portal/components/Row"; -import { Row } from "@/lib/shared/Row"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; import { BaseAddress } from "@/lib/shared/helpers/address"; import { translateCountry } from "@/lib/shared/helpers/i18n"; -import { LabeledValue, ValueList } from "./LabeledValue"; - interface AddressDetailsProps { address?: BaseAddress; } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AffectedPerson.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AffectedPerson.tsx index 402985c37..639d80185 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AffectedPerson.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AffectedPerson.tsx @@ -11,9 +11,9 @@ import { } from "@eshg/employee-portal-api/measlesProtection"; import { PersonDetails } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/PersonDetails"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; import { ContactDetails } from "./ContactDetails"; -import { DetailCard } from "./DetailCard"; export function AffectedPerson({ procedure, @@ -24,9 +24,9 @@ export function AffectedPerson({ const person = procedure.affectedPerson; return ( - <DetailCard data-testid="affectedPersonSection" title={title}> + <DetailsCard title={title}> <PersonDetails person={person} /> <ContactDetails persons={[person]} /> - </DetailCard> + </DetailsCard> ); } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/ContactDetails.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/ContactDetails.tsx index dbaa11da2..d1e7eefec 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/ContactDetails.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/ContactDetails.tsx @@ -11,7 +11,10 @@ import { import { isNonEmptyString } from "@eshg/lib-portal/helpers/guards"; import { isNonNullish } from "remeda"; -import { LabeledValue, ValueList } from "./LabeledValue"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; export function ContactDetails({ persons, diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/Custodians.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/Custodians.tsx index 37202aded..605f9540a 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/Custodians.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/Custodians.tsx @@ -9,9 +9,9 @@ import { } from "@eshg/employee-portal-api/measlesProtection"; import { PersonDetails } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/PersonDetails"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; import { ContactDetails } from "./ContactDetails"; -import { DetailCard } from "./DetailCard"; export function Custodians({ procedure, @@ -25,13 +25,12 @@ export function Custodians({ const custodians = procedure.custodians ?? []; return custodians.map((person, index) => ( - <DetailCard - data-testid="custodianSection" + <DetailsCard key={`custodian-${index}`} title="PSB - Personensorgeberechtigte:r" > <PersonDetails person={person} /> <ContactDetails persons={[person]} /> - </DetailCard> + </DetailsCard> )); } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/EditAccessRestrictionSidebar.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/EditAccessRestrictionSidebar.tsx index 948a6af97..c905ca7d0 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/EditAccessRestrictionSidebar.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/EditAccessRestrictionSidebar.tsx @@ -17,6 +17,7 @@ import { useCallback } from "react"; import { useUpdateAccessRestrictionMutation } from "@/lib/businessModules/measlesProtection/api/mutations/procedures"; import { DateAndButtonRow } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/DateAndButtonRow"; +import { LabeledValue } from "@/lib/shared/components/detailsCard/LabeledValue"; import { FormButtonBar } from "@/lib/shared/components/form/FormButtonBar"; import { SidebarForm } from "@/lib/shared/components/form/SidebarForm"; import { Sidebar } from "@/lib/shared/components/sidebar/Sidebar"; @@ -28,7 +29,6 @@ import { ACCESS_RESTRICTION_FIELDS, DateLabels, } from "./AccessRestrictionSidebar"; -import { LabeledValue } from "./LabeledValue"; export const EDIT_ACCESS_RESTRICTION_SEARCH_PARAM = "edit-access-restriction"; export const initialValues = { diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/Facility.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/Facility.tsx index 3cc4c2771..f2bb129ee 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/Facility.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/Facility.tsx @@ -10,12 +10,15 @@ import { import { isNonEmptyString } from "@eshg/lib-portal/helpers/guards"; import { facilityTypeNames } from "@/lib/businessModules/measlesProtection/components/procedures/constants"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; import { AddressDetails } from "./AddressDetails"; import { FacilityContactDetails } from "./ContactDetails"; -import { DetailCard } from "./DetailCard"; import { FacilityContacts } from "./FacilityContact"; -import { LabeledValue, ValueList } from "./LabeledValue"; export function Facility({ procedure, @@ -31,7 +34,7 @@ export function Facility({ return ( procedure.facility && ( <> - <DetailCard title="Einrichtung"> + <DetailsCard title="Einrichtung"> <ValueList> <LabeledValue label="Name" value={facility.name} /> <LabeledValue @@ -47,7 +50,7 @@ export function Facility({ </ValueList> <AddressDetails address={facility.contactAddress} /> <FacilityContactDetails facility={procedure.facility} /> - </DetailCard> + </DetailsCard> <FacilityContacts persons={facility.contactPersons} /> </> ) diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/FacilityContact.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/FacilityContact.tsx index 98524d800..6e10547ea 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/FacilityContact.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/FacilityContact.tsx @@ -4,21 +4,23 @@ */ import { ApiFacilityContactPerson } from "@eshg/employee-portal-api/measlesProtection"; +import { Row } from "@eshg/lib-portal/components/Row"; import { Grid } from "@mui/joy"; -import { Row } from "@/lib/shared/Row"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; import { SALUTATION_VALUES } from "@/lib/shared/components/personSidebar/constants"; -import { DetailCard } from "./DetailCard"; -import { LabeledValue, ValueList } from "./LabeledValue"; - export function FacilityContact({ person, }: { person: ApiFacilityContactPerson; }) { return ( - <DetailCard title="Kontaktperson der Einrichtung"> + <DetailsCard title="Kontaktperson der Einrichtung"> <ValueList> <Row> <LabeledValue @@ -44,7 +46,7 @@ export function FacilityContact({ href={`tel:${person.phoneNumber}`} /> </ValueList> - </DetailCard> + </DetailsCard> ); } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/NewCustodianButton.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/NewCustodianButton.tsx index 89241cd85..2da7bdd9d 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/NewCustodianButton.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/NewCustodianButton.tsx @@ -12,8 +12,8 @@ import { import { Add } from "@mui/icons-material"; import { Button } from "@mui/joy"; -import { DetailCard } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/DetailCard"; import { mapToApiPersonAddress } from "@/lib/businessModules/measlesProtection/shared/helpers"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; import { LegacyPerson, LegacyPersonFormConfig, @@ -68,7 +68,7 @@ export function mapToAddCustodianRequest( export function NewCustodianButton() { const [_, setAddCustodianOpen] = useSearchParam("add-custodian", "boolean"); return ( - <DetailCard title={"PSB - Personensorgeberechtigte:r"}> + <DetailsCard title={"PSB - Personensorgeberechtigte:r"}> <Button startDecorator={<Add />} variant="plain" @@ -76,6 +76,6 @@ export function NewCustodianButton() { > Hinzufügen </Button> - </DetailCard> + </DetailsCard> ); } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/NewFacilityButton.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/NewFacilityButton.tsx index bea009f92..0e3586a75 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/NewFacilityButton.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/NewFacilityButton.tsx @@ -8,15 +8,14 @@ import { Add } from "@mui/icons-material"; import { Button } from "@mui/joy"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; import { useSearchParam } from "@/lib/shared/hooks/searchParams/useSearchParam"; -import { DetailCard } from "./DetailCard"; - export function NewFacilityButton() { const [_open, setOpen] = useSearchParam("new-facility", "boolean"); return ( - <DetailCard title="Einrichtung"> + <DetailsCard title="Einrichtung"> <Button startDecorator={<Add />} variant="plain" @@ -24,6 +23,6 @@ export function NewFacilityButton() { > Hinzufügen </Button> - </DetailCard> + </DetailsCard> ); } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/PersonDetails.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/PersonDetails.tsx index de8c9f99d..9d4ff2efc 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/PersonDetails.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/PersonDetails.tsx @@ -7,20 +7,21 @@ import { ApiAffectedPerson, ApiCustodian, } from "@eshg/employee-portal-api/measlesProtection"; +import { Row } from "@eshg/lib-portal/components/Row"; import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import { AddressDetails } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AddressDetails"; -import { Row } from "@/lib/shared/Row"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; import { GENDER_VALUES, SALUTATION_VALUES, getOptionalTitle, } from "@/lib/shared/components/personSidebar/constants"; -import { LabeledValue, ValueList } from "./LabeledValue"; - type Person = ApiAffectedPerson | ApiCustodian; - interface PersonDetailsProps { person: Person; } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/ProofTab.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/ProofTab.tsx index 307507a96..ff809d124 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/ProofTab.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/ProofTab.tsx @@ -13,6 +13,7 @@ import { ApiProofSubmission, ApiSubmissionResult, } from "@eshg/employee-portal-api/measlesProtection"; +import { Row } from "@eshg/lib-portal/components/Row"; import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import { Add } from "@mui/icons-material"; import { Button, Grid, Stack } from "@mui/joy"; @@ -37,14 +38,16 @@ import { formatName, getPersonByIdFromProcedure, } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/helpers"; -import { Row } from "@/lib/shared/Row"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; import { useSearchParam } from "@/lib/shared/hooks/searchParams/useSearchParam"; import { AccessRestrictionSidebar } from "./AccessRestrictionSidebar"; import { AdditionalInfoSection } from "./AdditionalInfoSection"; -import { DetailCard } from "./DetailCard"; import { EditAccessRestrictionSidebar } from "./EditAccessRestrictionSidebar"; -import { LabeledValue, ValueList } from "./LabeledValue"; import { ProofSidebar } from "./ProofSidebar"; import { AccessRestrictionCard } from "./proof/AccessRestrictionCard"; import { AppointmentCard } from "./proof/AppointmentCard"; @@ -164,10 +167,7 @@ function ProofSubmissionsCard({ procedureClosed, }: Readonly<ProofSubmissionsProps>) { return ( - <DetailCard - title="Nachweisvorlage" - fullHeight={proofSubmissions.length > 0} - > + <DetailsCard title="Nachweisvorlage" fullHeight={true}> <Stack spacing={3} alignItems={"start"} width={"100%"}> {proofSubmissions.map((proof) => ( <ProofTabEntry key={proof.externalId}> @@ -199,7 +199,7 @@ function ProofSubmissionsCard({ </Button> )} </Stack> - </DetailCard> + </DetailsCard> ); } @@ -215,7 +215,7 @@ function FineCard({ procedureClosed, }: Readonly<FineCardProps>) { return ( - <DetailCard title="Bußgeld" fullHeight={monetaryFines.length > 0}> + <DetailsCard title="Bußgeld" fullHeight={true}> <Stack spacing={3} alignItems={"start"} width={"100%"}> {monetaryFines.length > 0 && ( <ValueList style={{ flexBasis: "auto" }}> @@ -239,7 +239,7 @@ function FineCard({ </Button> )} </Stack> - </DetailCard> + </DetailsCard> ); } @@ -257,10 +257,7 @@ function ProofRequestLetterCard({ proofSubmissionLetters, }: Readonly<ProofRequestLetterCardProps>) { return ( - <DetailCard - title={"Anschreiben Nachweisvorlage"} - fullHeight={proofSubmissionLetters.length > 0} - > + <DetailsCard title={"Anschreiben Nachweisvorlage"} fullHeight={true}> <Stack spacing={3} width={"100%"} alignItems={"start"}> {proofSubmissionLetters.map((letter, index) => ( <ProofTabEntry rowLayout key={index}> @@ -293,6 +290,6 @@ function ProofRequestLetterCard({ </Button> )} </Stack> - </DetailCard> + </DetailsCard> ); } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/UpdateProcedureSection.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/UpdateProcedureSection.tsx index 2f5152bc9..8edf47f84 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/UpdateProcedureSection.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/UpdateProcedureSection.tsx @@ -16,8 +16,8 @@ import { Formik, useFormikContext } from "formik"; import { PropsWithChildren, useCallback } from "react"; import { WrappedSelectField } from "@/lib/businessModules/measlesProtection/shared/WrappedSelectField"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; -import { DetailCard } from "./DetailCard"; import { OtherComment, reasons, @@ -102,11 +102,11 @@ export function UpdateProcedureSection({ initialValues={initialValues} > <Stack rowGap={3}> - <DetailCard title={title} data-testid="updateProcedureSection"> + <DetailsCard title={title}> <Stack gap={2} width="100%"> <UpdateProcedureSectionFields errorMessages={errorMessages} /> </Stack> - </DetailCard> + </DetailsCard> <EditActions isDraft={isDraft} isOpen={!procedureClosed} /> </Stack> </ProcedureForm> diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/AccessRestrictionCard.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/AccessRestrictionCard.tsx index 0d715ecdc..002b85891 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/AccessRestrictionCard.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/AccessRestrictionCard.tsx @@ -14,16 +14,16 @@ import { Button, IconButton, Stack } from "@mui/joy"; import { useIsNewFeatureEnabled } from "@/lib/businessModules/measlesProtection/api/queries/featureTogglesApi"; import { ACCESS_RESTRICTION_FIELDS } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/AccessRestrictionSidebar"; -import { DetailCard } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/DetailCard"; import { EDIT_ACCESS_RESTRICTION_SEARCH_PARAM } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/EditAccessRestrictionSidebar"; -import { - LabeledValue, - ValueList, -} from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/LabeledValue"; import { formatName, getPersonByIdFromProcedure, } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/helpers"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; import { useSearchParam } from "@/lib/shared/hooks/searchParams/useSearchParam"; import { ProofTabEntry } from "./ProofTabEntry"; @@ -55,9 +55,9 @@ export function AccessRestrictionCard({ ); return ( - <DetailCard + <DetailsCard title="Betretungsverbot" - fullHeight={!!accessRestriction} + fullHeight={true} {...(isEditAccessRestrictionEnabled && !procedureClosed && accessRestriction && { @@ -126,6 +126,6 @@ export function AccessRestrictionCard({ ) )} </Stack> - </DetailCard> + </DetailsCard> ); } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/AppointmentCard.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/AppointmentCard.tsx index 027a54acd..5609b5c03 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/AppointmentCard.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/AppointmentCard.tsx @@ -7,19 +7,18 @@ import { ApiAppointment } from "@eshg/employee-portal-api/measlesProtection"; import { formatDate, formatTime } from "@eshg/lib-portal/formatters/dateTime"; import { Add, DeleteOutline, EditOutlined } from "@mui/icons-material"; import { Button, Stack } from "@mui/joy"; -import { isDefined } from "remeda"; import { useDeleteAppointmentForProcedure } from "@/lib/businessModules/measlesProtection/api/mutations/appointmentBookingApi"; -import { DetailCard } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/DetailCard"; -import { - LabeledValue, - ValueList, -} from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/LabeledValue"; import { ActionsItem, ActionsMenu, } from "@/lib/shared/components/buttons/ActionsMenu"; import { useConfirmationDialog } from "@/lib/shared/components/confirmationDialog/ConfirmationDialogProvider"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; import { useSearchParam } from "@/lib/shared/hooks/searchParams/useSearchParam"; export interface AppointmentCardProps { @@ -68,9 +67,9 @@ export function AppointmentCard({ }, ]; return ( - <DetailCard + <DetailsCard title="Termin" - fullHeight={isDefined(appointment)} + fullHeight={true} actionButton={ appointment && ( <ActionsMenu @@ -99,17 +98,16 @@ export function AppointmentCard({ } /> </ValueList> - ) : ( + ) : !procedureClosed ? ( <Button variant="plain" startDecorator={<Add />} - disabled={procedureClosed} onClick={() => setAddingAppointment(true)} > Hinzufügen </Button> - )} + ) : null} </Stack> - </DetailCard> + </DetailsCard> ); } diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/ProofTabEntry.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/ProofTabEntry.tsx index b491f8ed4..c6fefe79d 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/ProofTabEntry.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/proof/ProofTabEntry.tsx @@ -5,7 +5,7 @@ import { ReactNode } from "react"; -import { ValueList } from "@/lib/businessModules/measlesProtection/components/procedures/procedureDetails/LabeledValue"; +import { ValueList } from "@/lib/shared/components/detailsCard/LabeledValue"; interface ProofTabEntryProps { children: ReactNode; diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/proceduresTable/ProcedureSearchBar.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/proceduresTable/ProcedureSearchBar.tsx index be6f40cca..c9588c940 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/proceduresTable/ProcedureSearchBar.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/proceduresTable/ProcedureSearchBar.tsx @@ -5,8 +5,9 @@ "use client"; +import { Row } from "@eshg/lib-portal/components/Row"; + import { NewPersonButton } from "@/lib/businessModules/measlesProtection/components/procedures/createProceduresForm/NewPersonButton"; -import { Row } from "@/lib/shared/Row"; import { ProceduresTableFilterButton } from "./ProceduresTableFilters"; diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/proceduresTable/ProceduresTable.tsx b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/proceduresTable/ProceduresTable.tsx index 57c54de03..1fd587b24 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/proceduresTable/ProceduresTable.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/proceduresTable/ProceduresTable.tsx @@ -7,6 +7,7 @@ import { ApiUserRole } from "@eshg/employee-portal-api/base"; import { ApiGetProcedure200Response } from "@eshg/employee-portal-api/measlesProtection"; +import { Row } from "@eshg/lib-portal/components/Row"; import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import { EditOutlined, Preview, ToggleOffOutlined } from "@mui/icons-material"; import { Chip } from "@mui/joy"; @@ -17,10 +18,8 @@ import { caseStatusNames, facilityTypeNames, } from "@/lib/businessModules/measlesProtection/components/procedures/constants"; -import { useTablePageParams } from "@/lib/businessModules/measlesProtection/hooks/useTablePageParams"; import { useProceduresContext } from "@/lib/businessModules/measlesProtection/shared/ProceduresContext"; import { routes } from "@/lib/businessModules/measlesProtection/shared/routes"; -import { Row } from "@/lib/shared/Row"; import { ActionsMenu } from "@/lib/shared/components/buttons/ActionsMenu"; import { Pagination } from "@/lib/shared/components/pagination/Pagination"; import { @@ -32,6 +31,7 @@ import { TablePage } from "@/lib/shared/components/table/TablePage"; import { TableSheet } from "@/lib/shared/components/table/TableSheet"; import { useTableControl } from "@/lib/shared/hooks/searchParams/useTableControl"; import { useHasUserRoleCheck } from "@/lib/shared/hooks/useAccessControl"; +import { useTablePageParams } from "@/lib/shared/hooks/useTablePageParams"; import { ProceduresTableFilters, @@ -150,6 +150,7 @@ function getProceduresColumns({ cell: ({ row: { original: procedure } }) => ( <Row justifyContent="flex-end"> <ActionsMenu + rowHeight actionItems={[ { label: procedure.isOpen ? "Bearbeiten" : "Anzeigen", @@ -182,7 +183,12 @@ function getProceduresColumns({ export function ProceduresTable() { const filters = useProceduresFilters(); - const tablePage = useTablePageParams(); + const tablePage = useTablePageParams({ + fieldNames: { + sortFieldName: "sortKey", + sortDirectionName: "sortDirection", + }, + }); const procedures = useProceduresQuery(tablePage, filters); const proceduresContext = useProceduresContext(); const { openProcedureReopenModal } = proceduresContext.action; @@ -214,6 +220,7 @@ export function ProceduresTable() { <DataTable data={procedures.data.procedures} sorting={tableControl.tableSorting} + enableSortingRemoval={false} columns={getProceduresColumns({ isMeaslesProtectionLeader, onReopenProcedure: (procedureId) => diff --git a/employee-portal/src/lib/businessModules/measlesProtection/hooks/useTablePageParams.ts b/employee-portal/src/lib/businessModules/measlesProtection/hooks/useTablePageParams.ts deleted file mode 100644 index 78ec7a9a5..000000000 --- a/employee-portal/src/lib/businessModules/measlesProtection/hooks/useTablePageParams.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { useSearchParams } from "next/navigation"; -import { useMemo } from "react"; - -export function useTablePageParams() { - const searchParams = useSearchParams(); - - const pageNumberNaN = parseInt(searchParams.get("pageNumber") ?? ""); - const pageNumber = isNaN(pageNumberNaN) ? undefined : pageNumberNaN; - - const pageSizeNaN = parseInt(searchParams.get("pageSize") ?? ""); - const pageSize = isNaN(pageSizeNaN) ? undefined : pageSizeNaN; - - const sortKey = searchParams.get("sortKey") ?? undefined; - const sortDirection = searchParams.get("sortDirection") ?? undefined; - const tableParams = useMemo( - () => ({ - pageNumber, - pageSize, - sortBy: sortKey, - sortOrder: sortDirection, - }), - [pageNumber, pageSize, sortKey, sortDirection], - ); - return tableParams; -} diff --git a/employee-portal/src/lib/businessModules/measlesProtection/layout/MeaslesProtectionProcedureLayout.tsx b/employee-portal/src/lib/businessModules/measlesProtection/layout/MeaslesProtectionProcedureLayout.tsx index 3cccb9587..d868fefbf 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/layout/MeaslesProtectionProcedureLayout.tsx +++ b/employee-portal/src/lib/businessModules/measlesProtection/layout/MeaslesProtectionProcedureLayout.tsx @@ -5,6 +5,7 @@ "use client"; +import { ApiUserRole } from "@eshg/employee-portal-api/base"; import { PropsWithChildren } from "react"; import { useProcedureQuery } from "@/lib/businessModules/measlesProtection/api/queries/procedures"; @@ -15,6 +16,7 @@ import { MainContentLayout } from "@/lib/shared/components/layout/MainContentLay import { StickyToolbarLayout } from "@/lib/shared/components/layout/StickyToolbarLayout"; import { TabNavigationItem } from "@/lib/shared/components/tabNavigation/types"; import { TabNavigationToolbar } from "@/lib/shared/components/tabNavigationToolbar/TabNavigationToolbar"; +import { useHasUserRoleCheck } from "@/lib/shared/hooks/useAccessControl"; export interface MeaslesProtectionProcedurePageParams extends PropsWithChildren { @@ -27,13 +29,18 @@ export function MeaslesProtectionProcedureLayout({ navItems, id, }: MeaslesProtectionProcedurePageParams) { + const hasMeaslesProtectionAdminRole = useHasUserRoleCheck( + ApiUserRole.MeaslesProtectionAdmin, + ); const procedure = useProcedureQuery(id).data; return ( <StickyToolbarLayout toolbar={ <TabNavigationToolbar items={navItems} - routeBack={routes.procedures.index} + routeBack={ + hasMeaslesProtectionAdminRole ? routes.procedures.index : undefined + } header={ <MeaslesProtectionTabHeader person={procedure.affectedPerson} /> } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/api/models/Location.ts b/employee-portal/src/lib/businessModules/schoolEntry/api/models/Location.ts new file mode 100644 index 000000000..711bbcf58 --- /dev/null +++ b/employee-portal/src/lib/businessModules/schoolEntry/api/models/Location.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export interface Location { + readonly id: string; + readonly name: string; +} + +interface LocationResponse { + id: string; + name: string; +} + +export function mapLocation(response: LocationResponse): Location { + return { + id: response.id, + name: response.name, + }; +} diff --git a/employee-portal/src/lib/businessModules/schoolEntry/api/models/Procedure.ts b/employee-portal/src/lib/businessModules/schoolEntry/api/models/Procedure.ts index 3a648a00e..2286415d0 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/api/models/Procedure.ts +++ b/employee-portal/src/lib/businessModules/schoolEntry/api/models/Procedure.ts @@ -4,30 +4,21 @@ */ import { - ApiSchool, ApiSchoolEntryProcedure, ApiSchoolEntryProcedureType, ApiSchoolEntryStatusType, } from "@eshg/employee-portal-api/schoolEntry"; -import { parseOptionalValue } from "@eshg/lib-portal/helpers/form"; - -import { - Label, - mapLabels, -} from "@/lib/businessModules/schoolEntry/api/models/Label"; import { BaseEntity, mapBaseEntity } from "./BaseEntity"; +import { Label, mapLabels } from "./Label"; +import { Location, mapLocation } from "./Location"; import { Person, mapPerson } from "./Person"; - -export interface School { - readonly id: string; - readonly name: string; -} +import { mapOptional } from "./utils"; export interface Procedure extends BaseEntity { readonly type: ApiSchoolEntryProcedureType; readonly child: Person; - readonly school: School; + readonly school?: Location; readonly labels: Label[]; readonly status: ApiSchoolEntryStatusType; readonly appointmentStart?: Date; @@ -42,7 +33,7 @@ export function mapProcedure(response: ApiSchoolEntryProcedure): Procedure { ...mapBaseEntity(response), type: response.type, child: mapPerson(response.child), - school: mapSchool(response.school), + school: mapOptional(response.school, mapLocation), labels: mapLabels(response.labels), status: response.status, appointmentStart: response.appointmentStart, @@ -52,10 +43,3 @@ export function mapProcedure(response: ApiSchoolEntryProcedure): Procedure { schoolYear: response.schoolYear, }; } - -function mapSchool(school?: ApiSchool): School { - return { - id: parseOptionalValue(school?.id), - name: parseOptionalValue(school?.name), - }; -} diff --git a/employee-portal/src/lib/businessModules/schoolEntry/api/models/ProcedureDetails.ts b/employee-portal/src/lib/businessModules/schoolEntry/api/models/ProcedureDetails.ts index 2e519fc4c..6ddbc778f 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/api/models/ProcedureDetails.ts +++ b/employee-portal/src/lib/businessModules/schoolEntry/api/models/ProcedureDetails.ts @@ -3,39 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - ApiAppointmentLocation, - ApiProcedureDetails, -} from "@eshg/employee-portal-api/schoolEntry"; -import { - mapOptionalValue, - parseOptionalValue, -} from "@eshg/lib-portal/helpers/form"; - -import { - Label, - mapLabels, -} from "@/lib/businessModules/schoolEntry/api/models/Label"; -import { - WaitingRoom, - mapWaitingRoom, -} from "@/lib/businessModules/schoolEntry/api/models/WaitingRoom"; +import { ApiProcedureDetails } from "@eshg/employee-portal-api/schoolEntry"; +import { mapOptionalValue } from "@eshg/lib-portal/helpers/form"; import { Appointment, mapAppointment } from "./Appointment"; +import { Label, mapLabels } from "./Label"; +import { Location, mapLocation } from "./Location"; import { PersonDetails, mapPersonDetails } from "./Person"; import { Procedure, mapProcedure } from "./Procedure"; +import { WaitingRoom, mapWaitingRoom } from "./WaitingRoom"; import { mapOptional } from "./utils"; -export interface Location { - readonly id: string; - readonly name: string; -} - export interface ProcedureDetails extends Procedure { readonly version: number; readonly labels: Label[]; readonly appointment?: Appointment; - readonly location: Location; + readonly location?: Location; readonly isEntryLevel: boolean; readonly child: PersonDetails; readonly isInvitationSent: boolean; @@ -55,7 +38,7 @@ export function mapProcedureDetails( version: response.version, labels: mapLabels(response.labels), appointment: mapOptional(response.appointment, mapAppointment), - location: mapLocation(response.location), + location: mapOptional(response.location, mapLocation), isEntryLevel: response.isEntryLevel, child: mapPersonDetails(response.child), isInvitationSent: response.isInvitationSent, @@ -67,10 +50,3 @@ export function mapProcedureDetails( schoolInfoLetterCreatedAt: response.schoolInfoLetterCreatedAt, }; } - -function mapLocation(location?: ApiAppointmentLocation): Location { - return { - id: parseOptionalValue(location?.id), - name: parseOptionalValue(location?.name), - }; -} diff --git a/employee-portal/src/lib/businessModules/schoolEntry/api/mutations/schoolEntryApi.ts b/employee-portal/src/lib/businessModules/schoolEntry/api/mutations/schoolEntryApi.ts index 83d86e035..de697cc65 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/api/mutations/schoolEntryApi.ts +++ b/employee-portal/src/lib/businessModules/schoolEntry/api/mutations/schoolEntryApi.ts @@ -47,46 +47,38 @@ interface ImportDataResult { statistics: ApiImportStatistics; } -export function useImportData(requireSchoolYear: boolean) { +export function useImportData() { const schoolEntryApi = useSchoolEntryApi(); return useHandledMutation({ mutationFn: (values: ImportDataValues) => values.listType === ImportListType.SchoolList ? schoolEntryApi - .importSchoolListRaw(mapSchoolFormValues(values, requireSchoolYear)) + .importSchoolListRaw(mapSchoolFormValues(values)) .then(parseImportResult) : values.listType === ImportListType.CitizenList ? schoolEntryApi - .importCitizenListRaw( - mapCitizenFormValues(values, requireSchoolYear), - ) + .importCitizenListRaw(mapCitizenFormValues(values)) .then(parseImportResult) : schoolEntryApi - .importPastProcedureListRaw( - mapPastProcedureFormValues(values, requireSchoolYear), - ) + .importPastProcedureListRaw(mapPastProcedureFormValues(values)) .then(parseImportResult), }); } function mapCitizenFormValues( values: ImportDataValues, - requireSchoolYear: boolean, ): ImportCitizenListRequest { return { file: mapRequiredValue(values.file), - schoolYear: requireSchoolYear - ? mapRequiredValue(values.schoolYear) - : new Date().getFullYear(), + schoolYear: mapRequiredValue(values.schoolYear), }; } function mapSchoolFormValues( values: ImportDataValues, - requireSchoolYear: boolean, ): ImportSchoolListRequest { return { - ...mapCitizenFormValues(values, requireSchoolYear), + ...mapCitizenFormValues(values), schoolId: mapRequiredValue(values.schoolId), locationId: mapOptionalValue(values.locationId), }; @@ -94,10 +86,9 @@ function mapSchoolFormValues( function mapPastProcedureFormValues( values: ImportDataValues, - requireSchoolYear: boolean, ): ImportPastProcedureListRequest { return { - ...mapCitizenFormValues(values, requireSchoolYear), + ...mapCitizenFormValues(values), schoolId: mapRequiredValue(values.schoolId), }; } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/AppointmentBlockGroupForm.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/AppointmentBlockGroupForm.tsx index 95c12d2a4..12249218c 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/AppointmentBlockGroupForm.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/AppointmentBlockGroupForm.tsx @@ -12,13 +12,13 @@ import { isDefined, isEmpty, mapToObj } from "remeda"; import { AppointmentTypeConfig } from "@/lib/businessModules/schoolEntry/api/models/AppointmentTypeConfig"; import { CreateAppointmentBlockGroupValues } from "@/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/CreateAppointmentBlockGroupForm"; -import { validateAppointmentBlock } from "@/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/validateAppointmentBlock"; import { APPOINTMENT_TYPE_OPTIONS } from "@/lib/businessModules/schoolEntry/features/procedures/options"; import { routes } from "@/lib/businessModules/schoolEntry/shared/routes"; import { AppointmentBlockGroupFields } from "@/lib/shared/components/appointmentBlocks/AppointmentBlockGroupFields"; import { AppointmentCountWithDays } from "@/lib/shared/components/appointmentBlocks/AppointmentCountWithDays"; import { AppointmentLocationSelection } from "@/lib/shared/components/appointmentBlocks/AppointmentLocationSelection"; import { AppointmentStaffSelection } from "@/lib/shared/components/appointmentBlocks/AppointmentStaffSelection"; +import { validateAppointmentBlock } from "@/lib/shared/components/appointmentBlocks/validateAppointmentBlock"; import { FormButtonBar } from "@/lib/shared/components/form/FormButtonBar"; import { FormSheet } from "@/lib/shared/components/form/FormSheet"; import { fullName } from "@/lib/shared/components/users/userFormatter"; @@ -53,7 +53,7 @@ function validateForm( if (isEmpty(values.physicians) && isEmpty(values.mfas)) { const msg = - "Es muss mindestens ein:e Arzt:in oder ein:e MFA ausgewählt sein."; + "Es muss mindestens ein Arzt/eine Ärztin oder ein:e MFA ausgewählt sein."; errors.physicians = msg; errors.mfas = msg; } @@ -99,49 +99,48 @@ export function AppointmentBlockGroupForm( validate={(values) => validateForm(values, props.allAppointmentTypes)} > {({ values, isSubmitting, handleSubmit }) => ( - <FormSheet onSubmit={handleSubmit}> - <Stack gap={3} divider={<Divider />}> - <Stack gap={4}> - <AppointmentBlockGroupFields - appointmentBlocksWithDays={values.appointmentBlocks} - options={APPOINTMENT_TYPE_OPTIONS} - showParallelExaminations - /> - </Stack> - {props.locationSelectionMode !== ApiLocationSelectionMode.None && ( - <AppointmentLocationSelection - contactCategory={props.locationSelectionMode} - /> - )} - <Stack gap={4}> - <AppointmentStaffSelection - physicianOptions={physicianOptions} - medicalAssistantOptions={medicalAssistantsOptions} - freeStaff={props.freeStaff} - blockedStaff={props.blockedStaff} - validateAvailability={() => props.validateAvailability(values)} - /> - </Stack> - <FormButtonBar - left={ - <AppointmentCountWithDays - appointments={values} - appointmentDurations={appointmentDurations} - parallelExaminations={ - isEmptyString(values.parallelExaminations) - ? DEFAULT_PARALLEL_EXAMINATIONS - : Math.max( - values.parallelExaminations, - DEFAULT_PARALLEL_EXAMINATIONS, - ) - } - /> - } - submitLabel="Planen" - submitting={isSubmitting} - onCancel={routes.appointmentBlockGroups.overview} + <FormSheet gap={5} onSubmit={handleSubmit}> + <Stack gap={5}> + <AppointmentBlockGroupFields + appointmentBlocksWithDays={values.appointmentBlocks} + options={APPOINTMENT_TYPE_OPTIONS} + showParallelExaminations /> </Stack> + {props.locationSelectionMode !== ApiLocationSelectionMode.None && ( + <AppointmentLocationSelection + contactCategory={props.locationSelectionMode} + /> + )} + <Stack gap={5}> + <AppointmentStaffSelection + physicianOptions={physicianOptions} + medicalAssistantOptions={medicalAssistantsOptions} + freeStaff={props.freeStaff} + blockedStaff={props.blockedStaff} + validateAvailability={() => props.validateAvailability(values)} + /> + </Stack> + <Divider /> + <FormButtonBar + left={ + <AppointmentCountWithDays + appointments={values} + appointmentDurations={appointmentDurations} + parallelExaminations={ + isEmptyString(values.parallelExaminations) + ? DEFAULT_PARALLEL_EXAMINATIONS + : Math.max( + values.parallelExaminations, + DEFAULT_PARALLEL_EXAMINATIONS, + ) + } + /> + } + submitLabel="Planen" + submitting={isSubmitting} + onCancel={routes.appointmentBlockGroups.overview} + /> </FormSheet> )} </Formik> diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/CreateAppointmentBlockGroupForm.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/CreateAppointmentBlockGroupForm.tsx index d0689c6b1..68a305739 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/CreateAppointmentBlockGroupForm.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/CreateAppointmentBlockGroupForm.tsx @@ -127,7 +127,7 @@ export function CreateAppointmentBlockGroupForm() { } if (values.physicians.length == 0 && values.mfas.length == 0) { snackbar.notification( - "Bitte mindestens ein:e Arzt:in oder ein:e MFA für die Validierung auswählen", + "Bitte mindestens einen Arzt/eine Ärztin oder ein:e MFA für die Validierung auswählen", ); return; } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/ProcedureToolbar.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/ProcedureToolbar.tsx index 88fcf82cf..9be026a4f 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/ProcedureToolbar.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/ProcedureToolbar.tsx @@ -5,6 +5,7 @@ "use client"; +import { ApiUserRole } from "@eshg/employee-portal-api/base"; import { FormatListBulletedOutlined, MedicalServicesOutlined, @@ -18,21 +19,31 @@ import { ProcedureTabHeader } from "@/lib/businessModules/schoolEntry/features/p import { routes } from "@/lib/businessModules/schoolEntry/shared/routes"; import { TabNavigationItem } from "@/lib/shared/components/tabNavigation/types"; import { TabNavigationToolbar } from "@/lib/shared/components/tabNavigationToolbar/TabNavigationToolbar"; +import { useHasUserRoleCheck } from "@/lib/shared/hooks/useAccessControl"; interface ProcedureToolbarProps { procedureId: string; } export function ProcedureToolbar(props: ProcedureToolbarProps) { + const hasSchoolEntryAdminRole = useHasUserRoleCheck( + ApiUserRole.SchoolEntryAdmin, + ); const procedure = useGetProcedure(props.procedureId); const tabItems = buildTabItems(props.procedureId); return ( <TabNavigationToolbar items={tabItems} - routeBack={routes.procedures.overview} + routeBack={ + hasSchoolEntryAdminRole ? routes.procedures.overview : undefined + } header={<ProcedureTabHeader child={procedure.data.child} />} - afterTabs={procedure.data.isClosed ? "Vorgang geschlossen" : undefined} + afterTabs={ + procedure.data.isClosed ? ( + <span data-testid="procedureStatus">Vorgang geschlossen</span> + ) : undefined + } /> ); } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/developmentScreening/DevelopmentScreeningForm.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/developmentScreening/DevelopmentScreeningForm.tsx index a74eebd4e..b61f3e5e1 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/developmentScreening/DevelopmentScreeningForm.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/developmentScreening/DevelopmentScreeningForm.tsx @@ -12,7 +12,6 @@ import { import { FormProps, OptionalFieldValue } from "@eshg/lib-portal/types/form"; import { Divider } from "@mui/joy"; import { Formik, FormikHelpers } from "formik"; -import { useState } from "react"; import { Percentiles } from "@/lib/businessModules/schoolEntry/api/models/examinations/Percentiles"; import { DevelopmentScreeningResultFields } from "@/lib/businessModules/schoolEntry/features/procedures/developmentScreening/DevelopmentScreeningResultFields"; @@ -20,7 +19,7 @@ import { HandicapFields, HandicapFieldsValues, } from "@/lib/businessModules/schoolEntry/features/procedures/developmentScreening/HandicapFields"; -import { Icd10Sidebar } from "@/lib/businessModules/schoolEntry/features/procedures/developmentScreening/Icd10Sidebar"; +import { useIdc10Sidebar } from "@/lib/businessModules/schoolEntry/features/procedures/developmentScreening/Icd10Sidebar"; import { MeasurementFields, MeasurementFieldsValues, @@ -38,7 +37,6 @@ import { SocioEducationalFieldsValues, } from "@/lib/businessModules/schoolEntry/features/procedures/developmentScreening/SocioEducationalFields"; import { FormFooter } from "@/lib/businessModules/schoolEntry/features/procedures/examinations/FormFooter"; -import { OverlayBoundary } from "@/lib/shared/components/boundaries/OverlayBoundary"; import { ConfirmLeaveDirtyFormEffect } from "@/lib/shared/components/form/ConfirmLeaveDirtyFormEffect"; import { FormStack } from "@/lib/shared/components/form/FormStack"; @@ -53,13 +51,6 @@ export interface DevelopmentScreeningFormValues { schoolFeedback: OptionalFieldValue<ApiSchoolFeedback>; } -type SidebarState = - | "closed" - | { - initiallySelectedCodes: string[]; - onSubmit: (selectedCodes: string[]) => void; - }; - interface DevelopmentScreeningFormProps extends FormProps<DevelopmentScreeningFormValues> { procedureId: string; @@ -67,13 +58,13 @@ interface DevelopmentScreeningFormProps } export function DevelopmentScreeningForm(props: DevelopmentScreeningFormProps) { - const [sidebarState, setSidebarState] = useState<SidebarState>("closed"); + const icd10Sidebar = useIdc10Sidebar(); function handleClickIcd10Code( currentCodes: string[], setFieldValue: (newCodes: string[]) => void, ) { - setSidebarState({ + icd10Sidebar.open({ initiallySelectedCodes: currentCodes, onSubmit: (selectedCodes) => setFieldValue(selectedCodes), }); @@ -128,16 +119,6 @@ export function DevelopmentScreeningForm(props: DevelopmentScreeningFormProps) { <Divider /> <DevelopmentScreeningResultFields /> <FormFooter isSubmitting={isSubmitting} /> - {sidebarState !== "closed" && ( - <OverlayBoundary> - <Icd10Sidebar - open - onClose={() => setSidebarState("closed")} - onSubmit={sidebarState.onSubmit} - initiallySelectedCodes={sidebarState.initiallySelectedCodes} - /> - </OverlayBoundary> - )} </FormStack> )} </Formik> diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/developmentScreening/Icd10Sidebar.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/developmentScreening/Icd10Sidebar.tsx index fac9a7297..eb060fb0d 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/developmentScreening/Icd10Sidebar.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/developmentScreening/Icd10Sidebar.tsx @@ -23,13 +23,21 @@ import { useDebounce } from "use-debounce"; import { useSearchIcd10Codes } from "@/lib/businessModules/schoolEntry/api/queries/schoolEntryApi"; import { ButtonBar } from "@/lib/shared/components/buttons/ButtonBar"; -import { Sidebar } from "@/lib/shared/components/sidebar/Sidebar"; +import { DrawerProps } from "@/lib/shared/components/drawer/drawerContext"; +import { + UseSidebarResult, + useSidebar, +} from "@/lib/shared/components/drawer/useSidebar"; import { SidebarActions } from "@/lib/shared/components/sidebar/SidebarActions"; import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; -interface Idc10SidebarProps { - open: boolean; - onClose: () => void; +export function useIdc10Sidebar(): UseSidebarResult<Idc10SidebarProps> { + return useSidebar({ + component: Icd10Sidebar, + }); +} + +interface Idc10SidebarProps extends DrawerProps { onSubmit: (selectedCodes: string[]) => void; initiallySelectedCodes: string[]; } @@ -43,7 +51,7 @@ const StyledTable = styled(Table)({ }, }); -export function Icd10Sidebar(props: Idc10SidebarProps) { +function Icd10Sidebar(props: Idc10SidebarProps) { const [selectedCodes, setSelectedCodes] = useState<string[]>( props.initiallySelectedCodes, ); @@ -88,7 +96,7 @@ export function Icd10Sidebar(props: Idc10SidebarProps) { } return ( - <Sidebar open={props.open} onClose={props.onClose}> + <> <SidebarContent title="ICD-10 Katalog"> <Stack gap={3}> <FormControl size="md"> @@ -165,7 +173,7 @@ export function Icd10Sidebar(props: Idc10SidebarProps) { right={[ <Button key="cancel" - onClick={props.onClose} + onClick={() => props.onClose()} color="neutral" variant="soft" > @@ -177,6 +185,6 @@ export function Icd10Sidebar(props: Idc10SidebarProps) { ]} /> </SidebarActions> - </Sidebar> + </> ); } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataFields.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataFields.tsx index 763b86156..d4b62b53f 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataFields.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataFields.tsx @@ -29,7 +29,6 @@ import { FileType } from "@/lib/shared/components/formFields/file/FileType"; interface ImportDataFieldsProps { listType: ImportListType; - requireSchoolYear: boolean; isPastProcedureImportEnabled: boolean; locationSelectionMode: ApiLocationSelectionMode; isDirectProcedureTypeAssignmentOnImport: boolean; @@ -135,14 +134,12 @@ export function ImportDataFields(props: ImportDataFieldsProps) { category={ApiContactCategory.HealthDepartment} /> )} - {props.requireSchoolYear && ( - <SchoolYearField - name="schoolYear" - label="Wählen Sie ein Schuljahr aus" - required="Bitte ein Schuljahr auswählen." - range={schoolYearRange} - /> - )} + <SchoolYearField + name="schoolYear" + label="Wählen Sie ein Schuljahr aus" + required="Bitte ein Schuljahr auswählen." + range={schoolYearRange} + /> <FileField name="file" label="Wählen Sie eine XLSX-Datei aus" diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataSidebar.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataSidebar.tsx index e005e1d02..3cd5915a9 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataSidebar.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportDataSidebar.tsx @@ -21,13 +21,22 @@ import { ImportListType } from "@/lib/businessModules/schoolEntry/features/proce import { routes } from "@/lib/businessModules/schoolEntry/shared/routes"; import { FormButtonBar } from "@/lib/shared/components/form/FormButtonBar"; import { SidebarForm } from "@/lib/shared/components/form/SidebarForm"; -import { Sidebar } from "@/lib/shared/components/sidebar/Sidebar"; import { SidebarActions } from "@/lib/shared/components/sidebar/SidebarActions"; import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; +import { + SidebarWithFormRefProps, + useSidebarWithFormRef, +} from "@/lib/shared/hooks/useSidebarWithFormRef"; import { ImportDataFields } from "./ImportDataFields"; import { ImportResult } from "./ImportResult"; +export function useImportDataSidebar() { + return useSidebarWithFormRef({ + component: ImportDataSidebar, + }); +} + const INITIAL_VALUES: ImportDataValues = { listType: ImportListType.SchoolList, file: null, @@ -44,18 +53,15 @@ export interface ImportDataValues { schoolYear: OptionalFieldValue<number>; } -export function ImportDataSidebar() { +function ImportDataSidebar(props: SidebarWithFormRefProps) { const router = useRouter(); - const isSchoolYearEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.SchoolYear, - ); const isPastProcedureImportEnabled = useIsNewFeatureEnabled( ApiSchoolEntryFeature.ImportPastProcedures, ); const locationSelectionMode = useGetLocationSelectionMode(); const isDirectProcedureTypeAssignmentOnImport = useIsDirectProcedureTypeAssignmentOnImport(); - const importData = useImportData(isSchoolYearEnabled); + const importData = useImportData(); async function handleSubmit(values: ImportDataValues) { await importData.mutateAsync(values).catch(); @@ -63,13 +69,11 @@ export function ImportDataSidebar() { function handleClose() { router.push(routes.procedures.overview); - if (importData.isSuccess) { - router.refresh(); - } + props.onClose(true); } return ( - <Sidebar open onClose={handleClose}> + <> <Formik initialValues={INITIAL_VALUES} onSubmit={handleSubmit}> {({ values, @@ -78,7 +82,7 @@ export function ImportDataSidebar() { isSubmitting, handleSubmit, }) => ( - <SidebarForm onSubmit={handleSubmit}> + <SidebarForm ref={props.formRef} onSubmit={handleSubmit}> <SidebarContent title="Daten importieren"> {importData.isSuccess ? ( <ImportResult @@ -94,7 +98,6 @@ export function ImportDataSidebar() { ) : ( <ImportDataFields listType={values.listType} - requireSchoolYear={isSchoolYearEnabled} isPastProcedureImportEnabled={isPastProcedureImportEnabled} locationSelectionMode={locationSelectionMode} isDirectProcedureTypeAssignmentOnImport={ @@ -120,7 +123,7 @@ export function ImportDataSidebar() { </SidebarForm> )} </Formik> - </Sidebar> + </> ); } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportResult.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportResult.tsx index e1ee070dc..967817098 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportResult.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/importData/ImportResult.tsx @@ -3,13 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - ApiImportStatistics, - ApiSchoolEntryFeature, -} from "@eshg/employee-portal-api/schoolEntry"; +import { ApiImportStatistics } from "@eshg/employee-portal-api/schoolEntry"; import { Stack, Typography } from "@mui/joy"; -import { useIsNewFeatureEnabled } from "@/lib/businessModules/schoolEntry/api/queries/featureTogglesApi"; import { ImportResultProceduresSummary } from "@/lib/businessModules/schoolEntry/features/procedures/importData/ImportResultProceduresSummary"; import { FileDownloadButton } from "@/lib/shared/components/buttons/FileDownloadButton"; @@ -17,7 +13,6 @@ import { ImportResultItem, ImportResultSummary } from "./ImportResultSummary"; import { formatDuplicatedCount, formatFailedCount, - formatImportedCount, formatTotalCount, } from "./formatters"; @@ -49,21 +44,6 @@ function buildStatisticItems( ]; } -function getStatusHeading( - statistics: ApiImportStatistics, - isMergeEnabled: boolean, -) { - if (isMergeEnabled) { - return "Vorgänge"; - } else { - if (statistics.total === 0 || statistics.created === 0) { - return "Keine Vorgänge angelegt"; - } - - return `${formatImportedCount(statistics.created)} erfolgreich neu angelegt`; - } -} - interface ImportResultProps { file: File; statistics: ApiImportStatistics; @@ -71,24 +51,18 @@ interface ImportResultProps { } export function ImportResult(props: ImportResultProps) { - const isMergeEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.MergeProceduresOnImport, - ); - return ( <Stack gap={3}> <ImportResultSummary items={buildStatisticItems(props.statistics)} /> <Stack gap={3}> <Stack gap={1}> <Typography level="h4" component="h2" data-testid="statusHeading"> - {getStatusHeading(props.statistics, isMergeEnabled)} + Vorgänge </Typography> - {isMergeEnabled && ( - <ImportResultProceduresSummary - result={props.statistics} - isImportWithMerge={props.isImportWithMerge} - /> - )} + <ImportResultProceduresSummary + result={props.statistics} + isImportWithMerge={props.isImportWithMerge} + /> </Stack> {props.statistics.total > 0 && ( <Stack gap={1}> diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/LabelSelection.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/LabelSelection.tsx index 3b616a004..1021665b6 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/LabelSelection.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/LabelSelection.tsx @@ -12,7 +12,7 @@ import { Label } from "@/lib/businessModules/schoolEntry/api/models/Label"; import { LabelAutocomplete } from "@/lib/businessModules/schoolEntry/features/procedures/procedureDetails/LabelAutocomplete"; interface LabelSelectionProps { - onChange: (newValue: Label[]) => void; + onChange?: (newValue: Label[]) => void; } export function LabelSelection(props: LabelSelectionProps) { @@ -25,7 +25,7 @@ export function LabelSelection(props: LabelSelectionProps) { value={field.input.value} onChange={(newValue) => { void field.helpers.setValue(newValue); - props.onChange(newValue); + props.onChange?.(newValue); }} /> </BaseField> diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureActionsPanel.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureActionsPanel.tsx index 5040b1245..e5ae9375a 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureActionsPanel.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureActionsPanel.tsx @@ -18,9 +18,6 @@ export function ProcedureActionsPanel(props: { procedure: ProcedureDetails }) { const closeProcedureEnabled = useIsNewFeatureEnabled( ApiSchoolEntryFeature.CloseProcedure, ); - const deleteProcedureEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.DeleteProcedure, - ); const reopenProcedureEnabled = useIsNewFeatureEnabled( ApiSchoolEntryFeature.ReopenProcedure, ); @@ -54,7 +51,7 @@ export function ProcedureActionsPanel(props: { procedure: ProcedureDetails }) { ); } - if (deleteProcedureEnabled && props.procedure.isDeletable) { + if (props.procedure.isDeletable) { buttons.push( <OpenModalButton key="deleteProcedure" diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureDetails.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureDetails.tsx index aca216b6e..74206cf43 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureDetails.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureDetails.tsx @@ -5,15 +5,11 @@ "use client"; -import { - ApiLocationSelectionMode, - ApiSchoolEntryFeature, -} from "@eshg/employee-portal-api/schoolEntry"; +import { ApiLocationSelectionMode } from "@eshg/employee-portal-api/schoolEntry"; import { Grid, Stack } from "@mui/joy"; import { isDefined } from "remeda"; import { ProcedureDetails as ProcedureDetailsType } from "@/lib/businessModules/schoolEntry/api/models/ProcedureDetails"; -import { useIsNewFeatureEnabled } from "@/lib/businessModules/schoolEntry/api/queries/featureTogglesApi"; import { AddCustodianPanel } from "@/lib/businessModules/schoolEntry/features/procedures/procedureDetails/AddCustodianPanel"; import { ProcedureActionsPanel } from "@/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureActionsPanel"; import { ProcedureDetailsSection } from "@/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureDetailsSection"; @@ -30,10 +26,6 @@ interface ProcedureDetailsProps { } export function ProcedureDetails(props: ProcedureDetailsProps) { - const waitingRoomEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.WaitingRoom, - ); - const procedure = props.procedure; return ( @@ -63,8 +55,7 @@ export function ProcedureDetails(props: ProcedureDetailsProps) { <Stack spacing={SPACING}> <ProcedureDetailsSection procedure={procedure} /> <ProcedureActionsPanel procedure={procedure} /> - {waitingRoomEnabled && - props.locationSelectionMode === ApiLocationSelectionMode.None && + {props.locationSelectionMode === ApiLocationSelectionMode.None && !procedure.isClosed && isDefined(procedure.appointment) && ( <WaitingRoomPanel procedure={procedure} /> diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureDetailsSection.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureDetailsSection.tsx index 4b3221e02..f3f5970a4 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureDetailsSection.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/ProcedureDetailsSection.tsx @@ -3,27 +3,21 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - ApiLocationSelectionMode, - ApiSchoolEntryFeature, -} from "@eshg/employee-portal-api/schoolEntry"; +import { ApiLocationSelectionMode } from "@eshg/employee-portal-api/schoolEntry"; import { formatDate, formatDateTime, } from "@eshg/lib-portal/formatters/dateTime"; import { Divider, Stack } from "@mui/joy"; -import { useState } from "react"; import { isDefined } from "remeda"; import { ProcedureDetails } from "@/lib/businessModules/schoolEntry/api/models/ProcedureDetails"; import { useGetLocationSelectionMode } from "@/lib/businessModules/schoolEntry/api/queries/configApi"; -import { useIsNewFeatureEnabled } from "@/lib/businessModules/schoolEntry/api/queries/featureTogglesApi"; import { LabelChip } from "@/lib/businessModules/schoolEntry/features/labels/LabelChip"; import { formatSchoolYear } from "@/lib/businessModules/schoolEntry/features/procedures/formatters"; import { InvitationDetails } from "@/lib/businessModules/schoolEntry/features/procedures/procedureDetails/InvitationDetails"; -import { UpdateProcedureSidebar } from "@/lib/businessModules/schoolEntry/features/procedures/procedureDetails/UpdateProcedureSidebar"; +import { useUpdateProcedureSidebar } from "@/lib/businessModules/schoolEntry/features/procedures/procedureDetails/UpdateProcedureSidebar"; import { PROCEDURE_TYPES } from "@/lib/businessModules/schoolEntry/features/procedures/translations"; -import { OverlayBoundary } from "@/lib/shared/components/boundaries/OverlayBoundary"; import { ContentPanel } from "@/lib/shared/components/contentPanel/ContentPanel"; import { DetailsCell } from "@/lib/shared/components/detailsSection/DetailsCell"; import { DetailsSection } from "@/lib/shared/components/detailsSection/DetailsSection"; @@ -33,15 +27,8 @@ interface ProcedureDetailsProps { } export function ProcedureDetailsSection(props: ProcedureDetailsProps) { - const isSchoolYearEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.SchoolYear, - ); const locationSelectionMode = useGetLocationSelectionMode(); - const [isOpen, setIsOpen] = useState(false); - - function handleClose() { - setIsOpen(false); - } + const updateProcedureSidebar = useUpdateProcedureSidebar(); return ( <> @@ -49,7 +36,12 @@ export function ProcedureDetailsSection(props: ProcedureDetailsProps) { <DetailsSection name="additional-infos" title="Zusatzinfos" - onEdit={() => setIsOpen(true)} + onEdit={() => + updateProcedureSidebar.open({ + procedure: props.procedure, + locationSelectionMode, + }) + } canEdit={!props.procedure.isClosed} > <Stack gap={2} divider={<Divider />}> @@ -59,17 +51,15 @@ export function ProcedureDetailsSection(props: ProcedureDetailsProps) { label="Art" value={PROCEDURE_TYPES[props.procedure.type]} /> - {isSchoolYearEnabled && ( - <DetailsCell - name="schoolYear" - label="Schuljahr" - value={ - isDefined(props.procedure.schoolYear) - ? formatSchoolYear(props.procedure.schoolYear) - : "Kein Schuljahr zugewiesen" - } - /> - )} + <DetailsCell + name="schoolYear" + label="Schuljahr" + value={ + isDefined(props.procedure.schoolYear) + ? formatSchoolYear(props.procedure.schoolYear) + : "Kein Schuljahr zugewiesen" + } + /> {props.procedure.labels.length > 0 && ( <DetailsCell name="labels" @@ -88,7 +78,7 @@ export function ProcedureDetailsSection(props: ProcedureDetailsProps) { name="school" label="Schule" value={ - props.procedure.school.name + isDefined(props.procedure.school) ? props.procedure.school.name : "Keine Schule zugewiesen" } @@ -99,7 +89,7 @@ export function ProcedureDetailsSection(props: ProcedureDetailsProps) { name="location" label="Gesundheitsamt" value={ - props.procedure.location.name + isDefined(props.procedure.location) ? props.procedure.location.name : "Kein Gesundheitsamt zugewiesen" } @@ -136,16 +126,6 @@ export function ProcedureDetailsSection(props: ProcedureDetailsProps) { </Stack> </DetailsSection> </ContentPanel> - {isOpen && ( - <OverlayBoundary> - <UpdateProcedureSidebar - procedure={props.procedure} - canEditSchoolYear={isSchoolYearEnabled} - onClose={handleClose} - locationSelectionMode={locationSelectionMode} - /> - </OverlayBoundary> - )} </> ); } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/UpdateProcedureSidebar.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/UpdateProcedureSidebar.tsx index 49efaa600..5a135dad9 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/UpdateProcedureSidebar.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/procedureDetails/UpdateProcedureSidebar.tsx @@ -3,10 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - ApiAddContact200Response, - ApiContactCategory, -} from "@eshg/employee-portal-api/base"; +import { ApiContactCategory } from "@eshg/employee-portal-api/base"; import { ApiAppointment, ApiLocationSelectionMode, @@ -29,18 +26,16 @@ import { } from "@eshg/lib-portal/helpers/form"; import { isEmptyString } from "@eshg/lib-portal/helpers/guards"; import { validatePastOrTodayDate } from "@eshg/lib-portal/helpers/validators"; +import { useHasChanged } from "@eshg/lib-portal/hooks/useHasChanged"; import { OptionalFieldValue } from "@eshg/lib-portal/types/form"; import { Divider, Stack } from "@mui/joy"; -import { Formik, FormikErrors } from "formik"; -import { ReactNode, useRef, useState } from "react"; -import { isDefined, isNullish } from "remeda"; +import { FormikProvider, useFormik } from "formik"; +import { ReactNode, useEffect } from "react"; +import { doNothing, isDefined } from "remeda"; import { Label } from "@/lib/businessModules/schoolEntry/api/models/Label"; -import { School } from "@/lib/businessModules/schoolEntry/api/models/Procedure"; -import { - Location, - ProcedureDetails, -} from "@/lib/businessModules/schoolEntry/api/models/ProcedureDetails"; +import { Location } from "@/lib/businessModules/schoolEntry/api/models/Location"; +import { ProcedureDetails } from "@/lib/businessModules/schoolEntry/api/models/ProcedureDetails"; import { useUpdateProcedure } from "@/lib/businessModules/schoolEntry/api/mutations/schoolEntryApi"; import { useGetFreeAppointmentsForProcedureUnsuspended } from "@/lib/businessModules/schoolEntry/api/queries/schoolEntryApi"; import { @@ -52,33 +47,38 @@ import { isDraft } from "@/lib/businessModules/schoolEntry/features/procedures/p import { SchoolYearField } from "@/lib/businessModules/schoolEntry/features/procedures/shared/schoolYear"; import { Appointment } from "@/lib/businessModules/travelMedicine/api/models/Appointment"; import { FormButtonBar } from "@/lib/shared/components/form/FormButtonBar"; -import { - SidebarForm, - SidebarFormHandle, -} from "@/lib/shared/components/form/SidebarForm"; +import { SidebarForm } from "@/lib/shared/components/form/SidebarForm"; import { CheckboxField } from "@/lib/shared/components/formFields/CheckboxField"; -import { Sidebar } from "@/lib/shared/components/sidebar/Sidebar"; import { SidebarActions } from "@/lib/shared/components/sidebar/SidebarActions"; import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; +import { + SidebarWithFormRefProps, + UseSidebarWithFormRefResult, + useSidebarWithFormRef, +} from "@/lib/shared/hooks/useSidebarWithFormRef"; import { SelectContactField } from "./SelectContactField"; -export interface ModifyProcedureValues { +export function useUpdateProcedureSidebar(): UseSidebarWithFormRefResult<UpdateProcedureSidebarProps> { + return useSidebarWithFormRef({ + component: UpdateProcedureSidebar, + }); +} + +export interface UpdateProcedureValues { procedureType: ApiSchoolEntryProcedureType; labels: Label[]; appointment: SelectObjectFieldValue<ApiAppointment, false>; isInvitationSent: boolean; - school: School; - location: Location; + school: Location | null; + location: Location | null; isDeceased: boolean; deceased: OptionalFieldValue<string>; schoolYear: OptionalFieldValue<number>; } -interface UpdateProcedureSidebarProps { +interface UpdateProcedureSidebarProps extends SidebarWithFormRefProps { procedure: ProcedureDetails; - canEditSchoolYear: boolean; - onClose: () => void; locationSelectionMode: ApiLocationSelectionMode; } @@ -87,7 +87,7 @@ function getId(label: Label) { } function mapValues( - values: ModifyProcedureValues, + values: UpdateProcedureValues, procedure: ProcedureDetails, ): UpdateProcedureRequest { return { @@ -101,8 +101,8 @@ function mapValues( : values.procedureType, appointment: values.appointment ?? undefined, isInvitationSent: values.isInvitationSent, - schoolId: values.school?.id ?? null, - locationId: values.location?.id ?? null, + schoolId: values.school?.id ?? undefined, + locationId: values.location?.id ?? undefined, isDeceased: values.isDeceased, deceased: isEmptyString(values.deceased) ? undefined @@ -118,210 +118,195 @@ function getAppointmentLabel(appointment: Appointment) { )}`; } -export function UpdateProcedureSidebar(props: UpdateProcedureSidebarProps) { - const procedure = props.procedure; +function useUpdateProcedureForm( + procedure: ProcedureDetails, + onSuccess: () => void, +) { + const updateProcedure = useUpdateProcedure(); + return useFormik<UpdateProcedureValues>({ + initialValues: { + procedureType: procedure.type, + labels: procedure.labels, + appointment: procedure.appointment ?? null, + isInvitationSent: procedure.isInvitationSent, + school: procedure.school ?? null, + location: procedure.location ?? null, + isDeceased: procedure.isDeceased, + deceased: isDefined(procedure.deceased) + ? toDateString(procedure.deceased) + : "", + schoolYear: parseOptionalValue(procedure.schoolYear), + }, + onSubmit: (values) => + updateProcedure + .mutateAsync(mapValues(values, procedure), { + onSuccess, + }) + .catch(doNothing), + }); +} + +function UpdateProcedureSidebar(props: UpdateProcedureSidebarProps) { + const { procedure, locationSelectionMode } = props; const isInitialEntryLevel = procedure.isEntryLevel && procedure.type === ApiSchoolEntryProcedureType.DraftSchoolImport; const isMissingChildAddress = !isDefined(procedure.child.contactAddress); + const hasInitialAppointment = procedure.appointment !== undefined; - const initialValues: ModifyProcedureValues = { - procedureType: procedure.type, - labels: procedure.labels, - appointment: procedure.appointment ?? null, - isInvitationSent: procedure.isInvitationSent, - school: procedure.school, - location: procedure.location, - isDeceased: procedure.isDeceased, - deceased: isDefined(procedure.deceased) - ? toDateString(procedure.deceased) - : "", - schoolYear: parseOptionalValue(procedure.schoolYear), - }; - const hasInitialAppointment = initialValues.appointment !== null; - - const modifyProcedure = useUpdateProcedure(); - - async function handleSubmit(values: ModifyProcedureValues) { - await modifyProcedure - .mutateAsync(mapValues(values, procedure), { - onSuccess: props.onClose, - }) - .catch(); - } - - const [type, setType] = useState(procedure.type); - const [labelIds, setLabelIds] = useState(procedure.labels.map(getId)); - const [locationId, setLocationId] = useState<string | undefined>( - procedure.location.id, - ); - - function handleChangeLabels(newValue: Label[]) { - setLabelIds(newValue.map(getId)); - } - - function handleLocationChanged( - locationSelectionMode: ApiLocationSelectionMode, - setFieldValue: ( - field: string, - value: null, - ) => Promise<void | FormikErrors<ModifyProcedureValues>>, - ) { - return function handleChanged( - contact: SelectObjectFieldValue<ApiAddContact200Response, false>, - ) { - if (props.locationSelectionMode === locationSelectionMode) { - setLocationId(contact?.id); - void setFieldValue("appointment", null); - } - }; - } + const form = useUpdateProcedureForm(procedure, () => props.onClose(true)); + const { values, isSubmitting, setFieldValue } = form; const getFreeAppointments = useGetFreeAppointmentsForProcedureUnsuspended({ procedureId: procedure.id, - procedureType: type, - labelIds, - locationId, + procedureType: values.procedureType, + labelIds: resolveLabelIds(values.labels), + // TODO ISSUE-6050: Explicitly pass school as separate parameter + locationId: resolveLocationId(locationSelectionMode, values), }); const freeAppointments = getFreeAppointments.data ?? []; const hasNoFreeAppointments = freeAppointments.length === 0; - const sidebarFormRef = useRef<SidebarFormHandle>(null); + // clear appointment when school or location changes + const schoolChanged = useHasChanged(values.school); + const locationChanged = useHasChanged(values.location); + const clearAppointment = + (isSchoolSelectionMode(locationSelectionMode) && schoolChanged) || + (isHealthDepartmentSelectionMode(locationSelectionMode) && locationChanged); + useEffect(() => { + if (clearAppointment) { + void setFieldValue("appointment", null); + } + }, [clearAppointment, setFieldValue]); return ( - <Sidebar open onClose={props.onClose}> - <Formik initialValues={initialValues} onSubmit={handleSubmit}> - {({ values, isSubmitting, setFieldValue }) => ( - <SidebarForm ref={sidebarFormRef}> - <SidebarContent title="Zusatzinfos"> - <Stack gap={2}> - <SelectField - label="Art" - name="procedureType" - options={ - isInitialEntryLevel - ? PROCEDURE_TYPE_OPTIONS_ENTRY_LEVEL - : PROCEDURE_TYPE_OPTIONS_EXCLUDING_DRAFT - } - onChange={(value) => - setType(value as ApiSchoolEntryProcedureType) - } - /> - {props.canEditSchoolYear && ( - <SchoolYearField name="schoolYear" label="Schuljahr" /> - )} - <LabelSelection onChange={handleChangeLabels} /> - <Divider /> + <> + <FormikProvider value={form}> + <SidebarForm ref={props.formRef}> + <SidebarContent title="Zusatzinfos"> + <Stack gap={2}> + <SelectField + label="Art" + name="procedureType" + options={ + isInitialEntryLevel + ? PROCEDURE_TYPE_OPTIONS_ENTRY_LEVEL + : PROCEDURE_TYPE_OPTIONS_EXCLUDING_DRAFT + } + /> + <SchoolYearField name="schoolYear" label="Schuljahr" /> + <LabelSelection /> + <Divider /> + <SelectContactField + name="school" + label="Schule" + category={ApiContactCategory.School} + /> + {isHealthDepartmentSelectionMode(locationSelectionMode) && ( <SelectContactField - name="school" - label="Schule" - category={ApiContactCategory.School} - onChange={handleLocationChanged( - ApiLocationSelectionMode.School, - setFieldValue, - )} - /> - {props.locationSelectionMode === - ApiLocationSelectionMode.HealthDepartment && ( - <SelectContactField - name="location" - label="Gesundheitsamt" - category={ApiContactCategory.HealthDepartment} - onChange={handleLocationChanged( - ApiLocationSelectionMode.HealthDepartment, - setFieldValue, - )} - /> - )} - <Divider /> - <SelectObjectField - name="appointment" - label="Termin" - required={ - hasInitialAppointment - ? "Termin darf nicht gelöscht werden." - : undefined - } - options={freeAppointments} - getOptionLabel={getAppointmentLabel} - loading={getFreeAppointments.isFetching} - disabled={hasNoFreeAppointments || isMissingChildAddress} - placeholder={ - hasNoFreeAppointments - ? "Keine freien Termine verfügbar." - : undefined - } - onValueChanged={() => - setFieldValue("isInvitationSent", false) - } + name="location" + label="Gesundheitsamt" + category={ApiContactCategory.HealthDepartment} /> - {displayWarningWhen(isMissingChildAddress, { - title: "Adresse fehlt", + )} + <Divider /> + <SelectObjectField + name="appointment" + label="Termin" + required={ + hasInitialAppointment + ? "Termin darf nicht gelöscht werden." + : undefined + } + options={freeAppointments} + getOptionLabel={getAppointmentLabel} + loading={getFreeAppointments.isFetching} + disabled={hasNoFreeAppointments || isMissingChildAddress} + placeholder={ + hasNoFreeAppointments + ? "Keine freien Termine verfügbar." + : undefined + } + onValueChanged={() => + void setFieldValue("isInvitationSent", false) + } + /> + {displayWarningWhen(isMissingChildAddress, { + title: "Adresse fehlt", + message: + "Erfassen Sie die Adresse des Kindes, um einen Termin zuweisen und eine Einladung versenden zu können.", + })} + {displayWarningWhen( + isSchoolSelectionMode(locationSelectionMode) && + values.school === null, + { + title: "Schule fehlt", + message: + "Erfassen Sie die Schule, um einen Termin zuweisen und eine Einladung versenden zu können.", + }, + )} + {displayWarningWhen( + isHealthDepartmentSelectionMode(locationSelectionMode) && + values.location === null, + { + title: "Gesundheitsamt fehlt", message: - "Erfassen Sie die Adresse des Kindes, um einen Termin zuweisen und eine Einladung versenden zu können.", - })} - {displayWarningWhen( - props.locationSelectionMode === - ApiLocationSelectionMode.School && - isContactEmpty(values.school), - { - title: "Schule fehlt", - message: - "Erfassen Sie die Schule, um einen Termin zuweisen und eine Einladung versenden zu können.", - }, - )} - {displayWarningWhen( - props.locationSelectionMode === - ApiLocationSelectionMode.HealthDepartment && - isContactEmpty(values.location), - { - title: "Gesundheitsamt fehlt", - message: - "Erfassen Sie das Gesundheitsamt, um einen Termin zuweisen und eine Einladung versenden zu können.", - }, - )} - {values.appointment !== null && ( - <CheckboxField - name="isInvitationSent" - label="Einladung versandt" - /> - )} - <Divider /> + "Erfassen Sie das Gesundheitsamt, um einen Termin zuweisen und eine Einladung versenden zu können.", + }, + )} + {values.appointment !== null && ( <CheckboxField - name="isDeceased" - label="Kind verstorben" - onChange={(event) => { - if (!event.target.checked) { - void setFieldValue("deceased", ""); - } - }} + name="isInvitationSent" + label="Einladung versandt" /> - {values.isDeceased && ( - <DateField - name="deceased" - label="am" - component={HorizontalField} - validate={validatePastOrTodayDate} - /> - )} - </Stack> - </SidebarContent> - <SidebarActions> - <FormButtonBar - submitting={isSubmitting} - submitLabel="Speichern" - onCancel={props.onClose} + )} + <Divider /> + <CheckboxField + name="isDeceased" + label="Kind verstorben" + onChange={(event) => { + if (!event.target.checked) { + void setFieldValue("deceased", ""); + } + }} /> - </SidebarActions> - </SidebarForm> - )} - </Formik> - </Sidebar> + {values.isDeceased && ( + <DateField + name="deceased" + label="am" + component={HorizontalField} + validate={validatePastOrTodayDate} + /> + )} + </Stack> + </SidebarContent> + <SidebarActions> + <FormButtonBar + submitting={isSubmitting} + submitLabel="Speichern" + onCancel={props.onClose} + /> + </SidebarActions> + </SidebarForm> + </FormikProvider> + </> ); } +function isSchoolSelectionMode( + locationSelectionMode: ApiLocationSelectionMode, +): boolean { + return locationSelectionMode === ApiLocationSelectionMode.School; +} + +function isHealthDepartmentSelectionMode( + locationSelectionMode: ApiLocationSelectionMode, +): boolean { + return locationSelectionMode === ApiLocationSelectionMode.HealthDepartment; +} + function displayWarningWhen( condition: boolean, props: Omit<AlertProps, "color">, @@ -329,6 +314,25 @@ function displayWarningWhen( return condition && <Alert {...props} color="warning" />; } -function isContactEmpty(school: School): boolean { - return isNullish(school) || school.id === ""; +function resolveLabelIds(labels: Label[]): string[] | undefined { + if (labels.length === 0) { + return undefined; + } + + return labels.map(getId); +} + +function resolveLocationId( + locationSelectionMode: ApiLocationSelectionMode, + values: Pick<UpdateProcedureValues, "school" | "location">, +): string | undefined { + if (isSchoolSelectionMode(locationSelectionMode)) { + return values.school?.id; + } + + if (isHealthDepartmentSelectionMode(locationSelectionMode)) { + return values.location?.id; + } + + return undefined; } 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 79a8d187b..25002e010 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 @@ -3,10 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - ApiSchoolEntryFeature, - GetProceduresRequest, -} from "@eshg/employee-portal-api/schoolEntry"; +import { GetProceduresRequest } from "@eshg/employee-portal-api/schoolEntry"; import { SelectOptions } from "@eshg/lib-portal/components/formFields/SelectOptions"; import { isDateString, @@ -17,7 +14,6 @@ import { FormControl, FormLabel, Input, Select } from "@mui/joy"; import { isDefined, isEmpty } from "remeda"; import { Label } from "@/lib/businessModules/schoolEntry/api/models/Label"; -import { useIsNewFeatureEnabled } from "@/lib/businessModules/schoolEntry/api/queries/featureTogglesApi"; import { PROCEDURE_TYPE_OPTIONS } from "@/lib/businessModules/schoolEntry/features/procedures/options"; import { LabelAutocomplete } from "@/lib/businessModules/schoolEntry/features/procedures/procedureDetails/LabelAutocomplete"; import { SearchSchoolFilter } from "@/lib/businessModules/schoolEntry/features/procedures/proceduresTable/SearchSchoolFilter"; @@ -76,10 +72,6 @@ function evaluateStringAsBoolean(value: string) { } export function ProcedureFilterSettings(props: ProcedureFilterSettingsProps) { - const isSchoolYearEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.SchoolYear, - ); - return ( <FilterSettingsSheet {...props.filterSettingsSheetProps}> <FilterSettingsContent @@ -181,20 +173,18 @@ export function ProcedureFilterSettings(props: ProcedureFilterSettingsProps) { /> </Select> </FormControl> - {isSchoolYearEnabled && ( - <FormControl> - <FormLabel>Schuljahr</FormLabel> - <SchoolYearAutocomplete - value={props.filterFormValues.schoolYearFilter ?? null} - onChange={(_, newValue) => { - props.setFilterFormValue( - "schoolYearFilter", - newValue ?? undefined, - ); - }} - /> - </FormControl> - )} + <FormControl> + <FormLabel>Schuljahr</FormLabel> + <SchoolYearAutocomplete + value={props.filterFormValues.schoolYearFilter ?? null} + onChange={(_, newValue) => { + props.setFilterFormValue( + "schoolYearFilter", + newValue ?? undefined, + ); + }} + /> + </FormControl> <FormControl> <FormLabel>Schule</FormLabel> <SearchSchoolFilter diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProcedureTableTitle.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProcedureTableTitle.tsx index 7376e7864..a36513ab2 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProcedureTableTitle.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProcedureTableTitle.tsx @@ -5,25 +5,15 @@ import { ApiCreateAppointmentsBulkResponse } from "@eshg/employee-portal-api/schoolEntry"; import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; -import { - CalendarMonthOutlined, - SubdirectoryArrowRightOutlined, -} from "@mui/icons-material"; -import { Button, Divider, Sheet, Stack, Typography, styled } from "@mui/joy"; +import { useAlert } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { CalendarMonthOutlined } from "@mui/icons-material"; +import { Button } from "@mui/joy"; import { RowSelectionState } from "@tanstack/react-table"; import { useCreateAppointmentsInBulk } from "@/lib/businessModules/schoolEntry/api/mutations/schoolEntryApi"; +import { RowSelectionTableToolbar } from "@/lib/shared/components/table/RowSelectionTableToolbar"; import { mapToRowIds } from "@/lib/shared/hooks/table/useRowSelection"; -const StyledSheet = styled(Sheet)(({ theme }) => ({ - display: "flex", - alignItems: "center", - padding: theme.spacing(0.5, 1.5), - borderRadius: 0, - height: 40, -})); - interface ProcedureTableTitleProps { rowSelection: RowSelectionState; } @@ -71,14 +61,15 @@ function errorMessage(numError: number) { function useDisplayOnSuccessMessageForBulkAppointmentCreation() { const snackbar = useSnackbar(); - const alert = useAlertContext(); + const alert = useAlert(); return (response: ApiCreateAppointmentsBulkResponse) => { if (response.numCreated > 0) { if (response.numUnmodified === 0 && response.numError === 0) { + alert.close(); snackbar.confirmation(createdMessage(response.numCreated)); } else { - alert?.setAlert({ + alert.warning({ title: "Terminzuweisung teilweise fehlgeschlagen", message: ( <> @@ -87,11 +78,11 @@ function useDisplayOnSuccessMessageForBulkAppointmentCreation() { {errorMessage(response.numError)} </> ), - color: "warning", + closeable: true, }); } } else { - alert?.setAlert({ + alert.error({ title: "Es konnten keine Termine vergeben werden.", message: ( <> @@ -99,7 +90,7 @@ function useDisplayOnSuccessMessageForBulkAppointmentCreation() { {errorMessage(response.numError)} </> ), - color: "danger", + closeable: true, }); } }; @@ -123,36 +114,27 @@ export function ProceduresTableTitle(props: ProcedureTableTitleProps) { } return ( - <StyledSheet variant="soft"> - <Stack direction="row" gap={2} alignItems="center"> - <SubdirectoryArrowRightOutlined - sx={{ transform: "rotate(90deg)", fontSize: "1.25rem" }} - /> - <Typography level="body-sm" data-testid="proceduresIndicator"> - <Typography fontWeight="bold"> - {selectedProcedureIds.length} - </Typography>{" "} - {selectedProcedureIds.length === 1 ? "Vorgang" : "Vorgänge"}{" "} - ausgewählt - </Typography> - {selectedProcedureIds.length > 0 && ( - <> - <Divider orientation="vertical" sx={{ marginY: 1 }} /> - <Button - startDecorator={<CalendarMonthOutlined />} - variant="plain" - color="neutral" - size="sm" - loading={createAppointmentsInBulk.isPending} - loadingPosition="start" - disabled={createAppointmentsInBulk.isPending} - onClick={handleClickBulkAppointmentButton} - > - Termin zuweisen - </Button> - </> - )} - </Stack> - </StyledSheet> + <RowSelectionTableToolbar + rowSelection={props.rowSelection} + elementName={{ + singular: "Vorgang ausgewählt", + plural: "Vorgänge ausgewählt", + }} + > + {selectedProcedureIds.length > 0 && ( + <Button + startDecorator={<CalendarMonthOutlined />} + variant="plain" + color="neutral" + size="sm" + loading={createAppointmentsInBulk.isPending} + loadingPosition="start" + disabled={createAppointmentsInBulk.isPending} + onClick={handleClickBulkAppointmentButton} + > + Termin zuweisen + </Button> + )} + </RowSelectionTableToolbar> ); } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProceduresTable.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProceduresTable.tsx index af903085e..188dca9ed 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProceduresTable.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/proceduresTable/ProceduresTable.tsx @@ -5,10 +5,7 @@ "use client"; -import { - ApiSchoolEntryFeature, - ApiSchoolEntryProcedureSortKey, -} from "@eshg/employee-portal-api/schoolEntry"; +import { ApiSchoolEntryProcedureSortKey } from "@eshg/employee-portal-api/schoolEntry"; import { formatDate, formatDateTime, @@ -23,7 +20,6 @@ import { ReactNode, useReducer } from "react"; import { isNullish } from "remeda"; import { Procedure } from "@/lib/businessModules/schoolEntry/api/models/Procedure"; -import { useIsNewFeatureEnabled } from "@/lib/businessModules/schoolEntry/api/queries/featureTogglesApi"; import { useGetProcedures } from "@/lib/businessModules/schoolEntry/api/queries/schoolEntryApi"; import { LabelChip } from "@/lib/businessModules/schoolEntry/features/labels/LabelChip"; import { formatSchoolYear } from "@/lib/businessModules/schoolEntry/features/procedures/formatters"; @@ -71,9 +67,6 @@ const initialSorting: ColumnSort = { }; export function ProceduresTable(props: ProceduresTableProps) { - const isSearchByKnowledgeFactorsEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.SearchByKnowledgeFactors, - ); const [activePanel, toggleActivePanel] = useReducer( reduceActivePanel, undefined, @@ -132,14 +125,12 @@ export function ProceduresTable(props: ProceduresTableProps) { isFilterVisible={activePanel === "filters"} onClick={() => toggleActivePanel("filters")} />, - isSearchByKnowledgeFactorsEnabled ? ( - <TogglePersonSearchButton - {...personSearch.buttonProps} - key="personSearchButton" - expanded={activePanel === "personSearch"} - onClick={() => toggleActivePanel("personSearch")} - /> - ) : null, + <TogglePersonSearchButton + {...personSearch.buttonProps} + key="personSearchButton" + expanded={activePanel === "personSearch"} + onClick={() => toggleActivePanel("personSearch")} + />, ]} right={props.buttons} alignItems="flex-end" @@ -193,7 +184,7 @@ export function ProceduresTable(props: ProceduresTableProps) { } const columnHelper = createColumnHelper<Procedure>(); -const CHILD_COLUMNS = [ +const COLUMNS = [ columnHelper.accessor("child.lastName", { header: "Name", cell: (props) => props.getValue(), @@ -227,9 +218,17 @@ const CHILD_COLUMNS = [ }, }, }), -]; - -const REST_COLUMNS = [ + columnHelper.accessor("schoolYear", { + header: "Schuljahr", + cell: (props) => formatSchoolYear(props.getValue()), + enableSorting: true, + meta: { + width: 116, + canNavigate: { + parentRow: true, + }, + }, + }), columnHelper.accessor("school.name", { header: "Schule", cell: (props) => props.getValue(), @@ -297,28 +296,8 @@ const REST_COLUMNS = [ }), ]; -const SCHOOL_YEAR_COLUMN = columnHelper.accessor("schoolYear", { - header: "Schuljahr", - cell: (props) => formatSchoolYear(props.getValue()), - enableSorting: true, - meta: { - width: 116, - canNavigate: { - parentRow: true, - }, - }, -}); - function useProcedureColumns(): TableOptions<Procedure>["columns"] { - const isSchoolYearEnabled = useIsNewFeatureEnabled( - ApiSchoolEntryFeature.SchoolYear, - ); - - return [ - ...CHILD_COLUMNS, - ...(isSchoolYearEnabled ? [SCHOOL_YEAR_COLUMN] : []), - ...REST_COLUMNS, - ]; + return COLUMNS; } type PanelName = "filters" | "personSearch"; diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/reports/MedicalReportSidebar.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/reports/MedicalReportSidebar.tsx index 88bb84eba..2d9aaadcf 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/reports/MedicalReportSidebar.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/reports/MedicalReportSidebar.tsx @@ -15,14 +15,22 @@ import { useCreateMedicalReport } from "@/lib/businessModules/schoolEntry/api/mu import { ButtonBar } from "@/lib/shared/components/buttons/ButtonBar"; import { SidebarForm } from "@/lib/shared/components/form/SidebarForm"; import { TextareaField } from "@/lib/shared/components/formFields/TextareaField"; -import { Sidebar } from "@/lib/shared/components/sidebar/Sidebar"; import { SidebarActions } from "@/lib/shared/components/sidebar/SidebarActions"; import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; +import { + SidebarWithFormRefProps, + UseSidebarWithFormRefResult, + useSidebarWithFormRef, +} from "@/lib/shared/hooks/useSidebarWithFormRef"; -interface MedicalReportSidebarProps { +export function useMedicalReportSidebar(): UseSidebarWithFormRefResult<MedicalReportSidebarProps> { + return useSidebarWithFormRef({ + component: MedicalReportSidebar, + }); +} + +interface MedicalReportSidebarProps extends SidebarWithFormRefProps { procedureId: string; - onClose: () => void; - open: boolean; } interface MedicalReportValues { @@ -35,7 +43,7 @@ const initialValues: MedicalReportValues = { remark: "", }; -export function MedicalReportSidebar(props: MedicalReportSidebarProps) { +function MedicalReportSidebar(props: MedicalReportSidebarProps) { const createMedicalReport = useCreateMedicalReport(props.procedureId); const { downloadContainerRef, download } = useFileDownload( createMedicalReport.mutateAsync, @@ -43,14 +51,14 @@ export function MedicalReportSidebar(props: MedicalReportSidebarProps) { async function handleSubmit(values: MedicalReportValues) { await download(values); - props.onClose(); + props.onClose(true); } return ( - <Sidebar open={props.open} onClose={props.onClose}> + <> <Formik initialValues={initialValues} onSubmit={handleSubmit}> {({ isSubmitting, handleSubmit }) => ( - <SidebarForm onSubmit={handleSubmit}> + <SidebarForm ref={props.formRef} onSubmit={handleSubmit}> <SidebarContent title="Arztbrief erstellen"> <Stack gap={2}> <BooleanSelectField @@ -75,7 +83,7 @@ export function MedicalReportSidebar(props: MedicalReportSidebarProps) { <Button variant="plain" color="primary" - onClick={props.onClose} + onClick={() => props.onClose()} > Abbrechen </Button> @@ -93,6 +101,6 @@ export function MedicalReportSidebar(props: MedicalReportSidebarProps) { </SidebarForm> )} </Formik> - </Sidebar> + </> ); } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/reports/SchoolInfoLetterSidebar.tsx b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/reports/SchoolInfoLetterSidebar.tsx index 2ee3d8d6b..8941d580c 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/reports/SchoolInfoLetterSidebar.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/features/procedures/reports/SchoolInfoLetterSidebar.tsx @@ -17,9 +17,19 @@ import { ButtonBar } from "@/lib/shared/components/buttons/ButtonBar"; import { SidebarForm } from "@/lib/shared/components/form/SidebarForm"; import { CheckboxField } from "@/lib/shared/components/formFields/CheckboxField"; import { TextareaField } from "@/lib/shared/components/formFields/TextareaField"; -import { Sidebar, SidebarProps } from "@/lib/shared/components/sidebar/Sidebar"; import { SidebarActions } from "@/lib/shared/components/sidebar/SidebarActions"; import { SidebarContent } from "@/lib/shared/components/sidebar/SidebarContent"; +import { + SidebarWithFormRefProps, + UseSidebarWithFormRefResult, + useSidebarWithFormRef, +} from "@/lib/shared/hooks/useSidebarWithFormRef"; + +export function useSchoolInfoLetterSidebar(): UseSidebarWithFormRefResult<SchoolInfoLetterSidebarProps> { + return useSidebarWithFormRef({ + component: SchoolInfoLetterSidebar, + }); +} interface SchoolInfoLetterFormValues { note: OptionalFieldValue<string>; @@ -37,14 +47,11 @@ const INITIAL_VALUES: SchoolInfoLetterFormValues = { referredToFurtherConsultationFromSchool: false, }; -interface SchoolInfoLetterSidebarProps { +interface SchoolInfoLetterSidebarProps extends SidebarWithFormRefProps { procedureId: string; - onClose: () => void; } -export function SchoolInfoLetterSidebar( - props: SchoolInfoLetterSidebarProps & SidebarProps, -) { +function SchoolInfoLetterSidebar(props: SchoolInfoLetterSidebarProps) { const createSchoolInfoLetter = useCreateSchoolInfoLetter(props.procedureId); const { downloadContainerRef, download } = useFileDownload( createSchoolInfoLetter.mutateAsync, @@ -52,18 +59,14 @@ export function SchoolInfoLetterSidebar( async function handleSubmit(values: SchoolInfoLetterFormValues) { await download(mapToRequest(values)); - props.onClose(); + props.onClose(true); } return ( - <Sidebar - open={props.open} - onClose={props.onClose} - aria-label={props["aria-label"]} - > + <> <Formik initialValues={INITIAL_VALUES} onSubmit={handleSubmit}> {({ handleSubmit, isSubmitting }) => ( - <SidebarForm onSubmit={handleSubmit}> + <SidebarForm ref={props.formRef} onSubmit={handleSubmit}> <SidebarContent title="Schulinfobrief erstellen"> <Stack gap={2}> <TextareaField @@ -101,7 +104,7 @@ export function SchoolInfoLetterSidebar( <Button variant="plain" color="primary" - onClick={props.onClose} + onClick={() => props.onClose()} > Abbrechen </Button> @@ -116,7 +119,7 @@ export function SchoolInfoLetterSidebar( </SidebarForm> )} </Formik> - </Sidebar> + </> ); } diff --git a/employee-portal/src/lib/businessModules/schoolEntry/shared/routes.ts b/employee-portal/src/lib/businessModules/schoolEntry/shared/routes.ts index 370571b83..5ba1297a0 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/shared/routes.ts +++ b/employee-portal/src/lib/businessModules/schoolEntry/shared/routes.ts @@ -10,9 +10,6 @@ export const routes = defineRoutes("/school-entry", (schoolEntryPath) => ({ schoolEntryPath("/procedures"), (proceduresPath) => ({ overview: proceduresPath("/"), - importData: proceduresPath("/import-data"), - importCitizenList: proceduresPath("/import-citizen-list"), - importSchoolList: proceduresPath("/import-school-list"), byId: (procedureId: string) => defineRoutes(proceduresPath(`/${procedureId}`), (procedurePath) => ({ details: procedurePath("/details"), diff --git a/employee-portal/src/lib/businessModules/schoolEntry/shared/sideNavigationItem.tsx b/employee-portal/src/lib/businessModules/schoolEntry/shared/sideNavigationItem.tsx index b4abeccd9..746128f02 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/shared/sideNavigationItem.tsx +++ b/employee-portal/src/lib/businessModules/schoolEntry/shared/sideNavigationItem.tsx @@ -4,10 +4,7 @@ */ import { ApiBaseFeature, ApiUserRole } from "@eshg/employee-portal-api/base"; -import { - ApiLocationSelectionMode, - ApiSchoolEntryFeature, -} from "@eshg/employee-portal-api/schoolEntry"; +import { ApiLocationSelectionMode } from "@eshg/employee-portal-api/schoolEntry"; import { EscalatorWarning } from "@mui/icons-material"; import { useQuery } from "@tanstack/react-query"; @@ -18,7 +15,6 @@ import { } from "@/lib/baseModule/components/layout/sideNavigation/types"; import { useConfigApi } from "@/lib/businessModules/schoolEntry/api/clients"; import { getLocationSelectionModeQuery } from "@/lib/businessModules/schoolEntry/api/queries/configApi"; -import { useIsNewFeatureEnabledUnsuspended } from "@/lib/businessModules/schoolEntry/api/queries/featureTogglesApi"; import { hasUserRole } from "@/lib/shared/helpers/accessControl"; import { routes } from "./routes"; @@ -57,9 +53,6 @@ const inboxNavigationItem: SideNavigationSubItem = { export function useSideNavigationItems(): SideNavigationItem[] { const isInboxEnabled = useIsNewFeatureEnabled(ApiBaseFeature.Inbox); - const { data: isWaitingRoomEnabled, isError: isWaitingRoomError } = - useIsNewFeatureEnabledUnsuspended(ApiSchoolEntryFeature.WaitingRoom); - const configApi = useConfigApi(); const { data: locationSelectionMode, isError: isLocationModeError } = useQuery({ @@ -72,9 +65,7 @@ export function useSideNavigationItems(): SideNavigationItem[] { const subItems = [ proceduresNavigationItem, - ...(isWaitingRoomEnabled && !hasLocationMode - ? [waitingRoomNavigationItem] - : []), + ...(hasLocationMode ? [] : [waitingRoomNavigationItem]), ...defaultSubItems, ...(isInboxEnabled ? [inboxNavigationItem] : []), ]; @@ -82,10 +73,9 @@ export function useSideNavigationItems(): SideNavigationItem[] { const sideNavigationItem = { name: "Einschulung", decorator: <EscalatorWarning />, - error: - isWaitingRoomError || isLocationModeError - ? "Bei der Verbindung zum Einschulungsmodul ist ein Fehler aufgetreten." - : undefined, + error: isLocationModeError + ? "Bei der Verbindung zum Einschulungsmodul ist ein Fehler aufgetreten." + : undefined, }; return [{ ...sideNavigationItem, subItems }]; diff --git a/employee-portal/src/lib/businessModules/statistics/api/models/reportDetailsViewTypes.ts b/employee-portal/src/lib/businessModules/statistics/api/models/reportDetailsViewTypes.ts index 75b9c396e..f5a31853b 100644 --- a/employee-portal/src/lib/businessModules/statistics/api/models/reportDetailsViewTypes.ts +++ b/employee-portal/src/lib/businessModules/statistics/api/models/reportDetailsViewTypes.ts @@ -6,16 +6,12 @@ import { FlatAttribute } from "./flatAttribute"; import { Evaluation } from "./statisticDetailsViewTypes"; -export interface SeriesInfo { - index: number; - length: number; -} export interface ReportDetailsView { id: string; seriesId: string; title: string; description?: string; - series?: SeriesInfo; + numberInSeries?: string; start: Date; end: Date; createdAt: Date; diff --git a/employee-portal/src/lib/businessModules/statistics/api/models/reportSeriesTypes.ts b/employee-portal/src/lib/businessModules/statistics/api/models/reportSeriesTypes.ts new file mode 100644 index 000000000..21e03680a --- /dev/null +++ b/employee-portal/src/lib/businessModules/statistics/api/models/reportSeriesTypes.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EnumMap } from "@eshg/lib-portal/types/helpers"; + +export const Interval = { + Month: "MONTH", + ThreeMonths: "THREE_MONTHS", + HalfYear: "HALF_YEAR", + Year: "YEAR", +} as const; +export type Interval = (typeof Interval)[keyof typeof Interval]; + +export const INTERVAL_TRANSLATION: EnumMap<Interval> = { + [Interval.Month]: "Monatlich", + [Interval.ThreeMonths]: "Alle 3 Monate", + [Interval.HalfYear]: "Alle 6 Monate", + [Interval.Year]: "Jährlich", +}; + +export const ReportingPeriod = { + Month: "MONTH", + ThreeMonths: "THREE_MONTHS", + HalfYear: "HALF_YEAR", + Year: "YEAR", +} as const; +export type ReportingPeriod = + (typeof ReportingPeriod)[keyof typeof ReportingPeriod]; + +export const REPORTING_PERIOD_TRANSLATION: EnumMap<ReportingPeriod> = { + [ReportingPeriod.Month]: "Letzter Monat", + [ReportingPeriod.ThreeMonths]: "Letzten 3 Monate", + [ReportingPeriod.HalfYear]: "Letzten 6 Monate", + [ReportingPeriod.Year]: "Letzten 12 Monate", +}; diff --git a/employee-portal/src/lib/businessModules/statistics/api/models/reportsOverviewTypes.ts b/employee-portal/src/lib/businessModules/statistics/api/models/reportsOverviewTypes.ts index d13fd7a46..a491d37da 100644 --- a/employee-portal/src/lib/businessModules/statistics/api/models/reportsOverviewTypes.ts +++ b/employee-portal/src/lib/businessModules/statistics/api/models/reportsOverviewTypes.ts @@ -3,13 +3,33 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ReportSeries, SingleReport } from "./statisticReports"; +import { + ReportSeries, + ReportSeriesItem, + SingleReport, +} from "./statisticReports"; -export type ReportForOverview = - | Omit<SingleReport, "status" | "datasetAmount" | "description"> - | ReportSeries; +export type ReportOverviewTableRow = + | SingleReportOverview + | ReportSeriesOverview + | ReportSeriesItemOverview; + +export type ReportSeriesOverview = Omit< + ReportSeries, + "description" | "subRows" +> & { subRows: ReportSeriesItemOverview[] }; + +export type SingleReportOverview = Omit< + SingleReport, + "description" | "datasetAmount" | "status" +>; + +export type ReportSeriesItemOverview = Omit< + ReportSeriesItem, + "datasetAmount" | "status" +>; export interface ReportsOverview { totalNumberOfElements: number; - reports: ReportForOverview[]; + reports: (SingleReportOverview | ReportSeriesOverview)[]; } diff --git a/employee-portal/src/lib/businessModules/statistics/api/models/statisticReports.ts b/employee-portal/src/lib/businessModules/statistics/api/models/statisticReports.ts index 30875358e..b11a0ed22 100644 --- a/employee-portal/src/lib/businessModules/statistics/api/models/statisticReports.ts +++ b/employee-portal/src/lib/businessModules/statistics/api/models/statisticReports.ts @@ -5,6 +5,8 @@ import { ApiReportState } from "@eshg/employee-portal-api/statistics"; +import { Interval, ReportingPeriod } from "./reportSeriesTypes"; + export const ReportDataType = { Single: "SINGLE", Child: "CHILD", @@ -16,15 +18,18 @@ export type ReportDataType = export interface StatisticReports { statisticId: string; title: string; - reports: SingleReport[]; //TODO replace with ReportData[] once series are allowed + reports: ReportData[]; + activeSeries?: ActiveSeriesInfo; } export type ReportData = SingleReport | ReportSeries; +export type ReportTableRow = SingleReport | ReportSeries | ReportSeriesItem; + export interface SingleReport extends ReportBase { - seriesId: string; type: Extract<ReportDataType, "SINGLE">; description?: string; + seriesId: string; } export interface ReportBase { @@ -39,15 +44,25 @@ export interface ReportBase { } export interface ReportSeries { - reports: ReportSeriesItem[]; + subRows: ReportSeriesItem[]; name: string; seriesId: string; timeRangeStart?: Date; timeRangeEnd?: Date; type: Extract<ReportDataType, "SERIES">; + description?: string; userId: string; } export interface ReportSeriesItem extends ReportBase { type: Extract<ReportDataType, "CHILD">; } + +export interface ActiveSeriesInfo { + seriesId: string; + name: string; + description?: string; + interval?: Interval; + reportingPeriod?: ReportingPeriod; + nextReport?: Date; +} diff --git a/employee-portal/src/lib/businessModules/statistics/api/mutations/useAddAutoReportSeries.ts b/employee-portal/src/lib/businessModules/statistics/api/mutations/useAddAutoReportSeries.ts index 5b35831cd..c2a89ce2c 100644 --- a/employee-portal/src/lib/businessModules/statistics/api/mutations/useAddAutoReportSeries.ts +++ b/employee-portal/src/lib/businessModules/statistics/api/mutations/useAddAutoReportSeries.ts @@ -13,10 +13,10 @@ import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvid import { useReportSeriesApi } from "@/lib/businessModules/statistics/api/clients"; import { - AutomateReportFormModel, Interval, ReportingPeriod, -} from "@/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/automateReportFormModel"; +} from "@/lib/businessModules/statistics/api/models/reportSeriesTypes"; +import { AutomateReportFormModel } from "@/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/automateReportFormModel"; function mapToApiStartMonth(startMonth: string) { return parseInt(startMonth) + 1; diff --git a/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeactivateReportSeries.ts b/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeactivateReportSeries.ts new file mode 100644 index 000000000..fa401e2c0 --- /dev/null +++ b/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeactivateReportSeries.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useHandledMutation } from "@eshg/lib-portal/api/useHandledMutation"; +import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; + +import { useReportSeriesApi } from "@/lib/businessModules/statistics/api/clients"; + +export function useDeactivateReportSeries() { + const snackbar = useSnackbar(); + const api = useReportSeriesApi(); + const mutation = useHandledMutation({ + mutationFn: (seriesId: string) => + // Currently the openAPI generator doesn't map type to @type. This seems to be a bug. The current solution is a quick fix. One potential workaround would be to use an extra endpoint. + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + api.updateReportSeries(seriesId, { + type: "DeactivateAutoReportSeriesRequest", + "@type": "DeactivateAutoReportSeriesRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any), + onSuccess: () => { + snackbar.confirmation("Automatisierung deaktiviert"); + }, + }); + + return (seriesId: string) => { + return mutation.mutate(seriesId); + }; +} diff --git a/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeleteReport.ts b/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeleteReport.ts index bb3b2ac4c..9bbc3a830 100644 --- a/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeleteReport.ts +++ b/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeleteReport.ts @@ -6,16 +6,16 @@ import { useHandledMutation } from "@eshg/lib-portal/api/useHandledMutation"; import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; -import { useReportSeriesApi } from "@/lib/businessModules/statistics/api/clients"; +import { useReportApi } from "@/lib/businessModules/statistics/api/clients"; export function useDeleteReport({ onSuccess, }: { onSuccess?: () => void } = {}) { const snackbar = useSnackbar(); - const api = useReportSeriesApi(); + const api = useReportApi(); const mutation = useHandledMutation({ - mutationFn: (seriesId: string) => api.deleteReportSeries(seriesId), - onSuccess: () => snackbar.confirmation("Report gelöscht"), + mutationFn: (reportId: string) => api.deleteReport(reportId), + onSuccess: () => snackbar.confirmation("Report wird gelöscht"), }); return (reportId: string) => { diff --git a/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeleteReportSeries.ts b/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeleteReportSeries.ts new file mode 100644 index 000000000..0dc28203a --- /dev/null +++ b/employee-portal/src/lib/businessModules/statistics/api/mutations/useDeleteReportSeries.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useHandledMutation } from "@eshg/lib-portal/api/useHandledMutation"; +import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; + +import { useReportSeriesApi } from "@/lib/businessModules/statistics/api/clients"; + +export function useDeleteReportSeries({ + onSuccess, +}: { onSuccess?: () => void } = {}) { + const snackbar = useSnackbar(); + const api = useReportSeriesApi(); + const mutation = useHandledMutation({ + mutationFn: (seriesId: string) => api.deleteReportSeries(seriesId), + onSuccess: () => snackbar.confirmation("Report-Serie gelöscht"), + }); + + return (reportId: string) => { + return mutation.mutate(reportId, { onSuccess }); + }; +} diff --git a/employee-portal/src/lib/businessModules/statistics/api/mutations/useUpdateDataBasis.ts b/employee-portal/src/lib/businessModules/statistics/api/mutations/useUpdateDataBasis.ts new file mode 100644 index 000000000..e0f3f6d7a --- /dev/null +++ b/employee-portal/src/lib/businessModules/statistics/api/mutations/useUpdateDataBasis.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useHandledMutation } from "@eshg/lib-portal/api/useHandledMutation"; +import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; +import { parseISO } from "date-fns"; +import { useRouter } from "next/navigation"; + +import { useStatisticApi } from "@/lib/businessModules/statistics/api/clients"; +import { mapTimeRangeEndFrontendToApi } from "@/lib/businessModules/statistics/api/mapper/mapTimeRangeEnd"; +import { TimeSpan } from "@/lib/shared/components/formFields/TimeSpanField"; + +export function useUpdateDataBasis({ + redirectRoute, +}: { + redirectRoute: string; +}) { + const snackbar = useSnackbar(); + const statisticApi = useStatisticApi(); + const router = useRouter(); + + const mutation = useHandledMutation({ + mutationFn: ({ + statisticId, + timeSpan, + }: { + statisticId: string; + timeSpan: TimeSpan; + }) => + statisticApi.updateStatistic(statisticId, { + type: "UpdateStatisticTimeRangeRequest", + timeRange: { + start: parseISO(timeSpan.start), + end: mapTimeRangeEndFrontendToApi(parseISO(timeSpan.end)), + }, + }), + onSuccess: () => { + snackbar.confirmation("Datenbasis wird aktualisiert"); + router.push(redirectRoute); + }, + }); + + return async (statisticId: string, timeSpan: TimeSpan) => { + return mutation + .mutateAsync({ + statisticId: statisticId, + timeSpan: timeSpan, + }) + .catch(); + }; +} diff --git a/employee-portal/src/lib/businessModules/statistics/api/queries/useGetReportDetails.ts b/employee-portal/src/lib/businessModules/statistics/api/queries/useGetReportDetails.ts index ac7e11bb0..f663690b4 100644 --- a/employee-portal/src/lib/businessModules/statistics/api/queries/useGetReportDetails.ts +++ b/employee-portal/src/lib/businessModules/statistics/api/queries/useGetReportDetails.ts @@ -3,7 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ApiGetReportDetailPageResponse } from "@eshg/employee-portal-api/statistics"; +import { + ApiGetReportDetailPageResponse, + ApiReportType, +} from "@eshg/employee-portal-api/statistics"; import { useSuspenseQuery } from "@tanstack/react-query"; import { isNonNullish } from "remeda"; @@ -17,7 +20,6 @@ import { ReportDetailsView } from "@/lib/businessModules/statistics/api/models/r import { reportApiQueryKey } from "./apiQueryKeys"; import { mapEvaluations } from "./useGetDetailPageInformation"; -//TODO currently only mapping single reports, also map series once this exists. (Map to ReportDetailsView.series) export function mapToReportDetailsView( response: ApiGetReportDetailPageResponse, ): ReportDetailsView { @@ -25,14 +27,17 @@ export function mapToReportDetailsView( const attributes: FlatAttribute[] = mapTableColumnHeadersToFlatAttributes( response.tableColumnHeaders, ); + const isReportOfSeries = response.reportType === ApiReportType.Auto; return { id: response.id, seriesId: response.reportSeriesId, - title: response.name, + title: isReportOfSeries + ? `${response.reportSeriesName} - Ausgabe #${response.name}` + : response.reportSeriesName, description: response.description, start: response.timeRangeStart, end: response.timeRangeEnd, - createdAt: response.createdAt, + createdAt: response.executionDate, createdBy: isNonNullish(user) ? `${user.firstName} ${user.lastName}` : undefined, @@ -44,6 +49,7 @@ export function mapToReportDetailsView( evaluations: mapEvaluations(response.evaluation, attributes), attributes: attributes, userId: response.userReport?.userId ?? response.userReportSeries!.userId, + numberInSeries: isReportOfSeries ? response.name : undefined, }; } diff --git a/employee-portal/src/lib/businessModules/statistics/api/queries/useGetReportsOverview.ts b/employee-portal/src/lib/businessModules/statistics/api/queries/useGetReportsOverview.ts index dfd069846..e27559854 100644 --- a/employee-portal/src/lib/businessModules/statistics/api/queries/useGetReportsOverview.ts +++ b/employee-portal/src/lib/businessModules/statistics/api/queries/useGetReportsOverview.ts @@ -6,15 +6,38 @@ import { ApiGetReportsRequest, ApiGetReportsResponse, + ApiReportInfo, + ApiReportSeries, } from "@eshg/employee-portal-api/statistics"; import { useSuspenseQuery } from "@tanstack/react-query"; import { useReportSeriesApi } from "@/lib/businessModules/statistics/api/clients"; -import { ReportsOverview } from "@/lib/businessModules/statistics/api/models/reportsOverviewTypes"; +import { + ReportSeriesItemOverview, + ReportSeriesOverview, + ReportsOverview, + SingleReportOverview, +} from "@/lib/businessModules/statistics/api/models/reportsOverviewTypes"; import { ReportDataType } from "@/lib/businessModules/statistics/api/models/statisticReports"; import { reportApiQueryKey } from "./apiQueryKeys"; +export function mapSingleReports( + singleReport: ApiReportInfo, + reportSeries: ApiReportSeries, + isChild = false, +): SingleReportOverview | ReportSeriesItemOverview { + return { + userId: reportSeries.userId, + reportId: singleReport.id, + seriesId: reportSeries.id, + name: isChild ? `# ${singleReport.name}` : singleReport.name, + timeRangeStart: singleReport.timeRangeStart, + timeRangeEnd: singleReport.timeRangeEnd, + type: isChild ? ReportDataType.Child : ReportDataType.Single, + }; +} + export function mapToReportsOverview( response: ApiGetReportsResponse, ): ReportsOverview { @@ -23,19 +46,29 @@ export function mapToReportsOverview( case "MANUAL": if (reportSeries.reportInfos.length === 1) { const singleReport = reportSeries.reportInfos[0]!; - return { - reportId: singleReport.id, - seriesId: reportSeries.id, - name: reportSeries.name, - timeRangeStart: singleReport.timeRangeStart, - timeRangeEnd: singleReport.timeRangeEnd, - type: ReportDataType.Single, - userId: reportSeries.userId, - }; + return mapSingleReports( + singleReport, + reportSeries, + ) as SingleReportOverview; } throw Error("reportInfos length doesn't match"); case "AUTO": - throw Error("not implemented yet"); + return { + subRows: reportSeries.reportInfos.map( + (reportInfo) => + mapSingleReports( + reportInfo, + reportSeries, + true, + ) as ReportSeriesItemOverview, + ), + name: reportSeries.name, + seriesId: reportSeries.id, + timeRangeStart: reportSeries.timeRangeStart, + timeRangeEnd: reportSeries.timeRangeEnd, + type: ReportDataType.Series, + userId: reportSeries.userId, + } satisfies ReportSeriesOverview; } }); return { diff --git a/employee-portal/src/lib/businessModules/statistics/api/queries/useGetStatisticReports.ts b/employee-portal/src/lib/businessModules/statistics/api/queries/useGetStatisticReports.ts index e05364daf..15f222554 100644 --- a/employee-portal/src/lib/businessModules/statistics/api/queries/useGetStatisticReports.ts +++ b/employee-portal/src/lib/businessModules/statistics/api/queries/useGetStatisticReports.ts @@ -4,22 +4,69 @@ */ import { + ApiFrequency, ApiGetReportSeriesEntriesOfStatisticResponse, ApiReportSeries, + ApiReportState, + ApiReportingPeriod, } from "@eshg/employee-portal-api/statistics"; import { useSuspenseQuery } from "@tanstack/react-query"; +import { isNonNullish } from "remeda"; +import { + Interval, + ReportingPeriod, +} from "@/lib/businessModules/statistics/api//models/reportSeriesTypes"; import { useStatisticApi } from "@/lib/businessModules/statistics/api/clients"; import { + ActiveSeriesInfo, ReportDataType, + ReportSeries, SingleReport, StatisticReports, } from "@/lib/businessModules/statistics/api/models/statisticReports"; import { getStatisticReportsQueryKey } from "./apiQueryKeys"; -function mapReport(apiReportSeries: ApiReportSeries): SingleReport { - //TODO need report series handling, once this exists +function mapToInterval(apiFrequency?: ApiFrequency): Interval | undefined { + switch (apiFrequency) { + case ApiFrequency.Month: + return Interval.Month; + case ApiFrequency.ThreeMonths: + return Interval.ThreeMonths; + case ApiFrequency.HalfYear: + return Interval.HalfYear; + case ApiFrequency.Year: + return Interval.Year; + default: + return undefined; + } +} + +function mapToReportingPeriod( + apiReportingPeriod?: ApiReportingPeriod, +): ReportingPeriod | undefined { + switch (apiReportingPeriod) { + case ApiReportingPeriod.Month: + return ReportingPeriod.Month; + case ApiReportingPeriod.ThreeMonths: + return ReportingPeriod.ThreeMonths; + case ApiReportingPeriod.HalfYear: + return ReportingPeriod.HalfYear; + case ApiReportingPeriod.Year: + return ReportingPeriod.Year; + default: + return undefined; + } +} + +function mapNextReport(series: ApiReportSeries): Date | undefined { + return series.reportInfos.find( + (report) => report.state === ApiReportState.Planned, + )?.executionDate; +} + +function mapSingleReport(apiReportSeries: ApiReportSeries): SingleReport { const apiReportInfo = apiReportSeries.reportInfos[0]!; return { reportId: apiReportInfo.id, @@ -35,13 +82,61 @@ function mapReport(apiReportSeries: ApiReportSeries): SingleReport { }; } +function mapSeriesReport(apiReportSeries: ApiReportSeries): ReportSeries { + return { + seriesId: apiReportSeries.id, + userId: apiReportSeries.userId, + name: apiReportSeries.name, + type: ReportDataType.Series, + description: apiReportSeries.description, + timeRangeStart: apiReportSeries.timeRangeStart, + timeRangeEnd: apiReportSeries.timeRangeEnd, + subRows: apiReportSeries.reportInfos.map((reportInfo) => ({ + type: ReportDataType.Child, + seriesId: apiReportSeries.id, + userId: apiReportSeries.userId, + reportId: reportInfo.id, + name: `# ${reportInfo.name}`, + timeRangeStart: reportInfo.timeRangeStart, + timeRangeEnd: reportInfo.timeRangeEnd, + datasetAmount: reportInfo.totalNumberOfElements, + status: reportInfo.state, + })), + }; +} + +function mapActiveSeries( + response: ApiGetReportSeriesEntriesOfStatisticResponse, +): ActiveSeriesInfo | undefined { + const activeReportSeries = response.reportSeriesEntries.find( + (reportSeriesEntry) => reportSeriesEntry.active, + ); + return isNonNullish(activeReportSeries) + ? { + seriesId: activeReportSeries.id, + name: activeReportSeries.name, + description: activeReportSeries.description, + interval: mapToInterval(activeReportSeries.frequency), + reportingPeriod: mapToReportingPeriod( + activeReportSeries.reportingPeriod, + ), + nextReport: mapNextReport(activeReportSeries), + } + : undefined; +} + export function mapToStatisticReports( response: ApiGetReportSeriesEntriesOfStatisticResponse, ): StatisticReports { return { statisticId: response.statisticId, title: response.statisticName, - reports: response.reportSeriesEntries.map(mapReport), + reports: response.reportSeriesEntries.map((reportSeriesEntry) => { + return reportSeriesEntry.reportType === "AUTO" + ? mapSeriesReport(reportSeriesEntry) + : mapSingleReport(reportSeriesEntry); + }), + activeSeries: mapActiveSeries(response), }; } diff --git a/employee-portal/src/lib/businessModules/statistics/components/reports/ReportDetailsTile.tsx b/employee-portal/src/lib/businessModules/statistics/components/reports/ReportDetailsTile.tsx index 4d6f51fab..7d63fe878 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/reports/ReportDetailsTile.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/reports/ReportDetailsTile.tsx @@ -12,8 +12,8 @@ import { headerHeightDesktop, simpleToolbarHeight, } from "@/lib/baseModule/components/layout/sizes"; -import { SeriesInfo } from "@/lib/businessModules/statistics/api/models/reportDetailsViewTypes"; -import { useDeleteReportWithConfirmation } from "@/lib/businessModules/statistics/components/reports/useDeleteReportWithConfirmation"; +import { ReportDataType } from "@/lib/businessModules/statistics/api/models/statisticReports"; +import { useDeleteWithConfirmation } from "@/lib/businessModules/statistics/components/reports/useDeleteWithConfirmation"; import { UpdateReportSidebar, UpdateReportSidebarReportInfo, @@ -26,14 +26,18 @@ import { LabelValuePair } from "@/lib/shared/components/infoTile/LabelValuePair" import { formatDateRangeNumeric } from "@/lib/shared/helpers/dateTime"; import { useCopy } from "@/lib/shared/hooks/useCopy"; -import { getReportActionItems } from "./getReportActionItems"; +import { + DeleteReport, + getReportActionItems, + getSharedURL, +} from "./getReportActionItems"; export interface ReportDetailsTileProps { id: string; seriesId: string; title: string; description?: string; - series?: SeriesInfo; + numberInSeries?: string; start: Date; end: Date; createdAt: Date; @@ -49,7 +53,7 @@ export function ReportDetailsTile(props: ReportDetailsTileProps) { useState<UpdateReportSidebarReportInfo | null>(null); const canWrite = useStatisticRoleChecks().canWrite(); const canDelete = useStatisticRoleChecks().canDelete(props.userId); - const deleteReportWithConfirmation = useDeleteReportWithConfirmation({ + const { deleteReportWithConfirmation } = useDeleteWithConfirmation({ redirectRoute: routes.reports.index, }); @@ -58,6 +62,7 @@ export function ReportDetailsTile(props: ReportDetailsTileProps) { seriesId: props.seriesId, name: props.title, description: props.description, + type: ReportDataType.Single, }); } @@ -83,7 +88,7 @@ export function ReportDetailsTile(props: ReportDetailsTileProps) { > {/* Uncomment in https://cronn-gmbh.atlassian.net/browse/ISSUE-5001 <Stack gap={2} direction={"row"}> - {isNonNullish(props.series) && ( + {isNonNullish(props.numberInSeries) && ( <Button variant="outlined" startDecorator={<BookmarksOutlined />}> Serie abonnieren </Button> @@ -106,12 +111,16 @@ export function ReportDetailsTile(props: ReportDetailsTileProps) { type: "update", action: updateReport, }, + { + type: "share", + action: async () => await copy(getSharedURL(props.id)), + }, ], - isNonNullish(props.series), - props.seriesId, - props.id, - copy, - deleteReportWithConfirmation, + isNonNullish(props.numberInSeries) ? "CHILD" : "SINGLE", + { + deleteReportWithConfirmation: deleteReportWithConfirmation, + reportId: props.id, + } satisfies DeleteReport, canWrite, canDelete, )} @@ -133,16 +142,13 @@ export function ReportDetailsTile(props: ReportDetailsTileProps) { label="Erstellungsdatum" value={formatDate(props.createdAt, "DE")} /> - {isNonNullish(props.series) && ( - <LabelValuePair - label="Ausgabe" - value={`${props.series.index} von ${props.series.length}`} - /> + {isNonNullish(props.numberInSeries) && ( + <LabelValuePair label="Ausgabe" value={props.numberInSeries} /> )} {isNonNullish(props.createdBy) && ( <LabelValuePair label="Erstellt von" - value={`${props.createdBy}${isNonNullish(props.series) ? " (automatisiert)" : ""}`} + value={`${props.createdBy}${isNonNullish(props.numberInSeries) ? " (automatisiert)" : ""}`} /> )} </Stack> diff --git a/employee-portal/src/lib/businessModules/statistics/components/reports/ReportsOverview.tsx b/employee-portal/src/lib/businessModules/statistics/components/reports/ReportsOverview.tsx index 9e5c8c3f8..cfc19c23f 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/reports/ReportsOverview.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/reports/ReportsOverview.tsx @@ -10,9 +10,10 @@ import { Box } from "@mui/joy"; import { startTransition, useState } from "react"; import { translateReportType } from "@/lib/businessModules/statistics/api/mapper/translateReportType"; +import { ReportOverviewTableRow } from "@/lib/businessModules/statistics/api/models/reportsOverviewTypes"; import { ReportDataType } from "@/lib/businessModules/statistics/api/models/statisticReports"; import { useGetReportsOverview } from "@/lib/businessModules/statistics/api/queries/useGetReportsOverview"; -import { useDeleteReportWithConfirmation } from "@/lib/businessModules/statistics/components/reports/useDeleteReportWithConfirmation"; +import { useDeleteWithConfirmation } from "@/lib/businessModules/statistics/components/reports/useDeleteWithConfirmation"; import { useStatisticRoleChecks } from "@/lib/businessModules/statistics/components/statistics/useStatisticRoleChecks"; import { routes } from "@/lib/businessModules/statistics/shared/routes"; import { NoSearchResults } from "@/lib/shared/components/NoSearchResult"; @@ -30,7 +31,7 @@ import { TableSheet } from "@/lib/shared/components/table/TableSheet"; import { usePagination } from "@/lib/shared/hooks/table/usePagination"; import { useCopy } from "@/lib/shared/hooks/useCopy"; -import { getId, getReportsOverviewColumns } from "./columns"; +import { getReportsOverviewColumns } from "./columns"; function mapFilterValuesToReportsFilter(filterValues: FilterValue[]): string[] { return filterValues.map((filterValue) => { @@ -65,7 +66,8 @@ const filterDefinitions: FilterDefinition[] = [ export function ReportsOverview() { const copy = useCopy(); - const deleteReportWithConfirmation = useDeleteReportWithConfirmation(); + const { deleteReportWithConfirmation, deleteReportSeriesWithConfirmation } = + useDeleteWithConfirmation(); const userPermissions = useStatisticRoleChecks(); const { resetPageNumber, page, pageSize, getPaginationProps } = @@ -99,6 +101,10 @@ export function ReportsOverview() { totalCount: reportsOverview.totalNumberOfElements, }); + function getSubRows(item: ReportOverviewTableRow) { + return item.type === "SERIES" ? item.subRows : undefined; + } + return ( <TablePage data-testid="statistics-reports-overview-table" @@ -118,11 +124,13 @@ export function ReportsOverview() { > <TableSheet footer={<Pagination {...paginationProps} />}> <DataTable + striped={false} wrapContent wrapHeader columns={getReportsOverviewColumns( copy, deleteReportWithConfirmation, + deleteReportSeriesWithConfirmation, userPermissions.canWrite(), userPermissions.canDelete, )} @@ -133,8 +141,11 @@ export function ReportsOverview() { </Box> )} rowNavRoute={(row) => - routes.reports.details(getId(row.original)).index + row.original.type !== "SERIES" + ? routes.reports.details(row.original.reportId).index + : undefined } + getSubRows={getSubRows} /> </TableSheet> </TablePage> diff --git a/employee-portal/src/lib/businessModules/statistics/components/reports/columns.tsx b/employee-portal/src/lib/businessModules/statistics/components/reports/columns.tsx index e727be237..e03c3b5fa 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/reports/columns.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/reports/columns.tsx @@ -7,29 +7,34 @@ import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import { createColumnHelper } from "@tanstack/react-table"; import { translateReportType } from "@/lib/businessModules/statistics/api/mapper/translateReportType"; -import { ReportForOverview } from "@/lib/businessModules/statistics/api/models/reportsOverviewTypes"; +import { ReportOverviewTableRow } from "@/lib/businessModules/statistics/api/models/reportsOverviewTypes"; +import { + ReportSeries, + ReportSeriesItem, + SingleReport, +} from "@/lib/businessModules/statistics/api/models/statisticReports"; import { ActionsMenu } from "@/lib/shared/components/buttons/ActionsMenu"; -import { getReportActionItems } from "./getReportActionItems"; +import { + DeleteReportOrSeries, + getReportActionItems, + getSharedURL, +} from "./getReportActionItems"; -const columnHelper = createColumnHelper<ReportForOverview>(); +const columnHelper = createColumnHelper<ReportOverviewTableRow>(); const meta = { canNavigate: { parentRow: true, + subRow: true, }, width: "10rem", }; -export function getId(reportData: ReportForOverview) { - return reportData.type === "SINGLE" - ? reportData.reportId - : reportData.seriesId; -} - export function getReportsOverviewColumns( share: (id: string) => Promise<void>, - deleteReportWithConfirmation: (id: string) => void, + deleteReportWithConfirmation: (reportId: string) => void, + deleteReportSeriesWithConfirmation: (seriesId: string) => void, canWrite: boolean, canDelete: (creatorUserId: string) => boolean, ) { @@ -63,12 +68,27 @@ export function getReportsOverviewColumns( cell: (props) => ( <ActionsMenu actionItems={getReportActionItems( - [], - props.row.original.type === "SERIES", - props.row.original.seriesId, - getId(props.row.original), - share, - deleteReportWithConfirmation, + [ + { + type: "share", + action: async () => + await share( + getSharedURL( + (props.row.original as SingleReport | ReportSeriesItem) + .reportId, + ), + ), + }, + ], + props.row.original.type, + { + deleteReportWithConfirmation: deleteReportWithConfirmation, + deleteReportSeriesWithConfirmation: + deleteReportSeriesWithConfirmation, + seriesId: (props.row.original as ReportSeries).seriesId, + reportId: (props.row.original as SingleReport | ReportSeriesItem) + .reportId, + } satisfies DeleteReportOrSeries, canWrite, canDelete(props.row.original.userId), )} diff --git a/employee-portal/src/lib/businessModules/statistics/components/reports/getReportActionItems.tsx b/employee-portal/src/lib/businessModules/statistics/components/reports/getReportActionItems.tsx index 3d8b1a031..f7a844c6b 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/reports/getReportActionItems.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/reports/getReportActionItems.tsx @@ -12,41 +12,50 @@ import { } from "@mui/icons-material"; import { isDefined } from "remeda"; +import { ReportDataType } from "@/lib/businessModules/statistics/api/models/statisticReports"; import { routes } from "@/lib/businessModules/statistics/shared/routes"; import { ActionsItem } from "@/lib/shared/components/buttons/ActionsMenu"; type OptionalActionItem = | { type: "remember"; action: () => void } | { type: "subscribe"; action: () => void } + | { type: "share"; action: () => Promise<void> } | { type: "update"; action: () => void }; +export function getSharedURL(detailLinkId: string) { + return new URL( + routes.reports.details(detailLinkId).index, + window.location.origin, + ).href; +} + +export interface DeleteReportOrSeries { + deleteReportWithConfirmation: (reportId: string) => void; + deleteReportSeriesWithConfirmation: (reportId: string) => void; + seriesId: string; + reportId: string; +} +export interface DeleteReport { + deleteReportWithConfirmation: (id: string) => void; + reportId: string; +} + export function getReportActionItems( optionalActionitems: OptionalActionItem[], - isSeries: boolean, - seriesId: string, - detailLinkId: string, - share: (id: string) => Promise<void>, - deleteReportWithConfirmation: (reportId: string) => void, + type: ReportDataType, + deleteActions: DeleteReportOrSeries | DeleteReport, canWrite: boolean, canDelete: boolean, ) { - async function handleClickCopyAddress() { - await share( - new URL( - routes.reports.details(detailLinkId).index, - window.location.origin, - ).href, - ); - } - function concatOptionalActionItem( itemName: OptionalActionItem["type"], actionsItem: Omit<ActionsItem, "onClick">, + typeChecked = true, ) { const foundItem = optionalActionitems.find( (item) => item.type === itemName, ); - return !!foundItem + return !!typeChecked && foundItem ? { ...actionsItem, onClick: foundItem.action, @@ -63,25 +72,48 @@ export function getReportActionItems( label: "Serie abonnieren", startDecorator: <Bookmarks />, }), - { - // TODO: Discuss and change after https://cronn-gmbh.atlassian.net/browse/ISSUE-5002 - label: "Teilen", - onClick: handleClickCopyAddress, - startDecorator: <Share />, - }, - canWrite - ? concatOptionalActionItem("update", { - label: "Bearbeiten", - startDecorator: <Edit />, - }) - : undefined, + concatOptionalActionItem( + "update", + { + label: + type === ReportDataType.Single + ? "Report bearbeiten" + : "Serie bearbeiten", + startDecorator: <Edit />, + }, + type !== ReportDataType.Child && canWrite, + ), + concatOptionalActionItem( + "share", + { + // TODO: Discuss and change after https://cronn-gmbh.atlassian.net/browse/ISSUE-5002 + label: "Teilen", + startDecorator: <Share />, + }, + type !== ReportDataType.Series, + ), canDelete - ? { - label: isSeries ? "Serie löschen" : "Report löschen", - onClick: () => deleteReportWithConfirmation(seriesId), - startDecorator: <Delete />, - color: "danger", - } + ? type === ReportDataType.Series && "seriesId" in deleteActions + ? { + label: "Serie löschen", + onClick: () => { + deleteActions.deleteReportSeriesWithConfirmation( + deleteActions.seriesId, + ); + }, + startDecorator: <Delete />, + color: "danger", + } + : { + label: "Report löschen", + onClick: () => { + deleteActions.deleteReportWithConfirmation( + deleteActions.reportId, + ); + }, + startDecorator: <Delete />, + color: "danger", + } : undefined, ].filter((it) => isDefined(it)) as ActionsItem[]; } diff --git a/employee-portal/src/lib/businessModules/statistics/components/reports/useDeleteReportWithConfirmation.tsx b/employee-portal/src/lib/businessModules/statistics/components/reports/useDeleteReportWithConfirmation.tsx deleted file mode 100644 index 6b25a1c55..000000000 --- a/employee-portal/src/lib/businessModules/statistics/components/reports/useDeleteReportWithConfirmation.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { List, ListItem } from "@mui/joy"; -import { useRouter } from "next/navigation"; -import { isDefined } from "remeda"; - -import { useDeleteReport } from "@/lib/businessModules/statistics/api/mutations/useDeleteReport"; -import { useConfirmationDialog } from "@/lib/shared/components/confirmationDialog/ConfirmationDialogProvider"; - -export function useDeleteReportWithConfirmation({ - redirectRoute, -}: { - redirectRoute?: string; -} = {}) { - const { openConfirmationDialog } = useConfirmationDialog(); - const router = useRouter(); - const deleteReport = useDeleteReport({ - onSuccess: () => { - if (isDefined(redirectRoute)) { - router.push(redirectRoute); - } - }, - }); - - function deleteReportWithConfirmation(seriesId: string) { - openConfirmationDialog({ - color: "danger", - title: "Report löschen?", - description: "Wenn Sie mit dem Löschen fortfahren, wird ...", - children: ( - <List marker="disc"> - <ListItem>der Report unwiderruflich gelöscht,</ListItem> - <ListItem>der Report aus allen Merklisten entfernt,</ListItem> - <ListItem> - eine Nachricht an die Nutzer:innen gesendet, die den Report in ihrer - Merkliste haben. - </ListItem> - </List> - ), - cancelLabel: "Abbrechen", - confirmLabel: "Löschen", - onConfirm: () => { - deleteReport(seriesId); - }, - }); - } - - return deleteReportWithConfirmation; -} diff --git a/employee-portal/src/lib/businessModules/statistics/components/reports/useDeleteWithConfirmation.tsx b/employee-portal/src/lib/businessModules/statistics/components/reports/useDeleteWithConfirmation.tsx new file mode 100644 index 000000000..e39798713 --- /dev/null +++ b/employee-portal/src/lib/businessModules/statistics/components/reports/useDeleteWithConfirmation.tsx @@ -0,0 +1,81 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { List, ListItem } from "@mui/joy"; +import { useRouter } from "next/navigation"; +import { isDefined } from "remeda"; + +import { useDeleteReport } from "@/lib/businessModules/statistics/api/mutations/useDeleteReport"; +import { useDeleteReportSeries } from "@/lib/businessModules/statistics/api/mutations/useDeleteReportSeries"; +import { + ConfirmationDialogOptions, + useConfirmationDialog, +} from "@/lib/shared/components/confirmationDialog/ConfirmationDialogProvider"; + +export function useDeleteWithConfirmation({ + redirectRoute, +}: { + redirectRoute?: string; +} = {}) { + const { openConfirmationDialog } = useConfirmationDialog(); + const router = useRouter(); + const deleteReportSeries = useDeleteReportSeries(); + const deleteReport = useDeleteReport({ + onSuccess: () => { + if (isDefined(redirectRoute)) { + router.push(redirectRoute); + } + }, + }); + + const sharedDialogProps: Omit<ConfirmationDialogOptions, "onConfirm"> = { + color: "danger", + description: "Wenn Sie mit dem Löschen fortfahren, wird ...", + cancelLabel: "Abbrechen", + confirmLabel: "Löschen", + }; + + function deleteReportSeriesWithConfirmation(seriesId: string) { + openConfirmationDialog({ + ...sharedDialogProps, + title: "Report-Serie löschen?", + children: ( + <List marker="disc"> + <ListItem>die Report-Serie unwiderruflich gelöscht,</ListItem> + <ListItem>alle Ausgaben der Serie werden gelöscht,</ListItem> + <ListItem>die Report-Serie aus allen Abo-Listen entfernt,</ListItem> + <ListItem> + eine Nachricht an die Nutzer:innen mit Abo gesendet. + </ListItem> + </List> + ), + onConfirm: () => { + deleteReportSeries(seriesId); + }, + }); + } + + function deleteReportWithConfirmation(reportId: string) { + openConfirmationDialog({ + ...sharedDialogProps, + title: "Report löschen?", + children: ( + <List marker="disc"> + <ListItem>der Report unwiderruflich gelöscht,</ListItem> + <ListItem>der Report aus allen Merklisten entfernt,</ListItem> + <ListItem> + eine Nachricht an die Nutzer:innen gesendet, die den Report in ihrer + Merkliste haben. + </ListItem> + </List> + ), + onConfirm: () => { + deleteReport(reportId); + }, + }); + } + + return { deleteReportSeriesWithConfirmation, deleteReportWithConfirmation }; +} diff --git a/employee-portal/src/lib/businessModules/statistics/components/shared/charts/ScatterChart.tsx b/employee-portal/src/lib/businessModules/statistics/components/shared/charts/ScatterChart.tsx index d3810de4f..d1d3966ca 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/shared/charts/ScatterChart.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/shared/charts/ScatterChart.tsx @@ -44,7 +44,9 @@ export function ScatterChart({ name: group.label, data: unique(group.dataPoints.map((it) => it.x)).map((it) => [ it, - group.trendline!.offset + group.trendline!.slope * it, + group.trendline + ? group.trendline.offset + group.trendline.slope * it + : undefined, ]), }); } diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/CreateStatisticSidebar/ChooseAttributesStep/ChooseAttributesStep.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/CreateStatisticSidebar/ChooseAttributesStep/ChooseAttributesStep.tsx index ca5f15439..a585204b8 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/CreateStatisticSidebar/ChooseAttributesStep/ChooseAttributesStep.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/CreateStatisticSidebar/ChooseAttributesStep/ChooseAttributesStep.tsx @@ -59,23 +59,19 @@ export function ChooseAttributesStep(props: { const value = attributeMap.get(event.target.value)!; const checked = event.target.checked; + let attributes = []; if (checked) { if (!values.selectedAttributes) { - void setFieldValue("selectedAttributes", [value]); + attributes = [value]; } else { - void setFieldValue("selectedAttributes", [ - ...values.selectedAttributes, - value, - ]); + attributes = [...values.selectedAttributes, value]; } } else { - void setFieldValue( - "selectedAttributes", - values.selectedAttributes!.filter( - (it) => mapAttributeToKey(it) !== mapAttributeToKey(value), - ), + attributes = values.selectedAttributes!.filter( + (it) => mapAttributeToKey(it) !== mapAttributeToKey(value), ); } + void setFieldValue("selectedAttributes", attributes, false); } return ( diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/CreateStatisticSidebar/ChooseAttributesStep/validateChooseAttributeStep.ts b/employee-portal/src/lib/businessModules/statistics/components/statistics/CreateStatisticSidebar/ChooseAttributesStep/validateChooseAttributeStep.ts index c71be1d4a..db9d392dc 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/CreateStatisticSidebar/ChooseAttributesStep/validateChooseAttributeStep.ts +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/CreateStatisticSidebar/ChooseAttributesStep/validateChooseAttributeStep.ts @@ -8,13 +8,13 @@ import { FormikErrors } from "formik"; import { ChooseAttributesStepFormModel } from "@/lib/businessModules/statistics/components/statistics/CreateStatisticSidebar/ChooseAttributesStep/chooseAttributesStepFormModel"; export function validateChooseAttributeStep( - model: ChooseAttributesStepFormModel, + model: ChooseAttributesStepFormModel & { _selectedAttributeKeys: string[] }, ): | FormikErrors<{ selectedAttributes: string; }> | undefined { - if ((model.selectedAttributes?.length ?? 0) === 0) { + if ((model._selectedAttributeKeys.length ?? 0) === 0) { return { selectedAttributes: "Bitte Attribut wählen.", }; diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/ReportStateChip.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/ReportStateChip.tsx index b7c51034b..cb3fe0f8c 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/ReportStateChip.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/ReportStateChip.tsx @@ -11,6 +11,7 @@ const statusNames = { [ApiReportState.Failed]: "Fehler", [ApiReportState.Creating]: "Wird erstellt", [ApiReportState.Planned]: "Geplant", + [ApiReportState.Deleting]: "Wird gelöscht", } satisfies Record<ApiReportState, string>; const statusColors = { @@ -18,6 +19,7 @@ const statusColors = { [ApiReportState.Failed]: "danger", [ApiReportState.Creating]: "warning", [ApiReportState.Planned]: "warning", + [ApiReportState.Deleting]: "warning", } satisfies Record<ApiReportState, ChipProps["color"]>; export function ReportStateChip({ value }: { value: ApiReportState }) { diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/StatisticsTable.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/StatisticsTable.tsx index f0952e901..359fc4789 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/StatisticsTable.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/StatisticsTable.tsx @@ -13,6 +13,7 @@ import { import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import { Add } from "@mui/icons-material"; import DeleteIcon from "@mui/icons-material/Delete"; +import Edit from "@mui/icons-material/Edit"; import FileCopyIcon from "@mui/icons-material/FileCopy"; import FullscreenIcon from "@mui/icons-material/Fullscreen"; import { Box, Button } from "@mui/joy"; @@ -23,6 +24,7 @@ import { isDefined } from "remeda"; import { getStatisticsQueryKey } from "@/lib/businessModules/statistics/api/queries/apiQueryKeys"; import { useIsNewFeatureEnabled } from "@/lib/businessModules/statistics/api/queries/useStatisticsFeatureToggle"; import { DuplicateStatisticSidebar } from "@/lib/businessModules/statistics/components/statistics/DuplicateStatisticSidebar/DuplicateStatisticSidebar"; +import { StatisticNameChangeModal } from "@/lib/businessModules/statistics/components/statistics/details/StatisticNameChangeModal"; import { useDeleteStatisticWithConfirmation } from "@/lib/businessModules/statistics/components/statistics/useDeleteStatisticWithConfirmation"; import { useStatisticRoleChecks } from "@/lib/businessModules/statistics/components/statistics/useStatisticRoleChecks"; import { routes } from "@/lib/businessModules/statistics/shared/routes"; @@ -66,8 +68,10 @@ function columns( deleteStatisticWithConfirmation: (id: string, statisticsName: string) => void, canDelete: (creatorUserId: string) => boolean, canWrite: (creatorUserId: string) => boolean, + canUpdateStatistic: (creatorUserId: string) => boolean, onDuplicate: (item: StatisticWithUserInfo) => void, duplicateStatisticEnabled: boolean, + onNameChange: (id: string, name: string) => void, ) { return [ columnHelper.accessor("name", { @@ -128,15 +132,30 @@ function columns( props.row.original.state !== ApiStatisticState.Completed, startDecorator: <FullscreenIcon />, }, - ...(canWrite(props.row.original.userId) && - duplicateStatisticEnabled && - props.row.original.state === ApiStatisticState.Completed + ...(canUpdateStatistic(props.row.original.userId) + ? [ + { + label: "Name ändern", + onClick: () => + onNameChange( + props.row.original.id, + props.row.original.name, + ), + disabled: + props.row.original.state !== ApiStatisticState.Completed, + startDecorator: <Edit />, + }, + ] + : []), + ...(canWrite(props.row.original.userId) && duplicateStatisticEnabled ? [ { label: "Duplizieren", onClick: () => { onDuplicate(props.row.original); }, + disabled: + props.row.original.state !== ApiStatisticState.Completed, startDecorator: <FileCopyIcon />, }, ] @@ -194,6 +213,8 @@ export function StatisticsTable({ ); const [duplicateStatisticAction, setDuplicateStatisticAction] = useState<StatisticWithUserInfo>(); + const [nameChangeAction, setNameChangeAction] = + useState<Pick<ApiStatisticInfo, "id" | "name">>(); const userPermissions = useStatisticRoleChecks(); @@ -251,8 +272,10 @@ export function StatisticsTable({ deleteStatisticsWithConfirmation, userPermissions.canDelete, userPermissions.canWrite, + userPermissions.canUpdateStatistic, setDuplicateStatisticAction, duplicateStatisticEnabled, + (id, name) => setNameChangeAction({ id, name }), )} sorting={tableControl.tableSorting} rowNavRoute={(row) => @@ -275,6 +298,7 @@ export function StatisticsTable({ /> </TableSheet> </TablePage> + {isDefined(duplicateStatisticAction) && ( <OverlayBoundary> <DuplicateStatisticSidebar @@ -283,6 +307,17 @@ export function StatisticsTable({ /> </OverlayBoundary> )} + + {isDefined(nameChangeAction) && ( + <OverlayBoundary> + <StatisticNameChangeModal + open={true} + onClose={() => setNameChangeAction(undefined)} + initialName={nameChangeAction.name} + statisticId={nameChangeAction.id} + /> + </OverlayBoundary> + )} </> ); } diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/DetailsInformationCard.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/DetailsInformationCard.tsx index 60ab151f6..bb431da92 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/DetailsInformationCard.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/DetailsInformationCard.tsx @@ -54,18 +54,17 @@ export function DetailsInformationCard(props: DetailsInformationCardProps) { startDecorator={<AddchartOutlined />} variant="solid" onClick={props.onEvaluationCreateClicked} - data-testid="create-evaluation-button" > Analyse erstellen </Button> - {/* TODO: Comment out for now, replace when feature-toggle is ready */} - {/* <Button - variant="outlined" - onClick={props.onDataBasisUpdateClicked} - data-testid="update-data-button" - > - Datenbasis aktualisieren - </Button> */} + {props.canUpdateStatistic && ( + <Button + variant="outlined" + onClick={props.onDataBasisUpdateClicked} + > + Datenbasis aktualisieren + </Button> + )} </Stack> ) } diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/StatisticDetails.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/StatisticDetails.tsx index e6c0f409d..222379974 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/StatisticDetails.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/StatisticDetails.tsx @@ -115,6 +115,7 @@ export function StatisticDetails( end: toDateString(detailsInformationCardProps.end), }, }} + statisticId={props.statisticId} /> </OverlayBoundary> )} diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/UpdateStatisticDataBasisSidebar.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/UpdateStatisticDataBasisSidebar.tsx index e72592127..82f0bcdd0 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/UpdateStatisticDataBasisSidebar.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/UpdateStatisticDataBasisSidebar.tsx @@ -5,6 +5,9 @@ import { Alert } from "@eshg/lib-portal/components/Alert"; +import { useUpdateDataBasis } from "@/lib/businessModules/statistics/api/mutations/useUpdateDataBasis"; +import { validateUpdateStatisticDataBasisStep } from "@/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/validateUpdateStatisticDataBasisStep"; +import { routes } from "@/lib/businessModules/statistics/shared/routes"; import { SidebarStepper } from "@/lib/shared/components/SidebarStepper/SidebarStepper"; import { useConfirmationDialog } from "@/lib/shared/components/confirmationDialog/ConfirmationDialogProvider"; @@ -14,21 +17,27 @@ import { UpdateStatisticDataBasisFormModel } from "./updateStatisticDataBasisFor export function UpdateStatisticDataBasisSidebar({ onClose, initialValues, + statisticId, }: { onClose: () => void; initialValues: UpdateStatisticDataBasisFormModel; + statisticId: string; }) { const { openConfirmationDialog } = useConfirmationDialog(); + const updateStatisticDataBasis = useUpdateDataBasis({ + redirectRoute: routes.statistics.index, + }); - async function onSubmit() { + async function handleSubmit(model: UpdateStatisticDataBasisFormModel) { await new Promise<void>((resolve) => { openConfirmationDialog({ - onConfirm: () => { - resolve(); + onConfirm: async () => { + await updateStatisticDataBasis(statisticId, model.timeSpan); onClose(); }, onClose: resolve, title: "Datenbasis aktualisieren?", + hideDescription: true, children: ( <Alert color="warning" @@ -45,7 +54,7 @@ export function UpdateStatisticDataBasisSidebar({ <SidebarStepper onClose={onClose} open={true} - onSubmit={onSubmit} + onSubmit={handleSubmit} initialValues={initialValues} saveLabel="Aktualisieren" steps={[ @@ -54,6 +63,7 @@ export function UpdateStatisticDataBasisSidebar({ step: { title: "Datenbasis aktualisieren", content: <UpdateStatisticDataBasisStep />, + validator: validateUpdateStatisticDataBasisStep, }, }, ]} diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/UpdateStatisticDataBasisStep.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/UpdateStatisticDataBasisStep.tsx index 973f62804..9ea5960b9 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/UpdateStatisticDataBasisStep.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/UpdateStatisticDataBasisStep.tsx @@ -16,7 +16,9 @@ export function UpdateStatisticDataBasisStep() { title="Duplikat erstellt?" message="Die Datenbasis wird nach der Aktualisierung unwiderruflich ersetzt. Um ein Backup zu erstellen, legen Sie in der Übersicht ein Duplikat der Auswertung an." /> - <Typography level="h3">Neuer Betrachtungszeitraum</Typography> + <Typography level="h3" component="h2"> + Neuer Betrachtungszeitraum + </Typography> <TimeSpanField initialExplicitStartAndEndChecked={true} name="timeSpan" diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/validateUpdateStatisticDataBasisStep.ts b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/validateUpdateStatisticDataBasisStep.ts new file mode 100644 index 000000000..ad758bf3b --- /dev/null +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/validateUpdateStatisticDataBasisStep.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { UpdateStatisticDataBasisFormModel } from "@/lib/businessModules/statistics/components/statistics/details/UpdateStatisticDataBasisSidebar/updateStatisticDataBasisFormModel"; +import { validateEndAfterStart } from "@/lib/shared/components/formFields/dateOrDateTimeFieldHelper"; + +export function validateUpdateStatisticDataBasisStep( + model: UpdateStatisticDataBasisFormModel, +) { + const result = validateEndAfterStart({ + start: model.timeSpan.start, + end: model.timeSpan.end, + wholeDay: true, + }); + if (result) { + return { + timeSpan: result, + }; + } + return undefined; +} diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/AutomateReportSidebar.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/AutomateReportSidebar.tsx index 3d34d32ad..d627711d7 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/AutomateReportSidebar.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/AutomateReportSidebar.tsx @@ -3,14 +3,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { + Interval, + ReportingPeriod, +} from "@/lib/businessModules/statistics/api/models/reportSeriesTypes"; import { useAddAutoReportSeries } from "@/lib/businessModules/statistics/api/mutations/useAddAutoReportSeries"; import { SidebarStepper } from "@/lib/shared/components/SidebarStepper/SidebarStepper"; import { AutomateReportStep } from "./AutomateReportStep"; import { AutomateReportFormModel, - Interval, - ReportingPeriod, getFirstPossibleStartMonth, } from "./automateReportFormModel"; diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/AutomateReportStep.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/AutomateReportStep.tsx index eb20346ed..482108a9b 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/AutomateReportStep.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/AutomateReportStep.tsx @@ -11,14 +11,16 @@ import { } from "@eshg/lib-portal/helpers/form"; import { Divider, Stack, Typography } from "@mui/joy"; -import { TextareaField } from "@/lib/shared/components/formFields/TextareaField"; - import { - AutomateReportFormModel, INTERVAL_TRANSLATION, Interval, REPORTING_PERIOD_TRANSLATION, ReportingPeriod, +} from "@/lib/businessModules/statistics/api/models/reportSeriesTypes"; +import { TextareaField } from "@/lib/shared/components/formFields/TextareaField"; + +import { + AutomateReportFormModel, getStartDateOptions as getStartMonthOptions, } from "./automateReportFormModel"; diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/automateReportFormModel.ts b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/automateReportFormModel.ts index 641e953e4..c56e87fab 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/automateReportFormModel.ts +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/AutomateReportSidebar/automateReportFormModel.ts @@ -4,9 +4,13 @@ */ import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; -import { EnumMap } from "@eshg/lib-portal/types/helpers"; import { addMonths, getMonth, startOfMonth, startOfToday } from "date-fns"; +import { + Interval, + ReportingPeriod, +} from "@/lib/businessModules/statistics/api/models/reportSeriesTypes"; + function getStartOfNextMonth() { return addMonths(startOfMonth(startOfToday()), 1); } @@ -27,37 +31,6 @@ export function getStartDateOptions() { } return dateOptions; } - -export const Interval = { - Month: "MONTH", - ThreeMonths: "THREE_MONTHS", - HalfYear: "HALF_YEAR", - Year: "YEAR", -} as const; -export type Interval = (typeof Interval)[keyof typeof Interval]; - -export const INTERVAL_TRANSLATION: EnumMap<Interval> = { - [Interval.Month]: "Monatlich", - [Interval.ThreeMonths]: "Alle 3 Monate", - [Interval.HalfYear]: "Alle 6 Monate", - [Interval.Year]: "Jährlich", -}; - -export const ReportingPeriod = { - Month: "MONTH", - ThreeMonths: "THREE_MONTHS", - HalfYear: "HALF_YEAR", - Year: "YEAR", -} as const; -export type ReportingPeriod = - (typeof ReportingPeriod)[keyof typeof ReportingPeriod]; - -export const REPORTING_PERIOD_TRANSLATION: EnumMap<ReportingPeriod> = { - [ReportingPeriod.Month]: "Letzter Monat", - [ReportingPeriod.ThreeMonths]: "Letzten 3 Monate", - [ReportingPeriod.HalfYear]: "Letzten 6 Monate", - [ReportingPeriod.Year]: "Letztes Jahr", -}; export interface AutomateReportFormModel { name: string; description: string; diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/ReportAutomationTile.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/ReportAutomationTile.tsx new file mode 100644 index 000000000..3901364ae --- /dev/null +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/ReportAutomationTile.tsx @@ -0,0 +1,117 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Alert } from "@eshg/lib-portal/components/Alert"; +import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; +import { Button, Stack } from "@mui/joy"; +import { isNonNullish } from "remeda"; + +import { + INTERVAL_TRANSLATION, + REPORTING_PERIOD_TRANSLATION, +} from "@/lib/businessModules/statistics/api/models/reportSeriesTypes"; +import { + ActiveSeriesInfo, + ReportDataType, +} from "@/lib/businessModules/statistics/api/models/statisticReports"; +import { InfoTile } from "@/lib/shared/components/infoTile/InfoTile"; +import { LabelValuePair } from "@/lib/shared/components/infoTile/LabelValuePair"; + +import { + ReportSeriesState, + ReportSeriesStateChip, +} from "./ReportSeriesStateChip"; +import { UpdateReportSidebarReportInfo } from "./UpdateReportSidebar/UpdateReportSidebar"; + +export function ReportAutomationTile({ + activeSeriesInfo, + onClickAutomate, + onClickDeactivate, + updateReportSeries, +}: { + activeSeriesInfo?: ActiveSeriesInfo; + onClickAutomate: () => void; + onClickDeactivate: (seriesId: string) => void; + updateReportSeries: (reportSeries: UpdateReportSidebarReportInfo) => void; +}) { + const isActiveSeries = isNonNullish(activeSeriesInfo); + return ( + <Stack flex={0}> + <InfoTile + name="Automatisierung" + title="Automatisierung" + onEdit={ + isNonNullish(activeSeriesInfo) + ? () => + updateReportSeries({ + ...activeSeriesInfo, + type: ReportDataType.Series, + }) + : undefined + } + > + <Stack gap={5}> + <Stack gap={3}> + <LabelValuePair + label={"Status"} + value={ + <ReportSeriesStateChip + value={ + isActiveSeries + ? ReportSeriesState.Activated + : ReportSeriesState.Deactivated + } + /> + } + /> + {isActiveSeries ? ( + <Stack gap={3}> + <LabelValuePair + label="Intervall" + value={ + isNonNullish(activeSeriesInfo.interval) + ? INTERVAL_TRANSLATION[activeSeriesInfo.interval] + : "" + } + /> + <LabelValuePair + label="Betrachtungszeitraum" + value={ + isNonNullish(activeSeriesInfo.reportingPeriod) + ? REPORTING_PERIOD_TRANSLATION[ + activeSeriesInfo.reportingPeriod + ] + : "" + } + /> + <LabelValuePair + label="Nächster Report am" + value={formatDate(activeSeriesInfo.nextReport, "DE")} + /> + </Stack> + ) : ( + <Alert + message="Aktivieren Sie diese Option, um in regelmäßigen Abständen eine Report-Serie zu erstellen." + color="primary" + /> + )} + </Stack> + {isActiveSeries ? ( + <Button + variant="outlined" + onClick={() => onClickDeactivate(activeSeriesInfo.seriesId)} + > + Automatisierung deaktivieren + </Button> + ) : ( + <Button variant="outlined" onClick={onClickAutomate}> + Report-Serie automatisieren + </Button> + )} + </Stack> + </InfoTile> + </Stack> + ); +} diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/StatisticReports.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/StatisticReports.tsx index e827ac4e5..cf62aefaa 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/StatisticReports.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/StatisticReports.tsx @@ -5,22 +5,33 @@ "use client"; -import { ApiStatisticState } from "@eshg/employee-portal-api/statistics"; -import { Alert } from "@eshg/lib-portal/components/Alert"; +import { + ApiReportState, + ApiStatisticState, +} from "@eshg/employee-portal-api/statistics"; import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import { Add } from "@mui/icons-material"; import { Box, Button, Stack } from "@mui/joy"; import { createColumnHelper } from "@tanstack/react-table"; import { useState } from "react"; +import { isDefined } from "remeda"; import { translateReportType } from "@/lib/businessModules/statistics/api/mapper/translateReportType"; import { + ReportDataType, + ReportSeries, + ReportSeriesItem, + ReportTableRow, SingleReport, StatisticReports as StatisticReportsType, } from "@/lib/businessModules/statistics/api/models/statisticReports"; +import { useDeactivateReportSeries } from "@/lib/businessModules/statistics/api/mutations/useDeactivateReportSeries"; import { getStatisticReportsQueryKey } from "@/lib/businessModules/statistics/api/queries/apiQueryKeys"; -import { getReportActionItems } from "@/lib/businessModules/statistics/components/reports/getReportActionItems"; -import { useDeleteReportWithConfirmation } from "@/lib/businessModules/statistics/components/reports/useDeleteReportWithConfirmation"; +import { + getReportActionItems, + getSharedURL, +} from "@/lib/businessModules/statistics/components/reports/getReportActionItems"; +import { useDeleteWithConfirmation } from "@/lib/businessModules/statistics/components/reports/useDeleteWithConfirmation"; import { ReportStateChip } from "@/lib/businessModules/statistics/components/statistics/ReportStateChip"; import { AddReportSidebar } from "@/lib/businessModules/statistics/components/statistics/details/reports/AddReportSidebar/AddReportSidebar"; import { @@ -33,30 +44,28 @@ import { NoSearchResults } from "@/lib/shared/components/NoSearchResult"; import { OverlayBoundary } from "@/lib/shared/components/boundaries/OverlayBoundary"; import { ActionsMenu } from "@/lib/shared/components/buttons/ActionsMenu"; import { RefreshButton } from "@/lib/shared/components/buttons/RefreshButton"; -import { InfoTile } from "@/lib/shared/components/infoTile/InfoTile"; -import { LabelValuePair } from "@/lib/shared/components/infoTile/LabelValuePair"; +import { useConfirmationDialog } from "@/lib/shared/components/confirmationDialog/ConfirmationDialogProvider"; import { DataTable } from "@/lib/shared/components/table/DataTable"; import { TablePage } from "@/lib/shared/components/table/TablePage"; import { TableSheet } from "@/lib/shared/components/table/TableSheet"; import { useCopy } from "@/lib/shared/hooks/useCopy"; import { AutomateReportSidebar } from "./AutomateReportSidebar/AutomateReportSidebar"; -import { - ReportSeriesState, - ReportSeriesStateChip, -} from "./ReportSeriesStateChip"; +import { ReportAutomationTile } from "./ReportAutomationTile"; -const columnHelper = createColumnHelper<SingleReport>(); +const columnHelper = createColumnHelper<ReportTableRow>(); const meta = { canNavigate: { parentRow: true, + subRow: true, }, width: "10rem", }; function columns( deleteReportWithConfirmation: (reportId: string) => void, + deleteReportSeriesWithConfirmation: (seriesId: string) => void, updateReport: (report: UpdateReportSidebarReportInfo) => void, share: (id: string) => Promise<void>, canDelete: (creatorUserId: string) => boolean, @@ -90,10 +99,14 @@ function columns( }), columnHelper.accessor("status", { header: "Status", - cell: (props) => <ReportStateChip value={props.getValue()} />, + cell: (props) => + isDefined(props.getValue()) ? ( + <ReportStateChip value={props.getValue() as ApiReportState} /> + ) : undefined, meta: { canNavigate: { parentRow: true, + subRow: true, }, width: "8rem", }, @@ -102,30 +115,48 @@ function columns( header: "Aktionen", id: "actions", enableSorting: false, - cell: (props) => ( - <ActionsMenu - actionItems={getReportActionItems( - [ + cell: (props) => { + const data = props.row.original; + return props.row.original.type === ReportDataType.Series || + props.row.original.status === ApiReportState.Completed ? ( + <ActionsMenu + actionItems={getReportActionItems( + [ + { + type: "update", + action: () => + updateReport({ + seriesId: (data as SingleReport | ReportSeries).seriesId, + name: data.name, + description: (data as SingleReport | ReportSeries) + .description, + type: data.type, + }), + }, + { + type: "share", + action: async () => + await share( + getSharedURL( + (data as SingleReport | ReportSeriesItem).reportId, + ), + ), + }, + ], + data.type, { - type: "update", - action: () => - updateReport({ - seriesId: props.row.original.seriesId, - name: props.row.original.name, - description: props.row.original.description, - }), + deleteReportWithConfirmation: deleteReportWithConfirmation, + deleteReportSeriesWithConfirmation: + deleteReportSeriesWithConfirmation, + seriesId: (data as SingleReport | ReportSeries).seriesId, + reportId: (data as SingleReport | ReportSeriesItem).reportId, }, - ], - false, - props.row.original.seriesId, - props.row.original.reportId, - share, - deleteReportWithConfirmation, - canWrite(), - canDelete(props.row.original.userId), - )} - /> - ), + canWrite(), + canDelete(props.row.original.userId), + )} + /> + ) : undefined; + }, meta: { width: "6rem", cellStyle: "button", @@ -150,13 +181,31 @@ export function StatisticReports({ useState<UpdateReportSidebarReportInfo | null>(null); const [openAutomateReportSidebar, setOpenAutomateReportSidebar] = useState(false); - const deleteReportWithConfirmation = useDeleteReportWithConfirmation(); + const { openConfirmationDialog } = useConfirmationDialog(); + const { deleteReportSeriesWithConfirmation, deleteReportWithConfirmation } = + useDeleteWithConfirmation(); + const deactivateReportSeries = useDeactivateReportSeries(); const userPermissions = useStatisticRoleChecks(); function updateReport(report: UpdateReportSidebarReportInfo) { setOpenUpdateReportSidebar({ ...report }); } + function getSubRows(item: ReportTableRow) { + return item.type === ReportDataType.Series ? item.subRows : undefined; + } + + function deactivateReportSeriesWithConfirmation(seriesId: string) { + openConfirmationDialog({ + color: "danger", + title: "Automatisierung deaktivieren?", + description: + "Die Automatisierung wird sofort deaktiviert und der nächste geplante Report wird nicht erstellt.", + confirmLabel: "Deaktivieren", + onConfirm: () => deactivateReportSeries(seriesId), + }); + } + return ( <> {openCreateReportSidebar && ( @@ -215,10 +264,12 @@ export function StatisticReports({ > <TableSheet> <DataTable + striped={false} wrapContent wrapHeader columns={columns( deleteReportWithConfirmation, + deleteReportSeriesWithConfirmation, updateReport, copy, userPermissions.canDelete, @@ -231,6 +282,7 @@ export function StatisticReports({ </Box> )} rowNavRoute={(row) => + row.original.type !== "SERIES" && row.original.status === ApiStatisticState.Completed ? routes.reports.details(row.original.reportId).index : undefined @@ -245,36 +297,17 @@ export function StatisticReports({ }, ], }} + getSubRows={getSubRows} /> </TableSheet> </TablePage> <Stack sx={{ width: { lg: RIGHT_STACK_WIDTH, xxs: "100%" } }}> - <Stack flex={0}> - <InfoTile name="Automatisierung" title="Automatisierung"> - <Stack gap={5}> - <Stack gap={3}> - <LabelValuePair - label={"Status"} - value={ - <ReportSeriesStateChip - value={ReportSeriesState.Deactivated} - /> - } - /> - <Alert - message="Aktivieren Sie diese Option, um in regelmäßigen Abständen eine Report-Serie zu erstellen." - color="primary" - /> - </Stack> - <Button - variant="outlined" - onClick={() => setOpenAutomateReportSidebar(true)} - > - Report-Serie automatisieren - </Button> - </Stack> - </InfoTile> - </Stack> + <ReportAutomationTile + activeSeriesInfo={data.activeSeries} + onClickAutomate={() => setOpenAutomateReportSidebar(true)} + onClickDeactivate={deactivateReportSeriesWithConfirmation} + updateReportSeries={updateReport} + /> </Stack> </Stack> </Stack> diff --git a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/UpdateReportSidebar/UpdateReportSidebar.tsx b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/UpdateReportSidebar/UpdateReportSidebar.tsx index 0b03e63f0..e1253a166 100644 --- a/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/UpdateReportSidebar/UpdateReportSidebar.tsx +++ b/employee-portal/src/lib/businessModules/statistics/components/statistics/details/reports/UpdateReportSidebar/UpdateReportSidebar.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ReportDataType } from "@/lib/businessModules/statistics/api/models/statisticReports"; import { useUpdateReport } from "@/lib/businessModules/statistics/api/mutations/useUpdateReport"; import { UpdateReportStep } from "@/lib/businessModules/statistics/components/statistics/details/reports/UpdateReportSidebar/UpdateReportStep"; import { UpdateReportFormModel } from "@/lib/businessModules/statistics/components/statistics/details/reports/UpdateReportSidebar/updateReportFormModel"; @@ -12,6 +13,7 @@ export interface UpdateReportSidebarReportInfo { seriesId: string; name: string; description?: string; + type: ReportDataType; } export interface UpdateReportSidebarProps { onClose: () => void; @@ -43,7 +45,10 @@ export function UpdateReportSidebar({ { type: "StandardStep", step: { - title: "Report bearbeiten", + title: + report.type === ReportDataType.Series + ? "Serie bearbeiten" + : "Report bearbeiten", content: <UpdateReportStep />, }, }, diff --git a/employee-portal/src/lib/businessModules/stiProtection/api/mutations/procedures.ts b/employee-portal/src/lib/businessModules/stiProtection/api/mutations/procedures.ts index b6ee26b09..22a57a805 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/api/mutations/procedures.ts +++ b/employee-portal/src/lib/businessModules/stiProtection/api/mutations/procedures.ts @@ -6,8 +6,10 @@ import { ApiCreateProcedureRequest, ApiCreateProcedureResponse, + ApiStiProtectionProcedure, } from "@eshg/employee-portal-api/stiProtection"; import { useHandledMutation } from "@eshg/lib-portal/api/useHandledMutation"; +import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; import { useStiProtectionProcedureApi } from "@/lib/businessModules/stiProtection/api/clients"; import { stiProtectionApiQueryKey } from "@/lib/businessModules/stiProtection/api/queries/apiQueryKeys"; @@ -29,3 +31,57 @@ export function useCreateStiProcedureMutation({ onError, }); } + +export function useCloseProcedureMutation({ + onSuccess, + onError, +}: MutationPassThrough<ApiStiProtectionProcedure, string> = {}) { + const api = useStiProtectionProcedureApi(); + return useHandledMutation({ + mutationFn: (id: string) => api.closeProcedure(id), + mutationKey: stiProtectionApiQueryKey(["procedures"]), + onSuccess, + onError, + }); +} + +export function useCloseProcedure({ + onSuccess, + onError, +}: MutationPassThrough<ApiStiProtectionProcedure, string> = {}) { + const snackbar = useSnackbar(); + return useCloseProcedureMutation({ + onSuccess(data, variables, context) { + onSuccess?.(data, variables, context); + snackbar.confirmation("Vorgang wird abgeschlossen"); + }, + onError, + }); +} + +export function useReopenProcedureMutation({ + onSuccess, + onError, +}: MutationPassThrough<ApiStiProtectionProcedure, string> = {}) { + const api = useStiProtectionProcedureApi(); + return useHandledMutation({ + mutationFn: (id: string) => api.reopenProcedure(id), + mutationKey: stiProtectionApiQueryKey(["procedures"]), + onSuccess, + onError, + }); +} + +export function useReopenProcedure({ + onSuccess, + onError, +}: MutationPassThrough<ApiStiProtectionProcedure, string> = {}) { + const snackbar = useSnackbar(); + return useReopenProcedureMutation({ + onSuccess(data, variables, context) { + onSuccess?.(data, variables, context); + snackbar.confirmation("Vorgang wird wieder geöffnet"); + }, + onError, + }); +} diff --git a/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/AppointmentBlockGroupForm.tsx b/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/AppointmentBlockGroupForm.tsx index caf9b5aa5..275af9a0c 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/AppointmentBlockGroupForm.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/AppointmentBlockGroupForm.tsx @@ -20,12 +20,12 @@ import { AppointmentBlockGroupValuesWithDays } from "@/lib/shared/components/app import { AppointmentBlockGroupFields } from "@/lib/shared/components/appointmentBlocks/AppointmentBlockGroupFields"; import { AppointmentCountWithDays } from "@/lib/shared/components/appointmentBlocks/AppointmentCountWithDays"; import { AppointmentStaffSelection } from "@/lib/shared/components/appointmentBlocks/AppointmentStaffSelection"; +import { validateAppointmentBlock } from "@/lib/shared/components/appointmentBlocks/validateAppointmentBlock"; import { FormButtonBar } from "@/lib/shared/components/form/FormButtonBar"; import { FormSheet } from "@/lib/shared/components/form/FormSheet"; import { fullName } from "@/lib/shared/components/users/userFormatter"; import { validateFieldArray } from "@/lib/shared/helpers/validators"; -import { validateAppointmentBlock } from "./ValidateAppointmentBlock"; import { APPOINTMENT_TYPE_OPTIONS } from "./options"; export interface AppointmentBlockGroupValues { @@ -76,7 +76,7 @@ function validateForm( if (isEmpty(values.physicians) && isEmpty(values.consultants)) { const msg = - "Es muss mindestens ein:e Arzt:in oder ein:e Berater:in ausgewählt sein."; + "Es muss mindestens ein Arzt/eine Ärztin oder ein:e Berater:in ausgewählt sein."; errors.physicians = msg; errors.mfas = msg; } @@ -128,14 +128,13 @@ export function AppointmentBlockGroupForm({ validate={(values) => validateForm(values, appointmentTypes)} > {({ values, isSubmitting, handleSubmit }) => ( - <FormSheet onSubmit={handleSubmit}> + <FormSheet gap={5} onSubmit={handleSubmit}> <Stack gap={4}> <AppointmentBlockGroupFields appointmentBlocksWithDays={values.appointmentBlocks} options={APPOINTMENT_TYPE_OPTIONS} /> </Stack> - <Divider /> <Stack gap={4}> <AppointmentStaffSelection blockedStaff={blockedStaff} @@ -145,6 +144,7 @@ export function AppointmentBlockGroupForm({ validateAvailability={() => validateAvailability(values)} /> </Stack> + <Divider /> <FormButtonBar left={ <AppointmentCountWithDays diff --git a/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/CreateAppointmentBlockGroupForm.tsx b/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/CreateAppointmentBlockGroupForm.tsx index 9cbb1de97..14a5acdf7 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/CreateAppointmentBlockGroupForm.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/CreateAppointmentBlockGroupForm.tsx @@ -126,7 +126,7 @@ export function CreateAppointmentBlockGroupForm() { } if (values.physicians.length == 0 && values.consultants.length == 0) { snackbar.notification( - "Bitte mindestens ein:e Arzt:in oder ein:e Berater:in für die Validierung auswählen", + "Bitte mindestens einen Arzt/eine Ärztin oder ein:e Berater:in für die Validierung auswählen", ); return; } diff --git a/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/ValidateAppointmentBlock.ts b/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/ValidateAppointmentBlock.ts deleted file mode 100644 index 98d77887a..000000000 --- a/employee-portal/src/lib/businessModules/stiProtection/components/appointmentBlocks/ValidateAppointmentBlock.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ApiAppointmentType } from "@eshg/employee-portal-api/schoolEntry"; -import { isEmptyString } from "@eshg/lib-portal/helpers/guards"; -import { OptionalFieldValue } from "@eshg/lib-portal/types/form"; -import { differenceInCalendarDays, isBefore, isEqual, isPast } from "date-fns"; -import { FormikErrors } from "formik"; -import { isEmpty } from "remeda"; - -import { AppointmentDurationsStiProtection } from "@/lib/businessModules/stiProtection/api/models/AppointmentBlockGroup"; -import { AppointmentBlockGroupValuesWithDays } from "@/lib/shared/components/appointmentBlocks/AppointmentBlockFormWithDays"; -import { - calculateAppointmentsPerBlock, - getAppointmentDurationInMinutes, -} from "@/lib/shared/components/appointmentBlocks/AppointmentCountWithDays"; -import { toLocalDateTime } from "@/lib/shared/helpers/dateTime"; - -const MAX_DAYS_IN_APPOINTMENT_BLOCK = 31; - -export function validateAppointmentBlock( - type: OptionalFieldValue<ApiAppointmentType>, - appointmentBlock: AppointmentBlockGroupValuesWithDays, - appointmentDurationsStiProtection: AppointmentDurationsStiProtection, -) { - const { startDate, endDate, startTime, endTime, daysOfWeek } = - appointmentBlock; - const errors: FormikErrors<AppointmentBlockGroupValuesWithDays> = {}; - if ( - isEmpty(startDate) || - isEmpty(endDate) || - isEmpty(startTime) || - isEmpty(endTime) || - !daysOfWeek.length - ) { - return errors; - } - - const start = toLocalDateTime(startDate, startTime); - - if (isPast(start)) { - errors.startTime = "Die Startzeit liegt in der Vergangenheit."; - } - - const end = toLocalDateTime(endDate, endTime); - const dailyStartTime = toLocalDateTime(startDate, startTime); - const dailyEndTime = toLocalDateTime(startDate, endTime); - - if (isEqual(dailyStartTime, dailyEndTime)) { - errors.endTime = "Die Endzeit ist identisch zur Startzeit."; - } else if (isBefore(dailyEndTime, dailyStartTime)) { - errors.endTime = "Die Endzeit liegt vor der Startzeit."; - } else if (isBefore(end, start)) { - errors.endDate = "Das Enddatum liegt vor dem Startdatum."; - } else if ( - differenceInCalendarDays(endDate, startDate) > MAX_DAYS_IN_APPOINTMENT_BLOCK - ) { - errors.endDate = `Der Datumsbereich für einen Terminblock ist auf ${MAX_DAYS_IN_APPOINTMENT_BLOCK} Tage begrenzt.`; - } else if ( - !isEmptyString(type) && - calculateAppointmentsPerBlock( - type, - start, - end, - appointmentDurationsStiProtection, - ) === 0 - ) { - const appointmentDurationInMinutes = getAppointmentDurationInMinutes( - type, - appointmentDurationsStiProtection, - ); - errors.endTime = `Die Dauer ist nicht teilbar durch die Terminlänge von ${appointmentDurationInMinutes} Minuten.`; - } - - return errors; -} diff --git a/employee-portal/src/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresSearchBar.tsx b/employee-portal/src/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresSearchBar.tsx index 2900f71cf..d4fb58641 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresSearchBar.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresSearchBar.tsx @@ -5,11 +5,11 @@ "use client"; +import { Row } from "@eshg/lib-portal/components/Row"; import { NavigationLink } from "@eshg/lib-portal/components/navigation/NavigationLink"; import { Add } from "@mui/icons-material"; import { Button } from "@mui/joy"; -import { Row } from "@/lib/shared/Row"; import { FilterButton } from "@/lib/shared/components/buttons/FilterButton"; import { useSearchParamLink } from "@/lib/shared/hooks/searchParams/useSearchParam"; diff --git a/employee-portal/src/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresTable.tsx b/employee-portal/src/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresTable.tsx index 640fb7927..0f3f62dcb 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresTable.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/components/procedures/proceduresTable/StiProtectionProceduresTable.tsx @@ -6,25 +6,36 @@ "use client"; import { ApiStiProtectionProcedureOverview } from "@eshg/employee-portal-api/stiProtection"; +import { Row } from "@eshg/lib-portal/components/Row"; import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; -import { EditOutlined } from "@mui/icons-material"; -import { createColumnHelper } from "@tanstack/react-table"; +import { EditOutlined, ToggleOffOutlined } from "@mui/icons-material"; +import { ColumnSort, createColumnHelper } from "@tanstack/react-table"; -import { useTablePageParams } from "@/lib/businessModules/measlesProtection/hooks/useTablePageParams"; import { useStiProceduresQuery } from "@/lib/businessModules/stiProtection/api/queries/procedures"; +import { + ReopenConfirmationDialog, + UseCloseAndReopenConfirmationDialog, + useCloseAndReopenProcedure, +} from "@/lib/businessModules/stiProtection/features/procedures/details/CloseAndReopenDialogs"; import { CONCERN_VALUES, GENDER_VALUES, PROCEDURE_STATUS_VALUES, } from "@/lib/businessModules/stiProtection/shared/constants"; +import { isProcedureOpen } from "@/lib/businessModules/stiProtection/shared/helpers"; import { routes } from "@/lib/businessModules/stiProtection/shared/routes"; -import { Row } from "@/lib/shared/Row"; import { ActionsMenu } from "@/lib/shared/components/buttons/ActionsMenu"; import { Pagination } from "@/lib/shared/components/pagination/Pagination"; import { DataTable } from "@/lib/shared/components/table/DataTable"; import { TablePage } from "@/lib/shared/components/table/TablePage"; import { TableSheet } from "@/lib/shared/components/table/TableSheet"; import { useTableControl } from "@/lib/shared/hooks/searchParams/useTableControl"; +import { useTablePageParams } from "@/lib/shared/hooks/useTablePageParams"; + +const initialSorting: ColumnSort = { + id: "createdAt", + desc: true, +}; export function formatProcedureId(procedureId: string): string { return procedureId.length > 8 @@ -34,7 +45,11 @@ export function formatProcedureId(procedureId: string): string { const columnHelper = createColumnHelper<ApiStiProtectionProcedureOverview>(); -function getProceduresColumns() { +function getProceduresColumns({ + reopenDialog, +}: { + reopenDialog: UseCloseAndReopenConfirmationDialog; +}) { return [ columnHelper.accessor("yearOfBirth", { header: "Geburtsjahr", @@ -89,20 +104,14 @@ function getProceduresColumns() { columnHelper.display({ id: "actions", header: "Aktionen", - cell: ({ - row: { - original: { id: procedureId }, - }, - }) => ( + cell: ({ row: { original: procedure } }) => ( <Row justifyContent="flex-end"> <ActionsMenu - actionItems={[ - { - label: "Bearbeiten", - onClick: getLinkToProcedure(procedureId), - startDecorator: <EditOutlined />, - }, - ]} + actionItems={ + isProcedureOpen(procedure) + ? openActions(procedure.id) + : closedActions({ procedure, reopenDialog }) + } /> </Row> ), @@ -113,17 +122,74 @@ function getProceduresColumns() { ]; } +type ColumnNames = keyof ApiStiProtectionProcedureOverview; + +function mapTableFieldToSortField(sortBy?: ColumnNames) { + if (!sortBy) return; + + switch (sortBy) { + case "yearOfBirth": + return "YEAR_OF_BIRTH"; + case "gender": + return "GENDER"; + case "status": + return "STATUS"; + case "concern": + return "CONCERN"; + case "createdAt": + return "CREATED_AT"; + default: + throw Error(`Unexpected sort field: ${sortBy}`); + } +} + +function openActions(procedureId: string) { + return [ + { + label: "Bearbeiten", + onClick: getLinkToProcedure(procedureId), + startDecorator: <EditOutlined />, + }, + ]; +} + +function closedActions({ + procedure, + reopenDialog, +}: { + procedure: ApiStiProtectionProcedureOverview; + reopenDialog: UseCloseAndReopenConfirmationDialog; +}) { + return [ + { + label: "Wiedereröffnen", + onClick: () => reopenDialog.requestFinalize(procedure), + startDecorator: <ToggleOffOutlined />, + }, + ]; +} + export function StiProtectionProceduresTable() { - const tablePage = useTablePageParams(); + const fieldNames = { + sortFieldName: "sortBy", + sortDirectionName: "sortOrder", + }; + const tableControl = useTableControl({ + serverSideSorting: true, + initialSorting, + ...fieldNames, + }); + + const tablePage = useTablePageParams<ColumnNames>({ + fieldNames, + mapColumnNames: mapTableFieldToSortField, + }); const { data: { procedures, totalElements }, isLoading, } = useStiProceduresQuery(tablePage); - const tableControl = useTableControl({ - serverSideSorting: false, - sortFieldName: "sortKey", - }); + const reopenDialog = useCloseAndReopenProcedure(); return ( <TablePage aria-label="Vorgänge"> @@ -139,12 +205,19 @@ export function StiProtectionProceduresTable() { <DataTable data={procedures} sorting={tableControl.tableSorting} - columns={getProceduresColumns()} + enableSortingRemoval={false} + columns={getProceduresColumns({ reopenDialog })} rowNavRoute={({ original: { id: procedureId } }) => routes.procedures.byId(procedureId).details } focusColumnHeader="id" /> + <ReopenConfirmationDialog + open={reopenDialog.isRequestingFinalize} + onClose={reopenDialog.abortFinalize} + onConfirm={reopenDialog.handleFinalizeProcedure} + procedure={reopenDialog.procedure} + /> </TableSheet> </TablePage> ); diff --git a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/ProcedureToolbar.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/ProcedureToolbar.tsx index 72208e65f..ba4dcad9c 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/ProcedureToolbar.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/ProcedureToolbar.tsx @@ -5,6 +5,7 @@ "use client"; +import { ApiUserRole } from "@eshg/employee-portal-api/base"; import { FormatListBulletedOutlined, MedicalServicesOutlined, @@ -16,18 +17,22 @@ import { import { routes } from "@/lib/businessModules/stiProtection/shared/routes"; import { TabNavigationItem } from "@/lib/shared/components/tabNavigation/types"; import { TabNavigationToolbar } from "@/lib/shared/components/tabNavigationToolbar/TabNavigationToolbar"; +import { useHasUserRoleCheck } from "@/lib/shared/hooks/useAccessControl"; import { ProcedureTabHeader } from "./ProcedureTabHeader"; export function ProcedureToolbar({ procedureId, }: Readonly<{ procedureId: string }>) { + const hasStiProtectionUserRole = useHasUserRoleCheck( + ApiUserRole.StiProtectionUser, + ); const tabItems = buildTabItems(procedureId); return ( <TabNavigationToolbar items={tabItems} - routeBack={routes.procedures.index} + routeBack={hasStiProtectionUserRole ? routes.procedures.index : undefined} header={<ProcedureTabHeader procedureId={procedureId} />} /> ); diff --git a/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AddNewProcedureSidebar.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/addNewProcedure/AddNewProcedureSidebar.tsx similarity index 100% rename from employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AddNewProcedureSidebar.tsx rename to employee-portal/src/lib/businessModules/stiProtection/features/procedures/addNewProcedure/AddNewProcedureSidebar.tsx diff --git a/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentForm.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/addNewProcedure/AppointmentForm.tsx similarity index 94% rename from employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentForm.tsx rename to employee-portal/src/lib/businessModules/stiProtection/features/procedures/addNewProcedure/AppointmentForm.tsx index 520e6552b..7b407f9c6 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentForm.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/addNewProcedure/AppointmentForm.tsx @@ -4,20 +4,23 @@ */ import { ApiAppointmentBookingType } from "@eshg/employee-portal-api/stiProtection"; +import { Row } from "@eshg/lib-portal/components/Row"; import { NumberField } from "@eshg/lib-portal/components/formFields/NumberField"; +import { + AppointmentPickerField, + FIELD_LABELS_DE, +} from "@eshg/lib-portal/components/formFields/appointmentPicker/AppointmentPickerField"; import { Button, Grid, Radio, Sheet, Stack, Typography } from "@mui/joy"; import { addMinutes, startOfHour } from "date-fns"; import { useFormikContext } from "formik"; import { useEffect, useId, useState } from "react"; import { useGetFreeAppointments } from "@/lib/businessModules/stiProtection/api/queries/appointmentBlocks"; -import { Row } from "@/lib/shared/Row"; import { DateTimeField } from "@/lib/shared/components/formFields/DateTimeField"; import { RadioGroupField } from "@/lib/shared/components/formFields/RadioGroupField"; import { validateTodayOrFutureDate } from "@/lib/shared/helpers/validators"; import { AddNewProcedureForm } from "./AddNewProcedureSidebar"; -import { AppointmentPickerField } from "./AppointmentPickerField"; function ConnectedAppointmentPicker({ name, @@ -41,11 +44,12 @@ function ConnectedAppointmentPicker({ return ( <AppointmentPickerField name={name} - required={"Bitte ein Termin auswählen"} active={active} currentMonth={currentMonth} setCurrentMonth={setCurrentMonth} monthAppointments={monthAppointments} + required={true} + labels={FIELD_LABELS_DE} /> ); } @@ -96,6 +100,7 @@ export function AppointmentForm() { ApiAppointmentBookingType.AppointmentBlock, ) } + // eslint-disable-next-line jsx-a11y/aria-props aria-description="Termin aus Terminblock wählen" > <Grid container spacing={3} direction="row"> @@ -134,6 +139,7 @@ export function AppointmentForm() { ApiAppointmentBookingType.UserDefined, ) } + // eslint-disable-next-line jsx-a11y/aria-props aria-description="Frei wählbarer Zeitraum für den Termin" > <Row> diff --git a/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/PersonalDataForm.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/addNewProcedure/PersonalDataForm.tsx similarity index 100% rename from employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/PersonalDataForm.tsx rename to employee-portal/src/lib/businessModules/stiProtection/features/procedures/addNewProcedure/PersonalDataForm.tsx diff --git a/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/SummaryForm.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/addNewProcedure/SummaryForm.tsx similarity index 100% rename from employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/SummaryForm.tsx rename to employee-portal/src/lib/businessModules/stiProtection/features/procedures/addNewProcedure/SummaryForm.tsx diff --git a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/AdditionalDataSection.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/AdditionalDataSection.tsx index 900659c21..15ed30d96 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/AdditionalDataSection.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/AdditionalDataSection.tsx @@ -6,10 +6,13 @@ import { ApiStiProtectionProcedure } from "@eshg/employee-portal-api/stiProtection"; import { CONCERN_VALUES } from "@/lib/businessModules/stiProtection/shared/constants"; +import { createOnlyIfProcedureOpen } from "@/lib/businessModules/stiProtection/shared/helpers"; import { EditButton } from "@/lib/shared/components/buttons/EditButton"; -import { ContentPanel } from "@/lib/shared/components/contentPanel/ContentPanel"; -import { DetailsCell } from "@/lib/shared/components/detailsSection/DetailsCell"; -import { DetailsSection } from "@/lib/shared/components/detailsSection/DetailsSection"; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; const dateFormater = new Intl.DateTimeFormat("de-DE", { dateStyle: "medium" }); const timeFormater = new Intl.DateTimeFormat("de-DE", { timeStyle: "short" }); @@ -23,24 +26,21 @@ function formatAppointmentTime(date?: Date) { export function AdditionalDataSection({ procedure, }: Readonly<{ procedure: ApiStiProtectionProcedure }>) { + const onlyIfOpen = createOnlyIfProcedureOpen(procedure); return ( - <ContentPanel> - <DetailsSection - name="additionalData" - title="Zusatzinfos" - buttons={<EditButton aria-label="Zusatzinfos bearbeiten" />} - > - <DetailsCell - name="type" - label="Art" - value={CONCERN_VALUES[procedure.concern]} - /> - <DetailsCell - name="nextAppointment" + <DetailsCard + title="Zusatzinfos" + actionButton={onlyIfOpen( + <EditButton aria-label="Zusatzinfos bearbeiten" />, + )} + > + <ValueList> + <LabeledValue label="Art" value={CONCERN_VALUES[procedure.concern]} /> + <LabeledValue label="Nächster Termin" value={formatAppointmentTime(procedure?.appointment?.start)} /> - </DetailsSection> - </ContentPanel> + </ValueList> + </DetailsCard> ); } diff --git a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/CloseAndReopenDialogs.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/CloseAndReopenDialogs.tsx new file mode 100644 index 000000000..28176240a --- /dev/null +++ b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/CloseAndReopenDialogs.tsx @@ -0,0 +1,144 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + ApiStiProtectionProcedure, + ApiStiProtectionProcedureOverview, +} from "@eshg/employee-portal-api/stiProtection"; +import { styled } from "@mui/joy"; +import { useState } from "react"; + +import { + useCloseProcedure, + useReopenProcedure, +} from "@/lib/businessModules/stiProtection/api/mutations/procedures"; +import { COUNTRY_CODE_LABELS } from "@/lib/businessModules/stiProtection/shared/countryCodes"; +import { isProcedureOpen } from "@/lib/businessModules/stiProtection/shared/helpers"; +import { ConfirmationDialog } from "@/lib/shared/components/confirmationDialog/ConfirmationDialog"; + +type Procedure = ApiStiProtectionProcedure | ApiStiProtectionProcedureOverview; +interface CloseAndReopenConfirmationDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => Promise<void>; + procedure: Procedure | undefined; +} + +export interface UseCloseAndReopenConfirmationDialog { + isRequestingFinalize: boolean; + requestFinalize: (s: Procedure) => void; + abortFinalize: () => void; + handleFinalizeProcedure: () => Promise<void>; + procedure: Procedure | undefined; +} + +export function useCloseAndReopenProcedure(): UseCloseAndReopenConfirmationDialog { + const [procedureToFinalize, requestFinalize] = useState< + Procedure | undefined + >(); + + const closeProcedure = useCloseProcedure({ + onSuccess() { + requestFinalize(undefined); + }, + }); + const reopenProcedure = useReopenProcedure({ + onSuccess() { + requestFinalize(undefined); + }, + }); + + async function handleFinalizeProcedure() { + if (!procedureToFinalize) { + throw Error("No procedure set"); + } + const isOpen = isProcedureOpen(procedureToFinalize); + if (isOpen) { + await closeProcedure.mutateAsync(procedureToFinalize.id); + } else { + await reopenProcedure.mutateAsync(procedureToFinalize.id); + } + requestFinalize(undefined); + } + + return { + isRequestingFinalize: !!procedureToFinalize, + requestFinalize, + abortFinalize: () => requestFinalize(undefined), + handleFinalizeProcedure, + procedure: procedureToFinalize, + }; +} + +export function CloseConfirmationDialog({ + open, + onClose, + onConfirm, +}: CloseAndReopenConfirmationDialogProps) { + return ( + <ConfirmationDialog + title="Vorgang abschließen?" + description="Möchten Sie diesen Vorgang wirklich abschließen?" + confirmLabel="Abschließen" + color="primary" + open={open} + onClose={onClose} + onConfirm={onConfirm} + /> + ); +} + +export function ReopenConfirmationDialog({ + open, + onClose, + onConfirm, + procedure, +}: CloseAndReopenConfirmationDialogProps) { + if (!procedure) { + return null; + } + const personDetails = "person" in procedure ? procedure.person : procedure; + return ( + <ConfirmationDialog + title={"Vorgang wiedereröffnen?"} + confirmLabel="Wiedereröffnen" + description="Durch das wiedereröffnen können existierende Daten geändert werden." + color="danger" + open={open} + onClose={onClose} + onConfirm={onConfirm} + > + <DetailsTable> + <tr> + <th scope="row">Aktenzeichen</th> + <td>-</td> + </tr> + <tr> + <th scope="row">Geburtsjahr</th> + <td>{personDetails.yearOfBirth}</td> + </tr> + <tr> + <th scope="row">Geburtsland</th> + <td> + {personDetails.countryOfBirth && + COUNTRY_CODE_LABELS[personDetails.countryOfBirth]} + </td> + </tr> + </DetailsTable> + </ConfirmationDialog> + ); +} + +const DetailsTable = styled("table")` + width: max-content; + text-align: left; + & th { + font-weight: 400; + padding-right: ${({ theme }) => theme.spacing(4)}; + } + & td { + font-weight: ${({ theme }) => theme.fontWeight.lg}; + } +`; diff --git a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/CloseProcedurePanel.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/CloseProcedurePanel.tsx index b1eee9298..6b50fcb00 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/CloseProcedurePanel.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/CloseProcedurePanel.tsx @@ -3,14 +3,65 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiStiProtectionProcedure } from "@eshg/employee-portal-api/stiProtection"; import { Button } from "@mui/joy"; +import { ReactEventHandler } from "react"; +import { isProcedureOpen } from "@/lib/businessModules/stiProtection/shared/helpers"; import { ContentPanel } from "@/lib/shared/components/contentPanel/ContentPanel"; -export function CloseProcedurePanel() { +import { + CloseConfirmationDialog, + ReopenConfirmationDialog, + useCloseAndReopenProcedure, +} from "./CloseAndReopenDialogs"; + +export function CloseAndReopenProcedurePanel({ + procedure, +}: Readonly<{ procedure: ApiStiProtectionProcedure }>) { + const { + isRequestingFinalize, + requestFinalize, + abortFinalize, + handleFinalizeProcedure, + } = useCloseAndReopenProcedure(); + + const isOpen = isProcedureOpen(procedure); + + const ActionButton = isOpen ? CloseButton : ReopenButton; + const ConfirmationDialog = isOpen + ? CloseConfirmationDialog + : ReopenConfirmationDialog; + return ( <ContentPanel> - <Button>Vorgang abschließen</Button> + <ActionButton onClick={() => requestFinalize(procedure)} /> + <ConfirmationDialog + open={isRequestingFinalize} + onClose={abortFinalize} + onConfirm={handleFinalizeProcedure} + procedure={procedure} + /> </ContentPanel> ); } + +function CloseButton({ + onClick, +}: { + onClick: ReactEventHandler<HTMLButtonElement>; +}) { + return <Button onClick={onClick}>Vorgang abschließen</Button>; +} + +function ReopenButton({ + onClick, +}: { + onClick: ReactEventHandler<HTMLButtonElement>; +}) { + return ( + <Button onClick={onClick} color="danger"> + Vorgang wiedereröffnen + </Button> + ); +} diff --git a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/PersonDetails.tsx b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/PersonDetails.tsx index f24923220..5c95852de 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/PersonDetails.tsx +++ b/employee-portal/src/lib/businessModules/stiProtection/features/procedures/details/PersonDetails.tsx @@ -4,64 +4,51 @@ */ import { ApiStiProtectionProcedure } from "@eshg/employee-portal-api/stiProtection"; -import { Divider, Stack } from "@mui/joy"; -import { SxProps } from "@mui/joy/styles/types"; import { GENDER_VALUES } from "@/lib/businessModules/stiProtection/shared/constants"; import { COUNTRY_CODE_LABELS } from "@/lib/businessModules/stiProtection/shared/countryCodes"; +import { createOnlyIfProcedureOpen } from "@/lib/businessModules/stiProtection/shared/helpers"; import { EditButton } from "@/lib/shared/components/buttons/EditButton"; -import { ContentPanel } from "@/lib/shared/components/contentPanel/ContentPanel"; -import { DetailsCell } from "@/lib/shared/components/detailsSection/DetailsCell"; -import { DetailsSection } from "@/lib/shared/components/detailsSection/DetailsSection"; - -const COLUMN_STYLE: SxProps = { flexGrow: 1, maxWidth: "calc(100%/3)" }; +import { DetailsCard } from "@/lib/shared/components/detailsCard/DetailsCard"; +import { + LabeledValue, + ValueList, +} from "@/lib/shared/components/detailsCard/LabeledValue"; export function PersonDetails({ procedure, }: Readonly<{ procedure: ApiStiProtectionProcedure }>) { + const onlyIfOpen = createOnlyIfProcedureOpen(procedure); return ( - <ContentPanel> - <DetailsSection - name="person" - title="Person" - buttons={<EditButton aria-label="Person bearbeiten" />} - > - <Stack - direction="row" - gap={3} - divider={<Divider orientation="vertical" />} - > - <Stack gap={1} sx={COLUMN_STYLE}> - <DetailsCell name="reference" label="Aktenzeichen" value="-" /> - <DetailsCell - name="yearOfBirth" - label="Geburtsjahr" - value={procedure.person.yearOfBirth} - /> - <DetailsCell - name="gender" - label="Geschlecht" - value={GENDER_VALUES[procedure.person.gender]} - /> - </Stack> - <Stack gap={1} sx={COLUMN_STYLE}> - <DetailsCell - name="countryOfBirth" - label="Geburtsland" - value={ - procedure.person.countryOfBirth - ? COUNTRY_CODE_LABELS[procedure.person.countryOfBirth] - : "-" - } - /> - <DetailsCell - name="inGermanySince" - label="In Deutschland seit" - value={procedure.person.inGermanySince ?? "-"} - /> - </Stack> - </Stack> - </DetailsSection> - </ContentPanel> + <DetailsCard + title="Person" + actionButton={onlyIfOpen(<EditButton aria-label="Person bearbeiten" />)} + > + <ValueList> + <LabeledValue label="Aktenzeichen" value="-" /> + <LabeledValue + label="Geburtsjahr" + value={procedure.person.yearOfBirth.toString()} + /> + <LabeledValue + label="Geschlecht" + value={GENDER_VALUES[procedure.person.gender]} + /> + </ValueList> + <ValueList> + <LabeledValue + label="Geburtsland" + value={ + procedure.person.countryOfBirth + ? COUNTRY_CODE_LABELS[procedure.person.countryOfBirth] + : undefined + } + /> + <LabeledValue + label="In Deutschland seit" + value={procedure.person.inGermanySince?.toString()} + /> + </ValueList> + </DetailsCard> ); } 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 d13064598..1657b7e80 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 @@ -10,7 +10,7 @@ import { Grid, Stack } from "@mui/joy"; import { useStiProcedureQuery } from "@/lib/businessModules/stiProtection/api/queries/procedures"; import { AdditionalDataSection } from "./AdditionalDataSection"; -import { CloseProcedurePanel } from "./CloseProcedurePanel"; +import { CloseAndReopenProcedurePanel } from "./CloseProcedurePanel"; import { PersonDetails } from "./PersonDetails"; const SPACING = { sm: 2, md: 3, xxl: 4 }; @@ -28,7 +28,7 @@ export function ProcedureDetails({ <Grid xs={4}> <Stack spacing={SPACING}> <AdditionalDataSection procedure={procedure} /> - <CloseProcedurePanel /> + <CloseAndReopenProcedurePanel procedure={procedure} /> </Stack> </Grid> </Grid> diff --git a/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentCalendar.tsx b/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentCalendar.tsx deleted file mode 100644 index 7825b58f3..000000000 --- a/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentCalendar.tsx +++ /dev/null @@ -1,258 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ChevronLeft, ChevronRight } from "@mui/icons-material"; -import { - Box, - Button, - IconButton, - Stack, - Typography, - styled, - useTheme, -} from "@mui/joy"; -import { - Interval, - addDays, - addMonths, - eachDayOfInterval, - endOfDay, - endOfMonth, - formatISO, - isSameDay, - isWithinInterval, - startOfDay, - startOfMonth, -} from "date-fns"; -import { PropsWithChildren, useId } from "react"; - -import { Row } from "@/lib/shared/Row"; - -const DaysGrid = styled("div")` - display: grid; - column-gap: 16px; - row-gap: 8px; - grid-template-columns: repeat(7, 36px); - grid-template-rows: repeat(7, 36px); - text-align: center; -`; - -function getMonthInterval(date: Date) { - const start = startOfMonth(date); - const end = endOfMonth(date); - return { start, end }; -} - -function getDays(interval: { start: Date; end: Date }) { - let { start } = interval; - const startDiff = start.getDay() - 1; - if (startDiff != 0) { - start = addDays(start, (startDiff > 0 ? 0 : -7) - startDiff); - } - let days = eachDayOfInterval({ start, end: interval.end }); - const requiredPadding = Math.ceil(days.length / 7) * 7 - days.length; - if (requiredPadding > 0) { - const last = days[days.length - 1]; - const paddingDays = new Array(requiredPadding) - .fill(last) - .map((day: Date, index) => addDays(day, index + 1)); - days = [...days, ...paddingDays]; - } - return days; -} - -interface AppointmentCalendarProps extends MonthSelectionProps { - selectedDay: Date | undefined; - onDateSelected: (d: Date) => unknown; - monthAppointments: Date[]; -} -export function AppointmentCalendar({ - selectedDay: selectedDate, - onDateSelected, - monthAppointments, - currentMonth, - setCurrentMonth, -}: AppointmentCalendarProps) { - const currentInterval = getMonthInterval(currentMonth); - const days = getDays(currentInterval); - return ( - <div style={{ width: "min-content" }}> - <Row justifyContent="space-around"> - <MonthSelection - currentMonth={currentMonth} - setCurrentMonth={setCurrentMonth} - /> - <DaysGrid role="group" aria-label={monthLabel(currentMonth)}> - <WeekHeader>Mo</WeekHeader> - <WeekHeader>Di</WeekHeader> - <WeekHeader>Mi</WeekHeader> - <WeekHeader>Do</WeekHeader> - <WeekHeader>Fr</WeekHeader> - <WeekHeader>Sa</WeekHeader> - <WeekHeader>So</WeekHeader> - {days.map((t) => ( - <Day - key={t.toString()} - date={t} - monthAppointments={monthAppointments} - selectedDay={selectedDate} - onDateSelected={onDateSelected} - currentInterval={currentInterval} - /> - ))} - </DaysGrid> - </Row> - </div> - ); -} - -const monthNameForm = Intl.DateTimeFormat("de-DE", { month: "long" }); - -function monthLabel(currentMonth: Date) { - return `${monthNameForm.format(currentMonth)} ${currentMonth.getFullYear()}`; -} - -export interface MonthSelectionProps { - currentMonth: Date; - setCurrentMonth: (d: Date) => void; -} -function MonthSelection({ - currentMonth, - setCurrentMonth, -}: MonthSelectionProps) { - const monthYearId = useId(); - return ( - <Row justifyContent="space-between" width="100%" alignItems="center"> - <Typography - level="title-md" - id={monthYearId} - aria-label="Termin Kalendermonat" - > - {monthLabel(currentMonth)} - </Typography> - <Row gap={2}> - <IconButton - size="sm" - color="primary" - variant="outlined" - aria-label="im Vormonat" - aria-controls={monthYearId} - onClick={() => setCurrentMonth(addMonths(currentMonth, -1))} - > - <ChevronLeft /> - </IconButton> - <IconButton - size="sm" - color="primary" - variant="outlined" - aria-label="im Folgemonat" - aria-controls={monthYearId} - onClick={() => setCurrentMonth(addMonths(currentMonth, 1))} - > - <ChevronRight /> - </IconButton> - </Row> - </Row> - ); -} - -function WeekHeader({ children }: PropsWithChildren) { - return ( - <Box - role="columnheader" - aria-label="" - fontWeight="bold" - justifyContent="center" - alignItems="center" - display="flex" - aria-hidden - > - {children} - </Box> - ); -} - -function isSunday(date: Date) { - return date.getDay() === 0; -} - -function isWeekendOrOutOfMonth(date: Date, month: Interval) { - return ( - isSunday(date) || date.getDay() === 6 || !isWithinInterval(date, month) - ); -} - -const dateInMonthForm = Intl.DateTimeFormat("de-DE", { - day: "numeric", - weekday: "long", -}); - -interface DayProps - extends Omit<AppointmentCalendarProps, "currentMonth" | "setCurrentMonth"> { - date: Date; - currentInterval: Interval; -} -function Day({ - date, - currentInterval, - selectedDay: selectedDate, - onDateSelected, - monthAppointments, -}: DayProps) { - // const inActiveInterval = isWithinInterval(currentInterval, date); - const theme = useTheme(); - const boldProp = isSunday(date) - ? { fontWeight: "bold" } - : { fontWeight: "normal" }; - const grayOut = { - color: isWeekendOrOutOfMonth(date, currentInterval) - ? theme.palette.text.secondary - : theme.palette.text.primary, - }; - const isSelected = selectedDate != null && isSameDay(selectedDate, date); - const selectedStyles = isSelected - ? { borderRadius: "100%", color: theme.palette.common.white } - : {}; - - const dayInterval = { start: startOfDay(date), end: endOfDay(date) }; - const hasAppointments = monthAppointments.some((t) => - isWithinInterval(t, dayInterval), - ); - - return ( - <Button - aria-selected={isSelected} - aria-label={dateInMonthForm.format(date)} - disabled={!hasAppointments} - color={isSelected ? "primary" : "neutral"} - variant={isSelected ? "solid" : "plain"} - sx={{ - ...grayOut, - ...boldProp, - ...selectedStyles, - display: "flex", - justifyContent: "center", - alignItems: "center", - }} - onClick={() => onDateSelected(date)} - {...boldProp} - > - <Stack - component={"time"} - dateTime={formatISO(date, { representation: "date" })} - > - {date.getDate()} - {hasAppointments && !isSelected && <AppointmentMarker aria-hidden />} - </Stack> - </Button> - ); -} - -const AppointmentMarker = styled("div")` - background-color: ${({ theme }) => theme.palette.primary[500]}; - height: ${({ theme }) => theme.spacing(0.5)}; - width: ${({ theme }) => theme.spacing(3)}; - border-radius: ${({ theme }) => theme.radius.md}; -`; diff --git a/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentPickerField.tsx b/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentPickerField.tsx deleted file mode 100644 index ec3d9543a..000000000 --- a/employee-portal/src/lib/businessModules/stiProtection/procedures/addNewProcedure/AppointmentPickerField.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { useBaseField } from "@eshg/lib-portal/components/formFields/BaseField"; -import { - Chip, - FormControl, - FormHelperText, - List, - ListItem, - Radio, - RadioGroup, - Stack, - Typography, -} from "@mui/joy"; -import { SxProps } from "@mui/joy/styles/types"; -import { endOfDay, isSameDay, isWithinInterval, startOfDay } from "date-fns"; -import { useState } from "react"; - -import { - AppointmentCalendar, - MonthSelectionProps, -} from "./AppointmentCalendar"; - -interface Appointment { - start: Date; -} - -export interface AppointmentPickerFieldProps<T extends Appointment> - extends MonthSelectionProps { - name: string; - sx?: SxProps; - className?: string; - required?: string | undefined; - active?: boolean; - monthAppointments: T[]; - onAppointmentSelected?: (d: T) => unknown; -} -export function AppointmentPickerField<T extends Appointment>({ - sx, - className, - active, - currentMonth, - setCurrentMonth, - monthAppointments, - onAppointmentSelected, - required, - ...props -}: AppointmentPickerFieldProps<T>) { - const field = useBaseField<T | undefined>({ - ...props, - required: active ? required : undefined, - type: "date", - }); - const [selectedDay, setSelectedDayRaw] = useState<Date | undefined>( - field.input.value?.start, - ); - - function setSelectedDay(d: Date) { - setSelectedDayRaw(d); - if (!selectedDay || !isSameDay(d, selectedDay)) { - void field.helpers.setValue(undefined); - } - } - - const dateAppointments = monthAppointments.map((t) => t.start); - - return ( - <Stack sx={sx} className={className}> - <AppointmentCalendar - selectedDay={active ? selectedDay : undefined} - onDateSelected={setSelectedDay} - currentMonth={currentMonth} - setCurrentMonth={setCurrentMonth} - monthAppointments={dateAppointments} - /> - <FormControl error={field.error} required={field.required}> - <AppointmentListForDate - field={field} - date={active ? selectedDay : undefined} - onAppointmentSelected={onAppointmentSelected} - monthAppointments={monthAppointments} - /> - {active && field.helperText != null && ( - <FormHelperText>{field.helperText}</FormHelperText> - )} - </FormControl> - </Stack> - ); -} - -const timeForm = Intl.DateTimeFormat("de-DE", { timeStyle: "short" }); -const dateFullForm = Intl.DateTimeFormat("de-DE", { - month: "long", - day: "numeric", - weekday: "long", - year: "numeric", -}); -function AppointmentListForDate<T extends Appointment>({ - date, - field, - monthAppointments, - onAppointmentSelected, -}: { - date: Date | undefined; - field: ReturnType<typeof useBaseField<T | undefined>>; - monthAppointments: T[]; - onAppointmentSelected?: (d: T) => unknown; -}) { - const currentInterval = date - ? { - start: startOfDay(date), - end: endOfDay(date), - } - : undefined; - const dayAppointments = - currentInterval != null - ? monthAppointments - .filter((t) => isWithinInterval(t.start, currentInterval)) - .sort() - : []; - const hasAppointments = dayAppointments.length > 0; - if (!hasAppointments || !date) { - return null; - } - - function createOnSelected(d: T) { - return () => { - onAppointmentSelected?.(d); - return field.helpers.setValue(d); - }; - } - - return ( - <RadioGroup> - <Typography level="title-md" my={2}> - Uhrzeit - </Typography> - <List - orientation="horizontal" - wrap - size="sm" - sx={{ marginBottom: "16px", gap: "8px", padding: 0 }} - aria-description={`Liste verfügbarer Termine für ${dateFullForm.format(date)}`} - > - {dayAppointments.map((apt) => { - const isSelected = field.input.value === apt; - return ( - <ListItem - sx={{ padding: 0, minHeight: 0 }} - key={apt.start.toString()} - > - <Chip - variant={isSelected ? "soft" : "plain"} - color={isSelected ? "primary" : "neutral"} - sx={{ minWidth: "56px", textAlign: "center" }} - > - <Radio - component={"time"} - dateTime={timeForm.format(apt.start)} - disableIcon - overlay - name={`appointments-${date.getDate()}`} - value={apt.start} - checked={isSelected} - onChange={createOnSelected(apt)} - label={timeForm.format(apt.start)} - /> - </Chip> - </ListItem> - ); - })} - </List> - </RadioGroup> - ); -} diff --git a/employee-portal/src/lib/businessModules/stiProtection/shared/helpers.ts b/employee-portal/src/lib/businessModules/stiProtection/shared/helpers.ts index 139350224..09c0ec26e 100644 --- a/employee-portal/src/lib/businessModules/stiProtection/shared/helpers.ts +++ b/employee-portal/src/lib/businessModules/stiProtection/shared/helpers.ts @@ -6,6 +6,8 @@ import { ApiAppointmentType, ApiConcern, + ApiStiProtectionProcedure, + ApiStiProtectionProcedureOverview, } from "@eshg/employee-portal-api/stiProtection"; export function concernToAppointmentType( @@ -47,3 +49,21 @@ export function deleteUndefined<T extends object>(obj: T): NoUndefined<T> { Object.entries(obj).filter(([_key, value]) => value !== undefined), ) as NoUndefined<T>; } + +export function isProcedureOpen( + procedure: ApiStiProtectionProcedure | ApiStiProtectionProcedureOverview, +) { + return procedure.status !== "CLOSED"; +} + +export function createOnlyIfProcedureOpen( + procedure: ApiStiProtectionProcedure | ApiStiProtectionProcedureOverview, +) { + const isOpen = isProcedureOpen(procedure); + return function onlyIfOpen<T>(t: T) { + if (!isOpen) { + return; + } + return t; + }; +} diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/AppointmentBlockGroupForm.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/AppointmentBlockGroupForm.tsx index 0219a965b..de3938c00 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/AppointmentBlockGroupForm.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/AppointmentBlockGroupForm.tsx @@ -21,13 +21,12 @@ import { calculateAppointmentCount, } from "@/lib/shared/components/appointmentBlocks/AppointmentCountWithDays"; import { AppointmentStaffSelection } from "@/lib/shared/components/appointmentBlocks/AppointmentStaffSelection"; +import { validateAppointmentBlock } from "@/lib/shared/components/appointmentBlocks/validateAppointmentBlock"; import { FormButtonBar } from "@/lib/shared/components/form/FormButtonBar"; import { FormSheet } from "@/lib/shared/components/form/FormSheet"; import { fullName } from "@/lib/shared/components/users/userFormatter"; import { validateFieldArray } from "@/lib/shared/helpers/validators"; -import { validateAppointmentBlock } from "./validateAppointmentBlock"; - const DEFAULT_PARALLEL_EXAMINATIONS = 1; function validateForm(values: AppointmentBlockGroupValues) { @@ -47,7 +46,8 @@ function validateForm(values: AppointmentBlockGroupValues) { } if (isEmpty(values.physicians) && isEmpty(values.mfas)) { - const msg = "Es muss mindestens eine Arzt:in oder ein MFA ausgewählt sein."; + const msg = + "Es muss mindestens ein Arzt/eine Ärztin oder ein:e MFA ausgewählt sein."; errors.physicians = msg; errors.mfas = msg; } @@ -117,16 +117,15 @@ export function AppointmentBlockGroupForm( validate={validateForm} > {({ values, isSubmitting, handleSubmit }) => ( - <FormSheet onSubmit={handleSubmit}> - <Stack gap={4}> + <FormSheet gap={5} onSubmit={handleSubmit}> + <Stack gap={5}> <AppointmentBlockGroupFields appointmentBlocksWithDays={values.appointmentBlocks} options={APPOINTMENT_TYPE_OPTIONS} showParallelExaminations /> </Stack> - <Divider /> - <Stack gap={4}> + <Stack gap={5}> <AppointmentStaffSelection physicianOptions={physicianOptions} medicalAssistantOptions={medicalAssistantsOptions} diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/CreateAppointmentBlockGroupForm.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/CreateAppointmentBlockGroupForm.tsx index d32f17ea8..120e027b9 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/CreateAppointmentBlockGroupForm.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/CreateAppointmentBlockGroupForm.tsx @@ -114,7 +114,7 @@ export function CreateAppointmentBlockGroupForm() { } if (values.physicians.length == 0 && values.mfas.length == 0) { snackbar.notification( - "Bitte mindestens ein:e Arzt:in oder ein:e MFA für die Validierung auswählen", + "Bitte mindestens einen Arzt/eine Ärztin oder ein:e MFA für die Validierung auswählen", ); return; } diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/validateAppointmentBlock.ts b/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/validateAppointmentBlock.ts deleted file mode 100644 index fbd4b620c..000000000 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/appointmentBlocks/appointmentBlocksGroupForm/validateAppointmentBlock.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright 2024 SCOOP Software GmbH, cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ApiAppointmentType } from "@eshg/employee-portal-api/travelMedicine"; -import { isEmptyString } from "@eshg/lib-portal/helpers/guards"; -import { OptionalFieldValue } from "@eshg/lib-portal/types/form"; -import { differenceInCalendarDays, isBefore, isEqual, isPast } from "date-fns"; -import { FormikErrors } from "formik"; -import { isEmpty } from "remeda"; - -import { AppointmentBlockGroupValuesWithDays } from "@/lib/shared/components/appointmentBlocks/AppointmentBlockFormWithDays"; -import { - calculateAppointmentsPerBlock, - getAppointmentDurationInMinutes, -} from "@/lib/shared/components/appointmentBlocks/AppointmentCountWithDays"; -import { toLocalDateTime } from "@/lib/shared/helpers/dateTime"; - -const MAX_DAYS_IN_APPOINTMENT_BLOCK = 31; - -export function validateAppointmentBlock( - type: OptionalFieldValue<ApiAppointmentType>, - appointmentBlock: AppointmentBlockGroupValuesWithDays, - allAppointmentTypes: Record<string, number>, -) { - const { startDate, endDate, startTime, endTime, daysOfWeek } = - appointmentBlock; - const errors: FormikErrors<AppointmentBlockGroupValuesWithDays> = {}; - if ( - isEmpty(startDate) || - isEmpty(endDate) || - isEmpty(startTime) || - isEmpty(endTime) || - !daysOfWeek.length - ) { - return errors; - } - - const start = toLocalDateTime(startDate, startTime); - - if (isPast(start)) { - errors.startTime = "Die Startzeit liegt in der Vergangenheit."; - } - - const end = toLocalDateTime(endDate, endTime); - const dailyStartTime = toLocalDateTime(startDate, startTime); - const dailyEndTime = toLocalDateTime(startDate, endTime); - - if (isEqual(dailyStartTime, dailyEndTime)) { - errors.endTime = "Die Endzeit ist identisch zur Startzeit."; - } else if (isBefore(dailyEndTime, dailyStartTime)) { - errors.endTime = "Die Endzeit liegt vor der Startzeit."; - } else if (isBefore(end, start)) { - errors.endDate = "Das Enddatum liegt vor dem Startdatum."; - } else if ( - differenceInCalendarDays(endDate, startDate) > MAX_DAYS_IN_APPOINTMENT_BLOCK - ) { - errors.endDate = `Der Datumsbereich für einen Terminblock ist auf ${MAX_DAYS_IN_APPOINTMENT_BLOCK} Tage begrenzt.`; - } else if ( - !isEmptyString(type) && - calculateAppointmentsPerBlock(type, start, end, allAppointmentTypes) === 0 - ) { - const appointmentDurationInMinutes = getAppointmentDurationInMinutes( - type, - allAppointmentTypes, - ); - errors.endTime = `Die Dauer ist nicht teilbar durch die Terminlänge von ${appointmentDurationInMinutes} Minuten.`; - } - - return errors; -} diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/diseases/DiseasesOverviewTable.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/diseases/DiseasesOverviewTable.tsx index dc36cf6d4..8b4de005e 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/diseases/DiseasesOverviewTable.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/diseases/DiseasesOverviewTable.tsx @@ -11,7 +11,7 @@ import { } from "@eshg/employee-portal-api/travelMedicine"; import { InputField } from "@eshg/lib-portal/components/formFields/InputField"; import { NumberField } from "@eshg/lib-portal/components/formFields/NumberField"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { validateLength } from "@eshg/lib-portal/helpers/validators"; import AddIcon from "@mui/icons-material/Add"; import { Button, Stack } from "@mui/joy"; @@ -54,7 +54,7 @@ export function DiseasesOverviewTable() { const putDisease = usePutDisease(); const deleteDisease = useDeleteDisease(); - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); function updateSidebarAndDisease( sideBarState: boolean, @@ -65,12 +65,6 @@ export function DiseasesOverviewTable() { resetAlertContext(); } - function resetAlertContext() { - if (alertContext !== null) { - alertContext.setAlert(null); - } - } - interface DiseaseFormData { diseaseName: string; estimatedFee: string; // no other way to get the optional number field cleared :( diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/otherServiceTemplates/OtherServiceTemplateForm.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/otherServiceTemplates/OtherServiceTemplateForm.tsx index 6a8e1255f..31c8d4ad2 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/otherServiceTemplates/OtherServiceTemplateForm.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/otherServiceTemplates/OtherServiceTemplateForm.tsx @@ -10,7 +10,7 @@ import { ApiPostPutOtherServiceTemplateRequest, } from "@eshg/employee-portal-api/travelMedicine"; import { useSnackbar } from "@eshg/lib-portal/components/snackbar/SnackbarProvider"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { useSuspenseQueries } from "@tanstack/react-query"; import { useState } from "react"; @@ -45,7 +45,7 @@ export function OtherServiceTemplateForm() { const [otherServiceFormValues, setOtherServiceFormValues] = useState<OtherServiceTemplateFormValues>(INITIAL_VALUES); - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); const createOtherServiceTemplateMutation = useAddOtherServiceTemplate(); const updateOtherServiceTemplateMutation = useUpdateOtherServiceTemplate(); @@ -56,12 +56,6 @@ export function OtherServiceTemplateForm() { resetAlertContext(); } - function resetAlertContext() { - if (alertContext !== null) { - alertContext.setAlert(null); - } - } - async function createOtherServiceTemplate( request: ApiPostPutOtherServiceTemplateRequest, ) { diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/VaccinationConsultationTabNavigationToolbar.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/VaccinationConsultationTabNavigationToolbar.tsx index 3e9a4f439..253a6139d 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/VaccinationConsultationTabNavigationToolbar.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/VaccinationConsultationTabNavigationToolbar.tsx @@ -5,6 +5,7 @@ "use client"; +import { ApiUserRole } from "@eshg/employee-portal-api/base"; import { FormatListBulletedOutlined, ReceiptOutlined, @@ -21,12 +22,16 @@ import { routes as businessRoutes } from "@/lib/businessModules/travelMedicine/s import { statusColors } from "@/lib/shared/components/procedures/constants"; import { TabNavigationItem } from "@/lib/shared/components/tabNavigation/types"; import { TabNavigationToolbar } from "@/lib/shared/components/tabNavigationToolbar/TabNavigationToolbar"; +import { useHasUserRoleCheck } from "@/lib/shared/hooks/useAccessControl"; export function VaccinationConsultationTabNavigationToolbar({ id, }: Readonly<{ id: string; }>) { + const hasTravelMedicineAdminRole = useHasUserRoleCheck( + ApiUserRole.TravelMedicineAdmin, + ); const tabItems = createTabItems(id); const [{ data: status }] = useSuspenseQueries({ queries: [useGetStatusQuery(id)], @@ -34,7 +39,9 @@ export function VaccinationConsultationTabNavigationToolbar({ return ( <TabNavigationToolbar - routeBack={businessRoutes.procedures.index} + routeBack={ + hasTravelMedicineAdminRole ? businessRoutes.procedures.index : undefined + } header={<VaccinationConsultationTabHeader id={id} />} afterTabs={ <Chip data-testid="tab-procedure-state" color={statusColors[status]}> diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/InformationStatementsTable.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/InformationStatementsTable.tsx index 52be080db..a2bd89820 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/InformationStatementsTable.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/InformationStatementsTable.tsx @@ -9,7 +9,7 @@ import { ApiInformationStatement, ApiTravelMedicineFeature, } from "@eshg/employee-portal-api/travelMedicine"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { AddOutlined } from "@mui/icons-material"; import { Button, Grid } from "@mui/joy"; import { useState } from "react"; @@ -50,13 +50,7 @@ export function InformationStatementsTable({ initialValues: { ...initialValuesInformationStatementSidebar }, }); - const alertContext = useAlertContext(); - - function resetAlertContext() { - if (alertContext !== null) { - alertContext.setAlert(null); - } - } + const resetAlertContext = useResetAlertContext(); const { closeSidebar } = useSidebarForm({ onClose: () => { diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/InitialAppointmentTile.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/InitialAppointmentTile.tsx index e81794fd5..91f2e95d8 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/InitialAppointmentTile.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/InitialAppointmentTile.tsx @@ -8,7 +8,7 @@ import { ApiAppointmentSummary, ApiAppointmentType, } from "@eshg/employee-portal-api/travelMedicine"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { formatDateTime } from "@eshg/lib-portal/formatters/dateTime"; import { Grid } from "@mui/joy"; import { useState } from "react"; @@ -41,19 +41,13 @@ export function InitialAppointmentTile( props: Readonly<InitialAppointmentTileProps>, ) { const [open, setOpen] = useState<boolean>(false); - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); function updateSidebar(sideBarState: boolean) { setOpen(sideBarState); resetAlertContext(); } - function resetAlertContext() { - if (alertContext !== null) { - alertContext.setAlert(null); - } - } - return ( <> <DetailsSection diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/PatientPanel.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/PatientPanel.tsx index 53f2492d2..291adc2cb 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/PatientPanel.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/PatientPanel.tsx @@ -6,7 +6,7 @@ "use client"; import { ApiPatient } from "@eshg/employee-portal-api/travelMedicine"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { Divider, Grid } from "@mui/joy"; import { useUpdatePatient } from "@/lib/businessModules/travelMedicine/api/mutations/vaccinationConsultation"; @@ -42,19 +42,13 @@ export function PatientPanel({ const updatePatientApi = useUpdatePatient(); - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); function updateSidebar(sideBarState: boolean) { setOpen(sideBarState); resetAlertContext(); } - function resetAlertContext() { - if (alertContext !== null) { - alertContext.setAlert(null); - } - } - function handleClose() { updateSidebar(false); } diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/ServicePlanTable.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/ServicePlanTable.tsx index c631ac798..a737409d1 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/ServicePlanTable.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/ServicePlanTable.tsx @@ -11,7 +11,7 @@ import { ApiServicePlanEntry, ApiServiceStatus, } from "@eshg/employee-portal-api/travelMedicine"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { toDateString } from "@eshg/lib-portal/helpers/dateTime"; import { AddOutlined } from "@mui/icons-material"; import { Button, Grid } from "@mui/joy"; @@ -146,13 +146,7 @@ export function ServicePlanTable({ "most-recent-users", ); - const alertContext = useAlertContext(); - - function resetAlertContext() { - if (alertContext !== null) { - alertContext.setAlert(null); - } - } + const resetAlertContext = useResetAlertContext(); function handleCancel( currentValues: diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/TravelDataTile.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/TravelDataTile.tsx index f2151ca6e..ee95f6f54 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/TravelDataTile.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/baseData/TravelDataTile.tsx @@ -9,7 +9,7 @@ import { ApiPatchVaccinationConsultationTravelDetailsRequest, ApiTravelType, } from "@eshg/employee-portal-api/travelMedicine"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import { isDateString, toUtcDate } from "@eshg/lib-portal/helpers/dateTime"; import { Stack } from "@mui/joy"; @@ -40,19 +40,13 @@ interface TravelDataTileProps { export function TravelDataTile(procedure: Readonly<TravelDataTileProps>) { const [open, setOpen] = useSearchParam("edit-travel-data", "boolean"); - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); function updateSidebar(sideBarState: boolean) { setOpen(sideBarState); resetAlertContext(); } - function resetAlertContext() { - if (alertContext !== null) { - alertContext.setAlert(null); - } - } - return ( <> <DetailsSection diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/medicalHistory/MedicalHistoryContent.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/medicalHistory/MedicalHistoryContent.tsx index e3612b900..284e8684c 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/medicalHistory/MedicalHistoryContent.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccinationConsultations/medicalHistory/MedicalHistoryContent.tsx @@ -9,7 +9,7 @@ import { ApiMedicalHistory, ApiProcedureStatus, } from "@eshg/employee-portal-api/travelMedicine"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; import EditNoteIcon from "@mui/icons-material/EditNote"; @@ -34,13 +34,7 @@ export function MedicalHistoriesContent({ const [editMode, setEditMode] = useState(false); const [medicalHistory, setMedicalHistory] = useState<ApiMedicalHistory>(); - const alertContext = useAlertContext(); - - function resetAlertContext() { - if (alertContext !== null) { - alertContext.setAlert(null); - } - } + const resetAlertContext = useResetAlertContext(); const [{ data: allMedicalHistories }, { data: status }] = useSuspenseQueries({ queries: [ diff --git a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccines/VaccinesOverviewTable.tsx b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccines/VaccinesOverviewTable.tsx index 6ecfef9e9..1625029ff 100644 --- a/employee-portal/src/lib/businessModules/travelMedicine/components/vaccines/VaccinesOverviewTable.tsx +++ b/employee-portal/src/lib/businessModules/travelMedicine/components/vaccines/VaccinesOverviewTable.tsx @@ -10,7 +10,7 @@ import { ApiTravelMedicineFeature, ApiVaccine, } from "@eshg/employee-portal-api/travelMedicine"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import AddIcon from "@mui/icons-material/Add"; import { Button } from "@mui/joy"; import { useSuspenseQueries } from "@tanstack/react-query"; @@ -62,7 +62,7 @@ export function VaccinesOverviewTable() { const formikRef = useRef<FormikProps<VaccineFormData>>(null); const { openConfirmationDialog } = useConfirmationDialog(); - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); function updateSidebarAndVaccine( sideBarState: boolean, @@ -73,12 +73,6 @@ export function VaccinesOverviewTable() { resetAlertContext(); } - function resetAlertContext() { - if (alertContext !== null) { - alertContext.setAlert(null); - } - } - function sidebarTitle(): string { return currentVaccine ? "Impfstoff bearbeiten" : "Impfstoff hinzufügen"; } diff --git a/employee-portal/src/lib/shared/api/mutations/approvalRequests.ts b/employee-portal/src/lib/shared/api/mutations/approvalRequests.ts index a8a23c5e7..1f90c2ff6 100644 --- a/employee-portal/src/lib/shared/api/mutations/approvalRequests.ts +++ b/employee-portal/src/lib/shared/api/mutations/approvalRequests.ts @@ -44,7 +44,7 @@ export function useGrantDeletionForAllRequestsTemplate( function grantDeletionForAllRequests(approvalRequestApi: ApprovalRequestApi) { return async function (approvalRequests: ApiApprovalRequest[]) { - for await (const request of approvalRequests) { + for (const request of approvalRequests) { await decideApprovalRequest(approvalRequestApi)({ approvalRequestId: request.approvalRequestId, decision: "GRANTED", diff --git a/employee-portal/src/lib/shared/components/BaseModal.tsx b/employee-portal/src/lib/shared/components/BaseModal.tsx index 3d668254e..8e0b2b81f 100644 --- a/employee-portal/src/lib/shared/components/BaseModal.tsx +++ b/employee-portal/src/lib/shared/components/BaseModal.tsx @@ -4,8 +4,8 @@ */ import { - ScopedAlert, - useAlertContext, + AlertSlot, + useResetAlertContext, } from "@eshg/lib-portal/errorHandling/AlertContext"; import { DialogTitle, Modal, ModalClose, ModalDialog } from "@mui/joy"; import { DefaultColorPalette, SxProps } from "@mui/joy/styles/types"; @@ -29,15 +29,13 @@ export function BaseModal({ onClose, sx, }: BaseModalProps) { - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); function handleClose() { if (onClose !== undefined) { onClose(); } - if (alertContext !== null) { - alertContext.setAlert(null); - } + resetAlertContext(); } return ( @@ -52,7 +50,7 @@ export function BaseModal({ {isDefined(modalTitle) && ( <DialogTitle color={color}>{modalTitle}</DialogTitle> )} - <ScopedAlert /> + <AlertSlot /> <ModalClose variant="outlined" aria-label="Schließen" diff --git a/employee-portal/src/lib/shared/components/FileCard.tsx b/employee-portal/src/lib/shared/components/FileCard.tsx index 382fb3040..47c7d30eb 100644 --- a/employee-portal/src/lib/shared/components/FileCard.tsx +++ b/employee-portal/src/lib/shared/components/FileCard.tsx @@ -8,6 +8,7 @@ import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import AlternateEmailOutlinedIcon from "@mui/icons-material/AlternateEmailOutlined"; import AudioFileOutlinedIcon from "@mui/icons-material/AudioFileOutlined"; import ImageOutlinedIcon from "@mui/icons-material/ImageOutlined"; +import ListAltOutlined from "@mui/icons-material/ListAltOutlined"; import PictureAsPdfOutlinedIcon from "@mui/icons-material/PictureAsPdfOutlined"; import { AspectRatio, @@ -37,6 +38,7 @@ export interface FileCardActionProps { export const CustomFileType = { Audio: "AUDIO", + Csv: "CSV", } as const; export type NonApiFileType = (typeof CustomFileType)[keyof typeof CustomFileType]; @@ -56,6 +58,7 @@ const iconByType = { PDF: PictureAsPdfOutlinedIcon, EML: AlternateEmailOutlinedIcon, AUDIO: AudioFileOutlinedIcon, + CSV: ListAltOutlined, } as const; export function FileCard(props: FileCardProps) { diff --git a/employee-portal/src/lib/shared/components/SidebarStepper/SidebarStepper.tsx b/employee-portal/src/lib/shared/components/SidebarStepper/SidebarStepper.tsx index 12d6028ed..849097ff4 100644 --- a/employee-portal/src/lib/shared/components/SidebarStepper/SidebarStepper.tsx +++ b/employee-portal/src/lib/shared/components/SidebarStepper/SidebarStepper.tsx @@ -4,7 +4,7 @@ */ import { SubmitButton } from "@eshg/lib-portal/components/buttons/SubmitButton"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useAlert } from "@eshg/lib-portal/errorHandling/AlertContext"; import { Button, DialogTitle, Stack, Typography, ZIndex } from "@mui/joy"; import { Formik, FormikErrors, FormikProps, FormikValues } from "formik"; import { useEffect, useRef, useState } from "react"; @@ -40,7 +40,7 @@ export function SidebarStepper<T extends FormikValues>({ const { sidebarFormRef, handleClose } = useSidebarForm({ onClose: onClose, }); - const alertContext = useAlertContext(); + const alert = useAlert(); const formikRef = useRef<FormikProps<T>>(null); const currentStep = steps[stepIndex]!; @@ -72,10 +72,10 @@ export function SidebarStepper<T extends FormikValues>({ if (typeof value === "object") { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument Object.keys(value).forEach( - (it) => void setFieldTouched(`${key}.${it}`, true), + (it) => void setFieldTouched(`${key}.${it}`, true, false), ); } else { - void setFieldTouched(key, true); + void setFieldTouched(key, true, false); } }); @@ -89,7 +89,7 @@ export function SidebarStepper<T extends FormikValues>({ } setStepIndex(stepIndex + 1); - alertContext?.setAlert(null); + alert.close(); }); } @@ -98,7 +98,7 @@ export function SidebarStepper<T extends FormikValues>({ return; } setStepIndex(stepIndex - 1); - alertContext?.setAlert(null); + alert.close(); } useEffect(() => { @@ -116,16 +116,15 @@ export function SidebarStepper<T extends FormikValues>({ validate={(model) => { const errors = stepProps(model).validator?.(model); if (errors === undefined) { - alertContext?.setAlert(null); + alert.close(); } else { const possibleErrors = Object.values(errors).filter( (it) => typeof it === "string", ); if (possibleErrors.length > 0) { - alertContext?.setAlert({ + alert.error({ title: "", message: possibleErrors[0], - color: "danger", }); } } @@ -190,15 +189,14 @@ export function SidebarStepper<T extends FormikValues>({ </Button> )} {stepIndex + 1 < steps.length && ( - <SubmitButton - submitting={false} + <Button onClick={() => { onNextStep(validateForm, values, setFieldTouched); }} disabled={isDisabledNextStep} > Weiter - </SubmitButton> + </Button> )} {stepIndex + 1 === steps.length && ( <SubmitButton diff --git a/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockFieldArrayWithDays.tsx b/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockFieldArrayWithDays.tsx index eaa708cc2..91ef5fd05 100644 --- a/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockFieldArrayWithDays.tsx +++ b/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockFieldArrayWithDays.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Add, Delete } from "@mui/icons-material"; -import { Grid } from "@mui/joy"; +import { Add } from "@mui/icons-material"; +import { Button, Divider, Grid } from "@mui/joy"; import { FieldArray } from "formik"; import { @@ -12,8 +12,6 @@ import { AppointmentBlockGroupValuesWithDays, emptyAppointmentBlockGroup, } from "@/lib/shared/components/appointmentBlocks/AppointmentBlockFormWithDays"; -import { FieldIconButton } from "@/lib/shared/components/buttons/FieldIconButton"; -import { FieldButtonBar } from "@/lib/shared/components/form/FieldButtonBar"; import { FormGroupGrid } from "@/lib/shared/components/form/FormGroupGrid"; const APPOINTMENT_BLOCK_GROUP_MAX_LENGTH = 5; @@ -26,44 +24,46 @@ interface AppointmentBlockFieldArrayWithDaysProps { export function AppointmentBlockFieldArrayWithDays( props: Readonly<AppointmentBlockFieldArrayWithDaysProps>, ) { + const hasReachedAppointmentBlockLimit = + props.appointmentBlocks.length >= APPOINTMENT_BLOCK_GROUP_MAX_LENGTH; + return ( <FieldArray name={props.name}> - {({ insert, remove }) => - props.appointmentBlocks.map((_value, index) => ( - <FormGroupGrid key={index} data-testid="appointmentBlockForm"> - <AppointmentBlockFormWithDays name={`appointmentBlocks.${index}`} /> - <Grid xs={2}> - <FieldButtonBar> - <FieldIconButton - title="Weiteren Terminblock hinzufügen" - disabled={ - props.appointmentBlocks.length >= - APPOINTMENT_BLOCK_GROUP_MAX_LENGTH - } + {({ remove, push }) => ( + <> + {props.appointmentBlocks.map((_value, index) => ( + <> + <FormGroupGrid key={index} data-testid="appointmentBlockForm"> + <AppointmentBlockFormWithDays + name={`appointmentBlocks.${index}`} + removeBlock={() => remove(index)} + blockCount={props.appointmentBlocks.length} + /> + </FormGroupGrid> + {props.appointmentBlocks.length > 1 && + index < props.appointmentBlocks.length - 1 && <Divider />} + </> + ))} + <> + <Divider /> + {!hasReachedAppointmentBlockLimit && ( + <Grid xs={2}> + <Button + variant="outlined" + startDecorator={<Add />} onClick={() => { - if ( - props.appointmentBlocks.length < - APPOINTMENT_BLOCK_GROUP_MAX_LENGTH - ) - insert(index + 1, emptyAppointmentBlockGroup()); + if (!hasReachedAppointmentBlockLimit) { + push(emptyAppointmentBlockGroup()); + } }} > - <Add /> - </FieldIconButton> - {props.appointmentBlocks.length > 1 && ( - <FieldIconButton - title="Terminblock entfernen" - onClick={() => remove(index)} - color="danger" - > - <Delete color="danger" /> - </FieldIconButton> - )} - </FieldButtonBar> - </Grid> - </FormGroupGrid> - )) - } + Terminblock hinzufügen + </Button> + </Grid> + )} + </> + </> + )} </FieldArray> ); } diff --git a/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockFormWithDays.tsx b/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockFormWithDays.tsx index 856bb1996..110cfade2 100644 --- a/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockFormWithDays.tsx +++ b/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockFormWithDays.tsx @@ -7,7 +7,8 @@ import { ApiDayOfWeek } from "@eshg/employee-portal-api/measlesProtection"; import { DateField } from "@eshg/lib-portal/components/formFields/DateField"; import { createFieldNameMapper } from "@eshg/lib-portal/helpers/form"; import { NestedFormProps } from "@eshg/lib-portal/types/form"; -import { Grid } from "@mui/joy"; +import { Delete } from "@mui/icons-material"; +import { Button, Grid } from "@mui/joy"; import { SxProps } from "@mui/joy/styles/types"; import { WeekdayCheckboxGroup } from "@/lib/shared/components/appointmentBlocks/WeekdayCheckboxGroup"; @@ -52,7 +53,7 @@ const dateTimeFieldStyle: SxProps = { export function emptyAppointmentBlockGroup(): AppointmentBlockGroupValuesWithDays { return { - daysOfWeek: [ApiDayOfWeek.Monday], + daysOfWeek: [], startDate: "", endDate: "", startTime: "", @@ -60,14 +61,21 @@ export function emptyAppointmentBlockGroup(): AppointmentBlockGroupValuesWithDay }; } -export function AppointmentBlockFormWithDays(props: Readonly<NestedFormProps>) { +export interface AppointmentBlockFormWithDaysProps extends NestedFormProps { + removeBlock: () => void; + blockCount: number; +} + +export function AppointmentBlockFormWithDays( + props: Readonly<AppointmentBlockFormWithDaysProps>, +) { const fieldName = createFieldNameMapper(props.name); const daysOfWeekOptions = WEEKDAY_CHECKBOX_OPTIONS.filter( ({ disabled }) => !disabled, ); return ( - <Grid direction="column" xs={10}> + <Grid direction="column" xs={10} paddingTop={0}> <Grid container xs={12} direction={"row"} columnGap={0}> <Grid xs={2} sx={{ ...dateTimeFieldStyle, pl: 0 }}> <DateField @@ -110,6 +118,21 @@ export function AppointmentBlockFormWithDays(props: Readonly<NestedFormProps>) { /> </Grid> </Grid> + {props.blockCount > 1 && ( + <Grid container xs={12} direction={"row"} padding={0}> + <Grid direction={"column"}> + <Button + variant="outlined" + startDecorator={<Delete />} + title="Terminblock entfernen" + onClick={props.removeBlock} + color="danger" + > + Löschen + </Button> + </Grid> + </Grid> + )} </Grid> ); } diff --git a/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockGroupFields.tsx b/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockGroupFields.tsx index eb2c8f595..679798a74 100644 --- a/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockGroupFields.tsx +++ b/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentBlockGroupFields.tsx @@ -11,7 +11,7 @@ import { validatePipe, validateRange, } from "@eshg/lib-portal/helpers/validators"; -import { Grid } from "@mui/joy"; +import { Divider, Grid } from "@mui/joy"; import { AppointmentBlockFieldArrayWithDays } from "@/lib/shared/components/appointmentBlocks/AppointmentBlockFieldArrayWithDays"; import { AppointmentBlockGroupValuesWithDays } from "@/lib/shared/components/appointmentBlocks/AppointmentBlockFormWithDays"; @@ -34,7 +34,7 @@ export function AppointmentBlockGroupFields( return ( <> <FormGroupGrid> - <Grid xs={4}> + <Grid xs={3}> <SelectField name="type" label="Art" @@ -44,7 +44,7 @@ export function AppointmentBlockGroupFields( /> </Grid> {props.showParallelExaminations ? ( - <Grid xs={2}> + <Grid xs={3}> <NumberField name="parallelExaminations" label="Parallele Untersuchungen" @@ -54,6 +54,7 @@ export function AppointmentBlockGroupFields( </Grid> ) : null} </FormGroupGrid> + <Divider /> <AppointmentBlockFieldArrayWithDays name="appointmentBlocks" appointmentBlocks={props.appointmentBlocksWithDays!} diff --git a/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentStaffSelection.tsx b/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentStaffSelection.tsx index 4f0741ffa..343133730 100644 --- a/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentStaffSelection.tsx +++ b/employee-portal/src/lib/shared/components/appointmentBlocks/AppointmentStaffSelection.tsx @@ -35,7 +35,7 @@ export function AppointmentStaffSelection( <Grid xs={4}> <AppointmentStaffField name="physicians" - label="Arzt:in" + label="Arzt/Ärztin" placeholder="auswählen" options={props.physicianOptions} freeStaff={props.freeStaff} diff --git a/employee-portal/src/lib/shared/components/appointmentBlocks/WeekdayCheckboxGroup.tsx b/employee-portal/src/lib/shared/components/appointmentBlocks/WeekdayCheckboxGroup.tsx index f0d5c5219..daf1a58a3 100644 --- a/employee-portal/src/lib/shared/components/appointmentBlocks/WeekdayCheckboxGroup.tsx +++ b/employee-portal/src/lib/shared/components/appointmentBlocks/WeekdayCheckboxGroup.tsx @@ -49,7 +49,7 @@ export function WeekdayCheckboxGroup({ return ( <> <FormLabel id={ariaLabelId} htmlFor={labelId}> - <Typography level="body-md" sx={{ fontWeight: "bold" }}> + <Typography level="body-sm" sx={{ fontWeight: 500 }}> {label} </Typography> </FormLabel> diff --git a/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/validateAppointmentBlock.ts b/employee-portal/src/lib/shared/components/appointmentBlocks/validateAppointmentBlock.ts similarity index 98% rename from employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/validateAppointmentBlock.ts rename to employee-portal/src/lib/shared/components/appointmentBlocks/validateAppointmentBlock.ts index 4f06a7fd2..86d123a77 100644 --- a/employee-portal/src/lib/businessModules/schoolEntry/features/appointmentBlocks/appointmentBlocksGroupForm/validateAppointmentBlock.ts +++ b/employee-portal/src/lib/shared/components/appointmentBlocks/validateAppointmentBlock.ts @@ -1,6 +1,6 @@ /** * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ import { ApiAppointmentType } from "@eshg/employee-portal-api/schoolEntry"; diff --git a/employee-portal/src/lib/shared/components/archiving/components/archiveView/ArchiveTable.tsx b/employee-portal/src/lib/shared/components/archiving/components/archiveView/ArchiveTable.tsx index de7b51981..79809e28e 100644 --- a/employee-portal/src/lib/shared/components/archiving/components/archiveView/ArchiveTable.tsx +++ b/employee-portal/src/lib/shared/components/archiving/components/archiveView/ArchiveTable.tsx @@ -106,6 +106,9 @@ export function ArchiveTable(props: ArchiveTableProps) { enableSortingRemoval={false} rowSelectionProps={rowSelectionProps} focusColumnHeader="Geschlossen am" + rowNavRoute={(row) => + props.procedureDetailsRoute(row.original.procedureId) + } /> </TableSheet> </TablePage> diff --git a/employee-portal/src/lib/shared/components/archiving/components/archiveView/ArchiveTableTitle.tsx b/employee-portal/src/lib/shared/components/archiving/components/archiveView/ArchiveTableTitle.tsx index 784e80080..ea1e564e0 100644 --- a/employee-portal/src/lib/shared/components/archiving/components/archiveView/ArchiveTableTitle.tsx +++ b/employee-portal/src/lib/shared/components/archiving/components/archiveView/ArchiveTableTitle.tsx @@ -4,16 +4,13 @@ */ import { ApiArchivingRelevance } from "@eshg/employee-portal-api/businessProcedures"; -import { - DeleteOutlined, - Inventory2Outlined, - SubdirectoryArrowRightOutlined, -} from "@mui/icons-material"; -import { Button, Divider, Sheet, Typography } from "@mui/joy"; +import { DeleteOutlined, Inventory2Outlined } from "@mui/icons-material"; +import { Button, Divider, Typography } from "@mui/joy"; import { RowSelectionState } from "@tanstack/react-table"; import { ArchiveTableProps } from "@/lib/shared/components/archiving/components/archiveView/ArchiveTable"; import { useConfirmationDialog } from "@/lib/shared/components/confirmationDialog/ConfirmationDialogProvider"; +import { RowSelectionTableToolbar } from "@/lib/shared/components/table/RowSelectionTableToolbar"; import { mapToRowIds } from "@/lib/shared/hooks/table/useRowSelection"; interface ArchiveTableTitleProps extends ArchiveTableProps { @@ -71,25 +68,13 @@ export function ArchiveTableTitle(props: ArchiveTableTitleProps) { } return ( - <Sheet - variant="soft" - sx={{ - display: "flex", - gap: 2, - alignItems: "center", - borderRadius: 0, - height: 48, - padding: (theme) => theme.spacing(0.5, 1.5), + <RowSelectionTableToolbar + rowSelection={props.rowSelection} + elementName={{ + singular: "Vorgang ausgewählt", + plural: "Vorgänge ausgewählt", }} > - <SubdirectoryArrowRightOutlined - sx={{ transform: "rotate(90deg)", fontSize: "1.25rem" }} - /> - <Typography level="body-sm" data-testid="selectedIndicator"> - <Typography fontWeight="bold">{selectedProcedureIds.length}</Typography>{" "} - {selectedProcedureIds.length === 1 ? "Vorgang" : "Vorgänge"} ausgewählt - </Typography> - <Divider orientation="vertical" /> {selectedProcedureIds.length === 0 && ( <Typography level="body-sm" color="danger"> Bitte Vorgänge auswählen @@ -122,6 +107,6 @@ export function ArchiveTableTitle(props: ArchiveTableTitleProps) { </Button> </> )} - </Sheet> + </RowSelectionTableToolbar> ); } diff --git a/employee-portal/src/lib/shared/components/archiving/components/archiveView/archiveTableColumns.tsx b/employee-portal/src/lib/shared/components/archiving/components/archiveView/archiveTableColumns.tsx index 41122fa13..372a3dc3a 100644 --- a/employee-portal/src/lib/shared/components/archiving/components/archiveView/archiveTableColumns.tsx +++ b/employee-portal/src/lib/shared/components/archiving/components/archiveView/archiveTableColumns.tsx @@ -19,6 +19,9 @@ export const archiveTableColumns = [ cell: (props) => formatDate(props.getValue()), meta: { width: "240px", + canNavigate: { + parentRow: true, + }, }, }), columnHelper.accessor("procedureType", { @@ -27,6 +30,9 @@ export const archiveTableColumns = [ cell: (props) => procedureTypeNames[props.getValue()], meta: { width: "260px", + canNavigate: { + parentRow: true, + }, }, }), columnHelper.accessor( @@ -39,6 +45,9 @@ export const archiveTableColumns = [ enableSorting: false, meta: { width: "260px", + canNavigate: { + parentRow: true, + }, }, }, ), diff --git a/employee-portal/src/lib/shared/components/buttons/ActionsMenu.tsx b/employee-portal/src/lib/shared/components/buttons/ActionsMenu.tsx index dbff8c6c0..664d0f37f 100644 --- a/employee-portal/src/lib/shared/components/buttons/ActionsMenu.tsx +++ b/employee-portal/src/lib/shared/components/buttons/ActionsMenu.tsx @@ -32,6 +32,7 @@ export interface ActionsMenuProps extends MenuButtonProps { sx?: SxProps; color?: ColorPaletteProp; disablePortal?: boolean; + rowHeight?: boolean; } function ActionLabel({ @@ -114,11 +115,16 @@ export function createActionsLinkOrButton(item: ActionsItem) { } export function ActionsMenu(props: ActionsMenuProps) { - const { actionItems, actionDescription, ...rest } = props; + const { actionItems, actionDescription, rowHeight, ...rest } = props; return ( <Dropdown> - <Stack direction="row" alignItems="center" justifyContent="flex-end"> + <Stack + direction="row" + alignItems="center" + justifyContent="flex-end" + sx={{ height: rowHeight ? 3 : undefined }} + > <MenuButton slots={{ root: IconButton }} slotProps={{ root: { variant: props.variant, color: props.color } }} diff --git a/employee-portal/src/lib/shared/components/buttons/IconTooltipButton.tsx b/employee-portal/src/lib/shared/components/buttons/IconTooltipButton.tsx new file mode 100644 index 000000000..9a551ba55 --- /dev/null +++ b/employee-portal/src/lib/shared/components/buttons/IconTooltipButton.tsx @@ -0,0 +1,82 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InfoOutlined } from "@mui/icons-material"; +import { IconButton as JoyIconButton, Tooltip } from "@mui/joy"; +import { SxProps } from "@mui/joy/styles/types"; +import { PropsWithChildren, ReactNode, forwardRef, useState } from "react"; + +import { BaseModal } from "@/lib/shared/components/BaseModal"; + +export function InfoIconTooltipButton({ + title, + sx, + size, + iconLabelledBy, + iconLabel: iconLabelProp, +}: Readonly<{ + title: ReactNode; + sx?: SxProps; + iconLabel?: string; + iconLabelledBy?: string; + size?: "sm" | "md" | "lg"; +}>) { + const iconLabel = + !iconLabelledBy && !iconLabelProp ? "Mehr Informationen" : iconLabelProp; + return ( + <IconTooltipButton + icon={<InfoOutlined sx={sx} size={size} />} + iconLabel={iconLabel} + iconLabelledBy={iconLabelledBy} + title={title} + /> + ); +} + +export function IconTooltipButton({ + icon, + iconLabel, + iconLabelledBy, + title, +}: Readonly<{ + icon: ReactNode; + iconLabel?: string; + iconLabelledBy?: string; + title: ReactNode; +}>) { + const [open, setOpen] = useState(false); + + return ( + <> + <Tooltip arrow variant="outlined" title={title}> + <IconButton + aria-label={iconLabel} + aria-labelledby={iconLabelledBy} + onClick={() => setOpen(true)} + > + {icon} + </IconButton> + </Tooltip> + <BaseModal open={open} onClose={() => setOpen(false)}> + {title} + </BaseModal> + </> + ); +} + +const IconButton = forwardRef< + HTMLButtonElement, + PropsWithChildren<{ + "aria-label"?: string; + "aria-labelledby"?: string; + onClick: () => void; + }> +>(function IconButton(props, ref) { + return ( + <JoyIconButton color="primary" size="sm" {...props} ref={ref}> + {props.children} + </JoyIconButton> + ); +}); diff --git a/employee-portal/src/lib/shared/components/buttons/ToggleButton.tsx b/employee-portal/src/lib/shared/components/buttons/ToggleButton.tsx index 4b80eea65..506a19004 100644 --- a/employee-portal/src/lib/shared/components/buttons/ToggleButton.tsx +++ b/employee-portal/src/lib/shared/components/buttons/ToggleButton.tsx @@ -23,8 +23,9 @@ interface ToggleButtonProps extends Omit<ButtonProps, "onToggle"> { * use aria-pressed on the Button or Icon Button component instead."<i> */ export function ToggleButton(props: ToggleButtonProps) { - const { onClick, onToggle, asIcon, children, ...restProps } = props; - const [pressed, setPressed] = useState(false); + const { onClick, onToggle, asIcon, children, defaultChecked, ...restProps } = + props; + const [pressed, setPressed] = useState(defaultChecked ?? false); const Component = asIcon ? IconButton : Button; diff --git a/employee-portal/src/lib/shared/components/cards/ProcedureCard.tsx b/employee-portal/src/lib/shared/components/cards/ProcedureCard.tsx index 7a791cd1c..cfc6b746e 100644 --- a/employee-portal/src/lib/shared/components/cards/ProcedureCard.tsx +++ b/employee-portal/src/lib/shared/components/cards/ProcedureCard.tsx @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Row } from "@eshg/lib-portal/components/Row"; import { InternalLinkIconButton } from "@eshg/lib-portal/components/navigation/InternalLinkIconButton"; import { formatDate } from "@eshg/lib-portal/formatters/dateTime"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import { Chip, Grid, Sheet, Typography } from "@mui/joy"; import { useId } from "react"; -import { Row } from "@/lib/shared/Row"; import { ProcedureLiteItem } from "@/lib/shared/components/legacyPersonSidebar/LegacyPersonSidebar"; import { procedureStatusNames, diff --git a/employee-portal/src/lib/shared/components/chat/MessageTeaserProvider.tsx b/employee-portal/src/lib/shared/components/chat/MessageTeaserProvider.tsx deleted file mode 100644 index b485cf9e6..000000000 --- a/employee-portal/src/lib/shared/components/chat/MessageTeaserProvider.tsx +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: Apache-2.0 - */ - -"use client"; - -import { useNavigation } from "@eshg/lib-portal/components/navigation/NavigationContext"; -import CloseIcon from "@mui/icons-material/Close"; -import { Box, Button, IconButton, Snackbar, Stack, Typography } from "@mui/joy"; -import { usePathname } from "next/navigation"; -import { - Dispatch, - ReactNode, - SetStateAction, - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; -import { v4 as uuidv4 } from "uuid"; - -import { useChat } from "@/lib/businessModules/chat/shared/ChatProvider"; -import { routes } from "@/lib/businessModules/chat/shared/routes"; -import { Presence } from "@/lib/businessModules/chat/shared/types"; -import { getStatusColor } from "@/lib/businessModules/chat/shared/utils"; - -interface SnackbarValues { - username: string; - text: string; - link: string; - userPresence: string; - key: string; -} - -export interface BaseSnackbarProps { - snackbar: SnackbarValues | undefined; - onClose: () => void; -} - -type SnackbarValuesWithoutKey = Omit<SnackbarValues, "key">; - -function BaseSnackbar({ snackbar, onClose }: Readonly<BaseSnackbarProps>) { - const pathname = usePathname(); - const { tryNavigate } = useNavigation(); - const { userSettings, messagesSidebar } = useChat(); - - useEffect(() => { - if (pathname === routes.index || messagesSidebar.isOpen) { - onClose(); - } - }, [onClose, pathname, messagesSidebar.isOpen]); - - function toggleMessagesSidebar(): void { - if (messagesSidebar.isOpen) { - messagesSidebar.close(); - } else { - messagesSidebar.open(); - } - } - - return ( - <Snackbar - open={!!snackbar} - variant="soft" - size="md" - anchorOrigin={{ vertical: "top", horizontal: "right" }} - autoHideDuration={5000} - key={snackbar?.key} - onClose={(_event, reason) => { - if (reason !== "clickaway") { - onClose(); - } - }} - slotProps={{ - root: { - sx: { - backgroundColor: "common.white", - top: { - xxs: "4rem", - sm: "5rem", - }, - }, - }, - }} - > - {snackbar && ( - <Box - display="flex" - gap={2} - sx={{ maxWidth: "21.5rem", maxHeight: "11.375rem" }} - > - <Box display="flex" flexDirection="column" sx={{ flexGrow: 1 }}> - <Box display="flex" flexDirection="row"> - <Box - display="flex" - flexDirection="row" - sx={{ - width: "100%", - boxSizing: "content-box", - alignItems: "center", - }} - > - {userSettings.sharePresence && snackbar.userPresence && ( - <Box - sx={{ - width: "0.625rem", - height: "0.625rem", - borderRadius: "100%", - backgroundColor: getStatusColor( - snackbar.userPresence as Presence, - ), - marginRight: 0.8, - }} - ></Box> - )} - <Typography - level="title-md" - fontWeight={500} - sx={{ - fontWeight: "bold", - height: "1.5rem", - maxWidth: "15rem", - textOverflow: "ellipsis", - }} - > - {snackbar.username} - </Typography> - </Box> - <IconButton - aria-label="Schließen" - onClick={onClose} - color="primary" - > - <CloseIcon /> - </IconButton> - </Box> - <Typography - level="body-md" - maxWidth="18rem" - textColor="common.black" - noWrap={true} - sx={{ - display: "-webkit-box", - overflow: "hidden", - WebkitBoxOrient: "vertical", - WebkitLineClamp: 3, - whiteSpace: "normal", - }} - > - {snackbar.text} - </Typography> - - <Stack - spacing={2} - display="flex" - flexDirection="row" - marginTop="1rem" - justifyContent="space-between" - sx={{ width: "100%" }} - > - <Button - variant="outlined" - size="sm" - sx={{ - width: "9.313rem", - height: "2rem", - paddingLeft: 1, - paddingRight: 1, - }} - onClick={() => { - tryNavigate(snackbar.link); - }} - > - Zum Chatbereich - </Button> - <Button - variant="soft" - size="sm" - color="primary" - sx={{ - width: "6.188rem", - height: "2rem", - paddingLeft: 1, - paddingRight: 1, - radius: "radius-sm", - border: "1px", - }} - onClick={() => { - toggleMessagesSidebar(); - onClose(); - }} - > - Antworten - </Button> - </Stack> - </Box> - </Box> - )} - </Snackbar> - ); -} - -const SnackbarContext = createContext<{ - snackbarValues: SnackbarValues | undefined; - setSnackbar: Dispatch<SetStateAction<SnackbarValues | undefined>>; -}>(null!); - -export function MessageTeaserProvider({ - children, -}: Readonly<{ children: ReactNode }>) { - const [snackbarValues, setSnackbar] = useState<SnackbarValues | undefined>(); - const contextValues = useMemo( - () => ({ snackbarValues, setSnackbar }), - [snackbarValues], - ); - return ( - <SnackbarContext.Provider value={contextValues}> - <BaseSnackbar - snackbar={snackbarValues} - onClose={() => setSnackbar(undefined)} - /> - {children} - </SnackbarContext.Provider> - ); -} - -export function useMessageTeaser() { - const context = useContext(SnackbarContext); - if (context === null) { - throw new Error("useSnackbar was called outside SnackbarProvider"); - } - const { setSnackbar } = context; - - return useCallback( - (values: SnackbarValuesWithoutKey | undefined) => { - setSnackbar(values ? { ...values, key: uuidv4() } : undefined); - }, - [setSnackbar], - ); -} diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/DetailCard.tsx b/employee-portal/src/lib/shared/components/detailsCard/DetailsCard.tsx similarity index 81% rename from employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/DetailCard.tsx rename to employee-portal/src/lib/shared/components/detailsCard/DetailsCard.tsx index 3d4143deb..51e691311 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/DetailCard.tsx +++ b/employee-portal/src/lib/shared/components/detailsCard/DetailsCard.tsx @@ -1,30 +1,28 @@ /** * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ "use client"; +import { Row } from "@eshg/lib-portal/components/Row"; import { Sheet, Typography, styled } from "@mui/joy"; import { PropsWithChildren, ReactElement } from "react"; -import { Row } from "@/lib/shared/Row"; - -export function DetailCard({ +export function DetailsCard({ title, fullHeight, children, - "data-testid": dataTestId, actionButton, -}: PropsWithChildren<{ title: string; fullHeight?: boolean }> & { - "data-testid"?: string; +}: PropsWithChildren<{ + title: string; + fullHeight?: boolean; actionButton?: ReactElement; -}) { +}>) { return ( <Sheet component="section" sx={{ padding: 3, height: fullHeight ? "100%" : "auto" }} - data-testid={dataTestId} > <Row marginBottom={3} minHeight={36} justifyContent="space-between"> <Typography diff --git a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/LabeledValue.tsx b/employee-portal/src/lib/shared/components/detailsCard/LabeledValue.tsx similarity index 98% rename from employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/LabeledValue.tsx rename to employee-portal/src/lib/shared/components/detailsCard/LabeledValue.tsx index 06fbc9fa5..d29fa1da4 100644 --- a/employee-portal/src/lib/businessModules/measlesProtection/components/procedures/procedureDetails/LabeledValue.tsx +++ b/employee-portal/src/lib/shared/components/detailsCard/LabeledValue.tsx @@ -1,6 +1,6 @@ /** * Copyright 2024 cronn GmbH - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: Apache-2.0 */ import { InternalLink } from "@eshg/lib-portal/components/navigation/InternalLink"; diff --git a/employee-portal/src/lib/shared/components/drawer/useSidebar.tsx b/employee-portal/src/lib/shared/components/drawer/useSidebar.tsx index 8e1b3abb4..21f4c7657 100644 --- a/employee-portal/src/lib/shared/components/drawer/useSidebar.tsx +++ b/employee-portal/src/lib/shared/components/drawer/useSidebar.tsx @@ -3,13 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { useUuid } from "@eshg/lib-portal/hooks/useUuid"; + import { DrawerOpenOptions, DrawerProps, isDrawer, useDrawerContext, } from "@/lib/shared/components/drawer/drawerContext"; -import { useUuid } from "@/lib/shared/hooks/useUuid"; export type UseSidebarResult<TSidebarProps extends DrawerProps = DrawerProps> = CustomSidebarProps<TSidebarProps> extends Record<string, never> diff --git a/employee-portal/src/lib/shared/components/facilitySidebar/FacilityForm.tsx b/employee-portal/src/lib/shared/components/facilitySidebar/FacilityForm.tsx index d560bed8a..94bc1cffd 100644 --- a/employee-portal/src/lib/shared/components/facilitySidebar/FacilityForm.tsx +++ b/employee-portal/src/lib/shared/components/facilitySidebar/FacilityForm.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Row } from "@eshg/lib-portal/components/Row"; import { InputArrayField } from "@eshg/lib-portal/components/formFields/InputArrayField"; import { InputField } from "@eshg/lib-portal/components/formFields/InputField"; import { createFieldNameMapper } from "@eshg/lib-portal/helpers/form"; @@ -13,7 +14,6 @@ import { Button, Divider, Grid, IconButton, Stack, Typography } from "@mui/joy"; import { FieldArray, Formik } from "formik"; import { Fragment, ReactNode, RefObject } from "react"; -import { Row } from "@/lib/shared/Row"; import { BaseFacility } from "@/lib/shared/components/facilitySidebar/types"; import { FormButtonBar } from "@/lib/shared/components/form/FormButtonBar"; import { diff --git a/employee-portal/src/lib/shared/components/form/FormSheet.tsx b/employee-portal/src/lib/shared/components/form/FormSheet.tsx index 0aa34418d..638ae44a0 100644 --- a/employee-portal/src/lib/shared/components/form/FormSheet.tsx +++ b/employee-portal/src/lib/shared/components/form/FormSheet.tsx @@ -9,16 +9,19 @@ import { Sheet, styled } from "@mui/joy"; import { SxProps } from "@mui/joy/styles/types"; import { FormEventHandler } from "react"; -const StyledSheet = styled(Sheet)(({ theme }) => ({ +const StyledSheet = styled(Sheet, { + shouldForwardProp: (prop) => prop !== "gap", +})<{ gap?: number }>(({ theme, gap }) => ({ display: "flex", flexDirection: "column", - gap: theme.spacing(3), + gap: theme.spacing(gap ?? 3), })) as typeof Sheet; interface FormSheetProps extends RequiresChildren { id?: string; onSubmit?: FormEventHandler<HTMLFormElement>; sx?: SxProps; + gap?: number; } export function FormSheet(props: FormSheetProps) { diff --git a/employee-portal/src/lib/shared/components/form/SidebarForm.tsx b/employee-portal/src/lib/shared/components/form/SidebarForm.tsx index 292360cb9..9014c4a6c 100644 --- a/employee-portal/src/lib/shared/components/form/SidebarForm.tsx +++ b/employee-portal/src/lib/shared/components/form/SidebarForm.tsx @@ -6,7 +6,7 @@ "use client"; import { FormPlus } from "@eshg/lib-portal/components/form/FormPlus"; -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { RequiresChildren } from "@eshg/lib-portal/types/react"; import { useFormikContext } from "formik"; import { @@ -64,20 +64,16 @@ export function useSidebarFormHandle( resetForm: () => void; }, ) { - const alertContext = useAlertContext(); - - const resetErrors = useCallback(() => { - alertContext?.setAlert?.(null); - }, [alertContext]); + const resetAlertContext = useResetAlertContext(); const resetForm = useCallback(() => { props.resetForm(); - resetErrors(); - }, [props, resetErrors]); + resetAlertContext(); + }, [props, resetAlertContext]); useImperativeHandle(ref, () => ({ dirty: props.dirty, resetForm, - resetErrors, + resetErrors: resetAlertContext, })); } diff --git a/employee-portal/src/lib/shared/components/formFields/TextareaField.tsx b/employee-portal/src/lib/shared/components/formFields/TextareaField.tsx index 791c43dc1..1846a2d8a 100644 --- a/employee-portal/src/lib/shared/components/formFields/TextareaField.tsx +++ b/employee-portal/src/lib/shared/components/formFields/TextareaField.tsx @@ -23,11 +23,12 @@ interface TextareaFieldProps extends ValidationRules<string> { "label-id"?: string; minRows?: number; untrimmedInput?: boolean; + disabled?: boolean; } export function TextareaField(props: TextareaFieldProps) { const field = useBaseField<string>(props); - const disabled = useIsFormDisabled(); + const disabled = useIsFormDisabled() || props.disabled; async function handleBlur(event: FocusEvent<HTMLTextAreaElement>) { if (!props.untrimmedInput) { @@ -48,6 +49,7 @@ export function TextareaField(props: TextareaFieldProps) { required={field.required} error={field.error} sx={props.sx} + disabled={disabled} > <Textarea aria-labelledby={props["label-id"]} diff --git a/employee-portal/src/lib/shared/components/layout/MainContentLayout.tsx b/employee-portal/src/lib/shared/components/layout/MainContentLayout.tsx index fed976c4b..7594b913f 100644 --- a/employee-portal/src/lib/shared/components/layout/MainContentLayout.tsx +++ b/employee-portal/src/lib/shared/components/layout/MainContentLayout.tsx @@ -5,7 +5,7 @@ "use client"; -import { ScopedAlert } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { AlertSlot } from "@eshg/lib-portal/errorHandling/AlertContext"; import { Stack, StackProps, styled } from "@mui/joy"; import { PAGE_ALERT_STYLE } from "@/lib/shared/styles"; @@ -46,7 +46,7 @@ export function MainContentLayout(props: MainContentLayoutProps) { {...stackProps} className={props.fullViewportHeight ? "fullViewportHeight" : undefined} > - <ScopedAlert sx={PAGE_ALERT_STYLE} /> + <AlertSlot sx={PAGE_ALERT_STYLE} /> {children} </LayoutStack> ); diff --git a/employee-portal/src/lib/shared/components/layout/Toolbar.tsx b/employee-portal/src/lib/shared/components/layout/Toolbar.tsx index 16e0474c0..e2482f61d 100644 --- a/employee-portal/src/lib/shared/components/layout/Toolbar.tsx +++ b/employee-portal/src/lib/shared/components/layout/Toolbar.tsx @@ -5,12 +5,12 @@ "use client"; +import { Row } from "@eshg/lib-portal/components/Row"; import { InternalLinkButton } from "@eshg/lib-portal/components/navigation/InternalLinkButton"; import ChevronLeft from "@mui/icons-material/ChevronLeft"; import { Sheet, Typography } from "@mui/joy"; import { simpleToolbarHeight } from "@/lib/baseModule/components/layout/sizes"; -import { Row } from "@/lib/shared/Row"; export interface ToolbarProps { title: string; diff --git a/employee-portal/src/lib/shared/components/pagination/RowsPerPageSelect.tsx b/employee-portal/src/lib/shared/components/pagination/RowsPerPageSelect.tsx index 6004d4668..93db8eb72 100644 --- a/employee-portal/src/lib/shared/components/pagination/RowsPerPageSelect.tsx +++ b/employee-portal/src/lib/shared/components/pagination/RowsPerPageSelect.tsx @@ -9,6 +9,7 @@ import { } from "@eshg/lib-portal/components/formFields/SelectOptions"; import { Select, SelectProps } from "@mui/joy"; import { SxProps } from "@mui/joy/styles/types"; +import { isNonNullish } from "remeda"; export function RowsPerPageSelect(props: { value: string; @@ -26,7 +27,13 @@ export function RowsPerPageSelect(props: { ...props.sx, }} value={props.value} - onChange={props.onChange} + onChange={(event, value) => { + // event is null when the select changes without user interaction, + // this seems to happen randomly when the page re-renders due to changed query parameters + if (isNonNullish(event)) { + props.onChange?.(event, value); + } + }} > <SelectOptions options={props.options} /> </Select> diff --git a/employee-portal/src/lib/shared/components/procedures/constants.ts b/employee-portal/src/lib/shared/components/procedures/constants.ts index 1cc061e05..44a9a9066 100644 --- a/employee-portal/src/lib/shared/components/procedures/constants.ts +++ b/employee-portal/src/lib/shared/components/procedures/constants.ts @@ -32,6 +32,11 @@ export const procedureTypeNames = { [ApiProcedureType.TmVaccinationConsultation]: "Impfberatung", [ApiProcedureType.MeaslesProtection]: "Masernschutzimpfung", [ApiProcedureType.StiProtection]: "HIV-STI-Schutz", + [ApiProcedureType.MedicalRegistryEntry]: "Medizinalkarteieintrag", + [ApiProcedureType.MedicalRegistryEmployeeDraft]: + "Entwurf Medizinalkarteieintrag Mitarbeiter", + [ApiProcedureType.MedicalRegistryCitizenDraft]: + "Entwurf Medizinalkarteieintrag Bürger", } satisfies Record<ApiProcedureType, string>; export const procedureStatusNames = { diff --git a/employee-portal/src/lib/shared/components/sidebar/Sidebar.tsx b/employee-portal/src/lib/shared/components/sidebar/Sidebar.tsx index f1e0a329b..57cf1e15e 100644 --- a/employee-portal/src/lib/shared/components/sidebar/Sidebar.tsx +++ b/employee-portal/src/lib/shared/components/sidebar/Sidebar.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { useResetAlertContext } from "@eshg/lib-portal/errorHandling/AlertContext"; import { Drawer, DrawerProps, ModalClose, Stack, ZIndex } from "@mui/joy"; import { PropsWithChildren } from "react"; @@ -25,7 +25,7 @@ export function Sidebar({ zIndex, children, }: SidebarProps) { - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); function handleClose( ...args: Parameters<NonNullable<DrawerProps["onClose"]>> @@ -33,9 +33,7 @@ export function Sidebar({ if (onClose !== undefined) { onClose(...args); } - if (alertContext !== null) { - alertContext.setAlert(null); - } + resetAlertContext(); } return ( diff --git a/employee-portal/src/lib/shared/components/sidebar/SidebarContent.tsx b/employee-portal/src/lib/shared/components/sidebar/SidebarContent.tsx index 01b5b26c4..b2796d17d 100644 --- a/employee-portal/src/lib/shared/components/sidebar/SidebarContent.tsx +++ b/employee-portal/src/lib/shared/components/sidebar/SidebarContent.tsx @@ -4,13 +4,17 @@ */ import { Alert, AlertProps } from "@eshg/lib-portal/components/Alert"; -import { useAlert } from "@eshg/lib-portal/errorHandling/AlertContext"; -import { Box, DialogTitle, Stack, Typography } from "@mui/joy"; +import { AlertSlot } from "@eshg/lib-portal/errorHandling/AlertContext"; +import { Box, DialogTitle, Stack, Typography, styled } from "@mui/joy"; import { ReactNode } from "react"; -import { isNonNullish } from "remeda"; +import { isDefined, isNonNullish } from "remeda"; import { sidebarPadding } from "@/lib/shared/components/sidebar/Sidebar"; +const AlertContainer = styled("div")(({ theme }) => ({ + paddingInline: theme.spacing(sidebarPadding), +})); + export interface SidebarContentProps { title?: string; subtitle?: string; @@ -30,9 +34,6 @@ export function SidebarContent({ footer, verticallyCenterContent, }: SidebarContentProps) { - const contextAlert = useAlert(); - const activeAlert = alert ?? contextAlert; - return ( <Stack flex={1} @@ -64,10 +65,12 @@ export function SidebarContent({ {isNonNullish(header) && ( <Stack sx={{ paddingLeft: sidebarPadding }}>{header}</Stack> )} - {isNonNullish(activeAlert) && ( - <Box sx={{ paddingRight: sidebarPadding, paddingLeft: sidebarPadding }}> - <Alert {...activeAlert} /> - </Box> + {isDefined(alert) ? ( + <AlertContainer> + <Alert {...alert} /> + </AlertContainer> + ) : ( + <AlertSlot container={AlertContainer} /> )} <Stack flex={1} diff --git a/employee-portal/src/lib/shared/components/tabNavigationToolbar/TabNavigationToolbar.tsx b/employee-portal/src/lib/shared/components/tabNavigationToolbar/TabNavigationToolbar.tsx index c2ef8743e..7294a1f41 100644 --- a/employee-portal/src/lib/shared/components/tabNavigationToolbar/TabNavigationToolbar.tsx +++ b/employee-portal/src/lib/shared/components/tabNavigationToolbar/TabNavigationToolbar.tsx @@ -15,7 +15,7 @@ export interface TabNavigationToolbarProps { /** tab definitions */ items: TabNavigationItem[]; /** route for back button */ - routeBack: string; + routeBack?: string; /** component to be displayed as header; required. */ header: ReactNode; /** component to be displayed right aligned beneath the tabs; optional. */ @@ -42,13 +42,15 @@ export function TabNavigationToolbar(props: TabNavigationToolbarProps) { overflowY: "hidden", }} > - <InternalLinkIconButton - href={props.routeBack} - aria-label="Zurück" - sx={{ minWidth: "3.5rem" }} - > - <ChevronLeft sx={{ width: "40px", height: "40px" }} /> - </InternalLinkIconButton> + {props.routeBack !== undefined && ( + <InternalLinkIconButton + href={props.routeBack} + aria-label="Zurück" + sx={{ minWidth: "3.5rem" }} + > + <ChevronLeft sx={{ width: "40px", height: "40px" }} /> + </InternalLinkIconButton> + )} <Stack divider={<Divider />} sx={{ flexGrow: 1, minWidth: 0 }}> <Box sx={{ paddingInline: 3 }}>{props.header}</Box> <Box diff --git a/employee-portal/src/lib/shared/components/table/DataTable.tsx b/employee-portal/src/lib/shared/components/table/DataTable.tsx index 094566ed7..df629e4e6 100644 --- a/employee-portal/src/lib/shared/components/table/DataTable.tsx +++ b/employee-portal/src/lib/shared/components/table/DataTable.tsx @@ -155,7 +155,10 @@ export function DataTable<TData>(props: Readonly<DataTableProps<TData>>) { const tableStyle: SxProps = { minWidth: props.minWidth, - "--TableCell-paddingY": (theme) => theme.spacing(1), + // 7px = 8px padding - 1px for border + // We only have one 1px border per row, + // so this leaves 1px free (25px content-space): go wild + "--TableCell-paddingY": "7px", "--TableCell-paddingX": (theme) => theme.spacing(1.5), ...(wrapHeader && { "& thead th": { diff --git a/employee-portal/src/lib/shared/components/table/RowSelectionTableToolbar.tsx b/employee-portal/src/lib/shared/components/table/RowSelectionTableToolbar.tsx new file mode 100644 index 000000000..76ebbd780 --- /dev/null +++ b/employee-portal/src/lib/shared/components/table/RowSelectionTableToolbar.tsx @@ -0,0 +1,58 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import SubdirectoryArrowRightOutlined from "@mui/icons-material/SubdirectoryArrowRightOutlined"; +import { Divider, Sheet, Stack, Typography, styled } from "@mui/joy"; +import { RowSelectionState } from "@tanstack/react-table"; +import { PropsWithChildren } from "react"; + +import { mapToRowIds } from "@/lib/shared/hooks/table/useRowSelection"; + +const StyledSheet = styled(Sheet)(({ theme }) => ({ + display: "flex", + alignItems: "center", + padding: theme.spacing(0.5, 1.5), + borderRadius: 0, + height: 40, +})); + +interface RowSelectionTableToolbarProps { + rowSelection: RowSelectionState; + elementName: { + singular: string; + plural: string; + }; +} + +export function RowSelectionTableToolbar( + props: PropsWithChildren<RowSelectionTableToolbarProps>, +) { + const rowIds = mapToRowIds(props.rowSelection); + return ( + <StyledSheet variant="soft"> + <Stack direction="row" gap={2} alignItems={"center"}> + <SubdirectoryArrowRightOutlined + size={"sm"} + color={"neutral"} + sx={{ + rotate: "90deg", + }} + /> + <Typography level="body-sm" data-testid="selectedIndicator"> + <Typography fontWeight="bold">{rowIds.length}</Typography>{" "} + {rowIds.length === 1 + ? props.elementName.singular + : props.elementName.plural} + </Typography> + {props.children && ( + <> + <Divider orientation={"vertical"} sx={{ marginY: 1 }} /> + {props.children} + </> + )} + </Stack> + </StyledSheet> + ); +} diff --git a/employee-portal/src/lib/shared/components/table/TableSheet.tsx b/employee-portal/src/lib/shared/components/table/TableSheet.tsx index 4767bbcbf..147b29420 100644 --- a/employee-portal/src/lib/shared/components/table/TableSheet.tsx +++ b/employee-portal/src/lib/shared/components/table/TableSheet.tsx @@ -5,7 +5,7 @@ import { LoadingOverlay } from "@eshg/lib-portal/components/LoadingOverlay"; import { RequiresChildren } from "@eshg/lib-portal/types/react"; -import { Box, Sheet, Theme, styled } from "@mui/joy"; +import { Box, Sheet, Stack, Theme, styled } from "@mui/joy"; import { ReactElement, ReactNode } from "react"; export const StyledSheet = styled(Sheet)(({ theme }) => ({ @@ -27,8 +27,10 @@ export interface TableSheetProps extends RequiresChildren { export function TableSheet(props: TableSheetProps): ReactElement { return ( <StyledSheet> - {props.title} - {props.hideTable ? <Box flex={1} overflow="auto" /> : props.children} + <Stack flex={1} overflow="auto"> + {props.title} + {props.hideTable ? <Box flex={1} overflow="auto" /> : props.children} + </Stack> {props.footer} {props.loading && <LoadingOverlay zIndex={zIndexTable} />} </StyledSheet> diff --git a/employee-portal/src/lib/shared/hooks/useTablePageParams.ts b/employee-portal/src/lib/shared/hooks/useTablePageParams.ts new file mode 100644 index 000000000..83ab8bf2f --- /dev/null +++ b/employee-portal/src/lib/shared/hooks/useTablePageParams.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useSearchParams } from "next/navigation"; +import { useMemo } from "react"; + +export interface TablePageParamNames { + sortFieldName: string; + sortDirectionName: string; + pageNumberName: string; + pageSizeName: string; +} + +export function useTablePageParams<ColumnName extends string = string>({ + fieldNames: givenNames = {}, + mapColumnNames, +}: { + fieldNames?: Partial<TablePageParamNames>; + mapColumnNames?: (t: ColumnName | undefined) => string | undefined; +} = {}) { + const fieldNames: TablePageParamNames = { + sortFieldName: "sortBy", + sortDirectionName: "sortOrder", + pageNumberName: "pageNumber", + pageSizeName: "pageSize", + ...givenNames, + }; + + const searchParams = useSearchParams(); + const pageNumberNaN = parseInt( + searchParams.get(fieldNames.pageNumberName) ?? "", + ); + const pageNumber = isNaN(pageNumberNaN) ? undefined : pageNumberNaN; + + const pageSizeNaN = parseInt(searchParams.get(fieldNames.pageSizeName) ?? ""); + const pageSize = isNaN(pageSizeNaN) ? undefined : pageSizeNaN; + + const sortKeyUnmapped = (searchParams.get(fieldNames.sortFieldName) ?? + undefined) as ColumnName | undefined; + + const sortDirection = ( + searchParams.get(fieldNames.sortDirectionName) ?? undefined + )?.toUpperCase(); + + const sortKey = mapColumnNames + ? mapColumnNames(sortKeyUnmapped) + : sortKeyUnmapped; + + const tableParams = useMemo( + () => ({ + pageNumber, + pageSize, + sortBy: sortKey, + sortOrder: sortDirection, + }), + [pageNumber, pageSize, sortKey, sortDirection], + ); + return tableParams; +} diff --git a/employee-portal/src/serviceWorker/common/validatePassword.ts b/employee-portal/src/serviceWorker/common/validatePassword.ts index 2322adb15..fb19779b2 100644 --- a/employee-portal/src/serviceWorker/common/validatePassword.ts +++ b/employee-portal/src/serviceWorker/common/validatePassword.ts @@ -29,31 +29,31 @@ export function getPasswordInfo(password: string): { }[] { const result = [ { - message: `Minimalpasswortlänge: ${minimalPasswordLength} Zeichen`, + message: `Mindestens ${minimalPasswordLength} Zeichen lang`, valid: validatePasswordLength(password), }, ]; if (upperCaseLetterRequired) { result.push({ - message: "Passwort muss mindestens einen Großbuchstaben enthalten", + message: "Mindestens ein Großbuchstabe", valid: validatePasswordUpperCase(password), }); } if (lowerCaseLetterRequired) { result.push({ - message: "Passwort muss mindestens einen Kleinbuchstaben enthalten", + message: "Mindestens ein Kleinbuchstabe", valid: validatePasswordLowerCase(password), }); } if (digitRequired) { result.push({ - message: "Passwort muss mindestens eine Ziffer enthalten", + message: "Mindestens eine Zahl", valid: validatePasswordDigit(password), }); } if (symbolRequired) { result.push({ - message: "Passwort muss mindestens ein Symbol enthalten", + message: "Mindestens ein Sonderzeichen (z.B. !,@,#,$)", valid: validatePasswordSymbol(password), }); } diff --git a/employee-portal/src/serviceWorker/sw/FilterByCacheControlPlugin.ts b/employee-portal/src/serviceWorker/sw/CacheableResponsePlugin.ts similarity index 70% rename from employee-portal/src/serviceWorker/sw/FilterByCacheControlPlugin.ts rename to employee-portal/src/serviceWorker/sw/CacheableResponsePlugin.ts index 339a23e81..18df95a8a 100644 --- a/employee-portal/src/serviceWorker/sw/FilterByCacheControlPlugin.ts +++ b/employee-portal/src/serviceWorker/sw/CacheableResponsePlugin.ts @@ -7,9 +7,9 @@ import { CacheWillUpdateCallbackParam, WorkboxPlugin } from "workbox-core"; import { PRE_CACHE_FOR_OFFLINE_MODE } from "@/serviceWorker/common/common"; -// Only cache responses for which the Cache-Control header holds the (non-standard) value "pre-cache-for-offline-mode". +// Only cache HTTP 2xx responses for which the Cache-Control header holds the (non-standard) value "pre-cache-for-offline-mode". // By manually setting the header accordingly only for requests we do want to cache we prevent cache-pollution. -export class FilterByCacheControlPlugin implements WorkboxPlugin { +export class CacheableResponsePlugin implements WorkboxPlugin { cacheWillUpdate({ request, response, @@ -20,9 +20,8 @@ export class FilterByCacheControlPlugin implements WorkboxPlugin { .get("cache-control") ?.split(",") .map((s) => s.trim().toLowerCase()); - const shouldPrecache = cacheDirectives?.includes( - PRE_CACHE_FOR_OFFLINE_MODE, - ); + const shouldPrecache = + response.ok && cacheDirectives?.includes(PRE_CACHE_FOR_OFFLINE_MODE); return Promise.resolve(shouldPrecache ? response : null); } } diff --git a/employee-portal/src/serviceWorker/sw/index.ts b/employee-portal/src/serviceWorker/sw/index.ts index 0c2ec6557..2ac90e7f6 100644 --- a/employee-portal/src/serviceWorker/sw/index.ts +++ b/employee-portal/src/serviceWorker/sw/index.ts @@ -16,8 +16,8 @@ import { PAGES_CACHE_NAME, PAGES_RSC_CACHE_NAME, } from "@/serviceWorker/common/common"; +import { CacheableResponsePlugin } from "@/serviceWorker/sw/CacheableResponsePlugin"; import { EncryptPlugin } from "@/serviceWorker/sw/EncryptPlugin"; -import { FilterByCacheControlPlugin } from "@/serviceWorker/sw/FilterByCacheControlPlugin"; import { RedirectOnErrorPlugin } from "@/serviceWorker/sw/RedirectOnErrorPlugin"; import { StripRscRequestPlugin } from "@/serviceWorker/sw/StripRscRequestPlugin"; import { @@ -65,7 +65,7 @@ registerRoute( cacheName: PAGES_CACHE_NAME, networkTimeoutSeconds: NETWORK_TIMEOUT_IN_SECONDS, plugins: [ - new FilterByCacheControlPlugin(), + new CacheableResponsePlugin(), new ExpirationPlugin({ maxEntries: 10_000, }), @@ -85,7 +85,7 @@ registerRoute( networkTimeoutSeconds: NETWORK_TIMEOUT_IN_SECONDS, plugins: [ new StripRscRequestPlugin(), - new FilterByCacheControlPlugin(), + new CacheableResponsePlugin(), new ExpirationPlugin({ maxEntries: 10_000, }), @@ -100,7 +100,7 @@ registerRoute( cacheName: API_CACHE_NAME, networkTimeoutSeconds: NETWORK_TIMEOUT_IN_SECONDS, plugins: [ - new FilterByCacheControlPlugin(), + new CacheableResponsePlugin(), new ExpirationPlugin({ maxEntries: 10_000, }), diff --git a/lib-portal/package.json b/lib-portal/package.json index 2eccdfef2..f8a7132ab 100644 --- a/lib-portal/package.json +++ b/lib-portal/package.json @@ -11,22 +11,22 @@ "@mui/icons-material": "5.16.7", "@mui/joy": "5.0.0-beta.48", "@mui/material": "npm:@mui/joy@5.0.0-beta.48", - "@tanstack/react-query": "5.56.2", - "next": "14.2.12", + "@tanstack/react-query": "5.59.10", + "next": "14.2.14", "react": "18.3.1", "react-dom": "18.3.1", "react-error-boundary": "4.0.13", "uuid": "10.0.0" }, "devDependencies": { - "@tanstack/eslint-plugin-query": "5.56.1", - "@types/react": "18.3.7", - "@types/react-dom": "18.3.0", + "@tanstack/eslint-plugin-query": "5.59.7", + "@types/react": "18.3.11", + "@types/react-dom": "18.3.1", "@types/uuid": "10.0.0", - "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-istanbul": "2.1.1", - "eslint-config-next": "14.2.12", + "@vitejs/plugin-react": "4.3.2", + "@vitest/coverage-istanbul": "2.1.2", + "eslint-config-next": "14.2.14", "vite-tsconfig-paths": "5.0.1", - "vitest": "2.1.1" + "vitest": "2.1.2" } } diff --git a/lib-portal/src/api/useHandledMutation.ts b/lib-portal/src/api/useHandledMutation.ts index cb683bedd..84ad2d44b 100644 --- a/lib-portal/src/api/useHandledMutation.ts +++ b/lib-portal/src/api/useHandledMutation.ts @@ -9,7 +9,7 @@ import { useMutation, } from "@tanstack/react-query"; -import { useAlertContext } from "../errorHandling/AlertContext"; +import { useAlert } from "../errorHandling/AlertContext"; import { getErrorAction, getErrorDescription, @@ -28,7 +28,7 @@ export function useHandledMutation< TVariables = void, TContext = unknown, >(options: UseMutationOptions<TData, TError, TVariables, TContext>) { - const alertContext = useAlertContext(); + const alert = useAlert(); return useMutation({ ...options, @@ -36,21 +36,14 @@ export function useHandledMutation< const { errorCode } = resolveError(error); const { title, message } = getErrorDescription(errorCode); - if (alertContext === null) { - throw new Error("No alert context available."); - } - - alertContext.setAlert({ - color: "danger", + alert.error({ title, message, action: getErrorAction(errorCode), }); }), onSuccess: runBefore(options.onSuccess, () => { - if (alertContext !== null) { - alertContext.setAlert(null); // we might need to add a key later on to only reset errors from this mutation - } + alert.close(); }), }); } diff --git a/lib-portal/src/components/Alert.tsx b/lib-portal/src/components/Alert.tsx index 177c010a9..411ddea4b 100644 --- a/lib-portal/src/components/Alert.tsx +++ b/lib-portal/src/components/Alert.tsx @@ -6,6 +6,7 @@ import { AccountCircleOutlined, CheckCircleOutlined, + CloseRounded, ErrorOutlineOutlined, InfoOutlined, WarningAmberOutlined, @@ -16,6 +17,7 @@ import { Box, Button, ButtonProps, + IconButton, Typography, } from "@mui/joy"; import { SxProps } from "@mui/joy/styles/types"; @@ -63,12 +65,13 @@ function renderAction( color: AlertProps["color"], variant: AlertProps["variant"], ): ReactNode { - const buttonProps: ActionButtonProps = { + const buttonProps = { + "data-testid": "action", variant, size: "sm", color, sx: { textTransform: "uppercase" }, - }; + } as const; if ("href" in action) { return ( @@ -96,6 +99,7 @@ export interface AlertProps { variant?: Extract<AlertPropsJoy["variant"], "soft" | "outlined">; action?: AlertAction; sx?: SxProps; + onClose?: () => void; } export function Alert({ @@ -105,6 +109,7 @@ export function Alert({ variant = "soft", action, sx, + onClose, }: AlertProps) { return ( <AlertJoy @@ -113,7 +118,20 @@ export function Alert({ sx={{ ...sx, alignItems: "flex-start" }} startDecorator={renderIcon(color)} endDecorator={ - isDefined(action) ? renderAction(action, color, variant) : undefined + <> + {isDefined(action) && renderAction(action, color, variant)} + {isDefined(onClose) && ( + <IconButton + variant={variant} + color={color} + size="sm" + aria-label="Schließen" + onClick={onClose} + > + <CloseRounded /> + </IconButton> + )} + </> } > <Box> diff --git a/employee-portal/src/lib/shared/Row.tsx b/lib-portal/src/components/Row.tsx similarity index 96% rename from employee-portal/src/lib/shared/Row.tsx rename to lib-portal/src/components/Row.tsx index 60a27f5e1..1ade54005 100644 --- a/employee-portal/src/lib/shared/Row.tsx +++ b/lib-portal/src/components/Row.tsx @@ -3,8 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -"use client"; - import { Box, BoxProps } from "@mui/joy"; export function Row({ children, ...props }: BoxProps) { diff --git a/lib-portal/src/components/formFields/InputField.tsx b/lib-portal/src/components/formFields/InputField.tsx index 6915adb60..0659b8f7c 100644 --- a/lib-portal/src/components/formFields/InputField.tsx +++ b/lib-portal/src/components/formFields/InputField.tsx @@ -46,7 +46,7 @@ export function InputField(props: Readonly<InputFieldProps>) { const FieldComponent = props.component ?? BaseField; const InputComponent = props.input ?? Input; const field = useBaseField<string>(props); - const disabled = useIsFormDisabled(); + const disabled = useIsFormDisabled() || props.disabled; function handleChange(event: ChangeEvent<HTMLInputElement>): void { field.input.onChange(event); @@ -74,7 +74,7 @@ export function InputField(props: Readonly<InputFieldProps>) { error={field.error} sx={props.sx} fieldDecorator={props.fieldDecorator} - disabled={disabled || props.disabled} + disabled={disabled} > <InputComponent type={props.type} @@ -86,7 +86,7 @@ export function InputField(props: Readonly<InputFieldProps>) { onBlur={handleBlur} onClick={props.onClick} readOnly={props.readOnly} - disabled={props.disabled} + disabled={disabled} startDecorator={props.startDecorator} endDecorator={props.endDecorator} color={props.primary ? "primary" : undefined} diff --git a/lib-portal/src/components/formFields/appointmentPicker/AppointmentCalendar.tsx b/lib-portal/src/components/formFields/appointmentPicker/AppointmentCalendar.tsx new file mode 100644 index 000000000..8dd47c248 --- /dev/null +++ b/lib-portal/src/components/formFields/appointmentPicker/AppointmentCalendar.tsx @@ -0,0 +1,87 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Row } from "../../Row"; + +import { Day, DaysGrid } from "./Day"; +import { MonthSelection, MonthSelectionProps } from "./MonthSelection"; +import { WeekdayHeaders } from "./WeekdayHeaders"; +import { + getDaysInAndAroundMonth, + getMonthInterval, + monthLabel, +} from "./helpers"; + +export type MonthSelectionPassThroughProps = Omit< + MonthSelectionProps, + "label" | "nextMonthLabel" | "prevMonthLabel" +>; +export interface AppointmentCalendarProps + extends MonthSelectionPassThroughProps { + selectedDay: Date | undefined; + onDateSelected: (d: Date) => unknown; + appointments: Date[]; + monthSelectionLabel: string; + nextMonthLabel: string; + prevMonthLabel: string; +} +export function AppointmentCalendar({ + selectedDay, + onDateSelected, + appointments, + currentMonth, + setCurrentMonth, + monthSelectionLabel, + nextMonthLabel, + prevMonthLabel, +}: AppointmentCalendarProps) { + return ( + <div style={{ width: "min-content" }}> + <Row justifyContent="space-around"> + <MonthSelection + currentMonth={currentMonth} + setCurrentMonth={setCurrentMonth} + label={monthSelectionLabel} + nextMonthLabel={nextMonthLabel} + prevMonthLabel={prevMonthLabel} + /> + <MonthGrid + currentMonth={currentMonth} + selectedDay={selectedDay} + onDateSelected={onDateSelected} + appointments={appointments} + /> + </Row> + </div> + ); +} + +export function MonthGrid({ + appointments, + selectedDay, + onDateSelected, + currentMonth, +}: Pick< + AppointmentCalendarProps, + "selectedDay" | "onDateSelected" | "currentMonth" | "appointments" +>) { + const currentInterval = getMonthInterval(currentMonth); + const days = getDaysInAndAroundMonth(currentInterval); + return ( + <DaysGrid role="grid" aria-label={monthLabel(currentMonth)}> + <WeekdayHeaders /> + {days.map((t) => ( + <Day + key={t.toString()} + date={t} + appointments={appointments} + selectedDay={selectedDay} + onDateSelected={onDateSelected} + currentInterval={currentInterval} + /> + ))} + </DaysGrid> + ); +} diff --git a/lib-portal/src/components/formFields/appointmentPicker/AppointmentListForDate.tsx b/lib-portal/src/components/formFields/appointmentPicker/AppointmentListForDate.tsx new file mode 100644 index 000000000..d7ad137e8 --- /dev/null +++ b/lib-portal/src/components/formFields/appointmentPicker/AppointmentListForDate.tsx @@ -0,0 +1,122 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Chip, List, ListItem, Radio, RadioGroup, Typography } from "@mui/joy"; +import { endOfDay, isWithinInterval, startOfDay } from "date-fns"; + +import { useBaseField } from "../BaseField"; + +import { Appointment } from "./AppointmentPickerField"; +import { dateFullForm, timeForm } from "./helpers"; + +export type AppointmentListDescriptionType = + | ((date: string) => string) + | string; + +export interface UseAppointmentListProps<T extends Appointment> { + selectedDay: Date | undefined; + monthAppointments: T[]; + listDescription: AppointmentListDescriptionType; +} +export function useAppointmentList<T extends Appointment>({ + selectedDay, + monthAppointments, + listDescription, +}: UseAppointmentListProps<T>): { appointments: T[]; description: string } { + const currentDayInterval = selectedDay + ? { + start: startOfDay(selectedDay), + end: endOfDay(selectedDay), + } + : undefined; + const dayAppointments = + currentDayInterval != null + ? monthAppointments + .filter((t) => isWithinInterval(t.start, currentDayInterval)) + .sort() + : []; + + const stringListDescription = + typeof listDescription === "function" + ? listDescription(dateFullForm.format(selectedDay)) + : listDescription; + + return { + description: stringListDescription, + appointments: dayAppointments, + }; +} + +export interface AppointmentListProps<T extends Appointment> { + date: Date | undefined; + field: ReturnType<typeof useBaseField<T | null>>; + appointments: T[]; + onAppointmentSelected?: (d: T) => unknown; + description: string; + label: string; +} +export function AppointmentListForDate<T extends Appointment>({ + date, + field, + appointments, + onAppointmentSelected, + description, + 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 ( + <RadioGroup> + <Typography level="title-md" my={2}> + {label} + </Typography> + <List + orientation="horizontal" + wrap + size="sm" + sx={{ marginBottom: "16px", gap: "8px", padding: 0 }} + // eslint-disable-next-line jsx-a11y/aria-props + aria-description={description} + > + {appointments.map((apt) => { + const isSelected = field.input.value === apt; + return ( + <ListItem + sx={{ padding: 0, minHeight: 0 }} + key={apt.start.getTime()} + > + <Chip + variant={isSelected ? "soft" : "plain"} + color={isSelected ? "primary" : "neutral"} + sx={{ minWidth: "56px", textAlign: "center" }} + > + <Radio + component={"time"} + dateTime={apt.start.toTimeString().slice(0, 5)} + disableIcon + overlay + value={apt.start} + checked={isSelected} + onChange={createOnSelected(apt)} + label={timeForm.format(apt.start)} + /> + </Chip> + </ListItem> + ); + })} + </List> + </RadioGroup> + ); +} diff --git a/lib-portal/src/components/formFields/appointmentPicker/AppointmentPickerField.tsx b/lib-portal/src/components/formFields/appointmentPicker/AppointmentPickerField.tsx new file mode 100644 index 000000000..4426081e3 --- /dev/null +++ b/lib-portal/src/components/formFields/appointmentPicker/AppointmentPickerField.tsx @@ -0,0 +1,161 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +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 { useBaseField } from "../BaseField"; + +import { + AppointmentCalendar, + MonthSelectionPassThroughProps, +} from "./AppointmentCalendar"; +import { + AppointmentListDescriptionType, + AppointmentListForDate, + AppointmentListProps, + useAppointmentList, +} from "./AppointmentListForDate"; + +export { FIELD_LABELS_DE } from "./labels"; + +export interface Appointment { + start: Date; +} + +export interface AppointmentPickerLayoutProps { + calendar: ReactNode; + appointmentList: ReactNode; + sx?: SxProps; + className?: string; +} + +export interface AppointmentPickerFieldLabels { + listDescription: AppointmentListDescriptionType; + listLabel: string; + monthSelection: string; + nextMonth: string; + prevMonth: string; + requiredDay: string; + requiredAppointment: string; +} + +export interface AppointmentPickerFieldProps<T extends Appointment> + extends MonthSelectionPassThroughProps { + name: string; + sx?: SxProps; + required?: boolean; + className?: string; + active?: boolean; + monthAppointments: T[]; + onAppointmentSelected?: (d: T) => unknown; + layout?: (props: AppointmentPickerLayoutProps) => ReactNode; + appointmentList?: (props: AppointmentListProps<T>) => ReactNode; + labels: AppointmentPickerFieldLabels; +} +export function AppointmentPickerField<T extends Appointment>({ + sx, + className, + active = true, + currentMonth, + setCurrentMonth, + monthAppointments, + onAppointmentSelected, + required, + appointmentList: AppointmentListOverride, + layout, + labels, + ...props +}: AppointmentPickerFieldProps<T>) { + const { + listDescription, + listLabel, + monthSelection: monthSelectionLabel, + nextMonth: nextMonthLabel, + prevMonth: prevMonthLabel, + requiredDay: requiredDayWarning, + requiredAppointment: requiredAppointmentWarning, + } = labels; + const { initialValues } = useFormikContext<{ [K in string]: T }>(); + const [selectedDay, setSelectedDayRaw] = useState<Date | undefined>( + initialValues[props.name]?.start, + ); + const requiredWarning = + selectedDay == null ? requiredDayWarning : requiredAppointmentWarning; + const field = useBaseField<T | null>({ + ...props, + required: active && required ? requiredWarning : undefined, + }); + + const listProps = useAppointmentList({ + selectedDay, + monthAppointments, + listDescription, + }); + + function setSelectedDay(d: Date) { + setSelectedDayRaw(d); + if (!selectedDay || !isSameDay(d, selectedDay)) { + void field.helpers.setValue(null); + } + } + + const dateAppointments = monthAppointments.map((t) => t.start); + + const Layout = layout ?? DefaultLayout; + const AppointmentList = AppointmentListOverride ?? AppointmentListForDate; + + return ( + <Layout + className={className} + sx={sx} + calendar={ + <AppointmentCalendar + selectedDay={active ? selectedDay : undefined} + onDateSelected={setSelectedDay} + currentMonth={currentMonth} + setCurrentMonth={setCurrentMonth} + appointments={dateAppointments} + monthSelectionLabel={monthSelectionLabel} + nextMonthLabel={nextMonthLabel} + prevMonthLabel={prevMonthLabel} + /> + } + appointmentList={ + <FormControl error={field.error} required={field.required}> + <AppointmentList + {...listProps} + field={field} + date={active ? selectedDay : undefined} + onAppointmentSelected={onAppointmentSelected} + label={listLabel} + /> + {field.helperText != null && ( + <FormHelperText component="p" sx={{ my: 1 }}> + {field.helperText} + </FormHelperText> + )} + </FormControl> + } + /> + ); +} + +function DefaultLayout({ + sx, + className, + calendar, + appointmentList, +}: AppointmentPickerLayoutProps) { + return ( + <Stack sx={sx} className={className}> + {calendar} + {appointmentList} + </Stack> + ); +} diff --git a/lib-portal/src/components/formFields/appointmentPicker/Day.tsx b/lib-portal/src/components/formFields/appointmentPicker/Day.tsx new file mode 100644 index 000000000..6873b2dc5 --- /dev/null +++ b/lib-portal/src/components/formFields/appointmentPicker/Day.tsx @@ -0,0 +1,99 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Button, Stack, styled, useTheme } from "@mui/joy"; +import { + Interval, + endOfDay, + formatISO, + isSameDay, + isSunday, + isWithinInterval, + startOfDay, +} from "date-fns"; + +import { AppointmentCalendarProps } from "./AppointmentCalendar"; +import { MonthSelectionProps } from "./MonthSelection"; +import { dateInMonthForm } from "./helpers"; + +export interface DayProps + extends Omit< + AppointmentCalendarProps, + "monthSelectionLabel" | keyof MonthSelectionProps + > { + date: Date; + currentInterval: Interval; +} + +export const DaysGrid = styled("div")` + display: grid; + column-gap: 16px; + row-gap: 8px; + grid-template-columns: repeat(7, 36px); + grid-template-rows: repeat(6, 36px); + text-align: center; +`; + +export function Day({ + date, + currentInterval, + selectedDay: selectedDate, + onDateSelected, + appointments: monthAppointments, +}: DayProps) { + const theme = useTheme(); + const boldProp = isSunday(date) + ? { fontWeight: "bold" } + : { fontWeight: "normal" }; + const grayOut = { + color: !isWithinInterval(date, currentInterval) + ? theme.palette.text.secondary + : theme.palette.text.primary, + }; + const isSelected = selectedDate != null && isSameDay(selectedDate, date); + const selectedStyles = isSelected + ? { borderRadius: "100%", color: theme.palette.common.white } + : {}; + + const dayInterval = { start: startOfDay(date), end: endOfDay(date) }; + const hasAppointments = monthAppointments.some((t) => + isWithinInterval(t, dayInterval), + ); + + return ( + <Button + aria-selected={isSelected || undefined} + aria-label={dateInMonthForm.format(date)} + disabled={!hasAppointments} + color={isSelected ? "primary" : "neutral"} + variant={isSelected ? "solid" : "plain"} + sx={{ + ...grayOut, + ...boldProp, + ...selectedStyles, + display: "flex", + justifyContent: "center", + alignItems: "center", + }} + onClick={() => onDateSelected(date)} + {...boldProp} + > + <Stack + component={"time"} + dateTime={formatISO(date, { representation: "date" })} + > + {date.getDate()} + {hasAppointments && !isSelected && <AppointmentMarker aria-hidden />} + </Stack> + </Button> + ); +} + +const AppointmentMarker = styled("div")` + background-color: ${({ theme }) => theme.palette.primary[500]}; + height: ${({ theme }) => theme.spacing(0.5)}; + width: ${({ theme }) => theme.spacing(3)}; + border-radius: ${({ theme }) => theme.radius.md}; +`; diff --git a/lib-portal/src/components/formFields/appointmentPicker/MonthSelection.tsx b/lib-portal/src/components/formFields/appointmentPicker/MonthSelection.tsx new file mode 100644 index 000000000..22bd97c12 --- /dev/null +++ b/lib-portal/src/components/formFields/appointmentPicker/MonthSelection.tsx @@ -0,0 +1,59 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ChevronLeft, ChevronRight } from "@mui/icons-material"; +import { IconButton, Typography } from "@mui/joy"; +import { addMonths } from "date-fns"; +import { useId } from "react"; + +import { Row } from "../../Row"; + +import { monthLabel } from "./helpers"; + +export interface MonthSelectionProps { + currentMonth: Date; + setCurrentMonth: (d: Date) => void; + label: string; + nextMonthLabel: string; + prevMonthLabel: string; +} +export function MonthSelection({ + currentMonth, + setCurrentMonth, + label, + nextMonthLabel, + prevMonthLabel, +}: MonthSelectionProps) { + const monthYearId = useId(); + return ( + <Row justifyContent="space-between" width="100%" alignItems="center"> + <Typography level="title-md" id={monthYearId} aria-label={label}> + {monthLabel(currentMonth)} + </Typography> + <Row gap={2}> + <IconButton + size="sm" + color="primary" + variant="outlined" + title={prevMonthLabel} + aria-controls={monthYearId} + onClick={() => setCurrentMonth(addMonths(currentMonth, -1))} + > + <ChevronLeft /> + </IconButton> + <IconButton + size="sm" + color="primary" + variant="outlined" + title={nextMonthLabel} + aria-controls={monthYearId} + onClick={() => setCurrentMonth(addMonths(currentMonth, 1))} + > + <ChevronRight /> + </IconButton> + </Row> + </Row> + ); +} diff --git a/lib-portal/src/components/formFields/appointmentPicker/WeekdayHeaders.tsx b/lib-portal/src/components/formFields/appointmentPicker/WeekdayHeaders.tsx new file mode 100644 index 000000000..33c35c98a --- /dev/null +++ b/lib-portal/src/components/formFields/appointmentPicker/WeekdayHeaders.tsx @@ -0,0 +1,36 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Box } from "@mui/joy"; +import { PropsWithChildren } from "react"; + +import { getWeekdayShortCodes } from "./helpers"; + +export function WeekdayHeaders() { + const weekdayShortCodes = getWeekdayShortCodes(); + return ( + <> + {weekdayShortCodes.map((w) => ( + <WeekdayHeader key={w}>{w}</WeekdayHeader> + ))} + </> + ); +} + +export function WeekdayHeader({ children }: PropsWithChildren) { + return ( + <Box + role="columnheader" + aria-label="" + fontWeight="bold" + justifyContent="center" + alignItems="center" + display="flex" + aria-hidden + > + {children} + </Box> + ); +} diff --git a/lib-portal/src/components/formFields/appointmentPicker/helpers.ts b/lib-portal/src/components/formFields/appointmentPicker/helpers.ts new file mode 100644 index 000000000..84a389410 --- /dev/null +++ b/lib-portal/src/components/formFields/appointmentPicker/helpers.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import { addDays, eachDayOfInterval, endOfMonth, startOfMonth } from "date-fns"; + +export const dateInMonthForm = Intl.DateTimeFormat(undefined, { + day: "numeric", + weekday: "long", +}); + +export function getMonthInterval(date: Date) { + const start = startOfMonth(date); + const end = endOfMonth(date); + return { start, end }; +} + +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); + } + let days = eachDayOfInterval({ start, end: interval.end }); + const requiredPadding = Math.ceil(days.length / 7) * 7 - days.length; + if (requiredPadding > 0) { + const last = days[days.length - 1]; + const paddingDays = new Array(requiredPadding) + .fill(last) + .map((day: Date, index) => addDays(day, index + 1)); + days = [...days, ...paddingDays]; + } + return days; +} + +export const monthNameForm = Intl.DateTimeFormat(undefined, { month: "long" }); + +export function monthLabel(currentMonth: Date) { + return `${monthNameForm.format(currentMonth)} ${currentMonth.getFullYear()}`; +} + +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 const timeForm = Intl.DateTimeFormat(undefined, { timeStyle: "short" }); +export const dateFullForm = Intl.DateTimeFormat(undefined, { + month: "long", + day: "numeric", + weekday: "long", + year: "numeric", +}); diff --git a/lib-portal/src/components/formFields/appointmentPicker/labels.ts b/lib-portal/src/components/formFields/appointmentPicker/labels.ts new file mode 100644 index 000000000..c84ed6680 --- /dev/null +++ b/lib-portal/src/components/formFields/appointmentPicker/labels.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2024 cronn GmbH + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AppointmentPickerFieldLabels } from "./AppointmentPickerField"; + +export const FIELD_LABELS_DE = { + requiredAppointment: "Bitte ein Termin auswählen", + requiredDay: "Bitte ein Tag auswählen", + monthSelection: "Termin Kalendermonat", + nextMonth: "zum nächsten Monat", + prevMonth: "zum vorherigen Monat", + listLabel: "Uhrzeit", + listDescription: (date: string) => `Liste verfügbarer Termine für ${date}`, +} as const satisfies AppointmentPickerFieldLabels; diff --git a/lib-portal/src/errorHandling/AlertContext.tsx b/lib-portal/src/errorHandling/AlertContext.tsx index bd496612a..2488e02d9 100644 --- a/lib-portal/src/errorHandling/AlertContext.tsx +++ b/lib-portal/src/errorHandling/AlertContext.tsx @@ -5,24 +5,65 @@ "use client"; -import { createContext, useContext, useMemo, useState } from "react"; +import { ReactNode, createContext, useContext, useMemo, useState } from "react"; +import { doNothing, isDefined } from "remeda"; import { Alert, AlertProps } from "../components/Alert"; +import { useUuid } from "../hooks/useUuid"; import { RequiresChildren } from "../types/react"; interface AlertContextValue { - alert: AlertValue; - setAlert: (alert: AlertValue) => void; + state: AlertState | null; + open: (alertId: string, props: AlertProps, options?: AlertOptions) => void; + close: (alertid?: string) => void; } -export type AlertValue = AlertProps | null; +interface AlertState { + alertId: string; + props: AlertProps; + options: AlertOptions; +} + +interface AlertOptions { + closeable?: boolean; +} const AlertContext = createContext<AlertContextValue | null>(null); export function AlertContextProvider(props: RequiresChildren) { - const [alert, setAlert] = useState<AlertValue>(null); + const [state, setState] = useState<AlertState | null>(null); + + const contextValue = useMemo(() => { + function open( + alertId: string, + props: AlertProps, + options: AlertOptions = {}, + ): void { + setState({ + alertId, + props, + options, + }); + } - const contextValue = useMemo(() => ({ alert, setAlert }), [alert, setAlert]); + function close(alertId?: string): void { + if (state === null) { + return; + } + + if (isDefined(alertId) && alertId !== state.alertId) { + return; + } + + setState(null); + } + + return { + state, + open, + close, + }; + }, [state, setState]); return ( <AlertContext.Provider value={contextValue}> @@ -31,18 +72,100 @@ export function AlertContextProvider(props: RequiresChildren) { ); } -export function useAlertContext() { - return useContext(AlertContext); +function useAlertContext() { + const alertContext = useContext(AlertContext); + + if (alertContext === null) { + throw new Error("AlertContext is not initialized"); + } + + return alertContext; +} + +interface UseAlertResult { + isOpen: boolean; + notification: (options: AlertOpenOptions) => void; + warning: (options: AlertOpenOptions) => void; + error: (options: AlertOpenOptions) => void; + close: () => void; } -export function useAlert() { +interface AlertOpenOptions + extends Pick<AlertProps, "title" | "message" | "action">, + AlertOptions {} + +export function useAlert(): UseAlertResult { + const alertContext = useAlertContext(); + const alertId = useUuid(); + + function openWithColor( + color: AlertProps["color"], + options: AlertOpenOptions, + ): void { + const { closeable = false, ...alertProps } = options; + alertContext.open( + alertId, + { + ...alertProps, + color, + }, + { closeable }, + ); + } + + function close(): void { + alertContext.close(alertId); + } + + return { + isOpen: alertContext.state?.alertId === alertId, + notification: (options) => openWithColor("primary", options), + warning: (options) => openWithColor("warning", options), + error: (options) => openWithColor("danger", options), + close, + }; +} + +export function useResetAlertContext(): () => void { + // TODO: replace by useAlertContext when all usages are within a QueryBoundary const alertContext = useContext(AlertContext); - return alertContext !== null ? alertContext.alert : null; + + if (alertContext === null) { + return doNothing; + } + + return alertContext.close; +} + +interface AlertSlotProps extends Pick<AlertProps, "sx"> { + container?: (props: RequiresChildren) => ReactNode; } -type ScopedAlertProps = Pick<AlertProps, "sx">; +export function AlertSlot(props: AlertSlotProps) { + const { container: Container, ...alertProps } = props; + const alertContext = useContext(AlertContext); + + if (alertContext === null) { + return null; + } + + const alertState = alertContext.state ?? null; + + if (alertState === null) { + return null; + } + + const { closeable } = alertState.options; + + const alert = ( + <Alert + {...alertProps} + {...alertState.props} + onClose={ + closeable ? () => alertContext.close(alertState.alertId) : undefined + } + /> + ); -export function ScopedAlert(props: ScopedAlertProps) { - const alert = useAlert(); - return alert === null ? null : <Alert {...props} {...alert} />; + return isDefined(Container) ? <Container>{alert}</Container> : alert; } diff --git a/lib-portal/src/errorHandling/errorMappers.tsx b/lib-portal/src/errorHandling/errorMappers.tsx index dfb595583..3740b2c2c 100644 --- a/lib-portal/src/errorHandling/errorMappers.tsx +++ b/lib-portal/src/errorHandling/errorMappers.tsx @@ -10,7 +10,7 @@ import { PropsWithChildren, ReactNode } from "react"; import { ActionButtonProps } from "../components/Alert"; -import { useAlertContext } from "./AlertContext"; +import { useResetAlertContext } from "./AlertContext"; import { PortalErrorCode } from "./PortalErrorCode"; interface ErrorDescription { @@ -121,13 +121,11 @@ interface ReloadButtonProps extends ActionButtonProps, PropsWithChildren {} function ReloadButton(props: ReloadButtonProps) { const router = useRouter(); const queryClient = useQueryClient(); - const alertContext = useAlertContext(); + const resetAlertContext = useResetAlertContext(); const { children, ...buttonProps } = props; function handleReload() { - if (alertContext !== null) { - alertContext.setAlert(null); - } + resetAlertContext(); void queryClient.invalidateQueries(); router.refresh(); } @@ -139,6 +137,6 @@ function ReloadButton(props: ReloadButtonProps) { ); } -function LoginButton() { - return <ReloadButton>Neu anmelden</ReloadButton>; +function LoginButton(props: Omit<ReloadButtonProps, "children">) { + return <ReloadButton {...props}>Neu anmelden</ReloadButton>; } diff --git a/employee-portal/src/lib/shared/hooks/useUuid.ts b/lib-portal/src/hooks/useUuid.ts similarity index 100% rename from employee-portal/src/lib/shared/hooks/useUuid.ts rename to lib-portal/src/hooks/useUuid.ts diff --git a/lib-portal/src/next/contentSecurityPolicyHeaderMiddleware.ts b/lib-portal/src/next/contentSecurityPolicyHeaderMiddleware.ts index e8eb63272..bffbaed37 100644 --- a/lib-portal/src/next/contentSecurityPolicyHeaderMiddleware.ts +++ b/lib-portal/src/next/contentSecurityPolicyHeaderMiddleware.ts @@ -48,12 +48,6 @@ export function buildContentSecurityPolicyHeaderValue( "https: http: 'unsafe-inline'", ); - const connectSrc = joinSourceValues( - "'self'", - // TODO: Remove chat whitelist once passed through the reverse proxy - options.developmentMode && "http://localhost:8008", - ); - const styleSrcElem = joinSourceValues( "'self'", options.developmentMode ? "'unsafe-inline'" : `'nonce-${options.nonce}'`, @@ -61,7 +55,7 @@ export function buildContentSecurityPolicyHeaderValue( const cspHeader = [ `default-src 'self'`, - `connect-src ${connectSrc}`, + `connect-src 'self'`, `script-src ${scriptSrc}`, `style-src-elem ${styleSrcElem}`, `style-src-attr 'unsafe-inline'`, diff --git a/package.json b/package.json index 1239028fb..25b2a09ea 100644 --- a/package.json +++ b/package.json @@ -5,25 +5,25 @@ "private": true, "dependencies": { "date-fns": "3.6.0", - "remeda": "2.13.0" + "remeda": "2.15.0" }, "devDependencies": { - "@cyclonedx/cdxgen": "10.9.11", + "@cyclonedx/cdxgen": "10.10.4", "@trivago/prettier-plugin-sort-imports": "4.3.0", "@types/eslint": "8.56.12", - "@types/node": "20.16.5", - "@typescript-eslint/eslint-plugin": "8.6.0", - "@typescript-eslint/parser": "8.6.0", - "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-istanbul": "2.1.1", + "@types/node": "20.16.11", + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", + "@vitejs/plugin-react": "4.3.2", + "@vitest/coverage-istanbul": "2.1.2", "eslint": "8.57.1", "eslint-config-prettier": "9.1.0", "eslint-import-resolver-typescript": "3.6.3", - "eslint-plugin-import": "2.30.0", + "eslint-plugin-import": "2.31.0", "eslint-plugin-unused-imports": "4.1.4", "prettier": "3.3.3", - "typescript": "5.6.2", + "typescript": "5.6.3", "vite-tsconfig-paths": "5.0.1", - "vitest": "2.1.1" + "vitest": "2.1.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97aac60d7..dae532442 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,12 +15,12 @@ importers: specifier: 3.6.0 version: 3.6.0 remeda: - specifier: 2.13.0 - version: 2.13.0 + specifier: 2.15.0 + version: 2.15.0 devDependencies: '@cyclonedx/cdxgen': - specifier: 10.9.11 - version: 10.9.11 + specifier: 10.10.4 + version: 10.10.4 '@trivago/prettier-plugin-sort-imports': specifier: 4.3.0 version: 4.3.0(prettier@3.3.3) @@ -28,20 +28,20 @@ importers: specifier: 8.56.12 version: 8.56.12 '@types/node': - specifier: 20.16.5 - version: 20.16.5 + specifier: 20.16.11 + version: 20.16.11 '@typescript-eslint/eslint-plugin': - specifier: 8.6.0 - version: 8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) + specifier: 8.8.1 + version: 8.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/parser': - specifier: 8.6.0 - version: 8.6.0(eslint@8.57.1)(typescript@5.6.2) + specifier: 8.8.1 + version: 8.8.1(eslint@8.57.1)(typescript@5.6.3) '@vitejs/plugin-react': - specifier: 4.3.1 - version: 4.3.1(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 4.3.2 + version: 4.3.2(vite@5.3.1(@types/node@20.16.11)(terser@5.36.0)) '@vitest/coverage-istanbul': - specifier: 2.1.1 - version: 2.1.1(vitest@2.1.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 2.1.2 + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)(terser@5.36.0)) eslint: specifier: 8.57.1 version: 8.57.1 @@ -50,25 +50,25 @@ importers: version: 9.1.0(eslint@8.57.1) eslint-import-resolver-typescript: specifier: 3.6.3 - version: 3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1) + version: 3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: - specifier: 2.30.0 - version: 2.30.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + specifier: 2.31.0 + version: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-unused-imports: specifier: 4.1.4 - version: 4.1.4(@typescript-eslint/eslint-plugin@8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1) + version: 4.1.4(@typescript-eslint/eslint-plugin@8.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1) prettier: specifier: 3.3.3 version: 3.3.3 typescript: - specifier: 5.6.2 - version: 5.6.2 + specifier: 5.6.3 + version: 5.6.3 vite-tsconfig-paths: specifier: 5.0.1 - version: 5.0.1(typescript@5.6.2)(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + version: 5.0.1(typescript@5.6.3)(vite@5.3.1(@types/node@20.16.11)(terser@5.36.0)) vitest: - specifier: 2.1.1 - version: 2.1.1(@types/node@20.16.5)(terser@5.31.1) + specifier: 2.1.2 + version: 2.1.2(@types/node@20.16.11)(terser@5.36.0) admin-portal: dependencies: @@ -77,10 +77,10 @@ importers: version: 11.13.1 '@emotion/react': specifier: 11.13.3 - version: 11.13.3(@types/react@18.3.7)(react@18.3.1) + version: 11.13.3(@types/react@18.3.11)(react@18.3.1) '@emotion/styled': specifier: 11.13.0 - version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) + version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) '@eshg/admin-portal-api': specifier: workspace:* version: link:../admin-portal-api @@ -89,16 +89,16 @@ importers: version: link:../lib-portal '@mui/icons-material': specifier: 5.16.7 - version: 5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) + version: 5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) '@mui/joy': specifier: 5.0.0-beta.48 - version: 5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material': specifier: npm:@mui/joy@5.0.0-beta.48 - version: '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' + version: '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' '@tanstack/react-query': - specifier: 5.56.2 - version: 5.56.2(react@18.3.1) + specifier: 5.59.10 + version: 5.59.10(react@18.3.1) '@tanstack/react-table': specifier: 8.20.5 version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -106,14 +106,14 @@ importers: specifier: 3.0.5 version: 3.0.5 i18next: - specifier: 23.15.1 - version: 23.15.1 + specifier: 23.15.2 + version: 23.15.2 i18next-resources-to-backend: specifier: 1.2.1 version: 1.2.1 next: - specifier: 14.2.12 - version: 14.2.12(@babel/core@7.24.7)(@playwright/test@1.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.2.14 + version: 14.2.14(@babel/core@7.24.7)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pkijs: specifier: 3.2.4 version: 3.2.4 @@ -128,41 +128,41 @@ importers: version: 18.3.1(react@18.3.1) react-i18next: specifier: 15.0.2 - version: 15.0.2(i18next@23.15.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.0.2(i18next@23.15.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) valibot: - specifier: 0.42.0 - version: 0.42.0(typescript@5.6.2) + specifier: 0.42.1 + version: 0.42.1(typescript@5.6.3) zod: specifier: 3.23.8 version: 3.23.8 devDependencies: '@next/bundle-analyzer': - specifier: 14.2.12 - version: 14.2.12 + specifier: 14.2.14 + version: 14.2.14 '@tanstack/eslint-plugin-query': - specifier: 5.56.1 - version: 5.56.1(eslint@8.57.1)(typescript@5.6.2) + specifier: 5.59.7 + version: 5.59.7(eslint@8.57.1)(typescript@5.6.3) '@types/react': - specifier: 18.3.7 - version: 18.3.7 + specifier: 18.3.11 + version: 18.3.11 '@types/react-dom': - specifier: 18.3.0 - version: 18.3.0 + specifier: 18.3.1 + version: 18.3.1 '@vitejs/plugin-react': - specifier: 4.3.1 - version: 4.3.1(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 4.3.2 + version: 4.3.2(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)) '@vitest/coverage-istanbul': - specifier: 2.1.1 - version: 2.1.1(vitest@2.1.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 2.1.2 + version: 2.1.2(vitest@2.1.2(@types/node@22.7.6)(terser@5.36.0)) eslint-config-next: - specifier: 14.2.12 - version: 14.2.12(eslint@8.57.1)(typescript@5.6.2) + specifier: 14.2.14 + version: 14.2.14(eslint@8.57.1)(typescript@5.6.3) vite-tsconfig-paths: specifier: 5.0.1 - version: 5.0.1(typescript@5.6.2)(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + version: 5.0.1(typescript@5.6.3)(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)) vitest: - specifier: 2.1.1 - version: 2.1.1(@types/node@20.16.5)(terser@5.31.1) + specifier: 2.1.2 + version: 2.1.2(@types/node@22.7.6)(terser@5.36.0) admin-portal-api: {} @@ -173,28 +173,31 @@ importers: version: 11.13.1 '@emotion/react': specifier: 11.13.3 - version: 11.13.3(@types/react@18.3.7)(react@18.3.1) + version: 11.13.3(@types/react@18.3.11)(react@18.3.1) '@emotion/styled': specifier: 11.13.0 - version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) + version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) '@eshg/citizen-portal-api': specifier: workspace:* version: link:../citizen-portal-api '@eshg/lib-portal': specifier: workspace:* version: link:../lib-portal + '@mdx-js/mdx': + specifier: 3.0.1 + version: 3.0.1 '@mui/icons-material': specifier: 5.16.7 - version: 5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) + version: 5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) '@mui/joy': specifier: 5.0.0-beta.48 - version: 5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material': specifier: npm:@mui/joy@5.0.0-beta.48 - version: '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' + version: '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' '@tanstack/react-query': - specifier: 5.56.2 - version: 5.56.2(react@18.3.1) + specifier: 5.59.10 + version: 5.59.10(react@18.3.1) '@tanstack/react-table': specifier: 8.20.5 version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -202,8 +205,8 @@ importers: specifier: 0.6.3 version: 0.6.3 i18next: - specifier: 23.15.1 - version: 23.15.1 + specifier: 23.15.2 + version: 23.15.2 i18next-resources-to-backend: specifier: 1.2.1 version: 1.2.1 @@ -211,57 +214,63 @@ importers: specifier: 0.6.3 version: 0.6.3 next: - specifier: 14.2.12 - version: 14.2.12(@babel/core@7.24.7)(@playwright/test@1.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.2.14 + version: 14.2.14(@babel/core@7.24.7)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) + server-only: + specifier: 0.0.1 + version: 0.0.1 valibot: - specifier: 0.42.0 - version: 0.42.0(typescript@5.6.2) + specifier: 0.42.1 + version: 0.42.1(typescript@5.6.3) devDependencies: '@next/bundle-analyzer': - specifier: 14.2.12 - version: 14.2.12 + specifier: 14.2.14 + version: 14.2.14 '@tanstack/eslint-plugin-query': - specifier: 5.56.1 - version: 5.56.1(eslint@8.57.1)(typescript@5.6.2) + specifier: 5.59.7 + version: 5.59.7(eslint@8.57.1)(typescript@5.6.3) + '@types/mdx': + specifier: 2.0.13 + version: 2.0.13 '@types/react': - specifier: 18.3.7 - version: 18.3.7 + specifier: 18.3.11 + version: 18.3.11 '@types/react-dom': - specifier: 18.3.0 - version: 18.3.0 + specifier: 18.3.1 + version: 18.3.1 '@vitejs/plugin-react': - specifier: 4.3.1 - version: 4.3.1(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 4.3.2 + version: 4.3.2(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)) '@vitest/coverage-istanbul': - specifier: 2.1.1 - version: 2.1.1(vitest@2.1.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 2.1.2 + version: 2.1.2(vitest@2.1.2(@types/node@22.7.6)(terser@5.36.0)) eslint-config-next: - specifier: 14.2.12 - version: 14.2.12(eslint@8.57.1)(typescript@5.6.2) + specifier: 14.2.14 + version: 14.2.14(eslint@8.57.1)(typescript@5.6.3) vite-tsconfig-paths: specifier: 5.0.1 - version: 5.0.1(typescript@5.6.2)(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + version: 5.0.1(typescript@5.6.3)(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)) vitest: - specifier: 2.1.1 - version: 2.1.1(@types/node@20.16.5)(terser@5.31.1) + specifier: 2.1.2 + version: 2.1.2(@types/node@22.7.6)(terser@5.36.0) citizen-portal-api: {} e2e: dependencies: otpauth: - specifier: 9.3.2 - version: 9.3.2 + specifier: 9.3.4 + version: 9.3.4 devDependencies: '@axe-core/playwright': specifier: 4.10.0 - version: 4.10.0(playwright-core@1.48.0) + version: 4.10.0(playwright-core@1.48.1) '@eshg/admin-portal-api': specifier: workspace:* version: link:../admin-portal-api @@ -275,11 +284,11 @@ importers: specifier: workspace:* version: link:../employee-portal-api '@keycloak/keycloak-admin-client': - specifier: 25.0.6 - version: 25.0.6 + specifier: 26.0.0 + version: 26.0.0 '@playwright/test': - specifier: 1.48.0 - version: 1.48.0 + specifier: 1.48.1 + version: 1.48.1 axe-html-reporter: specifier: 2.2.11 version: 2.2.11(axe-core@4.10.0) @@ -289,17 +298,17 @@ importers: employee-portal: dependencies: '@ducanh2912/next-pwa': - specifier: 10.2.8 - version: 10.2.8(@types/babel__core@7.20.5)(next@14.2.12(@babel/core@7.24.7)(@playwright/test@1.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.92.1) + specifier: 10.2.9 + version: 10.2.9(@types/babel__core@7.20.5)(next@14.2.14(@babel/core@7.24.7)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.92.1) '@emotion/cache': specifier: 11.13.1 version: 11.13.1 '@emotion/react': specifier: 11.13.3 - version: 11.13.3(@types/react@18.3.7)(react@18.3.1) + version: 11.13.3(@types/react@18.3.11)(react@18.3.1) '@emotion/styled': specifier: 11.13.0 - version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) + version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) '@eshg/employee-portal-api': specifier: workspace:* version: link:../employee-portal-api @@ -329,19 +338,22 @@ importers: version: 6.1.15(@fullcalendar/core@6.1.15) '@hello-pangea/dnd': specifier: 17.0.0 - version: 17.0.0(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 17.0.0(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mdx-js/mdx': + specifier: 3.0.1 + version: 3.0.1 '@mui/icons-material': specifier: 5.16.7 - version: 5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) + version: 5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) '@mui/joy': specifier: 5.0.0-beta.48 - version: 5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material': specifier: npm:@mui/joy@5.0.0-beta.48 - version: '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' + version: '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' '@tanstack/react-query': - specifier: 5.56.2 - version: 5.56.2(react@18.3.1) + specifier: 5.59.10 + version: 5.59.10(react@18.3.1) '@tanstack/react-table': specifier: 8.20.5 version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -364,8 +376,8 @@ importers: specifier: 2.4.6 version: 2.4.6(react@18.3.1) hpke-js: - specifier: 1.3.1 - version: 1.3.1 + specifier: 1.4.3 + version: 1.4.3 iso8601-duration: specifier: 2.1.2 version: 2.1.2 @@ -373,8 +385,8 @@ importers: specifier: 34.3.1 version: 34.3.1 next: - specifier: 14.2.12 - version: 14.2.12(@babel/core@7.24.7)(@playwright/test@1.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.2.14 + version: 14.2.14(@babel/core@7.24.7)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -387,6 +399,9 @@ importers: react-transition-group: specifier: 4.4.5 version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + server-only: + specifier: 0.0.1 + version: 0.0.1 use-debounce: specifier: 10.0.3 version: 10.0.3(react@18.3.1) @@ -394,21 +409,24 @@ importers: specifier: 10.0.0 version: 10.0.0 valibot: - specifier: 0.42.0 - version: 0.42.0(typescript@5.6.2) + specifier: 0.42.1 + version: 0.42.1(typescript@5.6.3) devDependencies: '@next/bundle-analyzer': - specifier: 14.2.12 - version: 14.2.12 + specifier: 14.2.14 + version: 14.2.14 '@tanstack/eslint-plugin-query': - specifier: 5.56.1 - version: 5.56.1(eslint@8.57.1)(typescript@5.6.2) + specifier: 5.59.7 + version: 5.59.7(eslint@8.57.1)(typescript@5.6.3) + '@types/mdx': + specifier: 2.0.13 + version: 2.0.13 '@types/react': - specifier: 18.3.7 - version: 18.3.7 + specifier: 18.3.11 + version: 18.3.11 '@types/react-dom': - specifier: 18.3.0 - version: 18.3.0 + specifier: 18.3.1 + version: 18.3.1 '@types/react-transition-group': specifier: 4.4.11 version: 4.4.11 @@ -416,20 +434,20 @@ importers: specifier: 10.0.0 version: 10.0.0 '@vitejs/plugin-react': - specifier: 4.3.1 - version: 4.3.1(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 4.3.2 + version: 4.3.2(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)) '@vitest/coverage-istanbul': - specifier: 2.1.1 - version: 2.1.1(vitest@2.1.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 2.1.2 + version: 2.1.2(vitest@2.1.2(@types/node@22.7.6)(terser@5.36.0)) eslint-config-next: - specifier: 14.2.12 - version: 14.2.12(eslint@8.57.1)(typescript@5.6.2) + specifier: 14.2.14 + version: 14.2.14(eslint@8.57.1)(typescript@5.6.3) vite-tsconfig-paths: specifier: 5.0.1 - version: 5.0.1(typescript@5.6.2)(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + version: 5.0.1(typescript@5.6.3)(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)) vitest: - specifier: 2.1.1 - version: 2.1.1(@types/node@20.16.5)(terser@5.31.1) + specifier: 2.1.2 + version: 2.1.2(@types/node@22.7.6)(terser@5.36.0) employee-portal-api: {} @@ -440,19 +458,19 @@ importers: version: link:../employee-portal-api '@mui/icons-material': specifier: 5.16.7 - version: 5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) + version: 5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) '@mui/joy': specifier: 5.0.0-beta.48 - version: 5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material': specifier: npm:@mui/joy@5.0.0-beta.48 - version: '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' + version: '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' '@tanstack/react-query': - specifier: 5.56.2 - version: 5.56.2(react@18.3.1) + specifier: 5.59.10 + version: 5.59.10(react@18.3.1) next: - specifier: 14.2.12 - version: 14.2.12(@babel/core@7.24.7)(@playwright/test@1.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.2.14 + version: 14.2.14(@babel/core@7.24.7)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -467,44 +485,50 @@ importers: version: 10.0.0 devDependencies: '@tanstack/eslint-plugin-query': - specifier: 5.56.1 - version: 5.56.1(eslint@8.57.1)(typescript@5.6.2) + specifier: 5.59.7 + version: 5.59.7(eslint@8.57.1)(typescript@5.6.3) '@types/react': - specifier: 18.3.7 - version: 18.3.7 + specifier: 18.3.11 + version: 18.3.11 '@types/react-dom': - specifier: 18.3.0 - version: 18.3.0 + specifier: 18.3.1 + version: 18.3.1 '@types/uuid': specifier: 10.0.0 version: 10.0.0 '@vitejs/plugin-react': - specifier: 4.3.1 - version: 4.3.1(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 4.3.2 + version: 4.3.2(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)) '@vitest/coverage-istanbul': - specifier: 2.1.1 - version: 2.1.1(vitest@2.1.1(@types/node@20.16.5)(terser@5.31.1)) + specifier: 2.1.2 + version: 2.1.2(vitest@2.1.2(@types/node@22.7.6)(terser@5.36.0)) eslint-config-next: - specifier: 14.2.12 - version: 14.2.12(eslint@8.57.1)(typescript@5.6.2) + specifier: 14.2.14 + version: 14.2.14(eslint@8.57.1)(typescript@5.6.3) vite-tsconfig-paths: specifier: 5.0.1 - version: 5.0.1(typescript@5.6.2)(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) + version: 5.0.1(typescript@5.6.3)(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)) vitest: - specifier: 2.1.1 - version: 2.1.1(@types/node@20.16.5)(terser@5.31.1) + specifier: 2.1.2 + version: 2.1.2(@types/node@22.7.6)(terser@5.36.0) performance-test: dependencies: + '@eshg/citizen-portal-api': + specifier: workspace:* + version: link:../citizen-portal-api '@eshg/employee-portal-api': specifier: workspace:* version: link:../employee-portal-api '@faker-js/faker': specifier: 9.0.3 version: 9.0.3 + '@grafana/schema': + specifier: 11.2.2 + version: 11.2.2 '@keycloak/keycloak-admin-client': - specifier: 25.0.6 - version: 25.0.6 + specifier: 26.0.0 + version: 26.0.0 '@types/k6': specifier: 0.54.1 version: 0.54.1 @@ -524,8 +548,8 @@ packages: peerDependencies: ajv: '>=8' - '@appthreat/atom@2.0.18': - resolution: {integrity: sha512-jYakvIH6yqV+8pz5QJQNN0Sl/igf1s6+I7X7wJk768kBaFSw6EwViQ/FIjle/1u8eZqUVA1lAM54VRRH3y8gDw==} + '@appthreat/atom@2.0.21': + resolution: {integrity: sha512-kdCW6ASdh7oUN9yz95eDmgzo1M/8/K0EmxPIMHf4JI+FLbLkdGtN3MEqZqyF9GHHMhfdwHtyZcrnRp3v6c27AQ==} engines: {node: '>=16.0.0'} hasBin: true @@ -542,14 +566,26 @@ packages: resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.25.7': + resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.24.7': resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.25.8': + resolution: {integrity: sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==} + engines: {node: '>=6.9.0'} + '@babel/core@7.24.7': resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} engines: {node: '>=6.9.0'} + '@babel/core@7.25.8': + resolution: {integrity: sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.17.7': resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} engines: {node: '>=6.9.0'} @@ -558,14 +594,14 @@ packages: resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.25.5': - resolution: {integrity: sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.25.6': resolution: {integrity: sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.25.7': + resolution: {integrity: sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.24.7': resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} engines: {node: '>=6.9.0'} @@ -578,6 +614,10 @@ packages: resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.25.7': + resolution: {integrity: sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==} + engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.24.7': resolution: {integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==} engines: {node: '>=6.9.0'} @@ -615,12 +655,22 @@ packages: resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.25.7': + resolution: {integrity: sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.24.7': resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.25.7': + resolution: {integrity: sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.24.7': resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} engines: {node: '>=6.9.0'} @@ -645,6 +695,10 @@ packages: resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} engines: {node: '>=6.9.0'} + '@babel/helper-simple-access@7.25.7': + resolution: {integrity: sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-skip-transparent-expression-wrappers@7.24.7': resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} engines: {node: '>=6.9.0'} @@ -661,14 +715,26 @@ packages: resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.7': + resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.24.7': resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.7': + resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.24.7': resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.7': + resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-wrap-function@7.24.7': resolution: {integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==} engines: {node: '>=6.9.0'} @@ -677,22 +743,30 @@ packages: resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.25.7': + resolution: {integrity: sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==} + engines: {node: '>=6.9.0'} + '@babel/highlight@7.24.7': resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} + '@babel/highlight@7.25.7': + resolution: {integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.24.7': resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.25.4': - resolution: {integrity: sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==} + '@babel/parser@7.25.6': + resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.25.6': - resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} + '@babel/parser@7.25.8': + resolution: {integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -1156,6 +1230,10 @@ packages: resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} engines: {node: '>=6.9.0'} + '@babel/template@7.25.7': + resolution: {integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.23.2': resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} engines: {node: '>=6.9.0'} @@ -1164,14 +1242,14 @@ packages: resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.25.4': - resolution: {integrity: sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.25.6': resolution: {integrity: sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.25.7': + resolution: {integrity: sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==} + engines: {node: '>=6.9.0'} + '@babel/types@7.17.0': resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} engines: {node: '>=6.9.0'} @@ -1180,14 +1258,14 @@ packages: resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} - '@babel/types@7.25.4': - resolution: {integrity: sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==} - engines: {node: '>=6.9.0'} - '@babel/types@7.25.6': resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} + '@babel/types@7.25.8': + resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==} + engines: {node: '>=6.9.0'} + '@bufbuild/protobuf@1.7.2': resolution: {integrity: sha512-i5GE2Dk5ekdlK1TR7SugY4LWRrKSfb5T1Qn4unpIMbfxoeGKERKQ59HG3iYewacGD10SR7UzevfPnh6my4tNmQ==} @@ -1225,8 +1303,8 @@ packages: resolution: {integrity: sha512-AQWpffIGygeQIr1f7QC1rXcp3O7DgYGQFZC9Jlwx0Bv/e8VKaw8nYoTNMukUms87bw8FC0jo3CWaje7iTX5svQ==} cpu: [x64] - '@cyclonedx/cdxgen@10.9.11': - resolution: {integrity: sha512-56U7I/QooNGElhya4wEkc8EmkRTcN8er70UnV8CR9UV3FsnYdmCb+aRANxIWTeTpZXNFpunMQqAG9QmUsLyemA==} + '@cyclonedx/cdxgen@10.10.4': + resolution: {integrity: sha512-H8QG91GPfuT5N3jpU6UZDwSHGlIABS/iiKP0X4soe9yQle2K0O1mcGxotyKBbyw3HdXRF22DGXE2L/wAaho9aw==} engines: {node: '>=20'} hasBin: true @@ -1237,8 +1315,8 @@ packages: '@drauu/core@0.4.1': resolution: {integrity: sha512-louDq3aq9ovgMUuwA1c1ZcLa0pJXdoDkZLFYReE13xKgG7czEZh20zJ6+dHUh1ng/3UPbGXlbAU7KGjkTNec6Q==} - '@ducanh2912/next-pwa@10.2.8': - resolution: {integrity: sha512-4USVoyJcueYXgAjzjyzblAawcdxgZgknmhhOfA8XF+0q2xMecEhKHegqWSMfX37kahyrtA5RVmgQopR9kHs4nA==} + '@ducanh2912/next-pwa@10.2.9': + resolution: {integrity: sha512-Wtu823+0Ga1owqSu1I4HqKgeRYarduCCKwsh1EJmJiJqgbt+gvVf5cFwFH8NigxYyyEvriAro4hzm0pMSrXdRQ==} peerDependencies: next: '>=14.0.0' webpack: '>=5.9.0' @@ -1652,26 +1730,33 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@grafana/schema@11.2.2': + resolution: {integrity: sha512-x5+FY3tx1DlxwQ9OsS5cgr4GFh7/FjV+sub1dQJQvV/ujkRmlO5Sp/cEVMZd83BZ6O+5bJfSOeE1WKw+Rha/OQ==} + '@hello-pangea/dnd@17.0.0': resolution: {integrity: sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q==} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - '@hpke/chacha20poly1305@1.3.1': - resolution: {integrity: sha512-kX8D5y04o+SxGJSJMA4r3+YtxOSHir7QxRFuANCVB4hr7Dm7oVwce9iVUQJtOPRSc4mddFlRFq4SQyy2mI4KQA==} + '@hpke/chacha20poly1305@1.4.3': + resolution: {integrity: sha512-0of93jR0wlq2lG4RvXyrzL2qXKvi/ITWSwEp1sSVfxys8wlGU3aKrEwj0NeCXoHHKb5ei/n9VzsdlpC0xZsvFg==} + engines: {node: '>=16.0.0'} + + '@hpke/common@1.4.3': + resolution: {integrity: sha512-jLM6eqINUOqFYHA36qxo/BoLL1k1TW3xqAl996GyMXK1E7cGL1jTp0igBcBrEmHPC+Pl2RbpyOZlYyWC91mt2g==} engines: {node: '>=16.0.0'} - '@hpke/core@1.3.1': - resolution: {integrity: sha512-ilWdCYbaTyvQGrMMi5LKcy320/sleGdSR2eKo8B1QOEEWi5EZT+k0B/K5o51AcPLJWaLiY1WFJhSIynTKGI35g==} + '@hpke/core@1.4.3': + resolution: {integrity: sha512-10ZwPlNcoM8t9NN4BV26Q437WQv+bKldSVTDL1XM8PvJYz7HbhhJUKrfyMmLIU8jA93t7hYPsOM12eGlDJtklQ==} engines: {node: '>=16.0.0'} - '@hpke/dhkem-x25519@1.3.1': - resolution: {integrity: sha512-BR0s6GYTbCufMRnG7byuRGvtby61kc3od1AaI+LJDkgmmLdsEcr7mk8cfbMncse1PlHVWLoFcMX67ATyGoDwcw==} + '@hpke/dhkem-x25519@1.4.3': + resolution: {integrity: sha512-QFb09/hR/iJKurTvOY1PJ/TGr0CgAf2AO6HeyhP93afa5U3dg4xR038FhMvCw1XTe/hTvww9QoMJ+bAgMOwu7A==} engines: {node: '>=16.0.0'} - '@hpke/dhkem-x448@1.3.1': - resolution: {integrity: sha512-xsy/1nfbi+pbWUZtQAx4uXj9IJ5Db8QAfuT8m0JZq3CZbWy/3qn9WV6NUFdg3pqNW7WEK99yirHyaIXBfqTOIA==} + '@hpke/dhkem-x448@1.4.3': + resolution: {integrity: sha512-V5r4AD2O5rNBknDW2G2UlV8+TIX3HEEYRfPbtRMvuP4iAV2o9XcTGFJf31MAYbfMUBFwaQ+abvcCmbOI6bmneA==} engines: {node: '>=16.0.0'} '@humanwhocodes/config-array@0.13.0': @@ -1713,17 +1798,14 @@ packages: '@jridgewell/source-map@0.3.6': resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - '@jridgewell/sourcemap-codec@1.4.15': - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@keycloak/keycloak-admin-client@25.0.6': - resolution: {integrity: sha512-rUvo6L0aT9+y/R2wFhDKb+lRD5rwj36XwnIGIbmC2HeQVfgroYB5u/79yc/Mo0inBFNIG8VuiwjRzU878fRE0Q==} + '@keycloak/keycloak-admin-client@26.0.0': + resolution: {integrity: sha512-fKWJ71hQKPG/vyT1YSLxjZBJ0c1CvXIQh3mssciG8uuavApbmxBMF+zZLx3gZ9VZPS24YqU/uzj/Im+AnbXStw==} engines: {node: '>=18'} '@matrix-org/matrix-sdk-crypto-wasm@7.0.0': @@ -1733,6 +1815,9 @@ packages: '@matrix-org/olm@3.2.15': resolution: {integrity: sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==} + '@mdx-js/mdx@3.0.1': + resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==} + '@mui/base@5.0.0-beta.40': resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} engines: {node: '>=12.0.0'} @@ -1832,65 +1917,65 @@ packages: '@types/react': optional: true - '@next/bundle-analyzer@14.2.12': - resolution: {integrity: sha512-X0ipzQcl3LoNErGnQKOI1dC1hu+FzltaogDuZAhkchuFi/1G+WnFJUVK5VBqXmXzRJPhpTxMfI4ZdTJjAl9Tmw==} + '@next/bundle-analyzer@14.2.14': + resolution: {integrity: sha512-n5DZtp3sdKidoBZhY50/BAiqkLBj8YUR2oqR3hiEuV8B8+fZ05x59nBJnb6kPTMpV5ACC7hEXNRrRnfqJ4oSkQ==} - '@next/env@14.2.12': - resolution: {integrity: sha512-3fP29GIetdwVIfIRyLKM7KrvJaqepv+6pVodEbx0P5CaMLYBtx+7eEg8JYO5L9sveJO87z9eCReceZLi0hxO1Q==} + '@next/env@14.2.14': + resolution: {integrity: sha512-/0hWQfiaD5//LvGNgc8PjvyqV50vGK0cADYzaoOOGN8fxzBn3iAiaq3S0tCRnFBldq0LVveLcxCTi41ZoYgAgg==} - '@next/eslint-plugin-next@14.2.12': - resolution: {integrity: sha512-cPrKbXtK8NTThOOFNxRGGTw+5s02Ek8z8ri/hZqeKs6uP8LOTGqFyBy6hpCXt7TvLzzriWiiwRyD4h0XYmPEEg==} + '@next/eslint-plugin-next@14.2.14': + resolution: {integrity: sha512-kV+OsZ56xhj0rnTn6HegyTGkoa16Mxjrpk7pjWumyB2P8JVQb8S9qtkjy/ye0GnTr4JWtWG4x/2qN40lKZ3iVQ==} - '@next/swc-darwin-arm64@14.2.12': - resolution: {integrity: sha512-crHJ9UoinXeFbHYNok6VZqjKnd8rTd7K3Z2zpyzF1ch7vVNKmhjv/V7EHxep3ILoN8JB9AdRn/EtVVyG9AkCXw==} + '@next/swc-darwin-arm64@14.2.14': + resolution: {integrity: sha512-bsxbSAUodM1cjYeA4o6y7sp9wslvwjSkWw57t8DtC8Zig8aG8V6r+Yc05/9mDzLKcybb6EN85k1rJDnMKBd9Gw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.12': - resolution: {integrity: sha512-JbEaGbWq18BuNBO+lCtKfxl563Uw9oy2TodnN2ioX00u7V1uzrsSUcg3Ep9ce+P0Z9es+JmsvL2/rLphz+Frcw==} + '@next/swc-darwin-x64@14.2.14': + resolution: {integrity: sha512-cC9/I+0+SK5L1k9J8CInahduTVWGMXhQoXFeNvF0uNs3Bt1Ub0Azb8JzTU9vNCr0hnaMqiWu/Z0S1hfKc3+dww==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.12': - resolution: {integrity: sha512-qBy7OiXOqZrdp88QEl2H4fWalMGnSCrr1agT/AVDndlyw2YJQA89f3ttR/AkEIP9EkBXXeGl6cC72/EZT5r6rw==} + '@next/swc-linux-arm64-gnu@14.2.14': + resolution: {integrity: sha512-RMLOdA2NU4O7w1PQ3Z9ft3PxD6Htl4uB2TJpocm+4jcllHySPkFaUIFacQ3Jekcg6w+LBaFvjSPthZHiPmiAUg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.12': - resolution: {integrity: sha512-EfD9L7o9biaQxjwP1uWXnk3vYZi64NVcKUN83hpVkKocB7ogJfyH2r7o1pPnMtir6gHZiGCeHKagJ0yrNSLNHw==} + '@next/swc-linux-arm64-musl@14.2.14': + resolution: {integrity: sha512-WgLOA4hT9EIP7jhlkPnvz49iSOMdZgDJVvbpb8WWzJv5wBD07M2wdJXLkDYIpZmCFfo/wPqFsFR4JS4V9KkQ2A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.12': - resolution: {integrity: sha512-iQ+n2pxklJew9IpE47hE/VgjmljlHqtcD5UhZVeHICTPbLyrgPehaKf2wLRNjYH75udroBNCgrSSVSVpAbNoYw==} + '@next/swc-linux-x64-gnu@14.2.14': + resolution: {integrity: sha512-lbn7svjUps1kmCettV/R9oAvEW+eUI0lo0LJNFOXoQM5NGNxloAyFRNByYeZKL3+1bF5YE0h0irIJfzXBq9Y6w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.12': - resolution: {integrity: sha512-rFkUkNwcQ0ODn7cxvcVdpHlcOpYxMeyMfkJuzaT74xjAa5v4fxP4xDk5OoYmPi8QNLDs3UgZPMSBmpBuv9zKWA==} + '@next/swc-linux-x64-musl@14.2.14': + resolution: {integrity: sha512-7TcQCvLQ/hKfQRgjxMN4TZ2BRB0P7HwrGAYL+p+m3u3XcKTraUFerVbV3jkNZNwDeQDa8zdxkKkw2els/S5onQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.12': - resolution: {integrity: sha512-PQFYUvwtHs/u0K85SG4sAdDXYIPXpETf9mcEjWc0R4JmjgMKSDwIU/qfZdavtP6MPNiMjuKGXHCtyhR/M5zo8g==} + '@next/swc-win32-arm64-msvc@14.2.14': + resolution: {integrity: sha512-8i0Ou5XjTLEje0oj0JiI0Xo9L/93ghFtAUYZ24jARSeTMXLUx8yFIdhS55mTExq5Tj4/dC2fJuaT4e3ySvXU1A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.12': - resolution: {integrity: sha512-FAj2hMlcbeCV546eU2tEv41dcJb4NeqFlSXU/xL/0ehXywHnNpaYajOUvn3P8wru5WyQe6cTZ8fvckj/2XN4Vw==} + '@next/swc-win32-ia32-msvc@14.2.14': + resolution: {integrity: sha512-2u2XcSaDEOj+96eXpyjHjtVPLhkAFw2nlaz83EPeuK4obF+HmtDJHqgR1dZB7Gb6V/d55FL26/lYVd0TwMgcOQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.12': - resolution: {integrity: sha512-yu8QvV53sBzoIVRHsxCHqeuS8jYq6Lrmdh0briivuh+Brsp6xjg80MAozUsBTAV9KNmY08KlX0KYTWz1lbPzEg==} + '@next/swc-win32-x64-msvc@14.2.14': + resolution: {integrity: sha512-MZom+OvZ1NZxuRovKt1ApevjiUJTcU2PmdJKL66xUPaJeRywnbGGRWUlaAOwunD6dX+pm83vj979NTC8QXjGWg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1905,6 +1990,10 @@ packages: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} + '@noble/hashes@1.5.0': + resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1991,8 +2080,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.48.0': - resolution: {integrity: sha512-W5lhqPUVPqhtc/ySvZI5Q8X2ztBOUgZ8LbAFy0JQgrXZs2xaILrUcNO3rQjwbLPfGK13+rZsDa1FpG+tqYkT5w==} + '@playwright/test@1.48.1': + resolution: {integrity: sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==} engines: {node: '>=18'} hasBin: true @@ -2181,16 +2270,16 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@tanstack/eslint-plugin-query@5.56.1': - resolution: {integrity: sha512-IUm2Zy5BXOqMbaa7QwNg3cPa5NP5Rm3pIFCFpe7Y3pLC7Ftp8Q0Y8GU2uNpCbMFW79jHJXdQ4Oxnu1eTQr8GXQ==} + '@tanstack/eslint-plugin-query@5.59.7': + resolution: {integrity: sha512-txQGX5yC+4gmbR81EXaum2tOxeDQkRCWnaLmaP/pSrbIVCUkbMbrxxsaoOgN+fBqqqGo9V3LoCVL6ez1tRUF7Q==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@tanstack/query-core@5.56.2': - resolution: {integrity: sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==} + '@tanstack/query-core@5.59.10': + resolution: {integrity: sha512-XxvnKeBWqDTHstyjA1qmSD5VS/FZ2g/qYvPMhFM7IZF0JnMqMxtzbiUkiTFaZ4YZo/Q84LS0hZi0UncKJ3vIhg==} - '@tanstack/react-query@5.56.2': - resolution: {integrity: sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==} + '@tanstack/react-query@5.59.10': + resolution: {integrity: sha512-CwXzqOhB4JZJ6Wa8pp+NmaaNuWhscIJAlVCyyiYhyR0Y8a9GprS96WTcOgmTgK9gad9Y+dLhNZwmPWeBAk83aw==} peerDependencies: react: ^18 || ^19 @@ -2226,6 +2315,9 @@ packages: resolution: {integrity: sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==} engines: {node: ^16.14.0 || >=18.0.0} + '@types/acorn@4.0.6': + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2247,15 +2339,24 @@ packages: '@types/eslint@8.56.12': resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@0.0.39': resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/events@3.0.3': resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hoist-non-react-statics@3.3.5': resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} @@ -2271,14 +2372,23 @@ packages: '@types/k6@0.54.1': resolution: {integrity: sha512-ezTMhuWr3TZIMOqAv51/4rD5T4FE1pSryrh5BCgxsniuqxbi5jQ3YEiOlO9C1+LvJcliC2byyd2Cw6cnUY7CLg==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} '@types/negotiator@0.6.3': resolution: {integrity: sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==} - '@types/node@20.16.5': - resolution: {integrity: sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==} + '@types/node@20.16.11': + resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==} + + '@types/node@22.7.6': + resolution: {integrity: sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -2286,14 +2396,14 @@ packages: '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} - '@types/react-dom@18.3.0': - resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + '@types/react-dom@18.3.1': + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} '@types/react-transition-group@4.4.11': resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} - '@types/react@18.3.7': - resolution: {integrity: sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==} + '@types/react@18.3.11': + resolution: {integrity: sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -2304,6 +2414,12 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.3': resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} @@ -2313,8 +2429,8 @@ packages: '@types/validator@13.12.0': resolution: {integrity: sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==} - '@typescript-eslint/eslint-plugin@8.5.0': - resolution: {integrity: sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==} + '@typescript-eslint/eslint-plugin@8.8.1': + resolution: {integrity: sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -2324,29 +2440,8 @@ packages: typescript: optional: true - '@typescript-eslint/eslint-plugin@8.6.0': - resolution: {integrity: sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@8.5.0': - resolution: {integrity: sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@8.6.0': - resolution: {integrity: sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==} + '@typescript-eslint/parser@8.8.1': + resolution: {integrity: sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2355,25 +2450,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.5.0': - resolution: {integrity: sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.6.0': - resolution: {integrity: sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/type-utils@8.5.0': - resolution: {integrity: sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==} + '@typescript-eslint/scope-manager@8.8.1': + resolution: {integrity: sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/type-utils@8.6.0': - resolution: {integrity: sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==} + '@typescript-eslint/type-utils@8.8.1': + resolution: {integrity: sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -2381,25 +2463,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@8.5.0': - resolution: {integrity: sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.6.0': - resolution: {integrity: sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==} + '@typescript-eslint/types@8.8.1': + resolution: {integrity: sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.5.0': - resolution: {integrity: sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/typescript-estree@8.6.0': - resolution: {integrity: sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==} + '@typescript-eslint/typescript-estree@8.8.1': + resolution: {integrity: sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -2407,47 +2476,37 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.5.0': - resolution: {integrity: sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - - '@typescript-eslint/utils@8.6.0': - resolution: {integrity: sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==} + '@typescript-eslint/utils@8.8.1': + resolution: {integrity: sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@typescript-eslint/visitor-keys@8.5.0': - resolution: {integrity: sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.6.0': - resolution: {integrity: sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==} + '@typescript-eslint/visitor-keys@8.8.1': + resolution: {integrity: sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@vitejs/plugin-react@4.3.1': - resolution: {integrity: sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==} + '@vitejs/plugin-react@4.3.2': + resolution: {integrity: sha512-hieu+o05v4glEBucTcKMK3dlES0OeJlD9YVOAPraVMOInBCwzumaIFiUjr4bHK7NPgnAHgiskUoceKercrN8vg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 - '@vitest/coverage-istanbul@2.1.1': - resolution: {integrity: sha512-ZQM8uLinwmhmLp49fxLxIM46nC7NisCbaiydcQoV1hLvQfFL92Gg3tInRvowZyV78G0IknjN10JzH7oqPlPjZw==} + '@vitest/coverage-istanbul@2.1.2': + resolution: {integrity: sha512-dg7ex3GKrTIenAV0oEp78JucTVFsPMzjl1gYWun22O7SBDxcHFC/REZjWLhMTHRHY8ihm4uBCvmu+CvEu5/Adg==} peerDependencies: - vitest: 2.1.1 + vitest: 2.1.2 - '@vitest/expect@2.1.1': - resolution: {integrity: sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==} + '@vitest/expect@2.1.2': + resolution: {integrity: sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==} - '@vitest/mocker@2.1.1': - resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==} + '@vitest/mocker@2.1.2': + resolution: {integrity: sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA==} peerDependencies: - '@vitest/spy': 2.1.1 + '@vitest/spy': 2.1.2 msw: ^2.3.5 vite: ^5.0.0 peerDependenciesMeta: @@ -2456,20 +2515,20 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.1': - resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==} + '@vitest/pretty-format@2.1.2': + resolution: {integrity: sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==} - '@vitest/runner@2.1.1': - resolution: {integrity: sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==} + '@vitest/runner@2.1.2': + resolution: {integrity: sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==} - '@vitest/snapshot@2.1.1': - resolution: {integrity: sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==} + '@vitest/snapshot@2.1.2': + resolution: {integrity: sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==} - '@vitest/spy@2.1.1': - resolution: {integrity: sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==} + '@vitest/spy@2.1.2': + resolution: {integrity: sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==} - '@vitest/utils@2.1.1': - resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==} + '@vitest/utils@2.1.2': + resolution: {integrity: sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==} '@webassemblyjs/ast@1.12.1': resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -2552,6 +2611,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.13.0: + resolution: {integrity: sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -2584,9 +2648,6 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@8.16.0: - resolution: {integrity: sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==} - ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -2624,8 +2685,8 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} @@ -2677,6 +2738,10 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} @@ -2692,18 +2757,15 @@ packages: resolution: {integrity: sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==} engines: {node: '>=4'} - axe-core@4.7.0: - resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} - engines: {node: '>=4'} - axe-html-reporter@2.2.11: resolution: {integrity: sha512-WlF+xlNVgNVWiM6IdVrsh+N0Cw7qupe5HT9N6Uyi+aN7f6SSi92RDomiP1noW8OWIV85V6x404m5oKMeqRV3tQ==} engines: {node: '>=8.9.0'} peerDependencies: axe-core: '>=3' - axobject-query@3.2.1: - resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -2724,6 +2786,9 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2746,9 +2811,9 @@ packages: blueimp-canvas-to-blob@3.29.0: resolution: {integrity: sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==} - body-parser@1.20.2: - resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.0.1: + resolution: {integrity: sha512-PagxbjvuPH6tv0f/kdVbFGcb79D236SLcDTs6DrQ7GizJ88S1UWP4nMXFEo/I4fdhGRGabvFfFjVGm3M7U8JwA==} + engines: {node: '>= 0.10'} boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -2771,6 +2836,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.24.0: + resolution: {integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bs58@6.0.0: resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} @@ -2835,8 +2905,11 @@ packages: resolution: {integrity: sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - caniuse-lite@1.0.30001636: - resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} + caniuse-lite@1.0.30001668: + resolution: {integrity: sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} chai@5.1.1: resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} @@ -2850,6 +2923,18 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -2891,6 +2976,9 @@ packages: resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2908,6 +2996,9 @@ packages: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3015,6 +3106,14 @@ packages: supports-color: optional: true + debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -3041,6 +3140,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -3049,6 +3151,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -3098,6 +3204,9 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -3164,6 +3273,9 @@ packages: electron-to-chromium@1.4.805: resolution: {integrity: sha512-8W4UJwX/w9T0QSzINJckTKG6CYpAUTqsaWcWIsdud3I1FYJcMgW9QqT1/4CBff/pP/TihWh13OmiyY8neto6vw==} + electron-to-chromium@1.5.36: + resolution: {integrity: sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -3187,6 +3299,10 @@ packages: resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -3213,6 +3329,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-iterator-helpers@1.0.19: resolution: {integrity: sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==} engines: {node: '>= 0.4'} @@ -3252,6 +3371,10 @@ packages: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -3263,8 +3386,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@14.2.12: - resolution: {integrity: sha512-fzUIlF6Ng1cUFFd013wn9H3YhKe3vV/cZBC0Ec9S64q/wGoTq0HlASA7WgiOwDAISSbzkLprInLiIMu6U8bqEw==} + eslint-config-next@14.2.14: + resolution: {integrity: sha512-TXwyjGICAlWC9O0OufS3koTsBKQH8l1xt3SY/aDuvtKHIwjTHplJKWVb1WOEX0OsDaxGbFXmfD2EY1sNfG0Y/w==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 typescript: '>=3.3.1' @@ -3294,8 +3417,8 @@ packages: eslint-plugin-import-x: optional: true - eslint-module-utils@2.11.0: - resolution: {integrity: sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==} + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -3336,21 +3459,21 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-import@2.30.0: - resolution: {integrity: sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==} + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 peerDependenciesMeta: '@typescript-eslint/parser': optional: true - eslint-plugin-jsx-a11y@6.8.0: - resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} + eslint-plugin-jsx-a11y@6.10.0: + resolution: {integrity: sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==} engines: {node: '>=4.0'} peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 eslint-plugin-react-hooks@4.6.2: resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} @@ -3411,6 +3534,21 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + estree-walker@1.0.1: resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} @@ -3435,6 +3573,9 @@ packages: exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3680,6 +3821,15 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-estree@3.1.0: + resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} + + hast-util-to-jsx-runtime@2.3.2: + resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -3687,8 +3837,8 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} - hpke-js@1.3.1: - resolution: {integrity: sha512-5qEeLXw1mROAPmuEhU7/JdEpdC49jXBLb6zj274ciG49HjoQjm8ABTnyZMd2jKl76wWwjzb9puLmm1bZ1PtygA==} + hpke-js@1.4.3: + resolution: {integrity: sha512-di8fhAcPSIcHPJpjtJRTZW8WG53SrnX/RYRM8UEdfVlWeMLddNf+BFFCLEYkyM5b3/JhojfaZ1+yuLj942aZhQ==} engines: {node: '>=16.0.0'} html-escaper@2.0.2: @@ -3733,11 +3883,11 @@ packages: i18next-resources-to-backend@1.2.1: resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} - i18next@23.15.1: - resolution: {integrity: sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==} + i18next@23.15.2: + resolution: {integrity: sha512-zcPSWzCvw6uKnuYHIqs4W7hTuB9e3AFcSdZgvCWoPXIZsBjBd4djN2/2uOHIB+1DFFkQnMBXvhNg7J3WyCuywQ==} - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + iconv-lite@0.5.2: + resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==} engines: {node: '>=0.10.0'} iconv-lite@0.6.3: @@ -3787,6 +3937,12 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -3795,6 +3951,16 @@ packages: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -3828,9 +3994,6 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} @@ -3843,6 +4006,9 @@ packages: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3862,6 +4028,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} @@ -3892,10 +4061,17 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-reference@3.0.2: + resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} + is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -4022,6 +4198,11 @@ packages: engines: {node: '>=4'} hasBin: true + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -4149,6 +4330,9 @@ packages: resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==} engines: {node: '>= 0.6.0'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -4196,6 +4380,10 @@ packages: resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} engines: {node: '>= 10'} + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + matcher@3.0.0: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} @@ -4210,6 +4398,33 @@ packages: matrix-widget-api@1.8.2: resolution: {integrity: sha512-kdmks3CvFNPIYN669Y4rO13KrazDvX8KHC7i6jOzJs8uZ8s54FNkuRVVyiQHeVCSZG5ixUqW9UuCj9lf03qxTQ==} + mdast-util-from-markdown@2.0.1: + resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.1.3: + resolution: {integrity: sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.0: + resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4224,6 +4439,90 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.1: + resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==} + + micromark-extension-mdx-expression@3.0.0: + resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==} + + micromark-extension-mdx-jsx@3.0.1: + resolution: {integrity: sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.0: + resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} + + micromark-factory-label@2.0.0: + resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} + + micromark-factory-mdx-expression@2.0.2: + resolution: {integrity: sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw==} + + micromark-factory-space@2.0.0: + resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} + + micromark-factory-title@2.0.0: + resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} + + micromark-factory-whitespace@2.0.0: + resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} + + micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + + micromark-util-chunked@2.0.0: + resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} + + micromark-util-classify-character@2.0.0: + resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} + + micromark-util-combine-extensions@2.0.0: + resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} + + micromark-util-decode-numeric-character-reference@2.0.1: + resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} + + micromark-util-decode-string@2.0.0: + resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} + + micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + micromark-util-events-to-acorn@2.0.2: + resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==} + + micromark-util-html-tag-name@2.0.0: + resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} + + micromark-util-normalize-identifier@2.0.0: + resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} + + micromark-util-resolve-all@2.0.0: + resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} + + micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + + micromark-util-subtokenize@2.0.1: + resolution: {integrity: sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==} + + micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + micromark@4.0.0: + resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} + micromatch@4.0.7: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} @@ -4355,8 +4654,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@14.2.12: - resolution: {integrity: sha512-cDOtUSIeoOvt1skKNihdExWMTybx3exnvbFbb9ecZDIxlvIbREQzt9A5Km3Zn3PfU+IFjyYGsHS+lN9VInAGKA==} + next@14.2.14: + resolution: {integrity: sha512-Q1coZG17MW0Ly5x76shJ4dkC23woLAhhnDnw+DfTc7EpZSGuWrlsZ3bZaO8t6u1Yu8FVfhkqJE+U8GC7E0GLPQ==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -4394,6 +4693,9 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + node-stream-zip@1.15.0: resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} engines: {node: '>=0.12.0'} @@ -4459,6 +4761,10 @@ packages: object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -4514,8 +4820,8 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - otpauth@9.3.2: - resolution: {integrity: sha512-KixtXWN9RGdS8WHPfDo7qsOYiivCbl+VeLBT+7HBTtJebBO6aXr/bpZXr+TwY2COecdY82VeBghm31mLYQVZlQ==} + otpauth@9.3.4: + resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==} p-cancelable@4.0.1: resolution: {integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==} @@ -4564,6 +4870,9 @@ packages: resolution: {integrity: sha512-01TvEktc68vwbJOtWZluyWeVGWjP+bZwXtPDMQVbBKzbJ/vZBif0L69KH1+cHv1SZ6e0FKLvjyHe8mqsIqYOmw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + parse-entities@4.0.1: + resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -4619,12 +4928,18 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + pg-connection-string@2.6.4: resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==} picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -4633,13 +4948,13 @@ packages: resolution: {integrity: sha512-Et9V5QpvBilPFgagJcaKBqXjKrrgF5JL2mSDELk1vvbOTt4fuBhSSsGn9Tcz0TQTfS5GCpXQ31Whrpqeqp0VRg==} engines: {node: '>=12.0.0'} - playwright-core@1.48.0: - resolution: {integrity: sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==} + playwright-core@1.48.1: + resolution: {integrity: sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==} engines: {node: '>=18'} hasBin: true - playwright@1.48.0: - resolution: {integrity: sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==} + playwright@1.48.1: + resolution: {integrity: sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==} engines: {node: '>=18'} hasBin: true @@ -4720,6 +5035,9 @@ packages: resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} engines: {node: '>=14'} + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -4734,8 +5052,8 @@ packages: resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} engines: {node: '>=6.0.0'} - qs@6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -4751,8 +5069,8 @@ packages: randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} engines: {node: '>= 0.8'} rc@1.2.8: @@ -4861,8 +5179,17 @@ packages: resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} hasBin: true - remeda@2.13.0: - resolution: {integrity: sha512-7HnFywwKBxzk8kOfKXxrGC2/RkIWW6goL76Z7cTXiaacfn51iVfwoTHiLWU3cSMs28BApG5QLVxH2fUqZS7LuQ==} + remark-mdx@3.0.1: + resolution: {integrity: sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.1: + resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + + remeda@2.15.0: + resolution: {integrity: sha512-Q0Xdg6z3pDKMGVCAI9wGZ+Yz0y0HOzaxxY3wc9gdjJyxqH93LywDUJ4PJ/zhweicYgcB4HbbOliR8HsIdse6mA==} require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -4969,11 +5296,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.2: - resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} - engines: {node: '>=10'} - hasBin: true - semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -5023,6 +5345,9 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -5117,6 +5442,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} @@ -5125,6 +5454,9 @@ packages: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -5147,6 +5479,10 @@ packages: resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ssri@11.0.0: + resolution: {integrity: sha512-aZpUoMN/Jj2MqA4vMCeiKGnc/8SuSyHbGSBdgFbZxP8OJGF/lFkIuElzPxsN0q8TQQ+prw3P4EDfB3TBHHgfXw==} + engines: {node: ^16.14.0 || >=18.0.0} + ssri@8.0.1: resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} engines: {node: '>= 8'} @@ -5165,6 +5501,10 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -5177,6 +5517,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + string.prototype.matchall@4.0.11: resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} engines: {node: '>= 0.4'} @@ -5195,6 +5539,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stringify-object@3.3.0: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} @@ -5223,6 +5570,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-object@0.4.4: + resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} + + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -5303,6 +5656,11 @@ packages: engines: {node: '>=10'} hasBin: true + terser@5.36.0: + resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} + engines: {node: '>=10'} + hasBin: true + test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -5363,6 +5721,12 @@ packages: resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -5416,10 +5780,6 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - type-fest@4.25.0: - resolution: {integrity: sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==} - engines: {node: '>=16'} - type-fest@4.26.1: resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==} engines: {node: '>=16'} @@ -5444,8 +5804,8 @@ packages: resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} engines: {node: '>= 0.4'} - typescript@5.6.2: - resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true @@ -5482,6 +5842,9 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-filename@1.1.1: resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} @@ -5500,6 +5863,24 @@ packages: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -5518,6 +5899,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -5560,8 +5947,8 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - valibot@0.42.0: - resolution: {integrity: sha512-igMdmHXxDiQY714ssh9bGisMqJ2yg7sko1KOmv/omnrIacGtP6mGrbvVT1IuV1bDrHyG9ybgpHwG1UElDiDCLg==} + valibot@0.42.1: + resolution: {integrity: sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -5586,8 +5973,14 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite-node@2.1.1: - resolution: {integrity: sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==} + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-node@2.1.2: + resolution: {integrity: sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -5627,15 +6020,15 @@ packages: terser: optional: true - vitest@2.1.1: - resolution: {integrity: sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==} + vitest@2.1.2: + resolution: {integrity: sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.1 - '@vitest/ui': 2.1.1 + '@vitest/browser': 2.1.2 + '@vitest/ui': 2.1.2 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -5659,8 +6052,8 @@ packages: walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} - watchpack@2.4.1: - resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} + watchpack@2.4.2: + resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} webidl-conversions@4.0.2: @@ -5865,6 +6258,9 @@ packages: zrender@5.6.0: resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@ampproject/remapping@2.3.0': @@ -5872,17 +6268,17 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@apideck/better-ajv-errors@0.3.6(ajv@8.16.0)': + '@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)': dependencies: - ajv: 8.16.0 + ajv: 8.17.1 json-schema: 0.4.0 jsonpointer: 5.0.1 leven: 3.1.0 - '@appthreat/atom@2.0.18': + '@appthreat/atom@2.0.21': dependencies: '@babel/parser': 7.25.6 - typescript: 5.6.2 + typescript: 5.6.3 yargs: 17.7.2 optional: true @@ -5891,32 +6287,59 @@ snapshots: '@bufbuild/protobuf': 1.7.2 optional: true - '@axe-core/playwright@4.10.0(playwright-core@1.48.0)': + '@axe-core/playwright@4.10.0(playwright-core@1.48.1)': dependencies: axe-core: 4.10.0 - playwright-core: 1.48.0 + playwright-core: 1.48.1 '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 + picocolors: 1.1.0 + + '@babel/code-frame@7.25.7': + dependencies: + '@babel/highlight': 7.25.7 picocolors: 1.0.1 '@babel/compat-data@7.24.7': {} + '@babel/compat-data@7.25.8': {} + '@babel/core@7.24.7': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.7 + '@babel/generator': 7.25.6 '@babel/helper-compilation-targets': 7.24.7 '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) '@babel/helpers': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 + '@babel/parser': 7.25.6 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 convert-source-map: 2.0.0 - debug: 4.3.5 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.25.8': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.25.7 + '@babel/generator': 7.25.7 + '@babel/helper-compilation-targets': 7.25.7 + '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) + '@babel/helpers': 7.25.7 + '@babel/parser': 7.25.8 + '@babel/template': 7.25.7 + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + convert-source-map: 2.0.0 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5931,33 +6354,33 @@ snapshots: '@babel/generator@7.24.7': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.6 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 - '@babel/generator@7.25.5': + '@babel/generator@7.25.6': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 - '@babel/generator@7.25.6': + '@babel/generator@7.25.7': dependencies: - '@babel/types': 7.25.6 + '@babel/types': 7.25.8 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 + jsesc: 3.0.2 '@babel/helper-annotate-as-pure@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': dependencies: - '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color @@ -5969,6 +6392,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.25.7': + dependencies: + '@babel/compat-data': 7.25.8 + '@babel/helper-validator-option': 7.25.7 + browserslist: 4.24.0 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -6004,21 +6435,21 @@ snapshots: '@babel/helper-environment-visitor@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-function-name@7.24.7': dependencies: '@babel/template': 7.24.7 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-hoist-variables@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-member-expression-to-functions@7.24.7': dependencies: - '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color @@ -6029,6 +6460,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.25.7': + dependencies: + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -6040,9 +6478,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-imports': 7.25.7 + '@babel/helper-simple-access': 7.25.7 + '@babel/helper-validator-identifier': 7.25.7 + '@babel/traverse': 7.25.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-plugin-utils@7.24.7': {} @@ -6066,63 +6514,88 @@ snapshots: '@babel/helper-simple-access@7.24.7': dependencies: - '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-simple-access@7.25.7': + dependencies: + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.24.7': dependencies: - '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color '@babel/helper-split-export-declaration@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 '@babel/helper-string-parser@7.24.7': {} '@babel/helper-string-parser@7.24.8': {} + '@babel/helper-string-parser@7.25.7': {} + '@babel/helper-validator-identifier@7.24.7': {} + '@babel/helper-validator-identifier@7.25.7': {} + '@babel/helper-validator-option@7.24.7': {} + '@babel/helper-validator-option@7.25.7': {} + '@babel/helper-wrap-function@7.24.7': dependencies: '@babel/helper-function-name': 7.24.7 '@babel/template': 7.25.0 - '@babel/traverse': 7.25.4 - '@babel/types': 7.25.4 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color '@babel/helpers@7.24.7': dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.7 + '@babel/template': 7.25.0 + '@babel/types': 7.25.6 + + '@babel/helpers@7.25.7': + dependencies: + '@babel/template': 7.25.7 + '@babel/types': 7.25.8 '@babel/highlight@7.24.7': dependencies: '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.1 + picocolors: 1.1.0 - '@babel/parser@7.24.7': + '@babel/highlight@7.25.7': dependencies: - '@babel/types': 7.24.7 + '@babel/helper-validator-identifier': 7.25.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.1.0 - '@babel/parser@7.25.4': + '@babel/parser@7.24.7': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.24.7 '@babel/parser@7.25.6': dependencies: '@babel/types': 7.25.6 + '@babel/parser@7.25.8': + dependencies: + '@babel/types': 7.25.8 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -6505,14 +6978,14 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-react-jsx-self@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-transform-react-jsx-self@7.24.7(@babel/core@7.25.8)': dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.25.8 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-react-jsx-source@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-transform-react-jsx-source@7.24.7(@babel/core@7.25.8)': dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.25.8 '@babel/helper-plugin-utils': 7.24.7 '@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.24.7)': @@ -6668,7 +7141,7 @@ snapshots: dependencies: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 esutils: 2.0.3 '@babel/regjsgen@0.8.0': {} @@ -6688,8 +7161,8 @@ snapshots: '@babel/template@7.24.7': dependencies: '@babel/code-frame': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@babel/template@7.25.0': dependencies: @@ -6697,6 +7170,12 @@ snapshots: '@babel/parser': 7.25.6 '@babel/types': 7.25.6 + '@babel/template@7.25.7': + dependencies: + '@babel/code-frame': 7.25.7 + '@babel/parser': 7.25.8 + '@babel/types': 7.25.8 + '@babel/traverse@7.23.2': dependencies: '@babel/code-frame': 7.24.7 @@ -6707,7 +7186,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.24.7 '@babel/types': 7.24.7 - debug: 4.3.5 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6715,38 +7194,38 @@ snapshots: '@babel/traverse@7.24.7': dependencies: '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.7 + '@babel/generator': 7.25.6 '@babel/helper-environment-visitor': 7.24.7 '@babel/helper-function-name': 7.24.7 '@babel/helper-hoist-variables': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 - debug: 4.3.5 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/traverse@7.25.4': + '@babel/traverse@7.25.6': dependencies: '@babel/code-frame': 7.24.7 - '@babel/generator': 7.25.5 - '@babel/parser': 7.25.4 + '@babel/generator': 7.25.6 + '@babel/parser': 7.25.6 '@babel/template': 7.25.0 - '@babel/types': 7.25.4 + '@babel/types': 7.25.6 debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/traverse@7.25.6': + '@babel/traverse@7.25.7': dependencies: - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.25.6 - '@babel/parser': 7.25.6 - '@babel/template': 7.25.0 - '@babel/types': 7.25.6 - debug: 4.3.5 + '@babel/code-frame': 7.25.7 + '@babel/generator': 7.25.7 + '@babel/parser': 7.25.8 + '@babel/template': 7.25.7 + '@babel/types': 7.25.8 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6762,16 +7241,16 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 - '@babel/types@7.25.4': + '@babel/types@7.25.6': dependencies: '@babel/helper-string-parser': 7.24.8 '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 - '@babel/types@7.25.6': + '@babel/types@7.25.8': dependencies: - '@babel/helper-string-parser': 7.24.8 - '@babel/helper-validator-identifier': 7.24.7 + '@babel/helper-string-parser': 7.25.7 + '@babel/helper-validator-identifier': 7.25.7 to-fast-properties: 2.0.0 '@bufbuild/protobuf@1.7.2': @@ -6798,7 +7277,7 @@ snapshots: '@cyclonedx/cdxgen-plugins-bin@1.6.3': optional: true - '@cyclonedx/cdxgen@10.9.11': + '@cyclonedx/cdxgen@10.10.4': dependencies: '@babel/parser': 7.25.6 '@babel/traverse': 7.25.6 @@ -6819,7 +7298,7 @@ snapshots: prettify-xml: 1.2.0 properties-reader: 2.3.0 semver: 7.6.3 - ssri: 10.0.6 + ssri: 11.0.0 table: 6.8.2 tar: 6.2.1 toml: 3.0.0 @@ -6828,7 +7307,7 @@ snapshots: xml-js: 1.6.11 yargs: 17.7.2 optionalDependencies: - '@appthreat/atom': 2.0.18 + '@appthreat/atom': 2.0.21 '@appthreat/cdx-proto': 1.0.1 '@cyclonedx/cdxgen-plugins-bin': 1.6.3 '@cyclonedx/cdxgen-plugins-bin-arm64': 1.6.3 @@ -6837,7 +7316,7 @@ snapshots: '@cyclonedx/cdxgen-plugins-bin-ppc64': 1.6.3 '@cyclonedx/cdxgen-plugins-bin-windows-amd64': 1.6.3 '@cyclonedx/cdxgen-plugins-bin-windows-arm64': 1.6.3 - body-parser: 1.20.2 + body-parser: 2.0.1 compression: 1.7.4 connect: 3.7.0 jsonata: 2.0.5 @@ -6859,11 +7338,11 @@ snapshots: '@drauu/core@0.4.1': {} - '@ducanh2912/next-pwa@10.2.8(@types/babel__core@7.20.5)(next@14.2.12(@babel/core@7.24.7)(@playwright/test@1.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.92.1)': + '@ducanh2912/next-pwa@10.2.9(@types/babel__core@7.20.5)(next@14.2.14(@babel/core@7.24.7)(@playwright/test@1.48.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.12(@babel/core@7.24.7)(@playwright/test@1.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - semver: 7.6.2 + next: 14.2.14(@babel/core@7.24.7)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + semver: 7.6.3 webpack: 5.92.1 workbox-build: 7.1.1(@types/babel__core@7.20.5) workbox-core: 7.1.0 @@ -6905,7 +7384,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1)': + '@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 '@emotion/babel-plugin': 11.12.0 @@ -6917,7 +7396,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 transitivePeerDependencies: - supports-color @@ -6931,18 +7410,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1)': + '@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 '@emotion/babel-plugin': 11.12.0 '@emotion/is-prop-valid': 1.3.0 - '@emotion/react': 11.13.3(@types/react@18.3.7)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) '@emotion/serialize': 1.3.1 '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) '@emotion/utils': 1.4.0 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 transitivePeerDependencies: - supports-color @@ -7107,7 +7586,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.5 + debug: 4.3.7 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -7171,7 +7650,11 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@hello-pangea/dnd@17.0.0(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@grafana/schema@11.2.2': + dependencies: + tslib: 2.6.3 + + '@hello-pangea/dnd@17.0.0(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 css-box-model: 1.2.1 @@ -7179,35 +7662,39 @@ snapshots: raf-schd: 4.0.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-redux: 9.1.2(@types/react@18.3.7)(react@18.3.1)(redux@5.0.1) + react-redux: 9.1.2(@types/react@18.3.11)(react@18.3.1)(redux@5.0.1) redux: 5.0.1 use-memo-one: 1.1.3(react@18.3.1) transitivePeerDependencies: - '@types/react' - '@hpke/chacha20poly1305@1.3.1': + '@hpke/chacha20poly1305@1.4.3': dependencies: - '@hpke/core': 1.3.1 + '@hpke/common': 1.4.3 '@noble/ciphers': 0.5.3 - '@hpke/core@1.3.1': {} + '@hpke/common@1.4.3': {} - '@hpke/dhkem-x25519@1.3.1': + '@hpke/core@1.4.3': dependencies: - '@hpke/core': 1.3.1 + '@hpke/common': 1.4.3 + + '@hpke/dhkem-x25519@1.4.3': + dependencies: + '@hpke/common': 1.4.3 '@noble/curves': 1.4.2 '@noble/hashes': 1.4.0 - '@hpke/dhkem-x448@1.3.1': + '@hpke/dhkem-x448@1.4.3': dependencies: - '@hpke/core': 1.3.1 + '@hpke/common': 1.4.3 '@noble/curves': 1.4.2 '@noble/hashes': 1.4.0 '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.5 + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7232,7 +7719,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/resolve-uri@3.1.2': {} @@ -7244,16 +7731,14 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 - '@keycloak/keycloak-admin-client@25.0.6': + '@keycloak/keycloak-admin-client@26.0.0': dependencies: camelize-ts: 3.0.0 url-join: 5.0.0 @@ -7263,137 +7748,165 @@ snapshots: '@matrix-org/olm@3.2.15': {} - '@mui/base@5.0.0-beta.40(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mdx-js/mdx@3.0.1': + dependencies: + '@types/estree': 1.0.6 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-build-jsx: 3.0.1 + estree-util-is-identifier-name: 3.0.0 + estree-util-to-js: 2.0.0 + estree-walker: 3.0.3 + hast-util-to-estree: 3.1.0 + hast-util-to-jsx-runtime: 2.3.2 + markdown-extensions: 2.0.0 + periscopic: 3.1.0 + remark-mdx: 3.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + source-map: 0.7.4 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@mui/base@5.0.0-beta.40(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 '@floating-ui/react-dom': 2.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/types': 7.2.15(@types/react@18.3.7) - '@mui/utils': 5.16.5(@types/react@18.3.7)(react@18.3.1) + '@mui/types': 7.2.15(@types/react@18.3.11) + '@mui/utils': 5.16.5(@types/react@18.3.11)(react@18.3.1) '@popperjs/core': 2.11.8 clsx: 2.1.1 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 '@mui/core-downloads-tracker@5.16.5': {} - '@mui/icons-material@5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.7)(react@18.3.1)': + '@mui/icons-material@5.16.7(@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 - '@mui/material': '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' + '@mui/material': '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' react: 18.3.1 optionalDependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 - '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@mui/joy@5.0.0-beta.48(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 - '@mui/base': 5.0.0-beta.40(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/base': 5.0.0-beta.40(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/core-downloads-tracker': 5.16.5 - '@mui/system': 5.16.5(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) - '@mui/types': 7.2.15(@types/react@18.3.7) - '@mui/utils': 5.16.5(@types/react@18.3.7)(react@18.3.1) + '@mui/system': 5.16.5(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + '@mui/types': 7.2.15(@types/react@18.3.11) + '@mui/utils': 5.16.5(@types/react@18.3.11)(react@18.3.1) clsx: 2.1.1 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@emotion/react': 11.13.3(@types/react@18.3.7)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) - '@types/react': 18.3.7 + '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + '@types/react': 18.3.11 - '@mui/private-theming@5.16.5(@types/react@18.3.7)(react@18.3.1)': + '@mui/private-theming@5.16.5(@types/react@18.3.11)(react@18.3.1)': dependencies: - '@babel/runtime': 7.25.4 - '@mui/utils': 5.16.5(@types/react@18.3.7)(react@18.3.1) + '@babel/runtime': 7.25.6 + '@mui/utils': 5.16.5(@types/react@18.3.11)(react@18.3.1) prop-types: 15.8.1 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 - '@mui/styled-engine@5.16.4(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(react@18.3.1)': + '@mui/styled-engine@5.16.4(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.25.4 + '@babel/runtime': 7.25.6 '@emotion/cache': 11.13.1 csstype: 3.1.3 prop-types: 15.8.1 react: 18.3.1 optionalDependencies: - '@emotion/react': 11.13.3(@types/react@18.3.7)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) - '@mui/system@5.16.5(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1)': + '@mui/system@5.16.5(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 - '@mui/private-theming': 5.16.5(@types/react@18.3.7)(react@18.3.1) - '@mui/styled-engine': 5.16.4(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1))(react@18.3.1) - '@mui/types': 7.2.15(@types/react@18.3.7) - '@mui/utils': 5.16.5(@types/react@18.3.7)(react@18.3.1) + '@mui/private-theming': 5.16.5(@types/react@18.3.11)(react@18.3.1) + '@mui/styled-engine': 5.16.4(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(react@18.3.1) + '@mui/types': 7.2.15(@types/react@18.3.11) + '@mui/utils': 5.16.5(@types/react@18.3.11)(react@18.3.1) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 18.3.1 optionalDependencies: - '@emotion/react': 11.13.3(@types/react@18.3.7)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.7)(react@18.3.1))(@types/react@18.3.7)(react@18.3.1) - '@types/react': 18.3.7 + '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) + '@types/react': 18.3.11 - '@mui/types@7.2.15(@types/react@18.3.7)': + '@mui/types@7.2.15(@types/react@18.3.11)': optionalDependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 - '@mui/utils@5.16.5(@types/react@18.3.7)(react@18.3.1)': + '@mui/utils@5.16.5(@types/react@18.3.11)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 - '@mui/types': 7.2.15(@types/react@18.3.7) + '@mui/types': 7.2.15(@types/react@18.3.11) '@types/prop-types': 15.7.12 clsx: 2.1.1 prop-types: 15.8.1 react: 18.3.1 react-is: 18.3.1 optionalDependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 - '@next/bundle-analyzer@14.2.12': + '@next/bundle-analyzer@14.2.14': dependencies: webpack-bundle-analyzer: 4.10.1 transitivePeerDependencies: - bufferutil - utf-8-validate - '@next/env@14.2.12': {} + '@next/env@14.2.14': {} - '@next/eslint-plugin-next@14.2.12': + '@next/eslint-plugin-next@14.2.14': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@14.2.12': + '@next/swc-darwin-arm64@14.2.14': optional: true - '@next/swc-darwin-x64@14.2.12': + '@next/swc-darwin-x64@14.2.14': optional: true - '@next/swc-linux-arm64-gnu@14.2.12': + '@next/swc-linux-arm64-gnu@14.2.14': optional: true - '@next/swc-linux-arm64-musl@14.2.12': + '@next/swc-linux-arm64-musl@14.2.14': optional: true - '@next/swc-linux-x64-gnu@14.2.12': + '@next/swc-linux-x64-gnu@14.2.14': optional: true - '@next/swc-linux-x64-musl@14.2.12': + '@next/swc-linux-x64-musl@14.2.14': optional: true - '@next/swc-win32-arm64-msvc@14.2.12': + '@next/swc-win32-arm64-msvc@14.2.14': optional: true - '@next/swc-win32-ia32-msvc@14.2.12': + '@next/swc-win32-ia32-msvc@14.2.14': optional: true - '@next/swc-win32-x64-msvc@14.2.12': + '@next/swc-win32-x64-msvc@14.2.14': optional: true '@noble/ciphers@0.5.3': {} @@ -7404,6 +7917,8 @@ snapshots: '@noble/hashes@1.4.0': {} + '@noble/hashes@1.5.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7562,9 +8077,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.48.0': + '@playwright/test@1.48.1': dependencies: - playwright: 1.48.0 + playwright: 1.48.1 '@polka/url@1.0.0-next.25': {} @@ -7615,7 +8130,7 @@ snapshots: '@rollup/pluginutils@5.1.0(rollup@2.79.1)': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: @@ -7727,19 +8242,19 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tanstack/eslint-plugin-query@5.56.1(eslint@8.57.1)(typescript@5.6.2)': + '@tanstack/eslint-plugin-query@5.59.7(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@typescript-eslint/utils': 8.5.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/utils': 8.8.1(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 transitivePeerDependencies: - supports-color - typescript - '@tanstack/query-core@5.56.2': {} + '@tanstack/query-core@5.59.10': {} - '@tanstack/react-query@5.56.2(react@18.3.1)': + '@tanstack/react-query@5.59.10(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.56.2 + '@tanstack/query-core': 5.59.10 react: 18.3.1 '@tanstack/react-table@8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -7772,51 +8287,64 @@ snapshots: '@tufjs/canonical-json': 2.0.0 minimatch: 9.0.4 + '@types/acorn@4.0.6': + dependencies: + '@types/estree': 1.0.6 + '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.6 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.24.7 + '@babel/types': 7.25.6 '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 - optional: true '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 8.56.12 - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 '@types/eslint@8.56.12': dependencies: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.6 + '@types/estree@0.0.39': {} '@types/estree@1.0.5': {} + '@types/estree@1.0.6': {} + '@types/events@3.0.3': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/hoist-non-react-statics@3.3.5': dependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 hoist-non-react-statics: 3.3.2 '@types/http-cache-semantics@4.0.4': {} @@ -7827,28 +8355,38 @@ snapshots: '@types/k6@0.54.1': {} - '@types/ms@0.7.34': - optional: true + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/ms@0.7.34': {} '@types/negotiator@0.6.3': {} - '@types/node@20.16.5': + '@types/node@20.16.11': dependencies: undici-types: 6.19.8 + '@types/node@22.7.6': + dependencies: + undici-types: 6.19.8 + optional: true + '@types/parse-json@4.0.2': {} '@types/prop-types@15.7.12': {} - '@types/react-dom@18.3.0': + '@types/react-dom@18.3.1': dependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 '@types/react-transition-group@4.4.11': dependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 - '@types/react@18.3.7': + '@types/react@18.3.11': dependencies: '@types/prop-types': 15.7.12 csstype: 3.1.3 @@ -7859,6 +8397,10 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.3': {} '@types/uuid@10.0.0': {} @@ -7866,182 +8408,128 @@ snapshots: '@types/validator@13.12.0': optional: true - '@typescript-eslint/eslint-plugin@8.5.0(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)': + '@typescript-eslint/eslint-plugin@8.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.10.1 - '@typescript-eslint/parser': 8.5.0(eslint@8.57.1)(typescript@5.6.2) - '@typescript-eslint/scope-manager': 8.5.0 - '@typescript-eslint/type-utils': 8.5.0(eslint@8.57.1)(typescript@5.6.2) - '@typescript-eslint/utils': 8.5.0(eslint@8.57.1)(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.5.0 + '@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/type-utils': 8.8.1(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/utils': 8.8.1(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.8.1 eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.6.2) - optionalDependencies: - typescript: 5.6.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/eslint-plugin@8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)': - dependencies: - '@eslint-community/regexpp': 4.10.1 - '@typescript-eslint/parser': 8.6.0(eslint@8.57.1)(typescript@5.6.2) - '@typescript-eslint/scope-manager': 8.6.0 - '@typescript-eslint/type-utils': 8.6.0(eslint@8.57.1)(typescript@5.6.2) - '@typescript-eslint/utils': 8.6.0(eslint@8.57.1)(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.6.0 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.1 - natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.6.2) - optionalDependencies: - typescript: 5.6.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2)': - dependencies: - '@typescript-eslint/scope-manager': 8.5.0 - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.5.0 - debug: 4.3.7 - eslint: 8.57.1 + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2)': + '@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@typescript-eslint/scope-manager': 8.6.0 - '@typescript-eslint/types': 8.6.0 - '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.6.0 + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.8.1 debug: 4.3.7 eslint: 8.57.1 optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.5.0': + '@typescript-eslint/scope-manager@8.8.1': dependencies: - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/visitor-keys': 8.5.0 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/visitor-keys': 8.8.1 - '@typescript-eslint/scope-manager@8.6.0': + '@typescript-eslint/type-utils@8.8.1(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 8.6.0 - '@typescript-eslint/visitor-keys': 8.6.0 - - '@typescript-eslint/type-utils@8.5.0(eslint@8.57.1)(typescript@5.6.2)': - dependencies: - '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.2) - '@typescript-eslint/utils': 8.5.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.8.1(eslint@8.57.1)(typescript@5.6.3) debug: 4.3.7 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - eslint - supports-color - '@typescript-eslint/type-utils@8.6.0(eslint@8.57.1)(typescript@5.6.2)': - dependencies: - '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.6.2) - '@typescript-eslint/utils': 8.6.0(eslint@8.57.1)(typescript@5.6.2) - debug: 4.3.7 - ts-api-utils: 1.3.0(typescript@5.6.2) - optionalDependencies: - typescript: 5.6.2 - transitivePeerDependencies: - - eslint - - supports-color - - '@typescript-eslint/types@8.5.0': {} - - '@typescript-eslint/types@8.6.0': {} + '@typescript-eslint/types@8.8.1': {} - '@typescript-eslint/typescript-estree@8.5.0(typescript@5.6.2)': + '@typescript-eslint/typescript-estree@8.8.1(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/visitor-keys': 8.5.0 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/visitor-keys': 8.8.1 debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.4 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.6.0(typescript@5.6.2)': - dependencies: - '@typescript-eslint/types': 8.6.0 - '@typescript-eslint/visitor-keys': 8.6.0 - debug: 4.3.7 - fast-glob: 3.3.2 - is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.2) - optionalDependencies: - typescript: 5.6.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.5.0(eslint@8.57.1)(typescript@5.6.2)': + '@typescript-eslint/utils@8.8.1(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.5.0 - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.2) + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) eslint: 8.57.1 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/utils@8.6.0(eslint@8.57.1)(typescript@5.6.2)': + '@typescript-eslint/visitor-keys@8.8.1': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.6.0 - '@typescript-eslint/types': 8.6.0 - '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.6.2) - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@8.5.0': - dependencies: - '@typescript-eslint/types': 8.5.0 - eslint-visitor-keys: 3.4.3 - - '@typescript-eslint/visitor-keys@8.6.0': - dependencies: - '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/types': 8.8.1 eslint-visitor-keys: 3.4.3 '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react@4.3.1(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1))': + '@vitejs/plugin-react@4.3.2(vite@5.3.1(@types/node@20.16.11)(terser@5.36.0))': dependencies: - '@babel/core': 7.24.7 - '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.24.7) + '@babel/core': 7.25.8 + '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.8) + '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.8) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.3.1(@types/node@20.16.5)(terser@5.31.1) + vite: 5.3.1(@types/node@20.16.11)(terser@5.36.0) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-react@4.3.2(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0))': + dependencies: + '@babel/core': 7.25.8 + '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.8) + '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.8) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 5.3.1(@types/node@22.7.6)(terser@5.36.0) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-istanbul@2.1.2(vitest@2.1.2(@types/node@20.16.11)(terser@5.36.0))': + dependencies: + '@istanbuljs/schema': 0.1.3 + debug: 4.3.7 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magicast: 0.3.4 + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.1.2(@types/node@20.16.11)(terser@5.36.0) transitivePeerDependencies: - supports-color - '@vitest/coverage-istanbul@2.1.1(vitest@2.1.1(@types/node@20.16.5)(terser@5.31.1))': + '@vitest/coverage-istanbul@2.1.2(vitest@2.1.2(@types/node@22.7.6)(terser@5.36.0))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.3.7 @@ -8053,47 +8541,55 @@ snapshots: magicast: 0.3.4 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.1.1(@types/node@20.16.5)(terser@5.31.1) + vitest: 2.1.2(@types/node@22.7.6)(terser@5.36.0) transitivePeerDependencies: - supports-color - '@vitest/expect@2.1.1': + '@vitest/expect@2.1.2': dependencies: - '@vitest/spy': 2.1.1 - '@vitest/utils': 2.1.1 + '@vitest/spy': 2.1.2 + '@vitest/utils': 2.1.2 chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1))': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.3.1(@types/node@20.16.11)(terser@5.36.0))': dependencies: - '@vitest/spy': 2.1.1 + '@vitest/spy': 2.1.2 estree-walker: 3.0.3 magic-string: 0.30.11 optionalDependencies: - vite: 5.3.1(@types/node@20.16.5)(terser@5.31.1) + vite: 5.3.1(@types/node@20.16.11)(terser@5.36.0) - '@vitest/pretty-format@2.1.1': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0))': + dependencies: + '@vitest/spy': 2.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.11 + optionalDependencies: + vite: 5.3.1(@types/node@22.7.6)(terser@5.36.0) + + '@vitest/pretty-format@2.1.2': dependencies: tinyrainbow: 1.2.0 - '@vitest/runner@2.1.1': + '@vitest/runner@2.1.2': dependencies: - '@vitest/utils': 2.1.1 + '@vitest/utils': 2.1.2 pathe: 1.1.2 - '@vitest/snapshot@2.1.1': + '@vitest/snapshot@2.1.2': dependencies: - '@vitest/pretty-format': 2.1.1 + '@vitest/pretty-format': 2.1.2 magic-string: 0.30.11 pathe: 1.1.2 - '@vitest/spy@2.1.1': + '@vitest/spy@2.1.2': dependencies: tinyspy: 3.0.0 - '@vitest/utils@2.1.1': + '@vitest/utils@2.1.2': dependencies: - '@vitest/pretty-format': 2.1.1 + '@vitest/pretty-format': 2.1.2 loupe: 3.1.1 tinyrainbow: 1.2.0 @@ -8188,20 +8684,26 @@ snapshots: negotiator: 0.6.3 optional: true - acorn-import-attributes@1.9.5(acorn@8.12.0): + acorn-import-attributes@1.9.5(acorn@8.13.0): dependencies: - acorn: 8.12.0 + acorn: 8.13.0 acorn-jsx@5.3.2(acorn@8.12.0): dependencies: acorn: 8.12.0 + acorn-jsx@5.3.2(acorn@8.13.0): + dependencies: + acorn: 8.13.0 + acorn-walk@8.3.3: dependencies: acorn: 8.12.0 acorn@8.12.0: {} + acorn@8.13.0: {} + agent-base@6.0.2: dependencies: debug: 4.3.7 @@ -8240,13 +8742,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.16.0: - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -8281,9 +8776,9 @@ snapshots: argparse@2.0.1: {} - aria-query@5.3.0: + aria-query@5.1.3: dependencies: - dequal: 2.0.3 + deep-equal: 2.2.3 array-buffer-byte-length@1.0.1: dependencies: @@ -8369,6 +8864,8 @@ snapshots: astral-regex@2.0.0: {} + astring@1.9.0: {} + async@3.2.5: {} at-least-node@1.0.0: {} @@ -8379,20 +8876,16 @@ snapshots: axe-core@4.10.0: {} - axe-core@4.7.0: {} - axe-html-reporter@2.2.11(axe-core@4.10.0): dependencies: axe-core: 4.10.0 mustache: 4.2.0 - axobject-query@3.2.1: - dependencies: - dequal: 2.0.3 + axobject-query@4.1.0: {} babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.25.4 + '@babel/runtime': 7.25.6 cosmiconfig: 7.1.0 resolve: 1.22.8 @@ -8420,6 +8913,8 @@ snapshots: transitivePeerDependencies: - supports-color + bail@2.0.2: {} + balanced-match@1.0.2: {} base-x@5.0.0: {} @@ -8448,18 +8943,17 @@ snapshots: blueimp-canvas-to-blob@3.29.0: {} - body-parser@1.20.2: + body-parser@2.0.1: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 + debug: 3.1.0 destroy: 1.2.0 http-errors: 2.0.0 - iconv-lite: 0.4.24 + iconv-lite: 0.5.2 on-finished: 2.4.1 - qs: 6.11.0 - raw-body: 2.5.2 + qs: 6.13.0 + raw-body: 3.0.0 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: @@ -8485,11 +8979,18 @@ snapshots: browserslist@4.23.1: dependencies: - caniuse-lite: 1.0.30001636 + caniuse-lite: 1.0.30001668 electron-to-chromium: 1.4.805 node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.1) + browserslist@4.24.0: + dependencies: + caniuse-lite: 1.0.30001668 + electron-to-chromium: 1.5.36 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.0) + bs58@6.0.0: dependencies: base-x: 5.0.0 @@ -8583,7 +9084,9 @@ snapshots: camelize-ts@3.0.0: {} - caniuse-lite@1.0.30001636: {} + caniuse-lite@1.0.30001668: {} + + ccount@2.0.1: {} chai@5.1.1: dependencies: @@ -8604,6 +9107,14 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + check-error@2.1.1: {} cheerio-select@2.1.0: @@ -8650,6 +9161,8 @@ snapshots: cmd-shim@6.0.3: {} + collapse-white-space@2.1.0: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -8665,6 +9178,8 @@ snapshots: color-support@1.1.3: optional: true + comma-separated-tokens@2.0.3: {} + commander@2.20.3: {} commander@7.2.0: {} @@ -8784,6 +9299,11 @@ snapshots: ms: 2.0.0 optional: true + debug@3.1.0: + dependencies: + ms: 2.0.0 + optional: true + debug@3.2.7: dependencies: ms: 2.1.3 @@ -8796,12 +9316,37 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 deep-eql@5.0.2: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.4 + is-arguments: 1.1.1 + is-array-buffer: 3.0.4 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + side-channel: 1.0.6 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + deep-extend@0.6.0: optional: true @@ -8841,6 +9386,10 @@ snapshots: detect-node@2.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -8851,7 +9400,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.25.4 + '@babel/runtime': 7.25.6 csstype: 3.1.3 dom-serializer@2.0.0: @@ -8912,6 +9461,8 @@ snapshots: electron-to-chromium@1.4.805: {} + electron-to-chromium@1.5.36: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -8939,6 +9490,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + enhanced-resolve@5.17.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + entities@4.5.0: {} env-paths@2.2.1: {} @@ -9004,6 +9560,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + es-iterator-helpers@1.0.19: dependencies: call-bind: 1.0.7 @@ -9100,6 +9668,8 @@ snapshots: escalade@3.1.2: {} + escalade@3.2.0: {} + escape-html@1.0.3: optional: true @@ -9107,21 +9677,21 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@14.2.12(eslint@8.57.1)(typescript@5.6.2): + eslint-config-next@14.2.14(eslint@8.57.1)(typescript@5.6.3): dependencies: - '@next/eslint-plugin-next': 14.2.12 + '@next/eslint-plugin-next': 14.2.14 '@rushstack/eslint-patch': 1.10.3 - '@typescript-eslint/eslint-plugin': 8.5.0(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) - '@typescript-eslint/parser': 8.5.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/eslint-plugin': 8.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1) eslint-plugin-react: 7.34.2(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - eslint-import-resolver-webpack - eslint-plugin-import-x @@ -9139,116 +9709,48 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.5 enhanced-resolve: 5.17.0 eslint: 8.57.1 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-bun-module: 1.1.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.3.5 - enhanced-resolve: 5.17.0 - eslint: 8.57.1 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) - fast-glob: 3.3.2 - get-tsconfig: 4.7.5 - is-bun-module: 1.1.0 - is-glob: 4.0.3 - optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-node - - eslint-import-resolver-webpack - - supports-color - - eslint-module-utils@2.11.0(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.5.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.11.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.6.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.5.0(eslint@8.57.1)(typescript@5.6.2) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.8.1(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.6.0(eslint@8.57.1)(typescript@5.6.2) - eslint: 8.57.1 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.5.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.15.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.0 - semver: 6.3.1 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.5.0(eslint@8.57.1)(typescript@5.6.2) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -9259,7 +9761,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -9268,23 +9770,23 @@ snapshots: object.groupby: 1.0.3 object.values: 1.2.0 semver: 6.3.1 + string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.6.0(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.1): + eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1): dependencies: - '@babel/runtime': 7.25.4 - aria-query: 5.3.0 + aria-query: 5.1.3 array-includes: 3.1.8 array.prototype.flatmap: 1.3.2 ast-types-flow: 0.0.8 - axe-core: 4.7.0 - axobject-query: 3.2.1 + axe-core: 4.10.0 + axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 es-iterator-helpers: 1.0.19 @@ -9293,8 +9795,9 @@ snapshots: jsx-ast-utils: 3.3.5 language-tags: 1.0.9 minimatch: 3.1.2 - object.entries: 1.1.8 object.fromentries: 2.0.8 + safe-regex-test: 1.0.3 + string.prototype.includes: 2.0.1 eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): dependencies: @@ -9322,11 +9825,11 @@ snapshots: semver: 6.3.1 string.prototype.matchall: 4.0.11 - eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1): + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1): dependencies: eslint: 8.57.1 optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/eslint-plugin': 8.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) eslint-scope@5.1.1: dependencies: @@ -9401,13 +9904,37 @@ snapshots: estraverse@5.3.0: {} + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.6 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.4 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + estree-walker@1.0.1: {} estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 esutils@2.0.3: {} @@ -9418,6 +9945,8 @@ snapshots: exponential-backoff@3.1.1: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.2: @@ -9675,7 +10204,7 @@ snapshots: lowercase-keys: 3.0.0 p-cancelable: 4.0.1 responselike: 3.0.0 - type-fest: 4.25.0 + type-fest: 4.26.1 graceful-fs@4.2.11: {} @@ -9710,6 +10239,51 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-estree@3.1.0: + dependencies: + '@types/estree': 1.0.6 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.4 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-jsx-runtime@2.3.2: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + style-to-object: 1.0.8 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -9718,12 +10292,13 @@ snapshots: dependencies: lru-cache: 10.2.2 - hpke-js@1.3.1: + hpke-js@1.4.3: dependencies: - '@hpke/chacha20poly1305': 1.3.1 - '@hpke/core': 1.3.1 - '@hpke/dhkem-x25519': 1.3.1 - '@hpke/dhkem-x448': 1.3.1 + '@hpke/chacha20poly1305': 1.4.3 + '@hpke/common': 1.4.3 + '@hpke/core': 1.4.3 + '@hpke/dhkem-x25519': 1.4.3 + '@hpke/dhkem-x448': 1.4.3 '@noble/hashes': 1.4.0 html-escaper@2.0.2: {} @@ -9795,11 +10370,11 @@ snapshots: dependencies: '@babel/runtime': 7.24.7 - i18next@23.15.1: + i18next@23.15.2: dependencies: - '@babel/runtime': 7.25.4 + '@babel/runtime': 7.25.6 - iconv-lite@0.4.24: + iconv-lite@0.5.2: dependencies: safer-buffer: 2.1.2 optional: true @@ -9844,6 +10419,10 @@ snapshots: ini@1.3.8: optional: true + inline-style-parser@0.1.1: {} + + inline-style-parser@0.2.4: {} + internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -9855,6 +10434,18 @@ snapshots: jsbn: 1.1.0 sprintf-js: 1.1.3 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arguments@1.1.1: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 @@ -9887,10 +10478,6 @@ snapshots: is-callable@1.2.7: {} - is-core-module@2.13.1: - dependencies: - hasown: 2.0.2 - is-core-module@2.15.1: dependencies: hasown: 2.0.2 @@ -9903,6 +10490,8 @@ snapshots: dependencies: has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.0.2: @@ -9919,6 +10508,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-lambda@1.0.1: {} is-map@2.0.3: {} @@ -9937,8 +10528,14 @@ snapshots: is-path-inside@3.0.3: {} + is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-reference@3.0.2: + dependencies: + '@types/estree': 1.0.6 + is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -9992,7 +10589,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.24.7 - '@babel/parser': 7.25.4 + '@babel/parser': 7.25.6 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.6.3 @@ -10055,7 +10652,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.16.5 + '@types/node': 20.16.11 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -10071,6 +10668,8 @@ snapshots: jsesc@2.5.2: {} + jsesc@3.0.2: {} + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -10175,6 +10774,8 @@ snapshots: loglevel@1.9.1: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -10208,8 +10809,8 @@ snapshots: magicast@0.3.4: dependencies: - '@babel/parser': 7.25.4 - '@babel/types': 7.25.4 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 source-map-js: 1.2.0 make-dir@4.0.0: @@ -10256,6 +10857,8 @@ snapshots: - supports-color optional: true + markdown-extensions@2.0.0: {} + matcher@3.0.0: dependencies: escape-string-regexp: 4.0.0 @@ -10285,6 +10888,104 @@ snapshots: '@types/events': 3.0.3 events: 3.3.0 + mdast-util-from-markdown@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.1.3: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + parse-entities: 4.0.1 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.1 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-decode-string: 2.0.0 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + media-typer@0.3.0: optional: true @@ -10294,6 +10995,214 @@ snapshots: merge2@1.4.1: {} + micromark-core-commonmark@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.0 + micromark-factory-label: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-factory-title: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-html-tag-name: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-mdx-expression@3.0.0: + dependencies: + '@types/estree': 1.0.6 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.2 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-mdx-jsx@3.0.1: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.6 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.2 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + vfile-message: 4.0.2 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.6 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.13.0 + acorn-jsx: 5.3.2(acorn@8.13.0) + micromark-extension-mdx-expression: 3.0.0 + micromark-extension-mdx-jsx: 3.0.1 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-destination@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-label@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-mdx-expression@2.0.2: + dependencies: + '@types/estree': 1.0.6 + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-factory-space@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-types: 2.0.0 + + micromark-factory-title@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-whitespace@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-character@2.1.0: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-chunked@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-classify-character@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-combine-extensions@2.0.0: + dependencies: + micromark-util-chunked: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-decode-numeric-character-reference@2.0.1: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-decode-string@2.0.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-symbol: 2.0.0 + + micromark-util-encode@2.0.0: {} + + micromark-util-events-to-acorn@2.0.2: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.6 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + vfile-message: 4.0.2 + + micromark-util-html-tag-name@2.0.0: {} + + micromark-util-normalize-identifier@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-resolve-all@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-util-sanitize-uri@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + micromark-util-subtokenize@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-symbol@2.0.0: {} + + micromark-util-types@2.0.0: {} + + micromark@4.0.0: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + transitivePeerDependencies: + - supports-color + micromatch@4.0.7: dependencies: braces: 3.0.3 @@ -10413,28 +11322,28 @@ snapshots: neo-async@2.6.2: {} - next@14.2.12(@babel/core@7.24.7)(@playwright/test@1.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.14(@babel/core@7.24.7)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.2.12 + '@next/env': 14.2.14 '@swc/helpers': 0.5.5 busboy: 1.6.0 - caniuse-lite: 1.0.30001636 + caniuse-lite: 1.0.30001668 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(@babel/core@7.24.7)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.12 - '@next/swc-darwin-x64': 14.2.12 - '@next/swc-linux-arm64-gnu': 14.2.12 - '@next/swc-linux-arm64-musl': 14.2.12 - '@next/swc-linux-x64-gnu': 14.2.12 - '@next/swc-linux-x64-musl': 14.2.12 - '@next/swc-win32-arm64-msvc': 14.2.12 - '@next/swc-win32-ia32-msvc': 14.2.12 - '@next/swc-win32-x64-msvc': 14.2.12 - '@playwright/test': 1.48.0 + '@next/swc-darwin-arm64': 14.2.14 + '@next/swc-darwin-x64': 14.2.14 + '@next/swc-linux-arm64-gnu': 14.2.14 + '@next/swc-linux-arm64-musl': 14.2.14 + '@next/swc-linux-x64-gnu': 14.2.14 + '@next/swc-linux-x64-musl': 14.2.14 + '@next/swc-win32-arm64-msvc': 14.2.14 + '@next/swc-win32-ia32-msvc': 14.2.14 + '@next/swc-win32-x64-msvc': 14.2.14 + '@playwright/test': 1.48.1 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -10481,6 +11390,8 @@ snapshots: node-releases@2.0.14: {} + node-releases@2.0.18: {} + node-stream-zip@1.15.0: {} nopt@5.0.0: @@ -10558,6 +11469,11 @@ snapshots: object-inspect@1.13.1: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + object-keys@1.1.1: {} object.assign@4.1.5: @@ -10630,9 +11546,9 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - otpauth@9.3.2: + otpauth@9.3.4: dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.5.0 p-cancelable@4.0.1: {} @@ -10698,6 +11614,17 @@ snapshots: just-diff: 6.0.2 just-diff-apply: 5.5.0 + parse-entities@4.0.1: + dependencies: + '@types/unist': 2.0.11 + character-entities: 2.0.2 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.24.7 @@ -10747,11 +11674,19 @@ snapshots: pathval@2.0.0: {} + periscopic@3.1.0: + dependencies: + '@types/estree': 1.0.6 + estree-walker: 3.0.3 + is-reference: 3.0.2 + pg-connection-string@2.6.4: optional: true picocolors@1.0.1: {} + picocolors@1.1.0: {} + picomatch@2.3.1: {} pkijs@3.2.4: @@ -10763,11 +11698,11 @@ snapshots: pvutils: 1.1.3 tslib: 2.6.3 - playwright-core@1.48.0: {} + playwright-core@1.48.1: {} - playwright@1.48.0: + playwright@1.48.1: dependencies: - playwright-core: 1.48.0 + playwright-core: 1.48.1 optionalDependencies: fsevents: 2.3.2 @@ -10781,7 +11716,7 @@ snapshots: postcss@8.4.31: dependencies: nanoid: 3.3.7 - picocolors: 1.0.1 + picocolors: 1.1.0 source-map-js: 1.2.0 postcss@8.4.38: @@ -10843,6 +11778,8 @@ snapshots: dependencies: mkdirp: 1.0.4 + property-information@6.5.0: {} + pump@3.0.0: dependencies: end-of-stream: 1.4.4 @@ -10857,7 +11794,7 @@ snapshots: pvutils@1.1.3: {} - qs@6.11.0: + qs@6.13.0: dependencies: side-channel: 1.0.6 optional: true @@ -10872,11 +11809,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 - raw-body@2.5.2: + raw-body@3.0.0: dependencies: bytes: 3.1.2 http-errors: 2.0.0 - iconv-lite: 0.4.24 + iconv-lite: 0.6.3 unpipe: 1.0.0 optional: true @@ -10901,11 +11838,11 @@ snapshots: react-fast-compare@2.0.4: {} - react-i18next@15.0.2(i18next@23.15.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-i18next@15.0.2(i18next@23.15.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.4 html-parse-stringify: 3.0.1 - i18next: 23.15.1 + i18next: 23.15.2 react: 18.3.1 optionalDependencies: react-dom: 18.3.1(react@18.3.1) @@ -10914,13 +11851,13 @@ snapshots: react-is@18.3.1: {} - react-redux@9.1.2(@types/react@18.3.7)(react@18.3.1)(redux@5.0.1): + react-redux@9.1.2(@types/react@18.3.11)(react@18.3.1)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.3 react: 18.3.1 use-sync-external-store: 1.2.2(react@18.3.1) optionalDependencies: - '@types/react': 18.3.7 + '@types/react': 18.3.11 redux: 5.0.1 react-refresh@0.14.2: {} @@ -10996,7 +11933,31 @@ snapshots: dependencies: jsesc: 0.5.0 - remeda@2.13.0: + remark-mdx@3.0.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.1 + micromark-util-types: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remeda@2.15.0: dependencies: type-fest: 4.26.1 @@ -11012,7 +11973,7 @@ snapshots: resolve@1.22.8: dependencies: - is-core-module: 2.13.1 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -11116,8 +12077,6 @@ snapshots: semver@6.3.1: {} - semver@7.6.2: {} - semver@7.6.3: {} sequelize-pool@7.1.0: @@ -11127,7 +12086,7 @@ snapshots: dependencies: '@types/debug': 4.1.12 '@types/validator': 13.12.0 - debug: 4.3.5 + debug: 4.3.7 dottie: 2.0.6 inflection: 1.13.4 lodash: 4.17.21 @@ -11155,6 +12114,8 @@ snapshots: dependencies: randombytes: 2.1.0 + server-only@0.0.1: {} + set-blocking@2.0.0: optional: true @@ -11271,12 +12232,16 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.4: {} + source-map@0.8.0-beta.0: dependencies: whatwg-url: 7.1.0 sourcemap-codec@1.4.8: {} + space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -11310,6 +12275,10 @@ snapshots: dependencies: minipass: 7.1.2 + ssri@11.0.0: + dependencies: + minipass: 7.1.2 + ssri@8.0.1: dependencies: minipass: 3.3.6 @@ -11325,6 +12294,10 @@ snapshots: std-env@3.7.0: {} + stop-iteration-iterator@1.0.0: + dependencies: + internal-slot: 1.0.7 + streamsearch@1.1.0: {} string-width@4.2.3: @@ -11339,6 +12312,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + string.prototype.matchall@4.0.11: dependencies: call-bind: 1.0.7 @@ -11378,6 +12357,11 @@ snapshots: safe-buffer: 5.2.1 optional: true + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + stringify-object@3.3.0: dependencies: get-own-enumerable-property-symbols: 3.0.2 @@ -11401,6 +12385,14 @@ snapshots: strip-json-comments@3.1.1: {} + style-to-object@0.4.4: + dependencies: + inline-style-parser: 0.1.1 + + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + styled-jsx@5.1.1(@babel/core@7.24.7)(react@18.3.1): dependencies: client-only: 0.0.1 @@ -11475,7 +12467,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 - terser: 5.31.1 + terser: 5.36.0 webpack: 5.92.1 terser@5.31.1: @@ -11485,6 +12477,13 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + terser@5.36.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.13.0 + commander: 2.20.3 + source-map-support: 0.5.21 + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 @@ -11529,13 +12528,17 @@ snapshots: treeverse@3.0.0: {} - ts-api-utils@1.3.0(typescript@5.6.2): + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@1.3.0(typescript@5.6.3): dependencies: - typescript: 5.6.2 + typescript: 5.6.3 - tsconfck@3.1.0(typescript@5.6.2): + tsconfck@3.1.0(typescript@5.6.3): optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 tsconfig-paths@3.15.0: dependencies: @@ -11578,8 +12581,6 @@ snapshots: type-fest@0.20.2: {} - type-fest@4.25.0: {} - type-fest@4.26.1: {} type-is@1.6.18: @@ -11620,7 +12621,7 @@ snapshots: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 - typescript@5.6.2: {} + typescript@5.6.3: {} unbox-primitive@1.0.2: dependencies: @@ -11648,6 +12649,16 @@ snapshots: unicorn-magic@0.1.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + unique-filename@1.1.1: dependencies: unique-slug: 2.0.2 @@ -11670,6 +12681,33 @@ snapshots: dependencies: crypto-random-string: 2.0.0 + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + universalify@2.0.1: {} unpipe@1.0.0: @@ -11681,7 +12719,13 @@ snapshots: dependencies: browserslist: 4.23.1 escalade: 3.1.2 - picocolors: 1.0.1 + picocolors: 1.1.0 + + update-browserslist-db@1.1.1(browserslist@4.24.0): + dependencies: + browserslist: 4.24.0 + escalade: 3.2.0 + picocolors: 1.1.0 uri-js@4.4.1: dependencies: @@ -11713,9 +12757,9 @@ snapshots: uuid@8.3.2: optional: true - valibot@0.42.0(typescript@5.6.2): + valibot@0.42.1(typescript@5.6.3): optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 validate-iri@1.0.1: {} @@ -11732,12 +12776,22 @@ snapshots: vary@1.1.2: optional: true - vite-node@2.1.1(@types/node@20.16.5)(terser@5.31.1): + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite-node@2.1.2(@types/node@20.16.11)(terser@5.36.0): dependencies: cac: 6.7.14 debug: 4.3.7 pathe: 1.1.2 - vite: 5.3.1(@types/node@20.16.5)(terser@5.31.1) + vite: 5.3.1(@types/node@20.16.11)(terser@5.36.0) transitivePeerDependencies: - '@types/node' - less @@ -11748,36 +12802,106 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@5.0.1(typescript@5.6.2)(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)): + vite-node@2.1.2(@types/node@22.7.6)(terser@5.36.0): + dependencies: + cac: 6.7.14 + debug: 4.3.7 + pathe: 1.1.2 + vite: 5.3.1(@types/node@22.7.6)(terser@5.36.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + + vite-tsconfig-paths@5.0.1(typescript@5.6.3)(vite@5.3.1(@types/node@20.16.11)(terser@5.36.0)): + dependencies: + debug: 4.3.5 + globrex: 0.1.2 + tsconfck: 3.1.0(typescript@5.6.3) + optionalDependencies: + vite: 5.3.1(@types/node@20.16.11)(terser@5.36.0) + transitivePeerDependencies: + - supports-color + - typescript + + vite-tsconfig-paths@5.0.1(typescript@5.6.3)(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)): dependencies: debug: 4.3.5 globrex: 0.1.2 - tsconfck: 3.1.0(typescript@5.6.2) + tsconfck: 3.1.0(typescript@5.6.3) optionalDependencies: - vite: 5.3.1(@types/node@20.16.5)(terser@5.31.1) + vite: 5.3.1(@types/node@22.7.6)(terser@5.36.0) transitivePeerDependencies: - supports-color - typescript - vite@5.3.1(@types/node@20.16.5)(terser@5.31.1): + vite@5.3.1(@types/node@20.16.11)(terser@5.36.0): dependencies: esbuild: 0.21.5 postcss: 8.4.38 rollup: 4.18.0 optionalDependencies: - '@types/node': 20.16.5 + '@types/node': 20.16.11 fsevents: 2.3.3 - terser: 5.31.1 + terser: 5.36.0 + + vite@5.3.1(@types/node@22.7.6)(terser@5.36.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.38 + rollup: 4.18.0 + optionalDependencies: + '@types/node': 22.7.6 + fsevents: 2.3.3 + terser: 5.36.0 + + vitest@2.1.2(@types/node@20.16.11)(terser@5.36.0): + dependencies: + '@vitest/expect': 2.1.2 + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.3.1(@types/node@20.16.11)(terser@5.36.0)) + '@vitest/pretty-format': 2.1.2 + '@vitest/runner': 2.1.2 + '@vitest/snapshot': 2.1.2 + '@vitest/spy': 2.1.2 + '@vitest/utils': 2.1.2 + chai: 5.1.1 + debug: 4.3.7 + magic-string: 0.30.11 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinyexec: 0.3.0 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.3.1(@types/node@20.16.11)(terser@5.36.0) + vite-node: 2.1.2(@types/node@20.16.11)(terser@5.36.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.16.11 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - stylus + - sugarss + - supports-color + - terser - vitest@2.1.1(@types/node@20.16.5)(terser@5.31.1): + vitest@2.1.2(@types/node@22.7.6)(terser@5.36.0): dependencies: - '@vitest/expect': 2.1.1 - '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.3.1(@types/node@20.16.5)(terser@5.31.1)) - '@vitest/pretty-format': 2.1.1 - '@vitest/runner': 2.1.1 - '@vitest/snapshot': 2.1.1 - '@vitest/spy': 2.1.1 - '@vitest/utils': 2.1.1 + '@vitest/expect': 2.1.2 + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.3.1(@types/node@22.7.6)(terser@5.36.0)) + '@vitest/pretty-format': 2.1.2 + '@vitest/runner': 2.1.2 + '@vitest/snapshot': 2.1.2 + '@vitest/spy': 2.1.2 + '@vitest/utils': 2.1.2 chai: 5.1.1 debug: 4.3.7 magic-string: 0.30.11 @@ -11787,11 +12911,11 @@ snapshots: tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.3.1(@types/node@20.16.5)(terser@5.31.1) - vite-node: 2.1.1(@types/node@20.16.5)(terser@5.31.1) + vite: 5.3.1(@types/node@22.7.6)(terser@5.36.0) + vite-node: 2.1.2(@types/node@22.7.6)(terser@5.36.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.16.5 + '@types/node': 22.7.6 transitivePeerDependencies: - less - lightningcss @@ -11806,7 +12930,7 @@ snapshots: walk-up-path@3.0.1: {} - watchpack@2.4.1: + watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -11825,7 +12949,7 @@ snapshots: html-escaper: 2.0.2 is-plain-object: 5.0.0 opener: 1.5.2 - picocolors: 1.0.1 + picocolors: 1.1.0 sirv: 2.0.4 ws: 7.5.10 transitivePeerDependencies: @@ -11842,15 +12966,15 @@ snapshots: webpack@5.92.1: dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 '@webassemblyjs/ast': 1.12.1 '@webassemblyjs/wasm-edit': 1.12.1 '@webassemblyjs/wasm-parser': 1.12.1 - acorn: 8.12.0 - acorn-import-attributes: 1.9.5(acorn@8.12.0) - browserslist: 4.23.1 + acorn: 8.13.0 + acorn-import-attributes: 1.9.5(acorn@8.13.0) + browserslist: 4.24.0 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.17.0 + enhanced-resolve: 5.17.1 es-module-lexer: 1.5.4 eslint-scope: 5.1.1 events: 3.3.0 @@ -11863,7 +12987,7 @@ snapshots: schema-utils: 3.3.0 tapable: 2.2.1 terser-webpack-plugin: 5.3.10(webpack@5.92.1) - watchpack: 2.4.1 + watchpack: 2.4.2 webpack-sources: 3.2.3 transitivePeerDependencies: - '@swc/core' @@ -11940,7 +13064,7 @@ snapshots: wkx@0.5.0: dependencies: - '@types/node': 20.16.5 + '@types/node': 20.16.11 optional: true word-wrap@1.2.5: {} @@ -11956,16 +13080,16 @@ snapshots: workbox-build@7.1.0(@types/babel__core@7.20.5): dependencies: - '@apideck/better-ajv-errors': 0.3.6(ajv@8.16.0) + '@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1) '@babel/core': 7.24.7 '@babel/preset-env': 7.24.7(@babel/core@7.24.7) - '@babel/runtime': 7.25.4 + '@babel/runtime': 7.25.6 '@rollup/plugin-babel': 5.3.1(@babel/core@7.24.7)(@types/babel__core@7.20.5)(rollup@2.79.1) '@rollup/plugin-node-resolve': 15.2.3(rollup@2.79.1) '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) '@rollup/plugin-terser': 0.4.4(rollup@2.79.1) '@surma/rollup-plugin-off-main-thread': 2.2.3 - ajv: 8.16.0 + ajv: 8.17.1 common-tags: 1.8.2 fast-json-stable-stringify: 2.1.0 fs-extra: 9.1.0 @@ -11999,16 +13123,16 @@ snapshots: workbox-build@7.1.1(@types/babel__core@7.20.5): dependencies: - '@apideck/better-ajv-errors': 0.3.6(ajv@8.16.0) + '@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1) '@babel/core': 7.24.7 '@babel/preset-env': 7.24.7(@babel/core@7.24.7) - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.6 '@rollup/plugin-babel': 5.3.1(@babel/core@7.24.7)(@types/babel__core@7.20.5)(rollup@2.79.1) '@rollup/plugin-node-resolve': 15.2.3(rollup@2.79.1) '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) '@rollup/plugin-terser': 0.4.4(rollup@2.79.1) '@surma/rollup-plugin-off-main-thread': 2.2.3 - ajv: 8.16.0 + ajv: 8.17.1 common-tags: 1.8.2 fast-json-stable-stringify: 2.1.0 fs-extra: 9.1.0 @@ -12167,3 +13291,5 @@ snapshots: zrender@5.6.0: dependencies: tslib: 2.3.0 + + zwitch@2.0.4: {} diff --git a/reverse-proxy/forward_headers.conf b/reverse-proxy/forward_headers.conf index 5846b25bb..a3218e6f2 100644 --- a/reverse-proxy/forward_headers.conf +++ b/reverse-proxy/forward_headers.conf @@ -1,4 +1,7 @@ +proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Real-IP $remote_addr; +# do not forward unverified header from client +proxy_set_header Forwarded ""; diff --git a/reverse-proxy/keycloak.conf b/reverse-proxy/keycloak.conf index ed45d6fab..738dc2fc7 100644 --- a/reverse-proxy/keycloak.conf +++ b/reverse-proxy/keycloak.conf @@ -33,18 +33,23 @@ server { } location /admin/realms/ { - proxy_pass http://host.docker.internal:9090/admin/realms/; + # Do NOT use a trailing slash here, to proxy_pass the *encoded* url + # since e.g. role names (incl. special chars like '[' and ']') can be part of the path! + proxy_pass http://host.docker.internal:9090; } location /realms/master/ { + include forward_headers.conf; proxy_pass http://host.docker.internal:9090/realms/master/; } location /realms/citizens/ { + include forward_headers.conf; proxy_pass http://host.docker.internal:9090/realms/citizens/; } location /realms/eshg/ { + include forward_headers.conf; proxy_pass http://host.docker.internal:9090/realms/eshg/; } -- GitLab