diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/caze/CaseCriteria.java b/sormas-api/src/main/java/de/symeda/sormas/api/caze/CaseCriteria.java index 7e6e398c3d9..0920c1f4874 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/caze/CaseCriteria.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/caze/CaseCriteria.java @@ -106,6 +106,9 @@ public class CaseCriteria extends CriteriaWithDateType implements ExternalShareC private Date newCaseDateTo; private Date creationDateFrom; private Date creationDateTo; + private Date birthdateFrom; + private Date birthdateTo; + private boolean includePartialMatch; private CriteriaDateType newCaseDateType; // Used to re-construct whether users have filtered by epi weeks or dates private DateFilterOption dateFilterOption = DateFilterOption.DATE; @@ -552,6 +555,31 @@ public CaseCriteria creationDateTo(Date creationDateTo) { return this; } + public Date getBirthdateFrom() { + return birthdateFrom; + } + + public void setBirthdateFrom(Date birthdateFrom) { + this.birthdateFrom = birthdateFrom; + } + + public Date getBirthdateTo() { + return birthdateTo; + } + + public void setBirthdateTo(Date birthdateTo) { + this.birthdateTo = birthdateTo; + } + + @IgnoreForUrl + public boolean isIncludePartialMatch() { + return includePartialMatch; + } + + public void setIncludePartialMatch(boolean includePartialMatch) { + this.includePartialMatch = includePartialMatch; + } + public Date getQuarantineTo() { return quarantineTo; } diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactCriteria.java b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactCriteria.java index 6f1086e30ea..c96d4349842 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactCriteria.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/contact/ContactCriteria.java @@ -136,6 +136,9 @@ public class ContactCriteria extends BaseCriteria implements Serializable { private Boolean onlyContactsFromOtherInstances; private Date creationDateFrom; private Date creationDateTo; + private Date birthdateFrom; + private Date birthdateTo; + private boolean includePartialMatch; private String reportingUserLike; private String personLike; private boolean excludeLimitedSyncRestrictions; @@ -665,6 +668,31 @@ public ContactCriteria creationDateTo(Date creationDateTo) { return this; } + public Date getBirthdateFrom() { + return birthdateFrom; + } + + public void setBirthdateFrom(Date birthdateFrom) { + this.birthdateFrom = birthdateFrom; + } + + public Date getBirthdateTo() { + return birthdateTo; + } + + public void setBirthdateTo(Date birthdateTo) { + this.birthdateTo = birthdateTo; + } + + @IgnoreForUrl + public boolean isIncludePartialMatch() { + return includePartialMatch; + } + + public void setIncludePartialMatch(boolean includePartialMatch) { + this.includePartialMatch = includePartialMatch; + } + public String getReportingUserLike() { return reportingUserLike; } diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java index ae028b9b556..1862ce0ba60 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java @@ -1993,6 +1993,7 @@ public interface Captions { String importSkips = "importSkips"; String importValueSeparator = "importValueSeparator"; String inaccessibleValue = "inaccessibleValue"; + String includePartialBirthdates = "includePartialBirthdates"; String info = "info"; String infrastructureImportAllowOverwrite = "infrastructureImportAllowOverwrite"; String lastName = "lastName"; diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Descriptions.java b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Descriptions.java index e45b067dca1..52a5bfebaef 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Descriptions.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Descriptions.java @@ -13,6 +13,7 @@ public interface Descriptions { String aefiDashboardDiseaseFilter = "aefiDashboardDiseaseFilter"; String aefiDashboardDistrictFilter = "aefiDashboardDistrictFilter"; String aefiDashboardRegionFilter = "aefiDashboardRegionFilter"; + String birthdateFilterPartialMatchDescription = "birthdateFilterPartialMatchDescription"; String Campaign_calculatedBasedOn = "Campaign.calculatedBasedOn"; String Campaign_campaignPhase = "Campaign.campaignPhase"; String CaseData_caseClassification = "CaseData.caseClassification"; diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java index 484d972c404..20a31f30878 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java @@ -705,6 +705,7 @@ public interface Strings { String headingImportSelfReports = "headingImportSelfReports"; String headingImportSubcontinents = "headingImportSubcontinents"; String headingImportTravelEntries = "headingImportTravelEntries"; + String headingIncorrectDateRange = "headingIncorrectDateRange"; String headingInformationSource = "headingInformationSource"; String headingInfrastructureLocked = "headingInfrastructureLocked"; String headingIntroduction = "headingIntroduction"; @@ -1419,6 +1420,7 @@ public interface Strings { String messageImportSuccessful = "messageImportSuccessful"; String messageImportSuccessfulWithSkips = "messageImportSuccessfulWithSkips"; String messageIncompleteGpsCoordinates = "messageIncompleteGpsCoordinates"; + String messageIncorrectDateRange = "messageIncorrectDateRange"; String messageInfrastructureLocked = "messageInfrastructureLocked"; String messageInvalidDatesLineListing = "messageInvalidDatesLineListing"; String messageLaboratoriesArchived = "messageLaboratoriesArchived"; @@ -1680,6 +1682,8 @@ public interface Strings { String promptAllDistricts = "promptAllDistricts"; String promptAllRegions = "promptAllRegions"; String promptArea = "promptArea"; + String promptBirthdateFrom = "promptBirthdateFrom"; + String promptBirthdateTo = "promptBirthdateTo"; String promptCampaign = "promptCampaign"; String promptCampaignSearch = "promptCampaignSearch"; String promptCaseOrContactEventSearchField = "promptCaseOrContactEventSearchField"; diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/person/PersonCriteria.java b/sormas-api/src/main/java/de/symeda/sormas/api/person/PersonCriteria.java index 0ef155b8d25..a78d4eadbef 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/person/PersonCriteria.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/person/PersonCriteria.java @@ -1,5 +1,6 @@ package de.symeda.sormas.api.person; +import java.util.Date; import java.util.Set; import de.symeda.sormas.api.infrastructure.community.CommunityReferenceDto; @@ -37,6 +38,9 @@ public class PersonCriteria extends BaseCriteria implements Cloneable { private CommunityReferenceDto community; private PersonAssociation personAssociation; private Set uuids; + private Date birthdateFrom; + private Date birthdateTo; + private boolean includePartialMatch; public PersonCriteria() { @@ -127,6 +131,31 @@ public PersonCriteria personAssociation(PersonAssociation personAssociation) { return this; } + public Date getBirthdateFrom() { + return birthdateFrom; + } + + public void setBirthdateFrom(Date birthdateFrom) { + this.birthdateFrom = birthdateFrom; + } + + public Date getBirthdateTo() { + return birthdateTo; + } + + public void setBirthdateTo(Date birthdateTo) { + this.birthdateTo = birthdateTo; + } + + @IgnoreForUrl + public boolean isIncludePartialMatch() { + return includePartialMatch; + } + + public void setIncludePartialMatch(boolean includePartialMatch) { + this.includePartialMatch = includePartialMatch; + } + @IgnoreForUrl public Set getUuids() { return uuids; diff --git a/sormas-api/src/main/resources/captions.properties b/sormas-api/src/main/resources/captions.properties index 4c7f34f7279..815de5ffe31 100644 --- a/sormas-api/src/main/resources/captions.properties +++ b/sormas-api/src/main/resources/captions.properties @@ -52,6 +52,7 @@ creationDate=Creation date changeDate=Date of last change notAvailableShort=NA inaccessibleValue=Confidential +includePartialBirthdates = Include partial birthdates numberOfCharacters=Number of characters: %d / %d remove=Remove notTestedYet=Not tested yet diff --git a/sormas-api/src/main/resources/descriptions.properties b/sormas-api/src/main/resources/descriptions.properties index 66bc7df81fd..7ecec3129c4 100644 --- a/sormas-api/src/main/resources/descriptions.properties +++ b/sormas-api/src/main/resources/descriptions.properties @@ -80,6 +80,7 @@ descContactOnlyWithReducedQuarantine = Only list contacts whose quarantine perio descContactIncludeContactsFromOtherJurisdictions = Include all contacts from other jurisdictions that you have access to, e.g. because you created them or their source case is in your jurisdiction descGdpr = Reminder: All comments entered must comply with GDPR rules as described during connection. discardDescription = Discards any unsaved changes +birthdateFilterPartialMatchDescription = If checked the search will include also the persons that have incomplete birthdate and have only higher level match eg. only year or only year and month # EpiData EpiData.bats = Did you have contact with live or dead bats or their excreta during the incubation period? diff --git a/sormas-api/src/main/resources/strings.properties b/sormas-api/src/main/resources/strings.properties index 2e9723c6cf6..b091bed4353 100644 --- a/sormas-api/src/main/resources/strings.properties +++ b/sormas-api/src/main/resources/strings.properties @@ -620,6 +620,7 @@ headingImportRegions= Import Regions headingImportTravelEntries = Import Travel Entries headingImportEnvironments = Import Environments headingImportSelfReports = Import Self Reports +headingIncorrectDateRange = Incorrect date range headingInformationSource = Source of Information headingInfrastructureLocked = Infrastructure locked headingIntroduction = Introduction @@ -1314,6 +1315,7 @@ messageImportSuccessful = Import successful!
All rows have been impor messageImportSuccessfulWithSkips = Import successful!
The import has been successful, but some of the rows were skipped. You can now close this window. messageUploadSuccessful = Upload successful! You can now close this window. messageIncompleteGpsCoordinates = GPS coordinates are incomplete +messageIncorrectDateRange = Date from is after date to messageExternalMessagesAssigned = The assignee has been changed for all selected messages messageLoginFailed = Please check your username and password and try again messageMissingCases = Please generate some cases before generating contacts @@ -1651,6 +1653,8 @@ promptActionChangeDateFrom = Date of action change from... promptActionChangeDateTo = ... to promptActionChangeEpiWeekFrom = Date of action change from epi week... promptActionChangeEpiWeekTo = ... to epi week +promptBirthdateFrom = Birthdate from +promptBirthdateTo = ... to promptCampaignSearch = ID, name promptCasesDateFrom = New cases from... promptCasesEpiWeekFrom = New cases from epi week... diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/caze/CaseService.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/caze/CaseService.java index fd359454347..ebcd4065d2e 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/caze/CaseService.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/caze/CaseService.java @@ -177,6 +177,7 @@ import de.symeda.sormas.backend.user.User; import de.symeda.sormas.backend.user.UserRole; import de.symeda.sormas.backend.user.UserService; +import de.symeda.sormas.backend.util.BirthdateRangeFilterPredicate; import de.symeda.sormas.backend.util.ExternalDataUtil; import de.symeda.sormas.backend.util.IterableHelper; import de.symeda.sormas.backend.util.JurisdictionHelper; @@ -912,6 +913,15 @@ public Predicate createCriteriaFilter(CaseCrite filter = CriteriaBuilderHelper.and(cb, filter, likeFilters); } } + + filter = BirthdateRangeFilterPredicate.createBirthdateRangeFilter( + caseCriteria.getBirthdateFrom(), + caseCriteria.getBirthdateTo(), + caseCriteria.isIncludePartialMatch(), + cb, + joins.getPerson(), + filter); + if (caseCriteria.getBirthdateYYYY() != null) { filter = CriteriaBuilderHelper.and(cb, filter, cb.equal(joins.getPerson().get(Person.BIRTHDATE_YYYY), caseCriteria.getBirthdateYYYY())); } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactService.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactService.java index 07b51eb8efa..3dfcd3377bf 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactService.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactService.java @@ -137,6 +137,7 @@ import de.symeda.sormas.backend.user.User; import de.symeda.sormas.backend.user.UserRole; import de.symeda.sormas.backend.user.UserService; +import de.symeda.sormas.backend.util.BirthdateRangeFilterPredicate; import de.symeda.sormas.backend.util.ExternalDataUtil; import de.symeda.sormas.backend.util.IterableHelper; import de.symeda.sormas.backend.util.JurisdictionHelper; @@ -1419,6 +1420,15 @@ public Predicate buildCriteriaFilter(ContactCriteria contactCriteria, ContactQue if (contactCriteria.getPerson() != null) { filter = CriteriaBuilderHelper.and(cb, filter, cb.equal(joins.getPerson().get(Person.UUID), contactCriteria.getPerson().getUuid())); } + + filter = BirthdateRangeFilterPredicate.createBirthdateRangeFilter( + contactCriteria.getBirthdateFrom(), + contactCriteria.getBirthdateTo(), + contactCriteria.isIncludePartialMatch(), + cb, + joins.getPerson(), + filter); + if (contactCriteria.getBirthdateYYYY() != null) { filter = CriteriaBuilderHelper.and(cb, filter, cb.equal(joins.getPerson().get(Person.BIRTHDATE_YYYY), contactCriteria.getBirthdateYYYY())); diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/person/PersonService.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/person/PersonService.java index 06c40972314..57d37c49f9f 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/person/PersonService.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/person/PersonService.java @@ -114,6 +114,7 @@ import de.symeda.sormas.backend.travelentry.services.TravelEntryService; import de.symeda.sormas.backend.user.User; import de.symeda.sormas.backend.user.UserService; +import de.symeda.sormas.backend.util.BirthdateRangeFilterPredicate; import de.symeda.sormas.backend.util.ExternalDataUtil; import de.symeda.sormas.backend.util.IterableHelper; import de.symeda.sormas.backend.util.JurisdictionHelper; @@ -386,6 +387,15 @@ public Predicate buildCriteriaFilter(PersonCriteria personCriteria, PersonQueryC filter = andEquals(cb, personFrom, filter, personCriteria.getBirthdateYYYY(), Person.BIRTHDATE_YYYY); filter = andEquals(cb, personFrom, filter, personCriteria.getBirthdateMM(), Person.BIRTHDATE_MM); filter = andEquals(cb, personFrom, filter, personCriteria.getBirthdateDD(), Person.BIRTHDATE_DD); + + filter = BirthdateRangeFilterPredicate.createBirthdateRangeFilter( + personCriteria.getBirthdateFrom(), + personCriteria.getBirthdateTo(), + personCriteria.isIncludePartialMatch(), + cb, + personFrom, + filter); + if (personCriteria.getNameAddressPhoneEmailLike() != null) { String[] textFilters = personCriteria.getNameAddressPhoneEmailLike().split("\\s+"); diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/util/BirthdateRangeFilterPredicate.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/util/BirthdateRangeFilterPredicate.java new file mode 100644 index 00000000000..b2dcfcbd287 --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/util/BirthdateRangeFilterPredicate.java @@ -0,0 +1,87 @@ +package de.symeda.sormas.backend.util; + +import java.util.Calendar; +import java.util.Date; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Predicate; + +import de.symeda.sormas.backend.common.CriteriaBuilderHelper; +import de.symeda.sormas.backend.person.Person; + +public class BirthdateRangeFilterPredicate { + + public static Predicate createBirthdateRangeFilter( + Date birthdateFrom, + Date birthdateTo, + boolean includePartialMatch, + CriteriaBuilder cb, + From personFrom, + Predicate filter) { + if (birthdateFrom != null) { + Calendar calendarBirthdateFrom = Calendar.getInstance(); + calendarBirthdateFrom.setTime(birthdateFrom); + int birthdateFromCriteriaYear = calendarBirthdateFrom.get(Calendar.YEAR); + int birthdateFromCriteriaMonth = calendarBirthdateFrom.get(Calendar.MONTH) + 1; + int birthdateFromCriteriaDay = calendarBirthdateFrom.get(Calendar.DAY_OF_MONTH); + + Predicate yearPredicate = cb.greaterThan(personFrom.get(Person.BIRTHDATE_YYYY), birthdateFromCriteriaYear); + + Predicate monthPredicate = cb.equal(personFrom.get(Person.BIRTHDATE_YYYY), birthdateFromCriteriaYear); + monthPredicate = cb.and(monthPredicate, cb.greaterThan(personFrom.get(Person.BIRTHDATE_MM), birthdateFromCriteriaMonth)); + + Predicate dayPredicate = cb.equal(personFrom.get(Person.BIRTHDATE_YYYY), birthdateFromCriteriaYear); + dayPredicate = cb.and(dayPredicate, cb.equal(personFrom.get(Person.BIRTHDATE_MM), birthdateFromCriteriaMonth)); + dayPredicate = cb.and(dayPredicate, cb.greaterThanOrEqualTo(personFrom.get(Person.BIRTHDATE_DD), birthdateFromCriteriaDay)); + + if (includePartialMatch) { + Predicate sameYearPartialMatchPredicate = cb + .and(cb.equal(personFrom.get(Person.BIRTHDATE_YYYY), birthdateFromCriteriaYear), cb.isNull(personFrom.get(Person.BIRTHDATE_MM))); + Predicate sameYearMonthPartialMatchPredicate = cb.and( + cb.equal(personFrom.get(Person.BIRTHDATE_YYYY), birthdateFromCriteriaYear), + cb.equal(personFrom.get(Person.BIRTHDATE_MM), birthdateFromCriteriaMonth), + cb.isNull(personFrom.get(Person.BIRTHDATE_DD))); + filter = CriteriaBuilderHelper.and( + cb, + filter, + cb.or(yearPredicate, monthPredicate, dayPredicate, sameYearPartialMatchPredicate, sameYearMonthPartialMatchPredicate)); + } else { + filter = CriteriaBuilderHelper.and(cb, filter, cb.or(yearPredicate, monthPredicate, dayPredicate)); + } + } + + if (birthdateTo != null) { + Calendar calendarBirthdateTo = Calendar.getInstance(); + calendarBirthdateTo.setTime(birthdateTo); + int birthdateToCriteriaYear = calendarBirthdateTo.get(Calendar.YEAR); + int birthdateToCriteriaMonth = calendarBirthdateTo.get(Calendar.MONTH) + 1; + int birthdateToCriteriaDay = calendarBirthdateTo.get(Calendar.DAY_OF_MONTH); + + Predicate yearPredicate = cb.lessThan(personFrom.get(Person.BIRTHDATE_YYYY), birthdateToCriteriaYear); + + Predicate monthPredicate = cb.equal(personFrom.get(Person.BIRTHDATE_YYYY), birthdateToCriteriaYear); + monthPredicate = cb.and(monthPredicate, cb.lessThan(personFrom.get(Person.BIRTHDATE_MM), birthdateToCriteriaMonth)); + + Predicate dayPredicate = cb.equal(personFrom.get(Person.BIRTHDATE_YYYY), birthdateToCriteriaYear); + dayPredicate = cb.and(dayPredicate, cb.equal(personFrom.get(Person.BIRTHDATE_MM), birthdateToCriteriaMonth)); + dayPredicate = cb.and(dayPredicate, cb.lessThanOrEqualTo(personFrom.get(Person.BIRTHDATE_DD), birthdateToCriteriaDay)); + + if (includePartialMatch) { + Predicate sameYearPartialMatchPredicate = + cb.and(cb.equal(personFrom.get(Person.BIRTHDATE_YYYY), birthdateToCriteriaYear), cb.isNull(personFrom.get(Person.BIRTHDATE_MM))); + Predicate sameYearMonthPartialMatchPredicate = cb.and( + cb.equal(personFrom.get(Person.BIRTHDATE_YYYY), birthdateToCriteriaYear), + cb.equal(personFrom.get(Person.BIRTHDATE_MM), birthdateToCriteriaMonth), + cb.isNull(personFrom.get(Person.BIRTHDATE_DD))); + filter = CriteriaBuilderHelper.and( + cb, + filter, + cb.or(yearPredicate, monthPredicate, dayPredicate, sameYearPartialMatchPredicate, sameYearMonthPartialMatchPredicate)); + } else { + filter = CriteriaBuilderHelper.and(cb, filter, cb.or(yearPredicate, monthPredicate, dayPredicate)); + } + } + return filter; + } +} diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseFilterForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseFilterForm.java index cd651e8c8d2..c495eb0d07d 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseFilterForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseFilterForm.java @@ -62,6 +62,7 @@ import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers; import de.symeda.sormas.ui.UiUtil; import de.symeda.sormas.ui.utils.AbstractFilterForm; +import de.symeda.sormas.ui.utils.BirthdateRangeFilterComponent; import de.symeda.sormas.ui.utils.CssStyles; import de.symeda.sormas.ui.utils.EpiWeekAndDateFilterComponent; import de.symeda.sormas.ui.utils.FieldConfiguration; @@ -72,6 +73,7 @@ public class CaseFilterForm extends AbstractFilterForm { private static final long serialVersionUID = -8326451364091398731L; private static final String WEEK_AND_DATE_FILTER = "moreFilters"; + private static final String BIRTHDATE_RANGE_FILTER = "birthdateRangeFilter"; private static final String MORE_FILTERS_HTML_LAYOUT = filterLocs( CaseCriteria.PRESENT_CONDITION, @@ -114,7 +116,8 @@ public class CaseFilterForm extends AbstractFilterForm { CaseCriteria.ONLY_ENTITIES_SHARED_WITH_EXTERNAL_SURV_TOOL, CaseCriteria.ONLY_ENTITIES_CHANGED_SINCE_LAST_SHARED_WITH_EXTERNAL_SURV_TOOL, CaseCriteria.ONLY_CASES_WITH_DONT_SHARE_WITH_EXTERNAL_SURV_TOOL) - + loc(WEEK_AND_DATE_FILTER); + + loc(WEEK_AND_DATE_FILTER) + + loc(BIRTHDATE_RANGE_FILTER); protected CaseFilterForm() { super( @@ -365,7 +368,8 @@ public void addMoreFilters(CustomLayout moreFiltersContainer) { CaseCriteria.ONLY_CASES_WITH_EVENTS, I18nProperties.getCaption(Captions.caseFilterRelatedToEvent), I18nProperties.getDescription(Descriptions.descCaseFilterRelatedToEvent), - CssStyles.CHECKBOX_FILTER_INLINE)).setVisible(UiUtil.permitted(UserRight.EVENT_VIEW)); + CssStyles.CHECKBOX_FILTER_INLINE)) + .setVisible(UiUtil.permitted(UserRight.EVENT_VIEW)); addField( moreFiltersContainer, @@ -448,6 +452,8 @@ public void addMoreFilters(CustomLayout moreFiltersContainer) { } moreFiltersContainer.addComponent(buildWeekAndDateFilter(isExternalShareEnabled), WEEK_AND_DATE_FILTER); + + moreFiltersContainer.addComponent(buildBirthdayRangeFilter(), BIRTHDATE_RANGE_FILTER); } @Override @@ -829,6 +835,19 @@ private HorizontalLayout buildWeekAndDateFilter(boolean isExternalShareEnabled) return dateFilterRowLayout; } + private HorizontalLayout buildBirthdayRangeFilter() { + BirthdateRangeFilterComponent birthdateRangeFilterComponent = new BirthdateRangeFilterComponent(false, this); + addApplyHandler(e -> onApplyClick(birthdateRangeFilterComponent)); + + HorizontalLayout dateFilterRowLayout = new HorizontalLayout(); + dateFilterRowLayout.setSpacing(true); + dateFilterRowLayout.setSizeUndefined(); + + dateFilterRowLayout.addComponent(birthdateRangeFilterComponent); + + return dateFilterRowLayout; + } + private void onApplyClick(EpiWeekAndDateFilterComponent weekAndDateFilter) { DateFilterOption dateFilterOption = (DateFilterOption) weekAndDateFilter.getDateFilterOptionFilter().getValue(); Date fromDate, toDate; @@ -852,6 +871,18 @@ private void onApplyClick(EpiWeekAndDateFilterComponent weekAn } } + private void onApplyClick(BirthdateRangeFilterComponent birthdateRangeFilter) { + Date birthdateFrom, birthdateTo; + Date dateFrom = birthdateRangeFilter.getDateFromFilter().getValue(); + birthdateFrom = dateFrom != null ? DateHelper.getStartOfDay(dateFrom) : null; + Date dateTo = birthdateRangeFilter.getDateToFilter().getValue(); + birthdateTo = dateTo != null ? DateHelper.getEndOfDay(dateTo) : null; + CaseCriteria criteria = getValue(); + criteria.setBirthdateFrom(birthdateFrom); + criteria.setBirthdateTo(birthdateTo); + criteria.setIncludePartialMatch(birthdateRangeFilter.getIncludePartialMatch().getValue()); + } + @Override public void setValue(CaseCriteria newCriteria) throws ReadOnlyException, Converter.ConversionException { diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactsFilterForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactsFilterForm.java index c7744674782..82167788cf3 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactsFilterForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/contact/ContactsFilterForm.java @@ -49,6 +49,7 @@ import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers; import de.symeda.sormas.ui.UiUtil; import de.symeda.sormas.ui.utils.AbstractFilterForm; +import de.symeda.sormas.ui.utils.BirthdateRangeFilterComponent; import de.symeda.sormas.ui.utils.CssStyles; import de.symeda.sormas.ui.utils.EpiWeekAndDateFilterComponent; import de.symeda.sormas.ui.utils.FieldConfiguration; @@ -60,6 +61,7 @@ public class ContactsFilterForm extends AbstractFilterForm { private static final String DISTRICT_INFO_LABEL_ID = "infoContactsViewRegionDistrictFilter"; private static final String WEEK_AND_DATE_FILTER = "moreFilters"; + private static final String BIRTHDATE_RANGE_FILTER = "birthdateRangeFilter"; private static final String CHECKBOX_STYLE = CssStyles.CHECKBOX_FILTER_INLINE + " " + CssStyles.VSPACE_3; @@ -92,7 +94,8 @@ public class ContactsFilterForm extends AbstractFilterForm { ContactCriteria.ONLY_CONTACTS_FROM_OTHER_INSTANCES, ContactCriteria.INCLUDE_CONTACTS_FROM_OTHER_JURISDICTIONS) - + loc(WEEK_AND_DATE_FILTER); + + loc(WEEK_AND_DATE_FILTER) + + loc(BIRTHDATE_RANGE_FILTER); protected ContactsFilterForm() { super( @@ -320,7 +323,8 @@ public void addMoreFilters(CustomLayout moreFiltersContainer) { ContactCriteria.ONLY_CONTACTS_SHARING_EVENT_WITH_SOURCE_CASE, I18nProperties.getCaption(Captions.contactOnlyWithSharedEventWithSourceCase), null, - CHECKBOX_STYLE)).setVisible(UiUtil.permitted(UserRight.EVENT_VIEW)); + CHECKBOX_STYLE)) + .setVisible(UiUtil.permitted(UserRight.EVENT_VIEW)); addField( moreFiltersContainer, @@ -344,6 +348,8 @@ public void addMoreFilters(CustomLayout moreFiltersContainer) { } moreFiltersContainer.addComponent(buildWeekAndDateFilter(), WEEK_AND_DATE_FILTER); + + moreFiltersContainer.addComponent(buildBirthdayRangeFilter(), BIRTHDATE_RANGE_FILTER); } @Override @@ -526,6 +532,19 @@ private HorizontalLayout buildWeekAndDateFilter() { return dateFilterRowLayout; } + private HorizontalLayout buildBirthdayRangeFilter() { + BirthdateRangeFilterComponent birthdateRangeFilterComponent = new BirthdateRangeFilterComponent(false, this); + addApplyHandler(e -> onApplyClick(birthdateRangeFilterComponent)); + + HorizontalLayout dateFilterRowLayout = new HorizontalLayout(); + dateFilterRowLayout.setSpacing(true); + dateFilterRowLayout.setSizeUndefined(); + + dateFilterRowLayout.addComponent(birthdateRangeFilterComponent); + + return dateFilterRowLayout; + } + private void onApplyClick(EpiWeekAndDateFilterComponent weekAndDateFilter) { ContactCriteria criteria = getValue(); @@ -555,6 +574,18 @@ private void onApplyClick(EpiWeekAndDateFilterComponent weekAnd } } + private void onApplyClick(BirthdateRangeFilterComponent birthdateRangeFilter) { + Date birthdateFrom, birthdateTo; + Date dateFrom = birthdateRangeFilter.getDateFromFilter().getValue(); + birthdateFrom = dateFrom != null ? DateHelper.getStartOfDay(dateFrom) : null; + Date dateTo = birthdateRangeFilter.getDateToFilter().getValue(); + birthdateTo = dateTo != null ? DateHelper.getEndOfDay(dateTo) : null; + ContactCriteria criteria = getValue(); + criteria.setBirthdateFrom(birthdateFrom); + criteria.setBirthdateTo(birthdateTo); + criteria.setIncludePartialMatch(birthdateRangeFilter.getIncludePartialMatch().getValue()); + } + private void populateContactResponsiblesForRegion(RegionReferenceDto regionReferenceDto) { List items = fetchContactResponsiblesByRegion(regionReferenceDto != null ? regionReferenceDto : currentUserDto().getRegion()); diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/person/PersonFilterForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/person/PersonFilterForm.java index 399151f8558..4b727d62fdd 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/person/PersonFilterForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/person/PersonFilterForm.java @@ -1,5 +1,11 @@ package de.symeda.sormas.ui.person; +import static de.symeda.sormas.ui.utils.LayoutUtil.loc; + +import java.util.Date; + +import com.vaadin.ui.CustomLayout; +import com.vaadin.ui.HorizontalLayout; import com.vaadin.v7.data.Property; import com.vaadin.v7.ui.ComboBox; import com.vaadin.v7.ui.TextField; @@ -19,11 +25,16 @@ import de.symeda.sormas.api.utils.DateHelper; import de.symeda.sormas.ui.UiUtil; import de.symeda.sormas.ui.utils.AbstractFilterForm; +import de.symeda.sormas.ui.utils.BirthdateRangeFilterComponent; import de.symeda.sormas.ui.utils.FieldConfiguration; import de.symeda.sormas.ui.utils.FieldHelper; public class PersonFilterForm extends AbstractFilterForm { + private static final String BIRTHDATE_RANGE_FILTER = "birthdateRangeFilter"; + + private static final String MORE_FILTERS_HTML_LAYOUT = loc(BIRTHDATE_RANGE_FILTER); + protected PersonFilterForm() { super( PersonCriteria.class, @@ -44,6 +55,11 @@ protected String[] getMainFilterLocators() { PersonCriteria.COMMUNITY }; } + @Override + protected String createMoreFiltersHtmlLayout() { + return MORE_FILTERS_HTML_LAYOUT; + } + @Override protected void addFields() { @@ -78,6 +94,37 @@ protected void addFields() { FieldConfiguration.withCaptionAndPixelSized(PersonCriteria.COMMUNITY, I18nProperties.getCaption(Captions.personCommunityPrompt), 140)); } + @Override + public void addMoreFilters(CustomLayout moreFiltersContainer) { + + moreFiltersContainer.addComponent(buildBirthdayRangeFilter(), BIRTHDATE_RANGE_FILTER); + } + + private HorizontalLayout buildBirthdayRangeFilter() { + BirthdateRangeFilterComponent birthdateRangeFilterComponent = new BirthdateRangeFilterComponent(false, this); + addApplyHandler(e -> onApplyClick(birthdateRangeFilterComponent)); + + HorizontalLayout dateFilterRowLayout = new HorizontalLayout(); + dateFilterRowLayout.setSpacing(true); + dateFilterRowLayout.setSizeUndefined(); + + dateFilterRowLayout.addComponent(birthdateRangeFilterComponent); + + return dateFilterRowLayout; + } + + private void onApplyClick(BirthdateRangeFilterComponent birthdateRangeFilter) { + Date birthdateFrom, birthdateTo; + Date dateFrom = birthdateRangeFilter.getDateFromFilter().getValue(); + birthdateFrom = dateFrom != null ? DateHelper.getStartOfDay(dateFrom) : null; + Date dateTo = birthdateRangeFilter.getDateToFilter().getValue(); + birthdateTo = dateTo != null ? DateHelper.getEndOfDay(dateTo) : null; + PersonCriteria criteria = getValue(); + criteria.setBirthdateFrom(birthdateFrom); + criteria.setBirthdateTo(birthdateTo); + criteria.setIncludePartialMatch(birthdateRangeFilter.getIncludePartialMatch().getValue()); + } + @Override protected void applyDependenciesOnFieldChange(String propertyId, Property.ValueChangeEvent event) { super.applyDependenciesOnFieldChange(propertyId, event); diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/BirthdateRangeFilterComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/BirthdateRangeFilterComponent.java new file mode 100644 index 00000000000..3bfded5b7e0 --- /dev/null +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/BirthdateRangeFilterComponent.java @@ -0,0 +1,104 @@ +package de.symeda.sormas.ui.utils; + +import static de.symeda.sormas.ui.utils.CssStyles.VSPACE_TOP_4; + +import java.util.Calendar; +import java.util.Date; + +import com.vaadin.server.Page; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Notification; +import com.vaadin.v7.ui.CheckBox; +import com.vaadin.v7.ui.PopupDateField; + +import de.symeda.sormas.api.i18n.Captions; +import de.symeda.sormas.api.i18n.Descriptions; +import de.symeda.sormas.api.i18n.I18nProperties; +import de.symeda.sormas.api.i18n.Strings; +import de.symeda.sormas.api.utils.DateHelper; + +public class BirthdateRangeFilterComponent extends HorizontalLayout { + + private static final long serialVersionUID = 8752630393182144501L; + + private final PopupDateField dateFromFilter; + private final PopupDateField dateToFilter; + private final CheckBox includePartialMatch; + + public BirthdateRangeFilterComponent(boolean showCaption, AbstractFilterForm parentFilterForm) { + setSpacing(true); + + Calendar c = Calendar.getInstance(); + c.setTime(new Date()); + + dateFromFilter = new PopupDateField(); + dateToFilter = new PopupDateField(); + includePartialMatch = new CheckBox(); + + addComponent(dateFromFilter); + addComponent(dateToFilter); + + // Date filter + dateFromFilter.setDateFormat(DateFormatHelper.getDateFormatPattern()); + dateFromFilter.setId("dateFrom"); + dateFromFilter.setWidth(200, Unit.PIXELS); + if (showCaption) { + dateFromFilter.setCaption(I18nProperties.getCaption(Captions.from)); + } + dateFromFilter.setInputPrompt(I18nProperties.getString(Strings.promptBirthdateFrom)); + + dateToFilter.setDateFormat(DateFormatHelper.getDateFormatPattern()); + dateToFilter.setId("dateTo"); + dateToFilter.setWidth(200, Unit.PIXELS); + if (showCaption) { + dateToFilter.setCaption(I18nProperties.getCaption(Captions.to)); + } + dateToFilter.setInputPrompt(I18nProperties.getString(Strings.promptBirthdateTo)); + + dateFromFilter.addValueChangeListener(e -> { + Date dateFrom = (Date) e.getProperty().getValue(); + Date dateTo = dateToFilter.getValue(); + notifyIfIncorrectRange(dateFrom, dateTo); + parentFilterForm.onChange(); + }); + + dateToFilter.addValueChangeListener(e -> { + Date dateTo = (Date) e.getProperty().getValue(); + Date dateFrom = dateFromFilter.getValue(); + notifyIfIncorrectRange(dateFrom, dateTo); + parentFilterForm.onChange(); + }); + + includePartialMatch.setCaption(I18nProperties.getCaption(Captions.includePartialBirthdates)); + includePartialMatch.addStyleName(VSPACE_TOP_4); + includePartialMatch.setDescription(I18nProperties.getDescription(Descriptions.birthdateFilterPartialMatchDescription)); + addComponent(includePartialMatch); + } + + private static void notifyIfIncorrectRange(Date dateFrom, Date dateTo) { + if (dateFrom != null & dateTo != null) { + if (DateHelper.isDateAfter(dateFrom, dateTo)) { + Notification notification = new Notification( + I18nProperties.getString(Strings.headingIncorrectDateRange), + I18nProperties.getString(Strings.messageIncorrectDateRange), + Notification.Type.WARNING_MESSAGE, + false); + + notification.setDelayMsec(-1); + notification.show(Page.getCurrent()); + } + } + } + + public PopupDateField getDateFromFilter() { + return dateFromFilter; + } + + public PopupDateField getDateToFilter() { + return dateToFilter; + } + + public CheckBox getIncludePartialMatch() { + return includePartialMatch; + } +}