diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactDto.java index a5e22f40d9b..c43741e4159 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactDto.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactDto.java @@ -20,6 +20,8 @@ import static de.symeda.sormas.api.utils.FieldConstraints.CHARACTER_LIMIT_BIG; import java.util.Date; +import java.util.HashSet; +import java.util.Set; import javax.validation.Valid; import javax.validation.constraints.Max; @@ -82,7 +84,7 @@ public class ContactDto extends SormasToSormasShareableDto implements IsContact public static final String CONTACT_IDENTIFICATION_SOURCE = "contactIdentificationSource"; public static final String CONTACT_IDENTIFICATION_SOURCE_DETAILS = "contactIdentificationSourceDetails"; public static final String CONTACT_OFFICER = "contactOfficer"; - public static final String CONTACT_PROXIMITY = "contactProximity"; + public static final String CONTACT_PROXIMITIES = "contactProximities"; public static final String CONTACT_PROXIMITY_DETAILS = "contactProximityDetails"; public static final String CONTACT_STATUS = "contactStatus"; public static final String DESCRIPTION = "description"; @@ -208,7 +210,7 @@ public class ContactDto extends SormasToSormasShareableDto implements IsContact @SensitiveData @Size(max = FieldConstraints.CHARACTER_LIMIT_DEFAULT, message = Validations.textTooLong) private String tracingAppDetails; - private ContactProximity contactProximity; + private Set contactProximities; @HideForCountriesExcept @SensitiveData @Size(max = FieldConstraints.CHARACTER_LIMIT_DEFAULT, message = Validations.textTooLong) @@ -219,15 +221,19 @@ public class ContactDto extends SormasToSormasShareableDto implements IsContact private ContactCategory contactCategory; private ContactClassification contactClassification; private ContactStatus contactStatus; - @Diseases(value = {Disease.INVASIVE_MENINGOCOCCAL_INFECTION}, hide = true) + @Diseases(value = { + Disease.INVASIVE_MENINGOCOCCAL_INFECTION }, hide = true) private FollowUpStatus followUpStatus; @SensitiveData @Size(max = CHARACTER_LIMIT_BIG, message = Validations.textTooLong) - @Diseases(value = {Disease.INVASIVE_MENINGOCOCCAL_INFECTION}, hide = true) + @Diseases(value = { + Disease.INVASIVE_MENINGOCOCCAL_INFECTION }, hide = true) private String followUpComment; - @Diseases(value = {Disease.INVASIVE_MENINGOCOCCAL_INFECTION}, hide = true) + @Diseases(value = { + Disease.INVASIVE_MENINGOCOCCAL_INFECTION }, hide = true) private Date followUpUntil; - @Diseases(value = {Disease.INVASIVE_MENINGOCOCCAL_INFECTION}, hide = true) + @Diseases(value = { + Disease.INVASIVE_MENINGOCOCCAL_INFECTION }, hide = true) private boolean overwriteFollowUpUntil; @SensitiveData @Size(max = CHARACTER_LIMIT_BIG, message = Validations.textTooLong) @@ -370,11 +376,15 @@ public class ContactDto extends SormasToSormasShareableDto implements IsContact @Outbreaks private VaccinationStatus vaccinationStatus; - @Diseases(value = {Disease.MEASLES}) - @HideForCountriesExcept(countries = {COUNTRY_CODE_LUXEMBOURG}) + @Diseases(value = { + Disease.MEASLES }) + @HideForCountriesExcept(countries = { + COUNTRY_CODE_LUXEMBOURG }) private Date vaccinationDoseOneDate; - @Diseases(value = {Disease.MEASLES}) - @HideForCountriesExcept(countries = {COUNTRY_CODE_LUXEMBOURG}) + @Diseases(value = { + Disease.MEASLES }) + @HideForCountriesExcept(countries = { + COUNTRY_CODE_LUXEMBOURG }) private Date vaccinationDoseTwoDate; private Date previousQuarantineTo; @@ -385,23 +395,30 @@ public class ContactDto extends SormasToSormasShareableDto implements IsContact private DeletionReason deletionReason; @Size(max = FieldConstraints.CHARACTER_LIMIT_TEXT, message = Validations.textTooLong) private String otherDeletionReason; - @HideForCountriesExcept(countries = {COUNTRY_CODE_LUXEMBOURG}) + @HideForCountriesExcept(countries = { + COUNTRY_CODE_LUXEMBOURG }) @SensitiveData @Diseases(Disease.INVASIVE_MENINGOCOCCAL_INFECTION) private Boolean prophylaxisPrescribed; - @HideForCountriesExcept(countries = {COUNTRY_CODE_LUXEMBOURG}) + @HideForCountriesExcept(countries = { + COUNTRY_CODE_LUXEMBOURG }) @SensitiveData @Diseases(Disease.INVASIVE_MENINGOCOCCAL_INFECTION) private Drug prescribedDrug; - @HideForCountriesExcept(countries = {COUNTRY_CODE_LUXEMBOURG}) + @HideForCountriesExcept(countries = { + COUNTRY_CODE_LUXEMBOURG }) @SensitiveData @Diseases(Disease.INVASIVE_MENINGOCOCCAL_INFECTION) private String prescribedDrugText; - @Diseases(value = {Disease.MEASLES}) - @HideForCountriesExcept(countries = {COUNTRY_CODE_LUXEMBOURG}) + @Diseases(value = { + Disease.MEASLES }) + @HideForCountriesExcept(countries = { + COUNTRY_CODE_LUXEMBOURG }) private boolean vaccinationProposed; - @Diseases(value = {Disease.MEASLES}) - @HideForCountriesExcept(countries = {COUNTRY_CODE_LUXEMBOURG}) + @Diseases(value = { + Disease.MEASLES }) + @HideForCountriesExcept(countries = { + COUNTRY_CODE_LUXEMBOURG }) private boolean immuneGlobulinProposed; public static ContactDto build() { @@ -543,12 +560,15 @@ public void setTracingAppDetails(String tracingAppDetails) { this.tracingAppDetails = tracingAppDetails; } - public ContactProximity getContactProximity() { - return contactProximity; + public Set getContactProximities() { + if (contactProximities == null) { + contactProximities = new HashSet<>(); + } + return contactProximities; } - public void setContactProximity(ContactProximity contactProximity) { - this.contactProximity = contactProximity; + public void setContactProximities(Set contactProximities) { + this.contactProximities = contactProximities; } public String getDescription() { diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactExportDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactExportDto.java index 8e0ac4942c1..20bc092b605 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactExportDto.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactExportDto.java @@ -120,7 +120,7 @@ public class ContactExportDto extends AbstractUuidDto implements IsContact { private TracingApp tracingApp; @SensitiveData private String tracingAppDetails; - private ContactProximity contactProximity; + private Set contactProximities; private ContactStatus contactStatus; private Float completeness; private FollowUpStatus followUpStatus; @@ -247,13 +247,17 @@ public class ContactExportDto extends AbstractUuidDto implements IsContact { private Boolean isInJurisdiction; //@formatter:off + /** + * Constructor for JPA queries where contactProximities cannot be directly selected (ElementCollection limitation). + * ContactProximities should be populated separately after query execution. + */ public ContactExportDto(long id, long personId, String uuid, String sourceCaseUuid, CaseClassification caseClassification, Disease disease, String diseaseDetails, ContactClassification contactClassification, Boolean multiDayContact, Date firstContactDate, Date lastContactDate, Date creationDate, String personUuid, String firstName, String lastName, String nationalHealthId, Salutation salutation, String otherSalutation, Sex sex, Integer birthdateDD, Integer birthdateMM, Integer birthdateYYYY, Integer approximateAge, ApproximateAgeType approximateAgeType, Date reportDate, ContactIdentificationSource contactIdentificationSource, - String contactIdentificationSourceDetails, TracingApp tracingApp, String tracingAppDetails, ContactProximity contactProximity, + String contactIdentificationSourceDetails, TracingApp tracingApp, String tracingAppDetails, Long contactIdPlaceholder, ContactStatus contactStatus, Float completeness, FollowUpStatus followUpStatus, Date followUpUntil, QuarantineType quarantine, String quarantineTypeDetails, Date quarantineFrom, Date quarantineTo, String quarantineHelpNeeded, boolean quarantineOrderedVerbally, boolean quarantineOrderedOfficialDocument, Date quarantineOrderedVerballyDate, Date quarantineOrderedOfficialDocumentDate, @@ -301,7 +305,7 @@ public ContactExportDto(long id, long personId, String uuid, String sourceCaseUu this.contactIdentificationSourceDetails = contactIdentificationSourceDetails; this.tracingApp = tracingApp; this.tracingAppDetails = tracingAppDetails; - this.contactProximity = contactProximity; + // contactProximities intentionally not set here - populated separately after query this.contactStatus = contactStatus; this.completeness = completeness; this.followUpStatus = followUpStatus; @@ -603,10 +607,10 @@ public String getTracingAppDetails() { } @Order(28) - @ExportProperty(ContactDto.CONTACT_PROXIMITY) + @ExportProperty(ContactDto.CONTACT_PROXIMITIES) @ExportGroup(ExportGroupType.CORE) - public ContactProximity getContactProximity() { - return contactProximity; + public Set getContactProximities() { + return contactProximities; } @Order(29) @@ -1368,8 +1372,8 @@ public void setTracingAppDetails(String tracingAppDetails) { this.tracingAppDetails = tracingAppDetails; } - public void setContactProximity(ContactProximity contactProximity) { - this.contactProximity = contactProximity; + public void setContactProximities(Set contactProximities) { + this.contactProximities = contactProximities; } public void setContactStatus(ContactStatus contactStatus) { diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDetailedDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDetailedDto.java index 22b51d8e215..5c7cff5a7d0 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDetailedDto.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDetailedDto.java @@ -65,7 +65,7 @@ public ContactIndexDetailedDto(String uuid, String personUuid, String nationalHe String cazeUuid, Disease disease, String diseaseDetails, String caseFirstName, String caseLastName, String regionName, String districtName, Date lastContactDate, ContactCategory contactCategory, - ContactProximity contactProximity, ContactClassification contactClassification, ContactStatus contactStatus, Float completeness, + Long id, ContactClassification contactClassification, ContactStatus contactStatus, Float completeness, FollowUpStatus followUpStatus, Date followUpUntil, SymptomJournalStatus symptomJournalStatus, VaccinationStatus vaccinationStatus, String contactOfficerUuid, String reportingUserUuid, Date reportDateTime, CaseClassification caseClassification, String caseRegionName, @@ -81,7 +81,7 @@ public ContactIndexDetailedDto(String uuid, String personUuid, String nationalHe //@formatter:off super(uuid, personUuid, nationalHealthId, personFirstName, personLastName, cazeUuid, disease, diseaseDetails, caseFirstName, caseLastName, - regionName, districtName, lastContactDate, contactCategory, contactProximity, contactClassification, contactStatus, + regionName, districtName, lastContactDate, contactCategory, id, contactClassification, contactStatus, completeness, followUpStatus, followUpUntil, symptomJournalStatus, vaccinationStatus, contactOfficerUuid, reportingUserUuid, reportDateTime, caseClassification, caseRegionName, caseDistrictName, changeDate, externalID, externalToken, internalToken, caseReferenceNumber, deletionReason, otherDeleteReason,isInJurisdiction, isCaseInJurisdiction , visitCount, prophylaxisPrescribed, prescribedDrug, prescribedDrugText); diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java index dcad2515070..21d1311ac50 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactIndexDto.java @@ -17,9 +17,6 @@ *******************************************************************************/ package de.symeda.sormas.api.contact; -import java.io.Serializable; -import java.util.Date; - import de.symeda.sormas.api.Disease; import de.symeda.sormas.api.caze.CaseClassification; import de.symeda.sormas.api.caze.CaseReferenceDto; @@ -31,6 +28,10 @@ import de.symeda.sormas.api.utils.pseudonymization.PseudonymizableIndexDto; import de.symeda.sormas.api.uuid.HasUuid; +import java.io.Serializable; +import java.util.Date; +import java.util.Set; + public class ContactIndexDto extends PseudonymizableIndexDto implements IsContact, HasUuid, Serializable, Cloneable { private static final long serialVersionUID = 7511900591141885152L; @@ -46,7 +47,7 @@ public class ContactIndexDto extends PseudonymizableIndexDto implements IsContac public static final String CAZE = "caze"; public static final String DISEASE = "disease"; public static final String LAST_CONTACT_DATE = "lastContactDate"; - public static final String CONTACT_PROXIMITY = "contactProximity"; + public static final String CONTACT_PROXIMITIES = "contactProximities"; public static final String CONTACT_CLASSIFICATION = "contactClassification"; public static final String CONTACT_STATUS = "contactStatus"; public static final String FOLLOW_UP_STATUS = "followUpStatus"; @@ -78,7 +79,7 @@ public class ContactIndexDto extends PseudonymizableIndexDto implements IsContac private Disease disease; private String diseaseDetails; private Date lastContactDate; - private ContactProximity contactProximity; + private Set contactProximities; private ContactClassification contactClassification; private ContactStatus contactStatus; private Float completeness; @@ -109,12 +110,17 @@ public class ContactIndexDto extends PseudonymizableIndexDto implements IsContac private String prescribedDrugText; private ContactJurisdictionFlagsDto contactJurisdictionFlagsDto; + private Long id; //@formatter:off + /** + * Constructor for JPA queries where contactProximities cannot be directly selected (ElementCollection limitation). + * ContactProximities should be populated separately after query execution using the id field. + */ public ContactIndexDto(String uuid, String personUuid, String nationalHealthId, String personFirstName, String personLastName, String cazeUuid, Disease disease, String diseaseDetails, String caseFirstName, String caseLastName, String regionName, String districtName, Date lastContactDate, ContactCategory contactCategory, - ContactProximity contactProximity, ContactClassification contactClassification, ContactStatus contactStatus, Float completeness, + Long id, ContactClassification contactClassification, ContactStatus contactStatus, Float completeness, FollowUpStatus followUpStatus, Date followUpUntil, SymptomJournalStatus symptomJournalStatus, VaccinationStatus vaccinationStatus, String contactOfficerUuid, String reportingUserUuid, Date reportDateTime, CaseClassification caseClassification, String caseRegionName, String caseDistrictName, @@ -138,7 +144,7 @@ public ContactIndexDto(String uuid, String personUuid, String nationalHealthId, this.diseaseDetails = diseaseDetails; this.lastContactDate = lastContactDate; this.contactCategory = contactCategory; - this.contactProximity = contactProximity; + this.id = id; this.contactClassification = contactClassification; this.contactStatus = contactStatus; this.completeness = completeness; @@ -232,12 +238,12 @@ public void setLastContactDate(Date lastContactDate) { this.lastContactDate = lastContactDate; } - public ContactProximity getContactProximity() { - return contactProximity; + public Set getContactProximities() { + return contactProximities; } - public void setContactProximity(ContactProximity contactProximity) { - this.contactProximity = contactProximity; + public void setContactProximities(Set contactProximities) { + this.contactProximities = contactProximities; } public ContactClassification getContactClassification() { @@ -464,6 +470,14 @@ public void setPrescribedDrugText(String prescribedDrugText) { this.prescribedDrugText = prescribedDrugText; } + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + @Override public Object clone() throws CloneNotSupportedException { return super.clone(); diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java index 9fb241f00e0..b3a9bd902b2 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/contact/SimilarContactDto.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.util.Date; +import java.util.Set; import de.symeda.sormas.api.caze.CaseReferenceDto; import de.symeda.sormas.api.utils.EmbeddedPersonalData; @@ -20,7 +21,7 @@ public class SimilarContactDto extends PseudonymizableIndexDto implements IsCont public static final String CASE_ID_EXTERNAL_SYSTEM = "caseIdExternalSystem"; public static final String CAZE = "caze"; public static final String LAST_CONTACT_DATE = "lastContactDate"; - public static final String CONTACT_PROXIMITY = "contactProximity"; + public static final String CONTACT_PROXIMITIES = "contactProximities"; public static final String CONTACT_CLASSIFICATION = "contactClassification"; public static final String CONTACT_STATUS = "contactStatus"; public static final String FOLLOW_UP_STATUS = "followUpStatus"; @@ -33,16 +34,17 @@ public class SimilarContactDto extends PseudonymizableIndexDto implements IsCont private CaseReferenceDto caze; private String caseIdExternalSystem; private Date lastContactDate; - private ContactProximity contactProximity; + private Set contactProximities; private ContactClassification contactClassification; private ContactStatus contactStatus; private FollowUpStatus followUpStatus; private ContactJurisdictionFlagsDto contactJurisdictionFlagsDto; + private Long id; //@formatter:off public SimilarContactDto(String firstName, String lastName, String uuid, String cazeUuid, String caseFirstName, String caseLastName, String caseIdExternalSystem, - Date lastContactDate, ContactProximity contactProximity, ContactClassification contactClassification, + Date lastContactDate, Long id, ContactClassification contactClassification, ContactStatus contactStatus, FollowUpStatus followUpStatus, boolean isInJurisdiction, boolean isCaseInJurisdiction) { //@formatter:on super(uuid); @@ -54,7 +56,7 @@ public SimilarContactDto(String firstName, String lastName, String uuid, } this.caseIdExternalSystem = caseIdExternalSystem; this.lastContactDate = lastContactDate; - this.contactProximity = contactProximity; + this.id = id; this.contactClassification = contactClassification; this.contactStatus = contactStatus; this.followUpStatus = followUpStatus; @@ -85,12 +87,12 @@ public void setLastContactDate(Date lastContactDate) { this.lastContactDate = lastContactDate; } - public ContactProximity getContactProximity() { - return contactProximity; + public Set getContactProximities() { + return contactProximities; } - public void setContactProximity(ContactProximity contactProximity) { - this.contactProximity = contactProximity; + public void setContactProximities(Set contactProximities) { + this.contactProximities = contactProximities; } public String getFirstName() { @@ -144,4 +146,12 @@ public Boolean getCaseInJurisdiction() { public ContactReferenceDto toReference() { return new ContactReferenceDto(getUuid(), getFirstName(), getLastName(), getCaze()); } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/Contact.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/Contact.java index e3b2386dd33..5049dd8e3ee 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/Contact.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/Contact.java @@ -27,7 +27,9 @@ import java.util.Set; import javax.persistence.CascadeType; +import javax.persistence.CollectionTable; import javax.persistence.Column; +import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; @@ -96,7 +98,7 @@ public class Contact extends CoreAdo implements IsContact, SormasToSormasShareab public static final String CONTACT_IDENTIFICATION_SOURCE = "contactIdentificationSource"; public static final String CONTACT_IDENTIFICATION_SOURCE_DETAILS = "contactIdentificationSourceDetails"; public static final String CONTACT_OFFICER = "contactOfficer"; - public static final String CONTACT_PROXIMITY = "contactProximity"; + public static final String CONTACT_PROXIMITIES = "contactProximities"; public static final String CONTACT_PROXIMITY_DETAILS = "contactProximityDetails"; public static final String CONTACT_STATUS = "contactStatus"; public static final String DESCRIPTION = "description"; @@ -168,12 +170,12 @@ public class Contact extends CoreAdo implements IsContact, SormasToSormasShareab public static final String VACCINATION_STATUS = "vaccinationStatus"; public static final String VISITS = "visits"; public static final String DUPLICATE_OF = "duplicateOf"; - public static final String SELF_REPORT ="selfReport"; - public static final String PROPHYLAXIS_PRESCRIBED ="prophylaxisPrescribed"; - public static final String PRESCRIBED_DRUG ="prescribedDrug"; - public static final String PRESCRIBED_DRUG_TEXT ="prescribedDrugText"; - public static final String VACCINATION_DOSE_ONE_DATE ="vaccinationDoseOneDate"; - public static final String VACCINATION_DOSE_TWO_DATE ="vaccinationDoseTwoDate"; + public static final String SELF_REPORT = "selfReport"; + public static final String PROPHYLAXIS_PRESCRIBED = "prophylaxisPrescribed"; + public static final String PRESCRIBED_DRUG = "prescribedDrug"; + public static final String PRESCRIBED_DRUG_TEXT = "prescribedDrugText"; + public static final String VACCINATION_DOSE_ONE_DATE = "vaccinationDoseOneDate"; + public static final String VACCINATION_DOSE_TWO_DATE = "vaccinationDoseTwoDate"; private Date reportDateTime; private User reportingUser; @@ -198,7 +200,7 @@ public class Contact extends CoreAdo implements IsContact, SormasToSormasShareab private String contactIdentificationSourceDetails; private TracingApp tracingApp; private String tracingAppDetails; - private ContactProximity contactProximity; + private Set contactProximities; private ContactClassification contactClassification; private ContactStatus contactStatus; private FollowUpStatus followUpStatus; @@ -440,13 +442,20 @@ public void setTracingAppDetails(String tracingAppDetails) { this.tracingAppDetails = tracingAppDetails; } + @ElementCollection(fetch = FetchType.LAZY) @Enumerated(EnumType.STRING) - public ContactProximity getContactProximity() { - return contactProximity; + @CollectionTable(name = "contact_contactproximities", + joinColumns = @JoinColumn(name = "contact_id", referencedColumnName = Contact.ID, nullable = false)) + @Column(name = "contactproximity", nullable = false) + public Set getContactProximities() { + if (contactProximities == null) { + contactProximities = new HashSet<>(); + } + return contactProximities; } - public void setContactProximity(ContactProximity contactProximity) { - this.contactProximity = contactProximity; + public void setContactProximities(Set contactProximities) { + this.contactProximities = contactProximities; } @Column(length = CHARACTER_LIMIT_DEFAULT) diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactFacadeEjb.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactFacadeEjb.java index 7253852c230..b16b793b29b 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactFacadeEjb.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactFacadeEjb.java @@ -31,6 +31,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -67,7 +68,6 @@ import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; -import de.symeda.sormas.api.CountryHelper; import org.apache.commons.collections4.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,6 +96,7 @@ import de.symeda.sormas.api.contact.ContactJurisdictionFlagsDto; import de.symeda.sormas.api.contact.ContactListEntryDto; import de.symeda.sormas.api.contact.ContactLogic; +import de.symeda.sormas.api.contact.ContactProximity; import de.symeda.sormas.api.contact.ContactReferenceDto; import de.symeda.sormas.api.contact.ContactSimilarityCriteria; import de.symeda.sormas.api.contact.ContactStatus; @@ -735,7 +736,7 @@ public List getExportList( contact.get(Contact.CONTACT_IDENTIFICATION_SOURCE_DETAILS), contact.get(Contact.TRACING_APP), contact.get(Contact.TRACING_APP_DETAILS), - contact.get(Contact.CONTACT_PROXIMITY), + contact.get(Contact.ID), contact.get(Contact.CONTACT_STATUS), contact.get(Contact.COMPLETENESS), contact.get(Contact.FOLLOW_UP_STATUS), @@ -815,7 +816,12 @@ public List getExportList( List resultContactsUuids = exportContacts.stream().map(ContactExportDto::getUuid).collect(Collectors.toList()); if (!exportContacts.isEmpty()) { - List exportContactIds = exportContacts.stream().map(e -> e.getId()).collect(Collectors.toList()); + List exportContactIds = exportContacts.stream().map(ContactExportDto::getId).collect(Collectors.toList()); + + // Populate contactProximities separately (ElementCollection cannot be selected in multiselect) + Map> contactProximitiesMap = service.getContactProximitiesByContactIds(exportContactIds); + exportContacts.forEach( + exportContact -> exportContact.setContactProximities(contactProximitiesMap.getOrDefault(exportContact.getId(), new HashSet<>()))); List visitSummaries = null; if (ExportHelper.shouldExportFields( @@ -1339,6 +1345,13 @@ public List getIndexList(ContactCriteria contactCriteria, Integ dtos.addAll(QueryHelper.getResultList(em, cq, new ContactIndexDtoResultTransformer(), null, null)); }); + // Populate contactProximities separately (ElementCollection cannot be selected in multiselect) + if (!dtos.isEmpty()) { + List contactIds = dtos.stream().map(ContactIndexDto::getId).collect(Collectors.toList()); + Map> contactProximitiesMap = service.getContactProximitiesByContactIds(contactIds); + dtos.forEach(dto -> dto.setContactProximities(contactProximitiesMap.getOrDefault(dto.getId(), new HashSet<>()))); + } + Pseudonymizer pseudonymizer = createGenericPlaceholderPseudonymizer(createSpecialAccessChecker(dtos)); pseudonymizer.pseudonymizeDtoCollection(ContactIndexDto.class, dtos, ContactIndexDto::getInJurisdiction, (c, isInJurisdiction) -> { if (c.getCaze() != null) { @@ -1388,6 +1401,13 @@ public List getIndexDetailedList( dtos.addAll(QueryHelper.getResultList(em, cq, new ContactIndexDetailedDtoResultTransformer(), null, null)); }); + // Populate contactProximities separately (ElementCollection cannot be selected in multiselect) + if (!dtos.isEmpty()) { + List contactIds = dtos.stream().map(ContactIndexDetailedDto::getId).collect(Collectors.toList()); + Map> contactProximitiesMap = service.getContactProximitiesByContactIds(contactIds); + dtos.forEach(dto -> dto.setContactProximities(contactProximitiesMap.getOrDefault(dto.getId(), new HashSet<>()))); + } + if (userService.hasRight(UserRight.EVENT_VIEW)) { // Load event count and latest events info per contact Map> eventSummaries = @@ -1543,7 +1563,7 @@ public Contact fillOrBuildEntity(@NotNull ContactDto source, Contact target, boo target.setContactIdentificationSourceDetails(source.getContactIdentificationSourceDetails()); target.setTracingApp(source.getTracingApp()); target.setTracingAppDetails(source.getTracingAppDetails()); - target.setContactProximity(source.getContactProximity()); + target.setContactProximities(source.getContactProximities()); if (source.getContactClassification() != null) { target.setContactClassification(source.getContactClassification()); } @@ -1895,7 +1915,7 @@ public static ContactDto toContactDto(Contact source) { target.setContactIdentificationSourceDetails(source.getContactIdentificationSourceDetails()); target.setTracingApp(source.getTracingApp()); target.setTracingAppDetails(source.getTracingAppDetails()); - target.setContactProximity(source.getContactProximity()); + target.setContactProximities(source.getContactProximities() != null ? new HashSet<>(source.getContactProximities()) : null); target.setContactClassification(source.getContactClassification()); target.setContactStatus(source.getContactStatus()); target.setFollowUpStatus(source.getFollowUpStatus()); @@ -2121,7 +2141,7 @@ public List getMatchingContacts(ContactSimilarityCriteria cri joins.getCasePerson().get(Person.LAST_NAME), contactRoot.get(Contact.CASE_ID_EXTERNAL_SYSTEM), contactRoot.get(Contact.LAST_CONTACT_DATE), - contactRoot.get(Contact.CONTACT_PROXIMITY), + contactRoot.get(Contact.ID), contactRoot.get(Contact.CONTACT_CLASSIFICATION), contactRoot.get(Contact.CONTACT_STATUS), contactRoot.get(Contact.FOLLOW_UP_STATUS))); @@ -2133,6 +2153,13 @@ public List getMatchingContacts(ContactSimilarityCriteria cri List contacts = em.createQuery(cq).getResultList(); + // Populate contactProximities separately (ElementCollection cannot be selected in multiselect) + if (!contacts.isEmpty()) { + Map> contactProximitiesMap = + service.getContactProximitiesByContactIds(contacts.stream().map(c -> c.getId()).collect(Collectors.toList())); + contacts.forEach(contact -> contact.setContactProximities(contactProximitiesMap.getOrDefault(contact.getId(), new HashSet<>()))); + } + Pseudonymizer pseudonymizer = createGenericPseudonymizer(createSpecialAccessChecker(contacts)); pseudonymizer.pseudonymizeDtoCollection(SimilarContactDto.class, contacts, SimilarContactDto::getInJurisdiction, null, false); @@ -2396,7 +2423,7 @@ private float calculateCompleteness(Contact contact) { if (contact.getContactCategory() != null) { completeness += 0.1f; } - if (contact.getContactProximity() != null) { + if (contact.getContactProximities() != null && !contact.getContactProximities().isEmpty()) { completeness += 0.1f; } if (contact.getContactStatus() != null && !ContactStatus.ACTIVE.equals(contact.getContactStatus())) { diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDetailedDtoResultTransformer.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDetailedDtoResultTransformer.java index 51458026648..cb483942e5c 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDetailedDtoResultTransformer.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDetailedDtoResultTransformer.java @@ -27,7 +27,6 @@ import de.symeda.sormas.api.contact.ContactCategory; import de.symeda.sormas.api.contact.ContactClassification; import de.symeda.sormas.api.contact.ContactIndexDetailedDto; -import de.symeda.sormas.api.contact.ContactProximity; import de.symeda.sormas.api.contact.ContactRelation; import de.symeda.sormas.api.contact.ContactStatus; import de.symeda.sormas.api.contact.FollowUpStatus; @@ -49,7 +48,7 @@ public ContactIndexDetailedDto transformTuple(Object[] tuple, String[] aliases) (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (Disease) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (Date) tuple[++index], (ContactCategory) tuple[++index], - (ContactProximity) tuple[++index], (ContactClassification) tuple[++index], (ContactStatus) tuple[++index], (Float) tuple[++index], + (Long) tuple[++index], (ContactClassification) tuple[++index], (ContactStatus) tuple[++index], (Float) tuple[++index], (FollowUpStatus) tuple[++index], (Date) tuple[++index], (SymptomJournalStatus) tuple[++index], (VaccinationStatus) tuple[++index], (String) tuple[++index], (String) tuple[++index], (Date) tuple[++index], (CaseClassification) tuple[++index], (String) tuple[++index], (String) tuple[++index], diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDtoResultTransformer.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDtoResultTransformer.java index f98614a5d52..593dd1c9b19 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDtoResultTransformer.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactIndexDtoResultTransformer.java @@ -27,7 +27,6 @@ import de.symeda.sormas.api.contact.ContactCategory; import de.symeda.sormas.api.contact.ContactClassification; import de.symeda.sormas.api.contact.ContactIndexDto; -import de.symeda.sormas.api.contact.ContactProximity; import de.symeda.sormas.api.contact.ContactStatus; import de.symeda.sormas.api.contact.FollowUpStatus; import de.symeda.sormas.api.person.SymptomJournalStatus; @@ -45,7 +44,7 @@ public ContactIndexDto transformTuple(Object[] tuple, String[] aliases) { (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (Disease) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (String) tuple[++index], (Date) tuple[++index], (ContactCategory) tuple[++index], - (ContactProximity) tuple[++index], (ContactClassification) tuple[++index], (ContactStatus) tuple[++index], (Float) tuple[++index], + (Long) tuple[++index], (ContactClassification) tuple[++index], (ContactStatus) tuple[++index], (Float) tuple[++index], (FollowUpStatus) tuple[++index], (Date) tuple[++index], (SymptomJournalStatus) tuple[++index], (VaccinationStatus) tuple[++index], (String) tuple[++index], (String) tuple[++index], (Date) tuple[++index], (CaseClassification) tuple[++index], (String) tuple[++index], (String) tuple[++index], diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactListCriteriaBuilder.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactListCriteriaBuilder.java index cd13b36aebe..0a7df5a4147 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactListCriteriaBuilder.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactListCriteriaBuilder.java @@ -108,7 +108,7 @@ public List> getContactIndexSelections(Root contact, Conta joins.getDistrict().get(District.NAME), contact.get(Contact.LAST_CONTACT_DATE), contact.get(Contact.CONTACT_CATEGORY), - contact.get(Contact.CONTACT_PROXIMITY), + contact.get(Contact.ID), contact.get(Contact.CONTACT_CLASSIFICATION), contact.get(Contact.CONTACT_STATUS), contact.get(Contact.COMPLETENESS), @@ -183,7 +183,7 @@ private List> getIndexOrders(SortProperty sortProperty, Root contactProximities = contact.getContactProximities(); + if (!diseaseConfigurationFacade.hasFollowUp(disease) + || (contactProximities != null && !contactProximities.isEmpty() + && contactProximities.stream().noneMatch(ContactProximity::hasFollowUp))) { contact.setFollowUpUntil(null); contact.setOverwriteFollowUpUntil(false); if (changeStatus) { @@ -2026,4 +2029,32 @@ protected String getDeleteReferenceField(DeletionReference deletionReference) { return super.getDeleteReferenceField(deletionReference); } + + /** + * Fetches contactProximities for a list of contact IDs. + * This is needed because @ElementCollection fields cannot be directly selected in Criteria API multiselect queries. + * + * @param contactIds List of contact IDs + * @return Map of contact ID to Set of ContactProximity + */ + public Map> getContactProximitiesByContactIds(List contactIds) { + if (contactIds == null || contactIds.isEmpty()) { + return new HashMap<>(); + } + + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(Contact.class); + Root from = cq.from(Contact.class); + + cq.where(from.get(Contact.ID).in(contactIds)); + cq.select(from); + + List contacts = em.createQuery(cq).getResultList(); + + return contacts.stream() + .collect(Collectors.toMap( + Contact::getId, + contact -> contact.getContactProximities() != null ? new HashSet<>(contact.getContactProximities()) : new HashSet<>() + )); + } } diff --git a/sormas-backend/src/main/resources/sql/sormas_schema.sql b/sormas-backend/src/main/resources/sql/sormas_schema.sql index 3b941bd6e76..3b5f1e33971 100644 --- a/sormas-backend/src/main/resources/sql/sormas_schema.sql +++ b/sormas-backend/src/main/resources/sql/sormas_schema.sql @@ -13893,7 +13893,7 @@ CREATE TRIGGER delete_history_trigger FOR EACH ROW EXECUTE PROCEDURE delete_history_trigger('systemconfigurationcategory_history', 'id'); ALTER TABLE systemconfigurationcategory_history OWNER TO sormas_user; -INSERT INTO systemconfigurationcategory(id, uuid, changedate, creationdate, name, caption, description) +INSERT INTO systemconfigurationcategory(id, uuid, changedate, creationdate, name, caption, description) VALUES (nextval('entity_seq'), generate_base32_uuid(), now(), now(), 'GENERAL_CATEGORY', 'i18n/General/categoryGeneral', 'i18n/General/categoryGeneral'); CREATE TABLE systemconfigurationvalue ( @@ -15062,4 +15062,50 @@ ALTER TABLE testreport_history ADD COLUMN serotype character varying(255); ALTER TABLE testreport_history ADD COLUMN straincallstatus character varying(255); INSERT INTO schema_version (version_number, comment) VALUES (602, 'External message additional fields'); + +-- 2025-12-11 - Convert contact proximity from single value to multi-select collection +CREATE TABLE contact_contactproximities ( + contact_id bigint NOT NULL, + contactproximity varchar(255) NOT NULL, + sys_period tstzrange NOT NULL +); + +ALTER TABLE contact_contactproximities OWNER TO sormas_user; +ALTER TABLE contact_contactproximities + ADD CONSTRAINT pk_contact_contactproximities PRIMARY KEY (contact_id, contactproximity); +ALTER TABLE contact_contactproximities + ADD CONSTRAINT fk_contact_contactproximities_contact_id + FOREIGN KEY (contact_id) REFERENCES contact(id); +CREATE TABLE contact_contactproximities_history (LIKE contact_contactproximities); +ALTER TABLE contact_contactproximities_history OWNER TO sormas_user; + +CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE ON contact_contactproximities + FOR EACH ROW EXECUTE PROCEDURE versioning('sys_period', 'contact_contactproximities_history', true); + +DROP TRIGGER IF EXISTS delete_history_trigger_contact_contactproximities ON contact; +CREATE TRIGGER delete_history_trigger_contact_contactproximities + AFTER DELETE ON contact + FOR EACH ROW EXECUTE PROCEDURE delete_history_trigger('contact_contactproximities_history', 'contact_id'); + +DROP TRIGGER IF EXISTS delete_history_trigger ON contact; +CREATE TRIGGER delete_history_trigger + AFTER DELETE ON contact + FOR EACH ROW EXECUTE PROCEDURE delete_history_trigger('contact_history', 'id'); + +DROP TRIGGER IF EXISTS delete_history_trigger_contacts_visits ON contact; +CREATE TRIGGER delete_history_trigger_contacts_visits + AFTER DELETE ON contact + FOR EACH ROW EXECUTE PROCEDURE delete_history_trigger('contacts_visits_history', 'contact_id'); + +INSERT INTO contact_contactproximities (contact_id, contactproximity, sys_period) +SELECT id, contactproximity, tstzrange(now(), null) +FROM contact +WHERE contactproximity IS NOT NULL; + +-- Drop old single-value column from both main and history tables (if exists) +ALTER TABLE contact DROP COLUMN IF EXISTS contactproximity; +ALTER TABLE contact_history DROP COLUMN IF EXISTS contactproximity; + +INSERT INTO schema_version (version_number, comment) VALUES (603, 'Change contactProximity to multi-select collection'); -- *** Insert new sql commands BEFORE this line. Remember to always consider _history tables. *** diff --git a/sormas-backend/src/test/java/de/symeda/sormas/backend/contact/ContactFacadeEjbTest.java b/sormas-backend/src/test/java/de/symeda/sormas/backend/contact/ContactFacadeEjbTest.java index 663f8a4bff6..273dd2d6ec9 100644 --- a/sormas-backend/src/test/java/de/symeda/sormas/backend/contact/ContactFacadeEjbTest.java +++ b/sormas-backend/src/test/java/de/symeda/sormas/backend/contact/ContactFacadeEjbTest.java @@ -2346,4 +2346,242 @@ public void testContactsForDashboard() { assertEquals(1, dashboardContactDtos.size()); assertEquals(VisitStatus.COOPERATIVE, dashboardContactDtos.get(0).getLastVisitStatus()); } + + @Test + public void testContactProximitiesMultiSelect() { + RDCF rdcf = creator.createRDCF(); + UserDto user = creator.createSurveillanceSupervisor(rdcf); + PersonDto cazePerson = creator.createPerson("Case", "Person"); + CaseDataDto caze = creator.createCase( + user.toReference(), + cazePerson.toReference(), + Disease.EVD, + CaseClassification.PROBABLE, + InvestigationStatus.PENDING, + new Date(), + rdcf); + PersonDto contactPerson = creator.createPerson("Contact", "Person"); + + ContactDto contact = creator.createContact( + user.toReference(), + user.toReference(), + contactPerson.toReference(), + caze, + new Date(), + new Date(), + null); + + Set proximities = new HashSet<>(); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.TOUCHED_FLUID); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.PHYSICAL_CONTACT); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.SAME_ROOM); + + contact.setContactProximities(proximities); + contact = getContactFacade().save(contact); + + ContactDto savedContact = getContactFacade().getByUuid(contact.getUuid()); + assertNotNull(savedContact.getContactProximities()); + assertEquals(3, savedContact.getContactProximities().size()); + assertTrue(savedContact.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.TOUCHED_FLUID)); + assertTrue(savedContact.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.PHYSICAL_CONTACT)); + assertTrue(savedContact.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.SAME_ROOM)); + } + + @Test + public void testContactProximitiesEmptySet() { + RDCF rdcf = creator.createRDCF(); + UserDto user = creator.createSurveillanceSupervisor(rdcf); + PersonDto cazePerson = creator.createPerson("Case", "Person"); + CaseDataDto caze = creator.createCase( + user.toReference(), + cazePerson.toReference(), + Disease.EVD, + CaseClassification.PROBABLE, + InvestigationStatus.PENDING, + new Date(), + rdcf); + PersonDto contactPerson = creator.createPerson("Contact", "Person"); + + ContactDto contact = creator.createContact( + user.toReference(), + user.toReference(), + contactPerson.toReference(), + caze, + new Date(), + new Date(), + null); + + contact.setContactProximities(new HashSet<>()); + contact = getContactFacade().save(contact); + + ContactDto savedContact = getContactFacade().getByUuid(contact.getUuid()); + assertNotNull(savedContact.getContactProximities()); + assertEquals(0, savedContact.getContactProximities().size()); + } + + @Test + public void testContactProximitiesSingleValue() { + RDCF rdcf = creator.createRDCF(); + UserDto user = creator.createSurveillanceSupervisor(rdcf); + PersonDto cazePerson = creator.createPerson("Case", "Person"); + CaseDataDto caze = creator.createCase( + user.toReference(), + cazePerson.toReference(), + Disease.EVD, + CaseClassification.PROBABLE, + InvestigationStatus.PENDING, + new Date(), + rdcf); + PersonDto contactPerson = creator.createPerson("Contact", "Person"); + + ContactDto contact = creator.createContact( + user.toReference(), + user.toReference(), + contactPerson.toReference(), + caze, + new Date(), + new Date(), + null); + + Set proximities = new HashSet<>(); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.CLOSE_CONTACT); + + contact.setContactProximities(proximities); + contact = getContactFacade().save(contact); + + ContactDto savedContact = getContactFacade().getByUuid(contact.getUuid()); + assertNotNull(savedContact.getContactProximities()); + assertEquals(1, savedContact.getContactProximities().size()); + assertTrue(savedContact.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.CLOSE_CONTACT)); + } + + @Test + public void testContactProximitiesInIndexDto() { + RDCF rdcf = creator.createRDCF(); + UserDto user = creator.createSurveillanceSupervisor(rdcf); + PersonDto cazePerson = creator.createPerson("Case", "Person"); + CaseDataDto caze = creator.createCase( + user.toReference(), + cazePerson.toReference(), + Disease.EVD, + CaseClassification.PROBABLE, + InvestigationStatus.PENDING, + new Date(), + rdcf); + PersonDto contactPerson = creator.createPerson("Contact", "Person"); + + ContactDto contact = creator.createContact( + user.toReference(), + user.toReference(), + contactPerson.toReference(), + caze, + new Date(), + new Date(), + null); + + Set proximities = new HashSet<>(); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.TOUCHED_FLUID); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.MEDICAL_UNSAFE); + + contact.setContactProximities(proximities); + contact = getContactFacade().save(contact); + + final String contactUuid = contact.getUuid(); + ContactCriteria criteria = new ContactCriteria(); + List indexList = getContactFacade().getIndexList(criteria, 0, 100, null); + + ContactIndexDto indexDto = indexList.stream().filter(c -> c.getUuid().equals(contactUuid)).findFirst().orElse(null); + assertNotNull(indexDto); + assertNotNull(indexDto.getContactProximities()); + assertEquals(2, indexDto.getContactProximities().size()); + assertTrue(indexDto.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.TOUCHED_FLUID)); + assertTrue(indexDto.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.MEDICAL_UNSAFE)); + } + + @Test + public void testContactProximitiesInDetailedIndexDto() { + RDCF rdcf = creator.createRDCF(); + UserDto user = creator.createSurveillanceSupervisor(rdcf); + PersonDto cazePerson = creator.createPerson("Case", "Person"); + CaseDataDto caze = creator.createCase( + user.toReference(), + cazePerson.toReference(), + Disease.EVD, + CaseClassification.PROBABLE, + InvestigationStatus.PENDING, + new Date(), + rdcf); + PersonDto contactPerson = creator.createPerson("Contact", "Person"); + + ContactDto contact = creator.createContact( + user.toReference(), + user.toReference(), + contactPerson.toReference(), + caze, + new Date(), + new Date(), + null); + + Set proximities = new HashSet<>(); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.AEROSOL); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.FACE_TO_FACE_LONG); + + contact.setContactProximities(proximities); + contact = getContactFacade().save(contact); + + final String contactUuid = contact.getUuid(); + ContactCriteria criteria = new ContactCriteria(); + List detailedList = getContactFacade().getIndexDetailedList(criteria, 0, 100, null); + + ContactIndexDetailedDto detailedDto = detailedList.stream().filter(c -> c.getUuid().equals(contactUuid)).findFirst().orElse(null); + assertNotNull(detailedDto); + assertNotNull(detailedDto.getContactProximities()); + assertEquals(2, detailedDto.getContactProximities().size()); + assertTrue(detailedDto.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.AEROSOL)); + assertTrue(detailedDto.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.FACE_TO_FACE_LONG)); + } + + @Test + public void testContactProximitiesUpdate() { + RDCF rdcf = creator.createRDCF(); + UserDto user = creator.createSurveillanceSupervisor(rdcf); + PersonDto cazePerson = creator.createPerson("Case", "Person"); + CaseDataDto caze = creator.createCase( + user.toReference(), + cazePerson.toReference(), + Disease.EVD, + CaseClassification.PROBABLE, + InvestigationStatus.PENDING, + new Date(), + rdcf); + PersonDto contactPerson = creator.createPerson("Contact", "Person"); + + ContactDto contact = creator.createContact( + user.toReference(), + user.toReference(), + contactPerson.toReference(), + caze, + new Date(), + new Date(), + null); + + Set proximities = new HashSet<>(); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.TOUCHED_FLUID); + contact.setContactProximities(proximities); + contact = getContactFacade().save(contact); + + ContactDto savedContact = getContactFacade().getByUuid(contact.getUuid()); + assertEquals(1, savedContact.getContactProximities().size()); + + proximities.add(de.symeda.sormas.api.contact.ContactProximity.PHYSICAL_CONTACT); + proximities.add(de.symeda.sormas.api.contact.ContactProximity.CLOSE_CONTACT); + savedContact.setContactProximities(proximities); + savedContact = getContactFacade().save(savedContact); + + ContactDto updatedContact = getContactFacade().getByUuid(contact.getUuid()); + assertEquals(3, updatedContact.getContactProximities().size()); + assertTrue(updatedContact.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.TOUCHED_FLUID)); + assertTrue(updatedContact.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.PHYSICAL_CONTACT)); + assertTrue(updatedContact.getContactProximities().contains(de.symeda.sormas.api.contact.ContactProximity.CLOSE_CONTACT)); + } } diff --git a/sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.cmp b/sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.cmp index 64a29aacd26..d517e91306d 100644 --- a/sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.cmp +++ b/sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.cmp @@ -17,7 +17,7 @@ Yes Empty Properties: -./. +[] ./. diff --git a/sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.txt b/sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.txt index 0e6a6d9d10e..f6a80f87b67 100644 --- a/sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.txt +++ b/sormas-backend/src/test/resources/docgeneration/emailTemplates/contacts/ContactEmail.txt @@ -19,7 +19,7 @@ $contact_returningTraveler Empty Properties: -$contact_contactProximity +$contact_contactProximities $contact_person_address_additionalInformation $!contact_person_address_addressTypeDetails $!contact_additionalDetails diff --git a/sormas-rest/swagger.json b/sormas-rest/swagger.json index 094ab7e1fdf..a52291f2691 100644 --- a/sormas-rest/swagger.json +++ b/sormas-rest/swagger.json @@ -13630,9 +13630,12 @@ "contactOfficer" : { "$ref" : "#/components/schemas/UserReferenceDto" }, - "contactProximity" : { - "type" : "string", - "enum" : [ "TOUCHED_FLUID", "PHYSICAL_CONTACT", "CLOTHES_OR_OTHER", "CLOSE_CONTACT", "FACE_TO_FACE_LONG", "MEDICAL_UNSAFE", "SAME_ROOM", "AIRPLANE", "FACE_TO_FACE_SHORT", "MEDICAL_SAFE", "MEDICAL_SAME_ROOM", "AEROSOL", "MEDICAL_DISTANT", "MEDICAL_LIMITED" ] + "contactProximities" : { + "type" : "array", + "items" : { + "type" : "string", + "enum" : [ "TOUCHED_FLUID", "PHYSICAL_CONTACT", "CLOTHES_OR_OTHER", "CLOSE_CONTACT", "FACE_TO_FACE_LONG", "MEDICAL_UNSAFE", "SAME_ROOM", "AIRPLANE", "FACE_TO_FACE_SHORT", "MEDICAL_SAFE", "MEDICAL_SAME_ROOM", "AEROSOL", "MEDICAL_DISTANT", "MEDICAL_LIMITED" ] + } }, "contactProximityDetails" : { "type" : "string", @@ -13977,9 +13980,12 @@ "contactOfficerUuid" : { "type" : "string" }, - "contactProximity" : { - "type" : "string", - "enum" : [ "TOUCHED_FLUID", "PHYSICAL_CONTACT", "CLOTHES_OR_OTHER", "CLOSE_CONTACT", "FACE_TO_FACE_LONG", "MEDICAL_UNSAFE", "SAME_ROOM", "AIRPLANE", "FACE_TO_FACE_SHORT", "MEDICAL_SAFE", "MEDICAL_SAME_ROOM", "AEROSOL", "MEDICAL_DISTANT", "MEDICAL_LIMITED" ] + "contactProximities" : { + "type" : "array", + "items" : { + "type" : "string", + "enum" : [ "TOUCHED_FLUID", "PHYSICAL_CONTACT", "CLOTHES_OR_OTHER", "CLOSE_CONTACT", "FACE_TO_FACE_LONG", "MEDICAL_UNSAFE", "SAME_ROOM", "AIRPLANE", "FACE_TO_FACE_SHORT", "MEDICAL_SAFE", "MEDICAL_SAME_ROOM", "AEROSOL", "MEDICAL_DISTANT", "MEDICAL_LIMITED" ] + } }, "contactStatus" : { "type" : "string", @@ -14145,9 +14151,12 @@ "contactOfficerUuid" : { "type" : "string" }, - "contactProximity" : { - "type" : "string", - "enum" : [ "TOUCHED_FLUID", "PHYSICAL_CONTACT", "CLOTHES_OR_OTHER", "CLOSE_CONTACT", "FACE_TO_FACE_LONG", "MEDICAL_UNSAFE", "SAME_ROOM", "AIRPLANE", "FACE_TO_FACE_SHORT", "MEDICAL_SAFE", "MEDICAL_SAME_ROOM", "AEROSOL", "MEDICAL_DISTANT", "MEDICAL_LIMITED" ] + "contactProximities" : { + "type" : "array", + "items" : { + "type" : "string", + "enum" : [ "TOUCHED_FLUID", "PHYSICAL_CONTACT", "CLOTHES_OR_OTHER", "CLOSE_CONTACT", "FACE_TO_FACE_LONG", "MEDICAL_UNSAFE", "SAME_ROOM", "AIRPLANE", "FACE_TO_FACE_SHORT", "MEDICAL_SAFE", "MEDICAL_SAME_ROOM", "AEROSOL", "MEDICAL_DISTANT", "MEDICAL_LIMITED" ] + } }, "contactStatus" : { "type" : "string", diff --git a/sormas-rest/swagger.yaml b/sormas-rest/swagger.yaml index 944023b08a4..412f6671307 100644 --- a/sormas-rest/swagger.yaml +++ b/sormas-rest/swagger.yaml @@ -11639,23 +11639,25 @@ components: minLength: 0 contactOfficer: $ref: '#/components/schemas/UserReferenceDto' - contactProximity: - type: string - enum: - - TOUCHED_FLUID - - PHYSICAL_CONTACT - - CLOTHES_OR_OTHER - - CLOSE_CONTACT - - FACE_TO_FACE_LONG - - MEDICAL_UNSAFE - - SAME_ROOM - - AIRPLANE - - FACE_TO_FACE_SHORT - - MEDICAL_SAFE - - MEDICAL_SAME_ROOM - - AEROSOL - - MEDICAL_DISTANT - - MEDICAL_LIMITED + contactProximities: + type: array + items: + type: string + enum: + - TOUCHED_FLUID + - PHYSICAL_CONTACT + - CLOTHES_OR_OTHER + - CLOSE_CONTACT + - FACE_TO_FACE_LONG + - MEDICAL_UNSAFE + - SAME_ROOM + - AIRPLANE + - FACE_TO_FACE_SHORT + - MEDICAL_SAFE + - MEDICAL_SAME_ROOM + - AEROSOL + - MEDICAL_DISTANT + - MEDICAL_LIMITED contactProximityDetails: type: string maxLength: 512 @@ -12041,23 +12043,25 @@ components: - NO_CONTACT contactOfficerUuid: type: string - contactProximity: - type: string - enum: - - TOUCHED_FLUID - - PHYSICAL_CONTACT - - CLOTHES_OR_OTHER - - CLOSE_CONTACT - - FACE_TO_FACE_LONG - - MEDICAL_UNSAFE - - SAME_ROOM - - AIRPLANE - - FACE_TO_FACE_SHORT - - MEDICAL_SAFE - - MEDICAL_SAME_ROOM - - AEROSOL - - MEDICAL_DISTANT - - MEDICAL_LIMITED + contactProximities: + type: array + items: + type: string + enum: + - TOUCHED_FLUID + - PHYSICAL_CONTACT + - CLOTHES_OR_OTHER + - CLOSE_CONTACT + - FACE_TO_FACE_LONG + - MEDICAL_UNSAFE + - SAME_ROOM + - AIRPLANE + - FACE_TO_FACE_SHORT + - MEDICAL_SAFE + - MEDICAL_SAME_ROOM + - AEROSOL + - MEDICAL_DISTANT + - MEDICAL_LIMITED contactStatus: type: string enum: @@ -12281,23 +12285,25 @@ components: - NO_CONTACT contactOfficerUuid: type: string - contactProximity: - type: string - enum: - - TOUCHED_FLUID - - PHYSICAL_CONTACT - - CLOTHES_OR_OTHER - - CLOSE_CONTACT - - FACE_TO_FACE_LONG - - MEDICAL_UNSAFE - - SAME_ROOM - - AIRPLANE - - FACE_TO_FACE_SHORT - - MEDICAL_SAFE - - MEDICAL_SAME_ROOM - - AEROSOL - - MEDICAL_DISTANT - - MEDICAL_LIMITED + contactProximities: + type: array + items: + type: string + enum: + - TOUCHED_FLUID + - PHYSICAL_CONTACT + - CLOTHES_OR_OTHER + - CLOSE_CONTACT + - FACE_TO_FACE_LONG + - MEDICAL_UNSAFE + - SAME_ROOM + - AIRPLANE + - FACE_TO_FACE_SHORT + - MEDICAL_SAFE + - MEDICAL_SAME_ROOM + - AEROSOL + - MEDICAL_DISTANT + - MEDICAL_LIMITED contactStatus: type: string enum: diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/AbstractContactGrid.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/AbstractContactGrid.java index 7601e9c4668..d04a0bd4685 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/AbstractContactGrid.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/AbstractContactGrid.java @@ -17,21 +17,16 @@ *******************************************************************************/ package de.symeda.sormas.ui.contact; -import java.text.DecimalFormat; -import java.util.Date; -import java.util.List; -import java.util.stream.Stream; - import com.vaadin.navigator.View; import com.vaadin.ui.Label; import com.vaadin.ui.renderers.DateRenderer; - import de.symeda.sormas.api.CountryHelper; import de.symeda.sormas.api.DiseaseHelper; import de.symeda.sormas.api.FacadeProvider; import de.symeda.sormas.api.caze.CaseIndexDto; import de.symeda.sormas.api.contact.ContactCriteria; import de.symeda.sormas.api.contact.ContactIndexDto; +import de.symeda.sormas.api.contact.ContactProximity; import de.symeda.sormas.api.contact.FollowUpStatus; import de.symeda.sormas.api.feature.FeatureType; import de.symeda.sormas.api.i18n.Captions; @@ -43,13 +38,14 @@ import de.symeda.sormas.ui.ControllerProvider; import de.symeda.sormas.ui.UiUtil; import de.symeda.sormas.ui.ViewModelProviders; -import de.symeda.sormas.ui.utils.CssStyles; -import de.symeda.sormas.ui.utils.DateFormatHelper; -import de.symeda.sormas.ui.utils.FieldAccessColumnStyleGenerator; -import de.symeda.sormas.ui.utils.FilteredGrid; -import de.symeda.sormas.ui.utils.ShowDetailsListener; -import de.symeda.sormas.ui.utils.UuidRenderer; -import de.symeda.sormas.ui.utils.ViewConfiguration; +import de.symeda.sormas.ui.utils.*; + +import java.text.DecimalFormat; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; @SuppressWarnings("serial") public abstract class AbstractContactGrid extends FilteredGrid { @@ -167,7 +163,18 @@ protected void initColumns() { getColumn(CaseIndexDto.EXTERNAL_ID).setHidden(true); getColumn(CaseIndexDto.EXTERNAL_TOKEN).setHidden(true); } - getColumn(ContactIndexDto.CONTACT_PROXIMITY).setWidth(200); + getColumn(ContactIndexDto.CONTACT_PROXIMITIES).setWidth(200); + ((Column>) getColumn(ContactIndexDto.CONTACT_PROXIMITIES)).setRenderer( + proximities -> { + if (proximities == null || proximities.isEmpty()) { + return ""; + } + return proximities.stream() + .map(I18nProperties::getEnumCaption) + .collect(Collectors.joining(", ")); + }, + new com.vaadin.ui.renderers.TextRenderer() + ); ((Column) getColumn(ContactIndexDto.UUID)).setRenderer(new UuidRenderer()); ((Column) getColumn(ContactIndexDto.PERSON_UUID)).setRenderer(new UuidRenderer()); ((Column) getColumn(ContactIndexDto.FOLLOW_UP_UNTIL)).setRenderer(new DateRenderer(DateFormatHelper.getDateFormat())); @@ -214,7 +221,7 @@ protected Stream getColumnList() { getEventColumns(), Stream.of( ContactIndexDto.CONTACT_CATEGORY, - ContactIndexDto.CONTACT_PROXIMITY, + ContactIndexDto.CONTACT_PROXIMITIES, ContactIndexDto.FOLLOW_UP_STATUS, ContactIndexDto.FOLLOW_UP_UNTIL, ContactIndexDto.SYMPTOM_JOURNAL_STATUS, diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCaseConversionSelectionGrid.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCaseConversionSelectionGrid.java index 5853619effd..c46dcd68673 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCaseConversionSelectionGrid.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCaseConversionSelectionGrid.java @@ -36,7 +36,7 @@ protected void setColumns() { SimilarContactDto.CAZE, SimilarContactDto.CASE_ID_EXTERNAL_SYSTEM, SimilarContactDto.LAST_CONTACT_DATE, - SimilarContactDto.CONTACT_PROXIMITY, + SimilarContactDto.CONTACT_PROXIMITIES, SimilarContactDto.CONTACT_CLASSIFICATION, SimilarContactDto.CONTACT_STATUS, SimilarContactDto.FOLLOW_UP_STATUS); diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCreateForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCreateForm.java index f209b3be041..46657d0163d 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCreateForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactCreateForm.java @@ -22,8 +22,8 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Set; -import com.google.common.collect.Sets; import com.vaadin.shared.ui.ContentMode; import com.vaadin.ui.Button; import com.vaadin.ui.Label; @@ -33,6 +33,7 @@ import com.vaadin.v7.ui.CheckBox; import com.vaadin.v7.ui.ComboBox; import com.vaadin.v7.ui.DateField; +import com.vaadin.v7.ui.OptionGroup; import com.vaadin.v7.ui.TextArea; import com.vaadin.v7.ui.TextField; @@ -40,7 +41,6 @@ import de.symeda.sormas.api.Disease; import de.symeda.sormas.api.FacadeProvider; import de.symeda.sormas.api.caze.CaseDataDto; -import de.symeda.sormas.api.contact.ContactCategory; import de.symeda.sormas.api.contact.ContactDto; import de.symeda.sormas.api.contact.ContactProximity; import de.symeda.sormas.api.contact.ContactRelation; @@ -78,7 +78,7 @@ public class ContactCreateForm extends AbstractEditForm { //@formatter:off private static final String HTML_LAYOUT = LayoutUtil.loc(PERSON_NAME_LOC) + - LayoutUtil.fluidRowLocs(ContactDto.PERSON) + + LayoutUtil.fluidRowLocs(ContactDto.PERSON) + LayoutUtil.fluidRowLocs(ContactDto.RETURNING_TRAVELER) + LayoutUtil.fluidRowLocs(ContactDto.REPORT_DATE_TIME, CaseDataDto.CASE_REFERENCE_NUMBER) + LayoutUtil.fluidRowLocs(ContactDto.DISEASE, ContactDto.DISEASE_DETAILS) + @@ -91,7 +91,7 @@ public class ContactCreateForm extends AbstractEditForm { LayoutUtil.fluidRowLocs(ContactDto.CASE_OR_EVENT_INFORMATION) + LayoutUtil.fluidRowLocs(ContactDto.REGION, ContactDto.DISTRICT) + LayoutUtil.fluidRowLocs(ContactDto.COMMUNITY) + - LayoutUtil.fluidRowLocs(ContactDto.CONTACT_PROXIMITY) + + LayoutUtil.fluidRowLocs(ContactDto.CONTACT_PROXIMITIES) + fluidRowLocs(ContactDto.CONTACT_PROXIMITY_DETAILS) + fluidRowLocs(ContactDto.CONTACT_CATEGORY) + LayoutUtil.fluidRowLocs(ContactDto.RELATION_TO_CASE) + @@ -100,7 +100,7 @@ public class ContactCreateForm extends AbstractEditForm { LayoutUtil.fluidRowLocs(ContactDto.DESCRIPTION); //@formatter:on - private NullableOptionGroup contactProximity; + private OptionGroup contactProximities; private Disease disease; private final Boolean hasCaseRelation; private final boolean asSourceContact; @@ -207,10 +207,19 @@ protected void addFields() { .setVisibleWhen(getFieldGroup(), ContactDto.FIRST_CONTACT_DATE, ContactDto.MULTI_DAY_CONTACT, Collections.singletonList(true), true); updateDateComparison(); - contactProximity = addField(ContactDto.CONTACT_PROXIMITY, NullableOptionGroup.class); - contactProximity.removeStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL); + contactProximities = addField(ContactDto.CONTACT_PROXIMITIES, OptionGroup.class); + contactProximities.setCaption(I18nProperties.getCaption(Captions.Contact_contactProximityLongForm)); + contactProximities.setMultiSelect(true); + contactProximities.removeStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL); if (isConfiguredServer(CountryHelper.COUNTRY_CODE_GERMANY)) { - contactProximity.addValueChangeListener(e -> updateContactCategory((ContactProximity) contactProximity.getNullableValue())); + contactProximities.addValueChangeListener(e -> { + Object value = contactProximities.getValue(); + if (value instanceof Set) { + @SuppressWarnings("unchecked") + Set proximities = (Set) value; + updateContactCategory(proximities); + } + }); contactProximityDetails = addField(ContactDto.CONTACT_PROXIMITY_DETAILS, TextField.class); contactCategory = addField(ContactDto.CONTACT_CATEGORY, NullableOptionGroup.class); } @@ -227,7 +236,7 @@ protected void addFields() { initializeVisibilitiesAndAllowedVisibilities(); - CssStyles.style(CssStyles.SOFT_REQUIRED, firstContactDate, lastContactDate, contactProximity, relationToCase); + CssStyles.style(CssStyles.SOFT_REQUIRED, firstContactDate, lastContactDate, contactProximities, relationToCase); region.addValueChangeListener(e -> { RegionReferenceDto regionDto = (RegionReferenceDto) e.getProperty().getValue(); @@ -254,7 +263,7 @@ protected void addFields() { cbDisease.addValueChangeListener(e -> { disease = (Disease) e.getProperty().getValue(); - setVisible(disease != null, ContactDto.CONTACT_PROXIMITY); + setVisible(disease != null, ContactDto.CONTACT_PROXIMITIES); if (isConfiguredServer(CountryHelper.COUNTRY_CODE_GERMANY)) { setVisible(disease == Disease.CORONAVIRUS, ContactDto.CONTACT_CATEGORY, ContactDto.CONTACT_PROXIMITY_DETAILS); } @@ -309,7 +318,7 @@ protected void addFields() { addValueChangeListener(e -> { updateFieldVisibilitiesByCase(hasCaseRelation); if (!hasCaseRelation && disease == null) { - setVisible(false, ContactDto.CONTACT_PROXIMITY); + setVisible(false, ContactDto.CONTACT_PROXIMITIES); if (isConfiguredServer("de")) { contactCategory.setVisible(false); contactProximityDetails.setVisible(false); @@ -332,34 +341,11 @@ protected void addFields() { /* * Only used for Systems in Germany. Follows specific rules for german systems. + * With multiple contact proximities selected, determines category based on highest risk proximity. */ - private void updateContactCategory(ContactProximity proximity) { - - if (proximity != null) { - switch (proximity) { - case FACE_TO_FACE_LONG: - case TOUCHED_FLUID: - case AEROSOL: - contactCategory.setValue(Sets.newHashSet(ContactCategory.HIGH_RISK)); - break; - case MEDICAL_UNSAFE: - contactCategory.setValue(Sets.newHashSet(ContactCategory.HIGH_RISK_MED)); - break; - case MEDICAL_LIMITED: - contactCategory.setValue(Sets.newHashSet(ContactCategory.MEDIUM_RISK_MED)); - break; - case SAME_ROOM: - case FACE_TO_FACE_SHORT: - case MEDICAL_SAME_ROOM: - contactCategory.setValue(Sets.newHashSet(ContactCategory.LOW_RISK)); - break; - case MEDICAL_DISTANT: - case MEDICAL_SAFE: - contactCategory.setValue(Sets.newHashSet(ContactCategory.NO_RISK)); - break; - default: - } - } + private void updateContactCategory(Set proximities) { + + ContactDataForm.deduceContactCategory(proximities, contactCategory); } private void updateFieldVisibilitiesByCase(boolean caseSelected) { @@ -386,11 +372,13 @@ private void updateFieldVisibilitiesByCase(boolean caseSelected) { private void updateContactProximity() { - ContactProximity value = (ContactProximity) contactProximity.getNullableValue(); + Object valueObj = contactProximities.getValue(); + @SuppressWarnings("unchecked") + Set value = valueObj instanceof Set ? (Set) valueObj : null; FieldHelper.updateEnumData( - contactProximity, + contactProximities, Arrays.asList(ContactProximity.getValues(disease, FacadeProvider.getConfigFacade().getCountryLocale()))); - contactProximity.setValue(value); + contactProximities.setValue(value); } private void hideAndFillJurisdictionFields() { diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java index bf4d3f5e447..7648bf32f55 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactDataForm.java @@ -32,8 +32,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.Set; -import com.google.common.collect.Sets; import com.vaadin.server.ErrorMessage; import com.vaadin.shared.ui.ErrorLevel; import com.vaadin.ui.Button; @@ -48,6 +48,7 @@ import com.vaadin.v7.ui.ComboBox; import com.vaadin.v7.ui.DateField; import com.vaadin.v7.ui.Field; +import com.vaadin.v7.ui.OptionGroup; import com.vaadin.v7.ui.TextArea; import com.vaadin.v7.ui.TextField; @@ -140,7 +141,7 @@ public class ContactDataForm extends AbstractEditForm { loc(ContactDto.CASE_OR_EVENT_INFORMATION) + fluidRowLocs(6, ContactDto.CONTACT_IDENTIFICATION_SOURCE, 6, ContactDto.TRACING_APP) + fluidRowLocs(6, ContactDto.CONTACT_IDENTIFICATION_SOURCE_DETAILS, 6, ContactDto.TRACING_APP_DETAILS) + - fluidRowLocs(ContactDto.CONTACT_PROXIMITY) + + fluidRowLocs(ContactDto.CONTACT_PROXIMITIES) + fluidRowLocs(ContactDto.CONTACT_PROXIMITY_DETAILS) + fluidRowLocs(ContactDto.CONTACT_CATEGORY) + fluidRowLocs(ContactDto.RELATION_TO_CASE) + @@ -182,7 +183,7 @@ public class ContactDataForm extends AbstractEditForm { private final Disease disease; private final boolean diseaseHasFollowUp; private final boolean luxMeasles; - private NullableOptionGroup contactProximity; + private OptionGroup contactProximities; private ComboBox region; private ComboBox district; private ComboBox community; @@ -305,16 +306,18 @@ protected void addFields() { FieldHelper .setVisibleWhen(getFieldGroup(), ContactDto.TRACING_APP_DETAILS, ContactDto.TRACING_APP, Arrays.asList(TracingApp.OTHER), true); } - contactProximity = addField(ContactDto.CONTACT_PROXIMITY, NullableOptionGroup.class); - contactProximity.setCaption(I18nProperties.getCaption(Captions.Contact_contactProximityLongForm)); - contactProximity.removeStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL); + contactProximities = addField(ContactDto.CONTACT_PROXIMITIES, OptionGroup.class); + contactProximities.setCaption(I18nProperties.getCaption(Captions.Contact_contactProximityLongForm)); + contactProximities.setMultiSelect(true); + contactProximities.removeStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL); addField(ContactDto.CONTACT_PROXIMITY_DETAILS, TextField.class); contactCategory = addField(ContactDto.CONTACT_CATEGORY, NullableOptionGroup.class); if (isConfiguredServer(CountryHelper.COUNTRY_CODE_GERMANY)) { - contactProximity.addValueChangeListener(e -> { - if (getInternalValue().getContactProximity() != e.getProperty().getValue() || contactCategory.isModified()) { - updateContactCategory((ContactProximity) contactProximity.getNullableValue()); + contactProximities.addValueChangeListener(e -> { + if (getInternalValue() != null + && (!getInternalValue().getContactProximities().equals(e.getProperty().getValue()) || contactCategory.isModified())) { + updateContactCategory((Set) contactProximities.getValue()); } }); } @@ -734,7 +737,7 @@ public String getFormattedHtmlMessage() { }); setRequired(true, ContactDto.CONTACT_CLASSIFICATION, ContactDto.CONTACT_STATUS, ContactDto.REPORT_DATE_TIME); - FieldHelper.addSoftRequiredStyle(firstContactDate, lastContactDate, contactProximity, relationToCase); + FieldHelper.addSoftRequiredStyle(firstContactDate, lastContactDate, contactProximities, relationToCase); // Prophylaxis details for IMI CheckBox prophylaxisPrescribed = addField(ContactDto.PROPHYLAXIS_PRESCRIBED, CheckBox.class); prophylaxisPrescribed.setCaption(I18nProperties.getCaption(Captions.Contact_prophylaxisPrescribed)); @@ -787,30 +790,41 @@ private void updateOverwriteFollowUpUntil() { /* * Only used for Systems in Germany. Follows specific rules for german systems. */ - private void updateContactCategory(ContactProximity proximity) { - if (proximity != null) { - switch (proximity) { - case FACE_TO_FACE_LONG: - case TOUCHED_FLUID: - case AEROSOL: - contactCategory.setValue(Sets.newHashSet(ContactCategory.HIGH_RISK)); - break; - case MEDICAL_UNSAFE: - contactCategory.setValue(Sets.newHashSet(ContactCategory.HIGH_RISK_MED)); - break; - case MEDICAL_LIMITED: - contactCategory.setValue(Sets.newHashSet(ContactCategory.MEDIUM_RISK_MED)); - break; - case SAME_ROOM: - case FACE_TO_FACE_SHORT: - case MEDICAL_SAME_ROOM: - contactCategory.setValue(Sets.newHashSet(ContactCategory.LOW_RISK)); - break; - case MEDICAL_DISTANT: - case MEDICAL_SAFE: - contactCategory.setValue(Sets.newHashSet(ContactCategory.NO_RISK)); - break; - default: + private void updateContactCategory(Set proximities) { + deduceContactCategory(proximities, contactCategory); + } + + static void deduceContactCategory(Set proximities, NullableOptionGroup contactCategory) { + if (proximities != null && !proximities.isEmpty()) { + ContactCategory highestRiskCategory = null; + + // Check for highest risk first (HIGH_RISK) + if (proximities.contains(ContactProximity.FACE_TO_FACE_LONG) + || proximities.contains(ContactProximity.TOUCHED_FLUID) + || proximities.contains(ContactProximity.AEROSOL)) { + highestRiskCategory = ContactCategory.HIGH_RISK; + } + // HIGH_RISK_MED + else if (proximities.contains(ContactProximity.MEDICAL_UNSAFE)) { + highestRiskCategory = ContactCategory.HIGH_RISK_MED; + } + // MEDIUM_RISK_MED + else if (proximities.contains(ContactProximity.MEDICAL_LIMITED)) { + highestRiskCategory = ContactCategory.MEDIUM_RISK_MED; + } + // LOW_RISK + else if (proximities.contains(ContactProximity.SAME_ROOM) + || proximities.contains(ContactProximity.FACE_TO_FACE_SHORT) + || proximities.contains(ContactProximity.MEDICAL_SAME_ROOM)) { + highestRiskCategory = ContactCategory.LOW_RISK; + } + // NO_RISK + else if (proximities.contains(ContactProximity.MEDICAL_DISTANT) || proximities.contains(ContactProximity.MEDICAL_SAFE)) { + highestRiskCategory = ContactCategory.NO_RISK; + } + + if (highestRiskCategory != null) { + contactCategory.setNullableValue(highestRiskCategory); } } } @@ -913,7 +927,7 @@ private void updateDiseaseConfiguration(Disease disease) { field -> false); FieldHelper.updateEnumData( - contactProximity, + contactProximities, Arrays.asList(ContactProximity.getValues(disease, FacadeProvider.getConfigFacade().getCountryLocale()))); } diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactGridDetailed.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactGridDetailed.java index 1bdfe889b89..54c6db99e57 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactGridDetailed.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactGridDetailed.java @@ -39,7 +39,7 @@ protected List getGridData( @Override protected Stream getColumnList() { List columnList = super.getColumnList().collect(Collectors.toList()); - columnList.add(columnList.indexOf(ContactIndexDetailedDto.CONTACT_PROXIMITY) + 1, ContactIndexDetailedDto.RELATION_TO_CASE); + columnList.add(columnList.indexOf(ContactIndexDetailedDto.CONTACT_PROXIMITIES) + 1, ContactIndexDetailedDto.RELATION_TO_CASE); return Stream.concat(columnList.stream(), Stream.of(ContactIndexDetailedDto.CAZE, ContactIndexDetailedDto.REPORTING_USER)); } diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionField.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionField.java index 560d15840c9..3b03d74e42c 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionField.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionField.java @@ -1,26 +1,19 @@ package de.symeda.sormas.ui.contact; -import de.symeda.sormas.api.person.PersonReferenceDto; -import java.util.function.Consumer; - -import com.vaadin.ui.Component; -import com.vaadin.ui.CustomField; -import com.vaadin.ui.HorizontalLayout; -import com.vaadin.ui.Label; -import com.vaadin.ui.Panel; -import com.vaadin.ui.RadioButtonGroup; -import com.vaadin.ui.VerticalLayout; - +import com.vaadin.ui.*; import de.symeda.sormas.api.contact.ContactDto; import de.symeda.sormas.api.contact.ContactSimilarityCriteria; import de.symeda.sormas.api.contact.SimilarContactDto; import de.symeda.sormas.api.i18n.Captions; import de.symeda.sormas.api.i18n.I18nProperties; import de.symeda.sormas.api.person.PersonDto; +import de.symeda.sormas.api.person.PersonReferenceDto; import de.symeda.sormas.ui.utils.CssStyles; import de.symeda.sormas.ui.utils.DateFormatHelper; import de.symeda.sormas.ui.utils.VaadinUiUtil; +import java.util.function.Consumer; + public class ContactSelectionField extends CustomField { private static final long serialVersionUID = 1595770585498718792L; @@ -164,10 +157,12 @@ private void addContactDetailsComponent() { lblLastContactDate.setCaption(I18nProperties.getPrefixCaption(ContactDto.I18N_PREFIX, ContactDto.LAST_CONTACT_DATE)); contactDetailsLayout.addComponent(lblLastContactDate); - final Label lblContactProximity = - new Label(referenceContact.getContactProximity() != null ? referenceContact.getContactProximity().toString() : ""); + final Label lblContactProximity = new Label( + referenceContact.getContactProximities() != null && !referenceContact.getContactProximities().isEmpty() + ? referenceContact.getContactProximities().stream().map(Object::toString).reduce((a, b) -> a + ", " + b).orElse("") + : ""); lblContactProximity.setWidthUndefined(); - lblContactProximity.setCaption(I18nProperties.getPrefixCaption(ContactDto.I18N_PREFIX, ContactDto.CONTACT_PROXIMITY)); + lblContactProximity.setCaption(I18nProperties.getPrefixCaption(ContactDto.I18N_PREFIX, ContactDto.CONTACT_PROXIMITIES)); contactDetailsLayout.addComponent(lblContactProximity); final Label lblContactClassification = diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionGrid.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionGrid.java index 487b0080305..8460a2e1415 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionGrid.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactSelectionGrid.java @@ -49,7 +49,7 @@ protected void setColumns() { SimilarContactDto.CAZE, SimilarContactDto.CASE_ID_EXTERNAL_SYSTEM, SimilarContactDto.LAST_CONTACT_DATE, - SimilarContactDto.CONTACT_PROXIMITY, + SimilarContactDto.CONTACT_PROXIMITIES, SimilarContactDto.CONTACT_CLASSIFICATION, SimilarContactDto.CONTACT_STATUS, SimilarContactDto.FOLLOW_UP_STATUS); diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/SourceContactListEntry.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/SourceContactListEntry.java index 27bdd0adb72..862177a4f25 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/SourceContactListEntry.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/SourceContactListEntry.java @@ -90,11 +90,15 @@ public SourceContactListEntry(ContactIndexDto contact) { lblContactUuid.setWidth(100, Unit.PERCENTAGE); rightColumn.addComponent(lblContactUuid); - if (contact.getContactProximity() != null) { - Label lblContactProximity = new Label(StringUtils.abbreviate(contact.getContactProximity().toString(), 50)); + if (contact.getContactProximities() != null && !contact.getContactProximities().isEmpty()) { + String proximitiesString = contact.getContactProximities().stream() + .map(Object::toString) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + Label lblContactProximity = new Label(StringUtils.abbreviate(proximitiesString, 50)); CssStyles.style(lblContactProximity, CssStyles.LABEL_TEXT_ALIGN_RIGHT); lblContactProximity.setWidth(100, Unit.PERCENTAGE); - lblContactProximity.setDescription(contact.getContactProximity().toString()); + lblContactProximity.setDescription(proximitiesString); rightColumn.addComponent(lblContactProximity); } diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/contactfield/ContactLineField.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/contactfield/ContactLineField.java index 9e61c1ce0fb..f9a21033d5d 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/contactfield/ContactLineField.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/contactfield/ContactLineField.java @@ -113,7 +113,7 @@ public void showCaptions() { dateOfReport.setCaption(I18nProperties.getPrefixCaption(CaseDataDto.I18N_PREFIX, CaseDataDto.REPORT_DATE)); dateOfReport.removeStyleName(CssStyles.CAPTION_HIDDEN); multiDay.showCaptions(); - typeOfContact.setCaption(I18nProperties.getPrefixCaption(ContactDto.I18N_PREFIX, ContactDto.CONTACT_PROXIMITY)); + typeOfContact.setCaption(I18nProperties.getPrefixCaption(ContactDto.I18N_PREFIX, ContactDto.CONTACT_PROXIMITIES)); typeOfContact.removeStyleName(CssStyles.CAPTION_HIDDEN); relationToCase.setCaption(I18nProperties.getPrefixCaption(ContactDto.I18N_PREFIX, ContactDto.RELATION_TO_CASE)); relationToCase.removeStyleName(CssStyles.CAPTION_HIDDEN); diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/layout/LineListingLayout.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/layout/LineListingLayout.java index 7a60f320ee2..e395ea6c4bc 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/layout/LineListingLayout.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/components/linelisting/layout/LineListingLayout.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.function.Consumer; @@ -21,6 +22,7 @@ import de.symeda.sormas.api.caze.BirthDateDto; import de.symeda.sormas.api.caze.CaseDataDto; import de.symeda.sormas.api.contact.ContactDto; +import de.symeda.sormas.api.contact.ContactProximity; import de.symeda.sormas.api.event.EventDto; import de.symeda.sormas.api.event.EventParticipantDto; import de.symeda.sormas.api.i18n.Captions; @@ -194,7 +196,10 @@ private LinkedList> getContactLineDtos() { contact.setMultiDayContact(layoutBean.getLineField().getMultiDaySelector().isMultiDay()); contact.setFirstContactDate(UtilDate.from(layoutBean.getLineField().getMultiDaySelector().getStartDate())); contact.setLastContactDate(UtilDate.from(layoutBean.getLineField().getMultiDaySelector().getEndDate())); - contact.setContactProximity(layoutBean.getLineField().getTypeOfContact()); + ContactProximity typeOfContact = layoutBean.getLineField().getTypeOfContact(); + if (typeOfContact != null) { + contact.setContactProximities(Collections.singleton(typeOfContact)); + } contact.setRelationToCase(layoutBean.getLineField().getRelationToCase()); if (UserProvider.getCurrent() != null) { contact.setReportingUser(UiUtil.getUserReference()); diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/NullableOptionGroup.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/NullableOptionGroup.java index 1512bae235f..6d7bb6f200d 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/NullableOptionGroup.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/NullableOptionGroup.java @@ -71,6 +71,14 @@ public Object getNullableValue() { return ObjectUtils.isNotEmpty(value) ? getFirstValue((Set) value) : null; } + public void setNullableValue(Object value) { + if (value != null) { + super.setValue(java.util.Collections.singleton(value)); + } else { + super.setValue(null); + } + } + @Override public void setRequired(boolean required) { boolean readOnly = isReadOnly();