pathogenTests, CaseRefer
// We decided this based on the intented text in the dialog but based on the test results instead of the sample overall result
if (hasVerifiedPositiveTest) {
// The final laboratory result of the sample the saved pathogen test belongs to is positive. <-- sample overall result
- // However, the case cannot be automatically classified as a confirmed case because it is missing some information.
+ // However, the case cannot be automatically classified as a confirmed case because it is missing some information.
// Do you want to set the case classification to confirmed anyway?
this.showConfirmCaseDialog(c); // Case classification
}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/PathogenTestForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/PathogenTestForm.java
index eb028305102..faea093a0da 100644
--- a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/PathogenTestForm.java
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/PathogenTestForm.java
@@ -18,258 +18,94 @@
package de.symeda.sormas.ui.samples;
import static de.symeda.sormas.ui.utils.CssStyles.H3;
-import static de.symeda.sormas.ui.utils.CssStyles.VSPACE_3;
-import static de.symeda.sormas.ui.utils.CssStyles.VSPACE_TOP_4;
-import static de.symeda.sormas.ui.utils.LayoutUtil.fluidRowLocs;
import static de.symeda.sormas.ui.utils.LayoutUtil.loc;
-import java.time.LocalTime;
-import java.time.ZoneId;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
import java.util.Collections;
import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
-import java.util.Map;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
-import org.apache.commons.collections4.CollectionUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.vaadin.server.UserError;
-import com.vaadin.ui.AbstractComponent;
+import com.vaadin.shared.Registration;
+import com.vaadin.ui.Component;
import com.vaadin.ui.Label;
-import com.vaadin.v7.data.Property;
-import com.vaadin.v7.data.fieldgroup.BeanFieldGroup;
-import com.vaadin.v7.data.util.BeanItem;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.v7.data.Validator.InvalidValueException;
+import com.vaadin.v7.data.fieldgroup.FieldGroup.CommitEvent;
+import com.vaadin.v7.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.v7.data.util.converter.Converter;
-import com.vaadin.v7.data.util.converter.Converter.ConversionException;
-import com.vaadin.v7.data.util.converter.ConverterUtil;
-import com.vaadin.v7.ui.AbstractSelect.ItemCaptionMode;
-import com.vaadin.v7.ui.CheckBox;
-import com.vaadin.v7.ui.ComboBox;
-import com.vaadin.v7.ui.DateField;
-import com.vaadin.v7.ui.Field;
-import com.vaadin.v7.ui.TextArea;
-import com.vaadin.v7.ui.TextField;
-
-import de.symeda.sormas.api.CountryHelper;
+
import de.symeda.sormas.api.Disease;
-import de.symeda.sormas.api.DiseaseHelper;
import de.symeda.sormas.api.FacadeProvider;
-import de.symeda.sormas.api.customizableenum.CustomizableEnumType;
+import de.symeda.sormas.api.common.DeletionReason;
import de.symeda.sormas.api.disease.DiseaseVariant;
import de.symeda.sormas.api.environment.environmentsample.EnvironmentSampleDto;
-import de.symeda.sormas.api.environment.environmentsample.Pathogen;
-import de.symeda.sormas.api.i18n.Captions;
-import de.symeda.sormas.api.i18n.I18nProperties;
-import de.symeda.sormas.api.i18n.Strings;
-import de.symeda.sormas.api.i18n.Validations;
-import de.symeda.sormas.api.infrastructure.facility.FacilityDto;
-import de.symeda.sormas.api.infrastructure.facility.FacilityReferenceDto;
-import de.symeda.sormas.api.sample.GenoType;
-import de.symeda.sormas.api.sample.PathogenSpecie;
-import de.symeda.sormas.api.sample.PathogenStrainCallStatus;
import de.symeda.sormas.api.sample.PathogenTestDto;
import de.symeda.sormas.api.sample.PathogenTestResultType;
-import de.symeda.sormas.api.sample.PathogenTestType;
import de.symeda.sormas.api.sample.SampleDto;
import de.symeda.sormas.api.sample.SamplePurpose;
-import de.symeda.sormas.api.sample.SeroGroupSpecification;
-import de.symeda.sormas.api.sample.Serotype;
-import de.symeda.sormas.api.sample.SerotypingMethod;
-import de.symeda.sormas.api.utils.fieldaccess.UiFieldAccessCheckers;
import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers;
-import de.symeda.sormas.ui.therapy.DrugSusceptibilityForm;
+import de.symeda.sormas.ui.samples.components.AdditionalTestInfoComponent;
+import de.symeda.sormas.ui.samples.components.DeletionComponent;
+import de.symeda.sormas.ui.samples.components.DiseaseSelectionComponent;
+import de.symeda.sormas.ui.samples.components.DiseaseVariantComponent;
+import de.symeda.sormas.ui.samples.components.FourFoldCtCqComponent;
+import de.symeda.sormas.ui.samples.components.PrescriberComponent;
+import de.symeda.sormas.ui.samples.components.ResultTextComponent;
+import de.symeda.sormas.ui.samples.components.TestIdentificationComponent;
+import de.symeda.sormas.ui.samples.components.TestMethodComponent;
+import de.symeda.sormas.ui.samples.components.TestResultComponent;
+import de.symeda.sormas.ui.samples.diseasesection.AbstractDiseaseSectionComponent;
+import de.symeda.sormas.ui.samples.diseasesection.DiseaseSectionFactory;
+import de.symeda.sormas.ui.samples.diseasesection.PathogenTestFormConfig;
+import de.symeda.sormas.ui.samples.events.DiseaseChangedEvent;
import de.symeda.sormas.ui.utils.AbstractEditForm;
-import de.symeda.sormas.ui.utils.CssStyles;
-import de.symeda.sormas.ui.utils.DateComparisonValidator;
-import de.symeda.sormas.ui.utils.DateFormatHelper;
-import de.symeda.sormas.ui.utils.DateTimeField;
import de.symeda.sormas.ui.utils.FieldAccessHelper;
-import de.symeda.sormas.ui.utils.FieldConfiguration;
-import de.symeda.sormas.ui.utils.FieldHelper;
-import de.symeda.sormas.ui.utils.NullableOptionGroup;
-import de.symeda.sormas.ui.utils.PhoneNumberValidator;
-
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.FormEventBus;
+
+/**
+ * Component-based PathogenTestForm using Vaadin 8 components.
+ *
+ * Extends {@link AbstractEditForm} for compatibility with
+ * {@link de.symeda.sormas.ui.utils.CommitDiscardWrapperComponent} and
+ * {@link de.symeda.sormas.ui.externalmessage.CorrectionPanel}.
+ *
+ * Each component owns its own Vaadin 8 Binder and manages its own field
+ * visibility via value change listeners (no FieldHelper.setVisibleWhen).
+ * The FieldGroup from AbstractEditForm is kept hollow — only DrugSusceptibilityForm
+ * (in disease sections) binds to it for legacy compatibility.
+ */
public class PathogenTestForm extends AbstractEditForm {
private static final long serialVersionUID = -1218707278398543154L;
- private final Logger logger = LoggerFactory.getLogger(getClass());
-
- private static final String PATHOGEN_TEST_HEADING_LOC = "pathogenTestHeadingLoc";
-
- private static final String PRESCRIBER_HEADING_LOC = "prescriberHeading";
-
- //@formatter:off
- private static final String HTML_LAYOUT =
- loc(PATHOGEN_TEST_HEADING_LOC) +
- fluidRowLocs(PathogenTestDto.REPORT_DATE, PathogenTestDto.VIA_LIMS) +
- fluidRowLocs(PathogenTestDto.EXTERNAL_ID, PathogenTestDto.EXTERNAL_ORDER_ID) +
- fluidRowLocs(PathogenTestDto.TESTED_DISEASE, PathogenTestDto.TESTED_DISEASE_DETAILS) +
- fluidRowLocs(PathogenTestDto.TEST_TYPE, PathogenTestDto.TEST_TYPE_TEXT) +
- fluidRowLocs(PathogenTestDto.PCR_TEST_SPECIFICATION, "") +
- fluidRowLocs(PathogenTestDto.TESTED_PATHOGEN, PathogenTestDto.TESTED_PATHOGEN_DETAILS) +
- fluidRowLocs(PathogenTestDto.TYPING_ID, "") +
- fluidRowLocs(PathogenTestDto.TEST_DATE_TIME, PathogenTestDto.LAB) +
- fluidRowLocs(6, "",6, PathogenTestDto.LAB_DETAILS) +
- fluidRowLocs(6,PathogenTestDto.TEST_RESULT, 4, PathogenTestDto.TEST_RESULT_VERIFIED, 2,PathogenTestDto.PRELIMINARY) +
- fluidRowLocs(6, PathogenTestDto.RESULT_DETAILS,3,PathogenTestDto.PERFORMED_BY_REFERENCE_LABORATORY,3, PathogenTestDto.RETEST_REQUESTED) +
- fluidRowLocs(PathogenTestDto.TESTED_DISEASE_VARIANT, PathogenTestDto.TESTED_DISEASE_VARIANT_DETAILS) +
- fluidRowLocs(PathogenTestDto.RIFAMPICIN_RESISTANT, PathogenTestDto.ISONIAZID_RESISTANT, "", "") +
- fluidRowLocs(PathogenTestDto.TEST_SCALE, "") +
- fluidRowLocs(PathogenTestDto.STRAIN_CALL_STATUS, "") +
- fluidRowLocs(PathogenTestDto.SPECIE, PathogenTestDto.SPECIE_TEXT) +
- fluidRowLocs(PathogenTestDto.PATTERN_PROFILE, "") +
- fluidRowLocs(PathogenTestDto.DRUG_SUSCEPTIBILITY) +
- fluidRowLocs(6,PathogenTestDto.SEROTYPE, 6,PathogenTestDto.SEROTYPE_TEXT) +
- fluidRowLocs(6,PathogenTestDto.SEROTYPING_METHOD, 6,PathogenTestDto.SERO_TYPING_METHOD_TEXT) +
- fluidRowLocs(6,PathogenTestDto.SERO_GROUP_SPECIFICATION , 6, PathogenTestDto.SERO_GROUP_SPECIFICATION_TEXT) +
- fluidRowLocs(6,PathogenTestDto.GENOTYPE,6, PathogenTestDto.GENOTYPE_TEXT) +
- fluidRowLocs(6,PathogenTestDto.ANTIBODY_TITRE) +
- fluidRowLocs(PathogenTestDto.FOUR_FOLD_INCREASE_ANTIBODY_TITER, "") +
- fluidRowLocs(PathogenTestDto.CQ_VALUE, "") +
- fluidRowLocs(PathogenTestDto.CT_VALUE_E, PathogenTestDto.CT_VALUE_N) +
- fluidRowLocs(PathogenTestDto.CT_VALUE_RDRP, PathogenTestDto.CT_VALUE_S) +
- fluidRowLocs(PathogenTestDto.CT_VALUE_ORF_1, PathogenTestDto.CT_VALUE_RDRP_S) +
- fluidRowLocs(PathogenTestDto.TUBE_NIL, PathogenTestDto.TUBE_NIL_GT10) +
- fluidRowLocs(PathogenTestDto.TUBE_AG_TB1, PathogenTestDto.TUBE_AG_TB1_GT10) +
- fluidRowLocs(PathogenTestDto.TUBE_AG_TB2, PathogenTestDto.TUBE_AG_TB2_GT10) +
- fluidRowLocs(PathogenTestDto.TUBE_MITOGENE, PathogenTestDto.TUBE_MITOGENE_GT10) +
- fluidRowLocs(PathogenTestDto.TEST_RESULT_TEXT) +
- fluidRowLocs(PRESCRIBER_HEADING_LOC) +
- fluidRowLocs(PathogenTestDto.PRESCRIBER_PHYSICIAN_CODE, "") +
- fluidRowLocs(PathogenTestDto.PRESCRIBER_FIRST_NAME, PathogenTestDto.PRESCRIBER_LAST_NAME) +
- fluidRowLocs(PathogenTestDto.PRESCRIBER_PHONE_NUMBER, "") +
- fluidRowLocs(PathogenTestDto.PRESCRIBER_ADDRESS, PathogenTestDto.PRESCRIBER_POSTAL_CODE) +
- fluidRowLocs(PathogenTestDto.PRESCRIBER_CITY, PathogenTestDto.PRESCRIBER_COUNTRY) +
- fluidRowLocs(PathogenTestDto.DELETION_REASON) +
- fluidRowLocs(PathogenTestDto.OTHER_DELETION_REASON);
- //@formatter:on
-
- //@formatter:off
- // map to decide the result type field value and enable/disable state
- public static final Map> RESULT_FIELD_DECISION_MAP = Collections.unmodifiableMap(new HashMap<>() {
- {
- put(Disease.INVASIVE_MENINGOCOCCAL_INFECTION, new ArrayList<>(List.of(PathogenTestType.SEROGROUPING,
- PathogenTestType.MULTILOCUS_SEQUENCE_TYPING, PathogenTestType.SLIDE_AGGLUTINATION, PathogenTestType.WHOLE_GENOME_SEQUENCING,
- PathogenTestType.SEQUENCING, PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY)));
- put(Disease.INVASIVE_PNEUMOCOCCAL_INFECTION, new ArrayList<>(List.of(PathogenTestType.SEROGROUPING, PathogenTestType.MULTILOCUS_SEQUENCE_TYPING,
- PathogenTestType.SLIDE_AGGLUTINATION, PathogenTestType.WHOLE_GENOME_SEQUENCING, PathogenTestType.SEQUENCING, PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY)));
- put(Disease.MEASLES, new ArrayList<>(List.of(PathogenTestType.GENOTYPING)));
- put(Disease.RESPIRATORY_SYNCYTIAL_VIRUS, new ArrayList<>(List.of(PathogenTestType.SEQUENCING, PathogenTestType.WHOLE_GENOME_SEQUENCING)));
- put(Disease.INFLUENZA, new ArrayList<>(List.of(PathogenTestType.ISOLATION)));
- put(Disease.CRYPTOSPORIDIOSIS, new ArrayList<>(List.of(PathogenTestType.GENOTYPING)));
- put(Disease.DENGUE, new ArrayList<>(List.of(PathogenTestType.NAAT, PathogenTestType.NEUTRALIZING_ANTIBODIES, PathogenTestType.PCR_RT_PCR)));
- put(Disease.MALARIA, new ArrayList<>(List.of( PathogenTestType.ANTIGEN_DETECTION, PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST,
- PathogenTestType.INDIRECT_FLUORESCENT_ANTIBODY,PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.LAMP,
- PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, PathogenTestType.OTHER_SEROLOGICAL_TEST, PathogenTestType.OTHER_MOLECULAR_ASSAY)));
- }
- });
-
- // map to decide the serotype field value and enable/disable state
- // Serotype should display, with @Herold code refactor, it should be removed from here.
- public static final Map> SEROTYPE_VISIBILITY_MAP = Collections.unmodifiableMap(new HashMap<>() {
- {
- put(Disease.INVASIVE_PNEUMOCOCCAL_INFECTION, Collections.unmodifiableList(Arrays.asList(PathogenTestType.WHOLE_GENOME_SEQUENCING,
- PathogenTestType.SLIDE_AGGLUTINATION, PathogenTestType.MULTILOCUS_SEQUENCE_TYPING, PathogenTestType.SEROGROUPING)));
- put(Disease.DENGUE, Collections.unmodifiableList(Arrays.asList(PathogenTestType.NAAT, PathogenTestType.PCR_RT_PCR, PathogenTestType.NEUTRALIZING_ANTIBODIES)));
- }
- });
- //@formatter:off
- public static final Map> RIFAMPICIN_RESISTANT_VISIBILITY_CONDITIONS = Collections.unmodifiableMap(new HashMap<>() {
-
- {
- put(PathogenTestDto.TESTED_DISEASE, Collections.unmodifiableList(Arrays.asList(Disease.LATENT_TUBERCULOSIS, Disease.TUBERCULOSIS)));
- put(PathogenTestDto.TEST_TYPE, Collections.unmodifiableList(Arrays.asList(PathogenTestType.PCR_RT_PCR)));
- put(PathogenTestDto.TEST_RESULT, Collections.unmodifiableList(Arrays.asList(PathogenTestResultType.POSITIVE)));
- }
- });
-
- public static final Map> TEST_SCALE_VISIBILITY_CONDITIONS = Collections.unmodifiableMap(new HashMap<>() {
-
- {
- put(PathogenTestDto.TESTED_DISEASE, Collections.unmodifiableList(Arrays.asList(Disease.LATENT_TUBERCULOSIS, Disease.TUBERCULOSIS)));
- put(PathogenTestDto.TEST_TYPE, Collections.unmodifiableList(Arrays.asList(PathogenTestType.MICROSCOPY)));
- }
- });
-
- public static final Map> STRAIN_CALL_STATUS_VISIBILITY_CONDITIONS = Collections.unmodifiableMap(new HashMap<>() {
-
- {
- put(PathogenTestDto.TESTED_DISEASE, Collections.unmodifiableList(Arrays.asList(Disease.LATENT_TUBERCULOSIS, Disease.TUBERCULOSIS)));
- put(PathogenTestDto.TEST_TYPE, Collections.unmodifiableList(Arrays.asList(PathogenTestType.BEIJINGGENOTYPING)));
- }
- });
- //@formatter:off
- // this map is to decide the species field value and enable/disable state.
- // this suppose to refactored with @Harold changes
- public static final Map> SPECIE_VISIBILITY_MAP = Collections.unmodifiableMap(new HashMap<>() {
-
- {
- put(Disease.LATENT_TUBERCULOSIS, Collections.unmodifiableList(Arrays.asList(PathogenTestType.SPOLIGOTYPING)));
- put(Disease.TUBERCULOSIS, Collections.unmodifiableList(Arrays.asList(PathogenTestType.SPOLIGOTYPING)));
- put(Disease.MALARIA, Collections.unmodifiableList(Arrays.asList(PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.ANTIGEN_DETECTION,
- PathogenTestType.RAPID_TEST, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, PathogenTestType.LAMP,PathogenTestType.INDIRECT_FLUORESCENT_ANTIBODY,
- PathogenTestType.OTHER_MOLECULAR_ASSAY, PathogenTestType.OTHER_SEROLOGICAL_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST,
- PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY)));
- }
- });
- //@formatter:on
- public static final Map> SPECIE_VISIBILITY_CONDITIONS = Collections.unmodifiableMap(new HashMap<>() {
-
- {
- put(PathogenTestDto.TESTED_DISEASE, Collections.unmodifiableList(Arrays.asList(Disease.LATENT_TUBERCULOSIS, Disease.TUBERCULOSIS)));
- put(PathogenTestDto.TEST_TYPE, Collections.unmodifiableList(Arrays.asList(PathogenTestType.SPOLIGOTYPING)));
- put(PathogenTestDto.TEST_RESULT, Collections.unmodifiableList(Arrays.asList(PathogenTestResultType.POSITIVE)));
- }
- });
-
- public static final Map> PATTERN_PROFILE_VISIBILITY_CONDITIONS = Collections.unmodifiableMap(new HashMap<>() {
-
- {
- put(PathogenTestDto.TESTED_DISEASE, Collections.unmodifiableList(Arrays.asList(Disease.LATENT_TUBERCULOSIS, Disease.TUBERCULOSIS)));
- put(PathogenTestDto.TEST_TYPE, Collections.unmodifiableList(Arrays.asList(PathogenTestType.MIRU_PATTERN_CODE)));
- }
- });
-
- public static final Map> PCR_TEST_SPECIFICATION_VISIBILITY_CONDITIONS = Collections.unmodifiableMap(new HashMap<>() {
-
- {
- put(PathogenTestDto.TESTED_DISEASE, Collections.unmodifiableList(Arrays.asList(Disease.CORONAVIRUS)));
- put(PathogenTestDto.TEST_TYPE, Collections.unmodifiableList(Arrays.asList(PathogenTestType.PCR_RT_PCR)));
- }
- });
+ private static final String COMPONENT_CONTAINER_LOC = "componentContainerLoc";
+ private static final String HTML_LAYOUT = loc(COMPONENT_CONTAINER_LOC);
private SampleDto sample;
private EnvironmentSampleDto environmentSample;
private AbstractSampleForm sampleForm;
private final int caseSampleCount;
private final boolean create;
+ private Disease disease;
+ private PathogenTestFormConfig formConfig;
- private Label pathogenTestHeadingLabel;
+ private final FormEventBus eventBus = new FormEventBus();
+ private final List> formComponents = new ArrayList<>();
+ private final List eventRegistrations = new ArrayList<>();
- private ComboBox testTypeField;
- private ComboBox diseaseField;
- private ComboBox testResultField;
- private DrugSusceptibilityForm drugSusceptibilityField;
- private TextField testTypeTextField;
- private ComboBox pcrTestSpecification;
- private Disease disease;
- private TextField typingIdField;
- private ComboBox specieField;
- private ComboBox genoTypingCB;
- private TextField genoTypingResultTextTF;
+ private Label headingLabel;
+ private TestIdentificationComponent testIdentificationComponent;
+ private TestMethodComponent testMethodComponent;
+ private TestResultComponent testResultComponent;
+ private DiseaseVariantComponent diseaseVariantComponent;
+ private DiseaseSelectionComponent diseaseSelectionComponent;
+ private DeletionComponent deletionComponent;
- private ComboBox seroGrpSepcCB;
- private TextField seroGrpSpecTxt;
- private ComboBox seroTypeField;
+ private AbstractDiseaseSectionComponent activeSection;
+ private VerticalLayout diseaseSectionSlot;
public PathogenTestForm(
AbstractSampleForm sampleForm,
@@ -278,6 +114,7 @@ public PathogenTestForm(
boolean isPseudonymized,
boolean inJurisdiction,
Disease disease) {
+
this(create, caseSampleCount, isPseudonymized, inJurisdiction, disease);
this.sampleForm = sampleForm;
this.disease = disease;
@@ -288,7 +125,6 @@ public PathogenTestForm(
}
public PathogenTestForm(SampleDto sample, boolean create, int caseSampleCount, boolean isPseudonymized, boolean inJurisdiction, Disease disease) {
-
this(create, caseSampleCount, isPseudonymized, inJurisdiction, disease);
this.sample = sample;
this.disease = disease;
@@ -299,7 +135,6 @@ public PathogenTestForm(SampleDto sample, boolean create, int caseSampleCount, b
}
public PathogenTestForm(EnvironmentSampleDto sample, boolean create, boolean isPseudonymized, boolean inJurisdiction, Disease disease) {
-
this(create, 0, isPseudonymized, inJurisdiction, disease);
this.environmentSample = sample;
addFields();
@@ -308,83 +143,168 @@ public PathogenTestForm(EnvironmentSampleDto sample, boolean create, boolean isP
}
}
- public PathogenTestForm(boolean create, int caseSampleCount, boolean isPseudonymized, boolean inJurisdiction, Disease disease) {
+ private PathogenTestForm(boolean create, int caseSampleCount, boolean isPseudonymized, boolean inJurisdiction, Disease disease) {
super(
PathogenTestDto.class,
PathogenTestDto.I18N_PREFIX,
false,
FieldVisibilityCheckers.withDisease(disease).andWithCountry(FacadeProvider.getConfigFacade().getCountryLocale()),
- FieldAccessHelper.getFieldAccessCheckers(create || inJurisdiction, !create && isPseudonymized));// Jurisdiction doesn't matter for creation forms // Pseudonymization doesn't matter for creation forms
+ FieldAccessHelper.getFieldAccessCheckers(create || inJurisdiction, !create && isPseudonymized));
this.caseSampleCount = caseSampleCount;
this.create = create;
+ this.disease = disease;
setWidth(900, Unit.PIXELS);
}
- private static void setCqValueVisibility(
- ComboBox diseaseField,
- TextField cqValueField,
- PathogenTestType testType,
- PathogenTestResultType testResultType) {
-
- if (diseaseField.getValue() == null || !List.of(Disease.TUBERCULOSIS).contains((Disease) diseaseField.getValue())) {
- if (((testType == PathogenTestType.PCR_RT_PCR && testResultType == PathogenTestResultType.POSITIVE))
- || testType == PathogenTestType.CQ_VALUE_DETECTION) {
- cqValueField.setVisible(true);
- } else if (Disease.MALARIA == (Disease) diseaseField.getValue() && testType == PathogenTestType.Q_PCR) {
- // CT value should be visible for Malaria, QPCR test.
- cqValueField.setVisible(true);
+ @Override
+ protected String createHtmlLayout() {
+ return HTML_LAYOUT;
+ }
+
+ @Override
+ protected void addFields() {
+ formConfig = PathogenTestFormConfig.fromCurrentConfig();
+
+ VerticalLayout container = new VerticalLayout();
+ container.setWidth(100, Unit.PERCENTAGE);
+ container.setMargin(false);
+ container.setSpacing(true);
+ getContent().addComponent(container, COMPONENT_CONTAINER_LOC);
+
+ headingLabel = new Label();
+ headingLabel.addStyleName(H3);
+ container.addComponent(headingLabel);
+
+ testIdentificationComponent = new TestIdentificationComponent(eventBus);
+ formComponents.add(testIdentificationComponent);
+ container.addComponent(testIdentificationComponent);
+
+ diseaseSelectionComponent = new DiseaseSelectionComponent(eventBus, disease, create, environmentSample != null);
+ formComponents.add(diseaseSelectionComponent);
+ container.addComponent(diseaseSelectionComponent);
+
+ testMethodComponent = new TestMethodComponent(eventBus, this::getSampleDate, disease);
+ formComponents.add(testMethodComponent);
+ container.addComponent(testMethodComponent);
+
+ testResultComponent = new TestResultComponent(eventBus, formConfig.isLuxembourg, disease);
+ formComponents.add(testResultComponent);
+ container.addComponent(testResultComponent);
+
+ diseaseVariantComponent = new DiseaseVariantComponent(eventBus, disease);
+ formComponents.add(diseaseVariantComponent);
+ container.addComponent(diseaseVariantComponent);
+
+ FourFoldCtCqComponent fourFoldCtCqComponent = new FourFoldCtCqComponent(eventBus, caseSampleCount, formConfig.isLuxembourg, disease);
+ formComponents.add(fourFoldCtCqComponent);
+ container.addComponent(fourFoldCtCqComponent);
+
+ diseaseSectionSlot = new VerticalLayout();
+ diseaseSectionSlot.setWidth(100, Unit.PERCENTAGE);
+ diseaseSectionSlot.setMargin(false);
+ diseaseSectionSlot.setSpacing(false);
+ container.addComponent(diseaseSectionSlot);
+
+ activeSection = DiseaseSectionFactory.forDisease(disease);
+ activeSection.initialize(getFieldGroup(), eventBus, formConfig, disease);
+ activeSection.setVisibilityCallback(visible -> diseaseSectionSlot.setVisible(visible));
+ diseaseSectionSlot.addComponent(activeSection);
+
+ AdditionalTestInfoComponent additionalTestInfoComponent = new AdditionalTestInfoComponent();
+ formComponents.add(additionalTestInfoComponent);
+ container.addComponent(additionalTestInfoComponent);
+
+ ResultTextComponent resultTextComponent = new ResultTextComponent();
+ formComponents.add(resultTextComponent);
+ container.addComponent(resultTextComponent);
+
+ PrescriberComponent prescriberComponent = new PrescriberComponent();
+ formComponents.add(prescriberComponent);
+ container.addComponent(prescriberComponent);
+
+ deletionComponent = new DeletionComponent();
+ formComponents.add(deletionComponent);
+ container.addComponent(deletionComponent);
+
+ wireEvents();
+
+ finalizeForm();
+ }
+
+ private void wireEvents() {
+ // Disease change → swap section + clear test type
+ eventRegistrations.add(eventBus.on(DiseaseChangedEvent.class, event -> {
+ Disease newDisease = event.getDisease();
+ if (newDisease != disease) {
+ testMethodComponent.getTestTypeField().clear();
+ }
+ disease = newDisease;
+ swapDiseaseSection(newDisease);
+ }));
+
+ // Disease variant → auto-set test result
+ eventRegistrations.add(diseaseVariantComponent.getDiseaseVariantField().addValueChangeListener(e -> {
+ DiseaseVariant variant = e.getValue();
+ if (variant != null) {
+ testResultComponent.getTestResultField().setValue(PathogenTestResultType.POSITIVE);
} else {
- cqValueField.setVisible(false);
- cqValueField.clear();
+ testResultComponent.getTestResultField().clear();
}
- }
+ }));
}
- private void updateDrugSusceptibilityFieldSpecifications(PathogenTestType testType, Disease disease) {
+ private void finalizeForm() {
+ testMethodComponent.setLabRequired(SamplePurpose.INTERNAL.equals(getSamplePurpose()));
+
+ initializeAccessAndAllowedAccesses();
+ initializeVisibilitiesAndAllowedVisibilities();
- // Hide or show drug susceptibility fields based on the disease and test type (if disease is null then drug susceptibility should be hidden)
- if (drugSusceptibilityField != null) {
- drugSusceptibilityField.updateFieldsVisibility(disease, testType);
+ for (FormComponent comp : formComponents) {
+ if (fieldVisibilityCheckers != null) {
+ comp.applyVisibility(fieldVisibilityCheckers, PathogenTestDto.class);
+ }
+ if (fieldAccessCheckers != null) {
+ comp.applyAccess(fieldAccessCheckers, PathogenTestDto.class);
+ }
}
- // if the disease is null, means that we are dealing with a environment sample
- // and we don't need to update the result field
- if (disease == null) {
- return;
+ if (fieldVisibilityCheckers != null) {
+ activeSection.applyVisibility(fieldVisibilityCheckers, PathogenTestDto.class);
}
- // if the test type is null we just clear the result field
- if (testType == null) {
- testResultField.setValue(null);
- return;
+ if (fieldAccessCheckers != null) {
+ activeSection.applyAccess(fieldAccessCheckers, PathogenTestDto.class);
}
- // FIXME: why was this here originally?
- // TODO: move this to another place, should be in listeners for disease/testType.
+ }
+
+ private void swapDiseaseSection(Disease newDisease) {
+ AbstractDiseaseSectionComponent newSection = DiseaseSectionFactory.forDisease(newDisease);
+ if (newSection.getClass() == activeSection.getClass()) {
+ return;
+ }
- if ((FacadeProvider.getConfigFacade().isConfiguredCountry(CountryHelper.COUNTRY_CODE_LUXEMBOURG))) {
+ activeSection.cleanup();
+ diseaseSectionSlot.removeAllComponents();
- // testResult=NOT_APPLICABLE for Tuberculosis diseases, test types BEIJINGGENOTYPING,MIRU_PATTERN_CODE,ANTIBIOTIC_SUSCEPTIBILITY
- if ((disease == Disease.LATENT_TUBERCULOSIS || disease == Disease.TUBERCULOSIS)
- && (testType == PathogenTestType.BEIJINGGENOTYPING
- || testType == PathogenTestType.MIRU_PATTERN_CODE
- || testType == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY)) {
- testResultField.setValue(PathogenTestResultType.NOT_APPLICABLE);
- }
+ activeSection = newSection;
+ activeSection.initialize(getFieldGroup(), eventBus, formConfig, newDisease);
+ activeSection.setVisibilityCallback(visible -> diseaseSectionSlot.setVisible(visible));
+ diseaseSectionSlot.addComponent(activeSection);
- // testResult=POSITIVE for Tuberculosis diseases, test type SPOLIGOTYPING
- if ((disease == Disease.LATENT_TUBERCULOSIS || disease == Disease.TUBERCULOSIS) && (testType == PathogenTestType.SPOLIGOTYPING)) {
- testResultField.setValue(PathogenTestResultType.POSITIVE);
- }
+ PathogenTestDto dto = getValue();
+ if (dto != null) {
+ activeSection.setDto(dto);
+ }
- // testResult=POSITIVE for IMI and IPI, test type ANTIBIOTIC_SUSCEPTIBILITY
- if ((disease == Disease.INVASIVE_MENINGOCOCCAL_INFECTION || disease == Disease.INVASIVE_PNEUMOCOCCAL_INFECTION)
- && testType == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY) {
- testResultField.setValue(PathogenTestResultType.POSITIVE);
- }
+ if (fieldVisibilityCheckers != null) {
+ activeSection.applyVisibility(fieldVisibilityCheckers, PathogenTestDto.class);
}
+ if (fieldAccessCheckers != null) {
+ activeSection.applyAccess(fieldAccessCheckers, PathogenTestDto.class);
+ }
}
private Date getSampleDate() {
@@ -411,848 +331,115 @@ private SamplePurpose getSamplePurpose() {
}
@Override
- protected String createHtmlLayout() {
- return HTML_LAYOUT;
- }
-
- @Override
- public void setHeading(String heading) {
- pathogenTestHeadingLabel.setValue(heading);
- }
-
- @Override
- public void setValue(PathogenTestDto newFieldValue) throws ReadOnlyException, Converter.ConversionException {
- super.setValue(newFieldValue);
- testTypeField.setValue(newFieldValue.getTestType());
- pcrTestSpecification.setValue(newFieldValue.getPcrTestSpecification());
- testTypeTextField.setValue(newFieldValue.getTestTypeText());
- if (!testResultField.isReadOnly()) {
- testResultField.setValue(newFieldValue.getTestResult());
- }
- typingIdField.setValue(newFieldValue.getTypingId());
- specieField.setValue(newFieldValue.getSpecie());
- if (!genoTypingCB.isReadOnly()) {
- genoTypingCB.setValue(newFieldValue.getGenoType());
+ public void preCommit(CommitEvent commitEvent) throws CommitException {
+ super.preCommit(commitEvent);
- }
+ List allCauses = new ArrayList<>();
- if (!genoTypingResultTextTF.isReadOnly()) {
- genoTypingResultTextTF.setValue(newFieldValue.getGenoTypeText());
+ for (FormComponent comp : formComponents) {
+ try {
+ comp.validate();
+ } catch (InvalidValueException e) {
+ collectCauses(e, allCauses);
+ }
}
- if (!seroGrpSepcCB.isReadOnly()) {
- seroGrpSepcCB.setValue(newFieldValue.getSeroGroupSpecification());
+ if (activeSection != null) {
+ try {
+ activeSection.validate();
+ } catch (InvalidValueException e) {
+ collectCauses(e, allCauses);
+ }
}
- if (!seroGrpSpecTxt.isReadOnly()) {
- seroGrpSpecTxt.setValue(newFieldValue.getSeroGroupSpecificationText());
+ if (!allCauses.isEmpty()) {
+ String joinedCaptions = allCauses.stream().map(InvalidValueException::getMessage).collect(Collectors.joining(", "));
+ throw new InvalidValueException(joinedCaptions, allCauses.toArray(new InvalidValueException[0]));
}
-
- drugSusceptibilityField.forceUpdateDrugSusceptibilityFields();
- markAsDirty();
}
- @Override
- protected void addFields() {
-
- pathogenTestHeadingLabel = new Label();
- pathogenTestHeadingLabel.addStyleName(H3);
- getContent().addComponent(pathogenTestHeadingLabel, PATHOGEN_TEST_HEADING_LOC);
-
- addDateField(PathogenTestDto.REPORT_DATE, DateField.class, 0);
- CheckBox viaLimsField = addField(PathogenTestDto.VIA_LIMS);
- addField(PathogenTestDto.EXTERNAL_ID);
- addField(PathogenTestDto.EXTERNAL_ORDER_ID);
- testTypeField = addField(PathogenTestDto.TEST_TYPE, ComboBox.class);
- testTypeField.setItemCaptionMode(ItemCaptionMode.ID_TOSTRING);
- testTypeField.setImmediate(true);
- TextField seroTypingMethodText = addField(PathogenTestDto.SERO_TYPING_METHOD_TEXT);
- seroTypingMethodText.setVisible(false);
- pcrTestSpecification = addField(PathogenTestDto.PCR_TEST_SPECIFICATION, ComboBox.class);
- testTypeTextField = addField(PathogenTestDto.TEST_TYPE_TEXT, TextField.class);
- DateTimeField testDateField = addField(PathogenTestDto.TEST_DATE_TIME, DateTimeField.class);
- testDateField.removeAllValidators();
- testDateField.addValidator(
- new DateComparisonValidator(
- testDateField,
- this::getSampleDate,
- false,
- false,
- true,
- I18nProperties.getValidationError(
- Validations.afterDateWithDate,
- testDateField.getCaption(),
- I18nProperties.getPrefixCaption(SampleDto.I18N_PREFIX, SampleDto.SAMPLE_DATE_TIME),
- DateFormatHelper.formatDate(getSampleDate()))));
- testDateField.addValueChangeListener(e -> {
- boolean hasTime =
- getSampleDate() != null && !getSampleDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().equals(LocalTime.MIDNIGHT);
-
- if (!hasTime) {
- return;
- }
-
- testDateField.removeAllValidators();
- testDateField.addValidator(
- new DateComparisonValidator(
- testDateField,
- this::getSampleDate,
- false,
- false,
- false,
- I18nProperties.getValidationError(
- Validations.afterDateWithDate,
- testDateField.getCaption(),
- I18nProperties.getPrefixCaption(SampleDto.I18N_PREFIX, SampleDto.SAMPLE_DATE_TIME),
- DateFormatHelper.formatLocalDateTime(getSampleDate()))));
-
- });
- ComboBox lab = addInfrastructureField(PathogenTestDto.LAB);
- lab.addItems(FacadeProvider.getFacilityFacade().getAllActiveLaboratories(true));
- TextField labDetails = addField(PathogenTestDto.LAB_DETAILS, TextField.class);
- labDetails.setVisible(false);
- typingIdField = addField(PathogenTestDto.TYPING_ID, TextField.class);
- typingIdField.setVisible(false);
-
- // Tested Desease or Tested Pathogen, depending on sample type
- diseaseField = addDiseaseField(PathogenTestDto.TESTED_DISEASE, true, create, false);
- addField(PathogenTestDto.TESTED_DISEASE_DETAILS, TextField.class);
- ComboBox diseaseVariantField = addCustomizableEnumField(PathogenTestDto.TESTED_DISEASE_VARIANT);
- diseaseVariantField.setNullSelectionAllowed(true);
- diseaseVariantField.setVisible(false);
- TextField diseaseVariantDetailsField = addField(PathogenTestDto.TESTED_DISEASE_VARIANT_DETAILS, TextField.class);
- diseaseVariantDetailsField.setVisible(false);
- if (DiseaseHelper.SUBTYPE_ALLOWED_DISEASES.contains(disease)) {
- diseaseVariantField.setCaption(I18nProperties.getCaption(Captions.PathogenTest_rsv_testedDiseaseVariant));
- diseaseVariantDetailsField.setCaption(I18nProperties.getCaption(Captions.PathogenTest_rsv_testedDiseaseVariantDetails));
- }
- genoTypingCB = addField(PathogenTestDto.GENOTYPE, ComboBox.class);
- genoTypingCB.setVisible(true);
- genoTypingResultTextTF = addField(PathogenTestDto.GENOTYPE_TEXT, TextField.class);
- genoTypingResultTextTF.setVisible(true);
-
- ComboBox testedPathogenField = addCustomizableEnumField(PathogenTestDto.TESTED_PATHOGEN);
- TextField testedPathogenDetailsField = addField(PathogenTestDto.TESTED_PATHOGEN_DETAILS, TextField.class);
- testedPathogenDetailsField.setVisible(false);
- FieldHelper
- .updateItems(testedPathogenField, FacadeProvider.getCustomizableEnumFacade().getEnumValues(CustomizableEnumType.PATHOGEN, disease));
- testedPathogenField.addValueChangeListener(e -> {
- Pathogen pathogen = (Pathogen) e.getProperty().getValue();
- if (pathogen != null && pathogen.isHasDetails()) {
- testedPathogenDetailsField.setVisible(true);
- } else {
- testedPathogenDetailsField.clear();
- testedPathogenDetailsField.setVisible(false);
- }
- });
-
- if (environmentSample == null) {
- diseaseField.setVisible(true);
- diseaseField.setRequired(true);
-
- testedPathogenField.setVisible(false);
- testedPathogenField.setRequired(false);
+ private static void collectCauses(InvalidValueException e, List target) {
+ if (e.getCauses().length > 0) {
+ Collections.addAll(target, e.getCauses());
} else {
- diseaseField.setVisible(false);
- diseaseField.setRequired(false);
-
- testedPathogenField.setVisible(true);
- testedPathogenField.setRequired(true);
- }
-
- testResultField = addField(PathogenTestDto.TEST_RESULT, ComboBox.class);
- testResultField.removeItem(PathogenTestResultType.NOT_DONE);
-
- if (!FacadeProvider.getConfigFacade().isConfiguredCountry(CountryHelper.COUNTRY_CODE_LUXEMBOURG)) {
- testResultField.removeItem(PathogenTestResultType.NOT_APPLICABLE);
+ target.add(e);
}
- seroTypeField = addField(PathogenTestDto.SEROTYPE, ComboBox.class);
- addField(PathogenTestDto.SEROTYPE_TEXT, TextField.class);
-
- NullableOptionGroup rifampicinResistantField = addField(PathogenTestDto.RIFAMPICIN_RESISTANT, NullableOptionGroup.class);
- rifampicinResistantField.setVisible(false);
-
- NullableOptionGroup isoniazidResistantField = addField(PathogenTestDto.ISONIAZID_RESISTANT, NullableOptionGroup.class);
- isoniazidResistantField.setVisible(false);
-
- ComboBox testScaleField = addField(PathogenTestDto.TEST_SCALE, ComboBox.class);
- testScaleField.setVisible(false);
-
- ComboBox strainCallStatusField = addField(PathogenTestDto.STRAIN_CALL_STATUS, ComboBox.class);
- strainCallStatusField.setItemCaptionMode(ItemCaptionMode.ID_TOSTRING);
- strainCallStatusField.setVisible(false);
-
- specieField = addField(PathogenTestDto.SPECIE, ComboBox.class);
- specieField.setVisible(false);
-
- addField(PathogenTestDto.SPECIE_TEXT, TextField.class);
-
- TextField patternProfileField = addField(PathogenTestDto.PATTERN_PROFILE, TextField.class);
- patternProfileField.setVisible(false);
-
- drugSusceptibilityField = (DrugSusceptibilityForm) addField(
- PathogenTestDto.DRUG_SUSCEPTIBILITY,
- new DrugSusceptibilityForm(
- FieldVisibilityCheckers.getNoop(),
- UiFieldAccessCheckers.getDefault(true, FacadeProvider.getConfigFacade().getCountryLocale())));
- drugSusceptibilityField.setCaption(null);
- //drugSusceptibilityField.setVisible(false);
- addToVisibleAllowedFields(drugSusceptibilityField);
-
- // Malaria and Dengue fields
- addField(PathogenTestDto.ANTIBODY_TITRE, TextField.class);
- addField(PathogenTestDto.PERFORMED_BY_REFERENCE_LABORATORY, NullableOptionGroup.class);
- addField(PathogenTestDto.RETEST_REQUESTED, NullableOptionGroup.class);
- Field> resultDetailsField = addField(PathogenTestDto.RESULT_DETAILS);
- resultDetailsField.setVisible(false);
-
- if (FacadeProvider.getConfigFacade().isConfiguredCountry(CountryHelper.COUNTRY_CODE_LUXEMBOURG)) {
- //tuberculosis-pcr test specification
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.RIFAMPICIN_RESISTANT, RIFAMPICIN_RESISTANT_VISIBILITY_CONDITIONS, true);
-
- //tuberculosis-microscopy test specification
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.TEST_SCALE, TEST_SCALE_VISIBILITY_CONDITIONS, true);
-
- //tuberculosis-beijinggenotyping test specification
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.STRAIN_CALL_STATUS, STRAIN_CALL_STATUS_VISIBILITY_CONDITIONS, true);
+ }
- //tuberculosis-spoligotyping test specification
- // FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.SPECIE, SPECIE_VISIBILITY_CONDITIONS, true);
+ @Override
+ public void setHeading(String heading) {
+ headingLabel.setValue(heading);
+ }
- //tuberculosis-miru-code test specification
- Map> tuberculosisMiruCodeDependencies = new HashMap<>() {
+ @Override
+ public void setValue(PathogenTestDto newFieldValue) throws ReadOnlyException, Converter.ConversionException {
+ super.setValue(newFieldValue);
- {
- put(PathogenTestDto.TESTED_DISEASE, Arrays.asList(Disease.TUBERCULOSIS, Disease.LATENT_TUBERCULOSIS));
- put(PathogenTestDto.TEST_TYPE, Arrays.asList(PathogenTestType.MIRU_PATTERN_CODE));
- }
- };
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.PATTERN_PROFILE, tuberculosisMiruCodeDependencies, true);
- //FieldHelper.setRequiredWhen(getFieldGroup(), PathogenTestDto.PATTERN_PROFILE, tuberculosisMiruCodeDependencies);
+ for (FormComponent comp : formComponents) {
+ comp.setDto(newFieldValue);
}
- seroTypeField.setVisible(false);
-
- ComboBox seroTypeMetCB = addField(PathogenTestDto.SEROTYPING_METHOD, ComboBox.class);
- seroTypeMetCB.setVisible(false);
- seroGrpSepcCB = addField(PathogenTestDto.SERO_GROUP_SPECIFICATION, ComboBox.class);
- seroGrpSepcCB.setVisible(false);
- seroGrpSpecTxt = addField(PathogenTestDto.SERO_GROUP_SPECIFICATION_TEXT, TextField.class);
-
- TextField cqValueField = addField(FieldConfiguration.withConversionError(PathogenTestDto.CQ_VALUE, Validations.onlyNumbersAllowed));
- if (!FacadeProvider.getConfigFacade().isConfiguredCountry(CountryHelper.COUNTRY_CODE_LUXEMBOURG)) {
- cqValueField.setVisible(false);
+ if (activeSection != null) {
+ activeSection.setDto(newFieldValue);
}
- addFields(
- FieldConfiguration.withConversionError(PathogenTestDto.CT_VALUE_E, Validations.onlyNumbersAllowed),
- FieldConfiguration.withConversionError(PathogenTestDto.CT_VALUE_N, Validations.onlyNumbersAllowed),
- FieldConfiguration.withConversionError(PathogenTestDto.CT_VALUE_RDRP, Validations.onlyNumbersAllowed),
- FieldConfiguration.withConversionError(PathogenTestDto.CT_VALUE_S, Validations.onlyNumbersAllowed),
- FieldConfiguration.withConversionError(PathogenTestDto.CT_VALUE_ORF_1, Validations.onlyNumbersAllowed),
- FieldConfiguration.withConversionError(PathogenTestDto.CT_VALUE_RDRP_S, Validations.onlyNumbersAllowed));
-
- setVisibleClear(
- false,
- PathogenTestDto.CQ_VALUE,
- PathogenTestDto.CT_VALUE_E,
- PathogenTestDto.CT_VALUE_N,
- PathogenTestDto.CT_VALUE_RDRP,
- PathogenTestDto.CT_VALUE_S,
- PathogenTestDto.CT_VALUE_ORF_1,
- PathogenTestDto.CT_VALUE_RDRP_S);
-
- //@formatter:off
- addFields(
- FieldConfiguration.builder(PathogenTestDto.TUBE_NIL)
- .validationMessageProperty(Validations.onlyNumbersAllowed)
- .valueChangeListener(new TuberculosisIGRAInputValueChangeListener(getFieldGroup(), PathogenTestDto.TUBE_NIL,PathogenTestDto.TUBE_NIL_GT10))
- .build(),
- FieldConfiguration.builder(PathogenTestDto.TUBE_AG_TB1)
- .validationMessageProperty(Validations.onlyNumbersAllowed)
- .valueChangeListener(new TuberculosisIGRAInputValueChangeListener(getFieldGroup(), PathogenTestDto.TUBE_AG_TB1,PathogenTestDto.TUBE_AG_TB1_GT10)).build(),
- FieldConfiguration.builder(PathogenTestDto.TUBE_AG_TB2)
- .validationMessageProperty(Validations.onlyNumbersAllowed)
- .valueChangeListener(new TuberculosisIGRAInputValueChangeListener(getFieldGroup(), PathogenTestDto.TUBE_AG_TB2,PathogenTestDto.TUBE_AG_TB2_GT10)).build(),
- FieldConfiguration.builder(PathogenTestDto.TUBE_MITOGENE)
- .validationMessageProperty(Validations.onlyNumbersAllowed)
- .valueChangeListener(new TuberculosisIGRAInputValueChangeListener(getFieldGroup(), PathogenTestDto.TUBE_MITOGENE,PathogenTestDto.TUBE_MITOGENE_GT10)).build());
- //@formatter:on
-
- //@formatter:off
- addFields(
- FieldConfiguration.builder(PathogenTestDto.TUBE_NIL_GT10).valueChangeListener(new TuberculosisIGRAGT10InputValueChangeListener(getFieldGroup(), PathogenTestDto.TUBE_NIL_GT10,PathogenTestDto.TUBE_NIL)).build(),
- FieldConfiguration.builder(PathogenTestDto.TUBE_AG_TB1_GT10).valueChangeListener(new TuberculosisIGRAGT10InputValueChangeListener(getFieldGroup(), PathogenTestDto.TUBE_AG_TB1_GT10,PathogenTestDto.TUBE_AG_TB1)).build(),
- FieldConfiguration.builder(PathogenTestDto.TUBE_AG_TB2_GT10).valueChangeListener(new TuberculosisIGRAGT10InputValueChangeListener(getFieldGroup(), PathogenTestDto.TUBE_AG_TB2_GT10,PathogenTestDto.TUBE_AG_TB2)).build(),
- FieldConfiguration.builder(PathogenTestDto.TUBE_MITOGENE_GT10).valueChangeListener(new TuberculosisIGRAGT10InputValueChangeListener(getFieldGroup(), PathogenTestDto.TUBE_MITOGENE_GT10,PathogenTestDto.TUBE_MITOGENE)).build());
- //@formatter:on
-
- setVisibleClear(
- false,
- PathogenTestDto.TUBE_NIL,
- PathogenTestDto.TUBE_NIL_GT10,
- PathogenTestDto.TUBE_AG_TB1,
- PathogenTestDto.TUBE_AG_TB1_GT10,
- PathogenTestDto.TUBE_AG_TB2,
- PathogenTestDto.TUBE_AG_TB2_GT10,
- PathogenTestDto.TUBE_MITOGENE,
- PathogenTestDto.TUBE_MITOGENE_GT10);
-
- NullableOptionGroup testResultVerifiedField = addField(PathogenTestDto.TEST_RESULT_VERIFIED, NullableOptionGroup.class);
- addField(PathogenTestDto.PRELIMINARY).addStyleName(CssStyles.VSPACE_4);
-
- // Make TEST_RESULT_VERIFIED required only when the test comes via LIMS (laboratory is directly connected)
- viaLimsField.addValueChangeListener(e -> {
- boolean isViaLims = Boolean.TRUE.equals(e.getProperty().getValue());
- testResultVerifiedField.setRequired(isViaLims);
- });
-
- // Set initial required state based on current viaLims value
- testResultVerifiedField.setRequired(Boolean.TRUE.equals(viaLimsField.getValue()));
-
- CheckBox fourFoldIncrease = addField(PathogenTestDto.FOUR_FOLD_INCREASE_ANTIBODY_TITER, CheckBox.class);
- CssStyles.style(fourFoldIncrease, VSPACE_3, VSPACE_TOP_4);
- fourFoldIncrease.setVisible(false);
- fourFoldIncrease.setEnabled(false);
-
- addField(PathogenTestDto.TEST_RESULT_TEXT, TextArea.class).setRows(6);
-
- addFields(PathogenTestDto.PRESCRIBER_PHYSICIAN_CODE, PathogenTestDto.PRESCRIBER_FIRST_NAME, PathogenTestDto.PRESCRIBER_LAST_NAME);
- TextField proscriberPhoneField = addField(PathogenTestDto.PRESCRIBER_PHONE_NUMBER, TextField.class);
- proscriberPhoneField.addValidator(
- new PhoneNumberValidator(I18nProperties.getValidationError(Validations.validPhoneNumber, proscriberPhoneField.getCaption())));
-
- addFields(PathogenTestDto.PRESCRIBER_ADDRESS, PathogenTestDto.PRESCRIBER_POSTAL_CODE, PathogenTestDto.PRESCRIBER_CITY);
- ComboBox prescriberCountrField = addInfrastructureField(PathogenTestDto.PRESCRIBER_COUNTRY);
- FieldHelper.updateItems(prescriberCountrField, FacadeProvider.getCountryFacade().getAllActiveAsReference());
-
- addField(PathogenTestDto.DELETION_REASON);
- addField(PathogenTestDto.OTHER_DELETION_REASON, TextArea.class).setRows(3);
- setVisible(false, PathogenTestDto.DELETION_REASON, PathogenTestDto.OTHER_DELETION_REASON);
-
- pcrTestSpecification.setVisible(false);
-
- Label prescriberHeadingLabel = new Label(I18nProperties.getCaption(Captions.PathogenTest_prescriber));
- prescriberHeadingLabel.addStyleName(H3);
- getContent().addComponent(prescriberHeadingLabel, PRESCRIBER_HEADING_LOC);
-
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.PCR_TEST_SPECIFICATION, PCR_TEST_SPECIFICATION_VISIBILITY_CONDITIONS, true);
- FieldHelper.setVisibleWhen(
- getFieldGroup(),
- PathogenTestDto.TEST_TYPE_TEXT,
- PathogenTestDto.TEST_TYPE,
- Arrays.asList(PathogenTestType.PCR_RT_PCR, PathogenTestType.OTHER),
- true);
- FieldHelper.setVisibleWhen(
- getFieldGroup(),
- PathogenTestDto.TESTED_DISEASE_DETAILS,
- PathogenTestDto.TESTED_DISEASE,
- Arrays.asList(Disease.OTHER),
- true);
- FieldHelper.setVisibleWhen(
- getFieldGroup(),
- PathogenTestDto.TYPING_ID,
- PathogenTestDto.TEST_TYPE,
- Arrays.asList(PathogenTestType.PCR_RT_PCR, PathogenTestType.DNA_MICROARRAY, PathogenTestType.SEQUENCING),
- true);
-
- FieldHelper.setVisibleWhen(
- getFieldGroup(),
- PathogenTestDto.SERO_TYPING_METHOD_TEXT,
- PathogenTestDto.SEROTYPING_METHOD,
- SerotypingMethod.OTHER,
- true);
- // End of IPI visibility check
-
- //IMI serogroup specification
- Map> imiSeroTypingDependencies = new HashMap<>() {
-
- {
- put(PathogenTestDto.TESTED_DISEASE, Arrays.asList(Disease.INVASIVE_MENINGOCOCCAL_INFECTION));
- put(PathogenTestDto.TEST_RESULT, Arrays.asList(PathogenTestResultType.POSITIVE));
- put(
- PathogenTestDto.TEST_TYPE,
- Arrays.asList(
- PathogenTestType.SEROGROUPING,
- PathogenTestType.MULTILOCUS_SEQUENCE_TYPING,
- PathogenTestType.SLIDE_AGGLUTINATION,
- PathogenTestType.WHOLE_GENOME_SEQUENCING));
- }
- };
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.SERO_GROUP_SPECIFICATION, imiSeroTypingDependencies, true);
- FieldHelper.setVisibleWhen(
- getFieldGroup(),
- PathogenTestDto.SERO_GROUP_SPECIFICATION_TEXT,
- PathogenTestDto.SERO_GROUP_SPECIFICATION,
- SeroGroupSpecification.OTHER,
- true);
-
- // antibody titre visibility
- FieldHelper.setVisibleWhen(
- getFieldGroup(),
- PathogenTestDto.ANTIBODY_TITRE,
- PathogenTestDto.TEST_TYPE,
- PathogenTestType.NEUTRALIZING_ANTIBODIES,
- true);
- // End of IMI serogroup specification
- //Cryptosporidiosis for all countries Genotyping specification
- Map> cryptoGenoTypingDependencies = new HashMap<>() {
-
- {
- put(PathogenTestDto.TESTED_DISEASE, Arrays.asList(Disease.MEASLES, Disease.CRYPTOSPORIDIOSIS));
- put(PathogenTestDto.TEST_TYPE, Arrays.asList(PathogenTestType.GENOTYPING));
- put(PathogenTestDto.TEST_RESULT, Arrays.asList(PathogenTestResultType.POSITIVE));
- }
- };
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.GENOTYPE, cryptoGenoTypingDependencies, true);
-
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.GENOTYPE_TEXT, PathogenTestDto.GENOTYPE, GenoType.OTHER, true);
-
- //disease variant specifications for RSV and Influenza
- Map> diseaseVariantDependencies = new HashMap<>() {
-
- {
- put(PathogenTestDto.TESTED_DISEASE, Arrays.asList(Disease.RESPIRATORY_SYNCYTIAL_VIRUS, Disease.INFLUENZA));
- put(
- PathogenTestDto.TEST_TYPE,
- Arrays.asList(
- PathogenTestType.SEQUENCING,
- PathogenTestType.WHOLE_GENOME_SEQUENCING,
- PathogenTestType.PCR_RT_PCR,
- PathogenTestType.ISOLATION,
- PathogenTestType.OTHER));
- }
- };
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.TESTED_DISEASE_VARIANT, diseaseVariantDependencies, true);
-
- Consumer updateDiseaseVariantField = disease -> {
- List diseaseVariants =
- FacadeProvider.getCustomizableEnumFacade().getEnumValues(CustomizableEnumType.DISEASE_VARIANT, disease);
- FieldHelper.updateItems(diseaseVariantField, diseaseVariants);
- diseaseVariantField.setVisible(
- disease != null && isVisibleAllowed(PathogenTestDto.TESTED_DISEASE_VARIANT) && CollectionUtils.isNotEmpty(diseaseVariants));
- };
-
- updateDiseaseVariantField.accept((Disease) diseaseField.getValue());
-
- // Need to address these visibility issues
- // @Herold
- BiConsumer updateSerotypeField = (Disease disease, PathogenTestType testType) -> {
- setVisibleClear(
- SEROTYPE_VISIBILITY_MAP.containsKey(disease) && SEROTYPE_VISIBILITY_MAP.get(disease).contains(testType),
- PathogenTestDto.SEROTYPE);
- };
-
- BiConsumer updateSpecieField = (Disease disease, PathogenTestType testType) -> {
- setVisibleClear(
- SPECIE_VISIBILITY_MAP.containsKey(disease) && SPECIE_VISIBILITY_MAP.get(disease).contains(testType),
- PathogenTestDto.SPECIE);
- };
-
- diseaseField.addValueChangeListener((ValueChangeListener) valueChangeEvent -> {
- Disease latestDisease = (Disease) valueChangeEvent.getProperty().getValue();
- // If the disease changed, test type field should be updated with its respective test types
- if (latestDisease != disease) {
- testTypeField.clear();
- }
- disease = latestDisease;
- updateDiseaseVariantField.accept(disease);
-
- FieldHelper.updateItems(
- testTypeField,
- Arrays.asList(PathogenTestType.values()),
- FieldVisibilityCheckers.withDisease(disease),
- PathogenTestType.class);
- // serotype values should be changed based on the disease
- // FieldHelper.updateItems(seroTypeField, Arrays.asList(Serotype.values()), FieldVisibilityCheckers.withDisease(disease), Serotype.class);
- FieldHelper.updateItems(disease, seroTypeField, Serotype.class);
- FieldHelper.updateItems(disease, specieField, PathogenSpecie.class);
- if (FacadeProvider.getConfigFacade().isConfiguredCountry(CountryHelper.COUNTRY_CODE_LUXEMBOURG)) {
- FieldHelper.updateItems(
- strainCallStatusField,
- Arrays.asList(PathogenStrainCallStatus.values()),
- FieldVisibilityCheckers.withDisease(disease),
- PathogenStrainCallStatus.class);
-
- updateDrugSusceptibilityFieldSpecifications((PathogenTestType) testTypeField.getValue(), disease);
- }
- });
- diseaseVariantField.addValueChangeListener(e -> {
- DiseaseVariant diseaseVariant = (DiseaseVariant) e.getProperty().getValue();
- if (diseaseVariant != null) {
- testResultField.setValue(PathogenTestResultType.POSITIVE);
- } else {
- testResultField.clear();
- }
- diseaseVariantDetailsField.setVisible(diseaseVariant != null && diseaseVariant.matchPropertyValue(DiseaseVariant.HAS_DETAILS, true));
- });
-
- testTypeField.addValueChangeListener(e -> {
- PathogenTestType testType = (PathogenTestType) e.getProperty().getValue();
- if (testType != null) {
- // For Dengue IGG serum antibody, fourFoldIncrease fild should be visible.
- // and its caption will be renamed with the caption as Seroconversion/ 4-fold increase
- if (Disease.DENGUE == (Disease) diseaseField.getValue() && testType == PathogenTestType.IGG_SERUM_ANTIBODY) {
- fourFoldIncrease.setCaption(I18nProperties.getCaption(Captions.PathogenTest_fourFoldIncreaseAntibodyTiter_DENGUE));
- fourFoldIncrease.setVisible(true);
- fourFoldIncrease.setEnabled(true);
- } else if (testType == PathogenTestType.IGM_SERUM_ANTIBODY || testType == PathogenTestType.IGG_SERUM_ANTIBODY) {
- fourFoldIncrease.setVisible(true);
- fourFoldIncrease.setEnabled(caseSampleCount >= 2);
- } else {
- fourFoldIncrease.setVisible(false);
- fourFoldIncrease.setEnabled(false);
- }
-
- if (diseaseField.getValue() == null || !List.of(Disease.TUBERCULOSIS).contains((Disease) diseaseField.getValue())) {
- setVisibleClear(
- PathogenTestType.PCR_RT_PCR == testType,
- PathogenTestDto.CQ_VALUE,
- PathogenTestDto.CT_VALUE_E,
- PathogenTestDto.CT_VALUE_N,
- PathogenTestDto.CT_VALUE_RDRP,
- PathogenTestDto.CT_VALUE_S,
- PathogenTestDto.CT_VALUE_ORF_1,
- PathogenTestDto.CT_VALUE_RDRP_S);
- } else {
- setVisibleClear(
- false,
- PathogenTestDto.CQ_VALUE,
- PathogenTestDto.CT_VALUE_E,
- PathogenTestDto.CT_VALUE_N,
- PathogenTestDto.CT_VALUE_RDRP,
- PathogenTestDto.CT_VALUE_S,
- PathogenTestDto.CT_VALUE_ORF_1,
- PathogenTestDto.CT_VALUE_RDRP_S);
- }
- // Show tube IGRA fields only for IGRA tests and Luxembourg
- setVisibleClear(
- PathogenTestType.IGRA == testType && FacadeProvider.getConfigFacade().isConfiguredCountry(CountryHelper.COUNTRY_CODE_LUXEMBOURG),
- PathogenTestDto.TUBE_NIL,
- PathogenTestDto.TUBE_NIL_GT10,
- PathogenTestDto.TUBE_AG_TB1,
- PathogenTestDto.TUBE_AG_TB1_GT10,
- PathogenTestDto.TUBE_AG_TB2,
- PathogenTestDto.TUBE_AG_TB2_GT10,
- PathogenTestDto.TUBE_MITOGENE,
- PathogenTestDto.TUBE_MITOGENE_GT10);
- FieldHelper.updateItems((Disease) diseaseField.getValue(), genoTypingCB, GenoType.class);
- // verifying the serotype field visibility. reason for this pattern is that, this should display disease+pathogentest combination.
- updateSerotypeField.accept(disease, testType);
-
- updateSpecieField.accept(disease, testType);
- // Result details should be visible for Malaria and test-types with PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.Q_PCR
- setVisibleClear(
- Disease.MALARIA == disease && Arrays.asList(PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.Q_PCR).contains(testType),
- PathogenTestDto.RESULT_DETAILS);
- setVisibleClear(
- testType == PathogenTestType.SEROGROUPING && Disease.INVASIVE_PNEUMOCOCCAL_INFECTION == disease,
- PathogenTestDto.SEROTYPING_METHOD);
- } else {
- setVisibleClear(
- testTypeField.getValue() != null,
- PathogenTestDto.SEROTYPE,
- PathogenTestDto.SEROTYPING_METHOD,
- PathogenTestDto.SERO_GROUP_SPECIFICATION);
- // hide tube fields when no test type selected
- setVisibleClear(
- false,
- PathogenTestDto.TUBE_NIL,
- PathogenTestDto.TUBE_NIL_GT10,
- PathogenTestDto.TUBE_AG_TB1,
- PathogenTestDto.TUBE_AG_TB1_GT10,
- PathogenTestDto.TUBE_AG_TB2,
- PathogenTestDto.TUBE_AG_TB2_GT10,
- PathogenTestDto.TUBE_MITOGENE,
- PathogenTestDto.TUBE_MITOGENE_GT10);
- testResultField.clear();
- testResultField.setEnabled(true);
- }
-
- if (RESULT_FIELD_DECISION_MAP.containsKey(disease) && RESULT_FIELD_DECISION_MAP.get(disease).contains(testType)) {
- testResultField.setValue(PathogenTestResultType.POSITIVE);
- } else {
- testResultField.clear();
- }
-
- updateDrugSusceptibilityFieldSpecifications(testType, (Disease) diseaseField.getValue());
- });
-
- lab.addValueChangeListener(event ->
-
- {
- if (event.getProperty().getValue() != null
- && ((FacilityReferenceDto) event.getProperty().getValue()).getUuid().equals(FacilityDto.OTHER_FACILITY_UUID)) {
- labDetails.setVisible(true);
- labDetails.setRequired(isEditableAllowed(labDetails));
- } else {
- labDetails.setVisible(false);
- labDetails.setRequired(false);
- labDetails.clear();
- }
- });
-
- testTypeField.addValueChangeListener(e -> {
- PathogenTestType testType = (PathogenTestType) e.getProperty().getValue();
- setCqValueVisibility(diseaseField, cqValueField, testType, (PathogenTestResultType) testResultField.getValue());
- });
-
- testResultField.addValueChangeListener(e -> {
- PathogenTestResultType testResult = (PathogenTestResultType) e.getProperty().getValue();
- setCqValueVisibility(diseaseField, cqValueField, (PathogenTestType) testTypeField.getValue(), testResult);
- });
-
- if (SamplePurpose.INTERNAL.equals(getSamplePurpose())) { // this only works for already saved samples
- setRequired(true, PathogenTestDto.LAB);
- }
- setRequired(true, PathogenTestDto.TEST_TYPE, PathogenTestDto.TEST_RESULT);
+ markAsDirty();
+ }
- initializeAccessAndAllowedAccesses();
- initializeVisibilitiesAndAllowedVisibilities();
+ /** Reveals the typingId field if the existing test has a preset value. */
+ public void showTypingIdIfPreset(String typingId) {
+ testMethodComponent.showTypingIdIfPreset(typingId);
+ }
- // displaying the serotype text field only if the serotype is "other" and it has the visibility
- if (isVisibleAllowed(PathogenTestDto.SEROTYPE)) {
- // FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.SEROTYPE, SEROTYPE_VISIBILITY_MAP, true);
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.SEROTYPE_TEXT, PathogenTestDto.SEROTYPE, Serotype.OTHER, true);
- }
- if (isVisibleAllowed(PathogenTestDto.SPECIE)) {
- FieldHelper.setVisibleWhen(getFieldGroup(), PathogenTestDto.SPECIE_TEXT, PathogenTestDto.SPECIE, PathogenSpecie.OTHER, true);
+ /** Sets initial field values for a newly created pathogen test. */
+ public void initializeForNewTest(Disease disease) {
+ testResultComponent.getTestResultField().setValue(PathogenTestResultType.PENDING);
+ if (disease != null) {
+ diseaseSelectionComponent.getDiseaseField().setValue(disease);
}
+ }
- // Hide/show prescriber heading after the visibilities have been initialized
- prescriberHeadingLabel.setVisible(
- isVisibleAllowed(PathogenTestDto.PRESCRIBER_PHYSICIAN_CODE)
- || isVisibleAllowed(PathogenTestDto.PRESCRIBER_FIRST_NAME)
- || isVisibleAllowed(PathogenTestDto.PRESCRIBER_LAST_NAME)
- || isVisibleAllowed(PathogenTestDto.PRESCRIBER_PHONE_NUMBER)
- || isVisibleAllowed(PathogenTestDto.PRESCRIBER_ADDRESS)
- || isVisibleAllowed(PathogenTestDto.PRESCRIBER_POSTAL_CODE)
- || isVisibleAllowed(PathogenTestDto.PRESCRIBER_CITY)
- || isVisibleAllowed(PathogenTestDto.PRESCRIBER_COUNTRY));
+ /** Updates lab field required state (e.g. when sample purpose changes). */
+ public void setLabRequired(boolean required) {
+ testMethodComponent.setLabRequired(required);
}
/**
- * This class is to be used for the Tuberculosis IGRA input value change listeners.
- * It will check/uncheck the Tuberculosis IGRA greater than 10 checkbox dependiong on the value of the input field.
- *
- * Note: ideally a custom component should be used for both fields, to avoid potential race conditions between the two listeners.
+ * Registers a validator ensuring test date is not before sample date.
+ * Evaluated during form commit via {@link de.symeda.sormas.ui.utils.FormComponent#validate()}.
*/
- protected static class TuberculosisIGRAInputValueChangeListener implements ValueChangeListener {
-
- private final String igraInputFieldId;
- private final String igraGT10FieldId;
- private final BeanFieldGroup fieldGroup;
-
- public TuberculosisIGRAInputValueChangeListener(BeanFieldGroup fg, String igraInputFieldId, String igraGT10FieldId) {
- this.igraInputFieldId = igraInputFieldId;
- this.igraGT10FieldId = igraGT10FieldId;
- this.fieldGroup = fg;
- }
-
- @SuppressWarnings({
- "unchecked",
- "rawtypes" })
- @Override
- public void valueChange(Property.ValueChangeEvent event) {
-
- final Field> igraInputField = fieldGroup.getField(igraInputFieldId);
-
- if (igraInputField == null) {
- return;
- }
-
- if (igraInputField instanceof AbstractComponent) {
- ((AbstractComponent) igraInputField).setComponentError(null);
- }
-
- final BeanItem> beanItemDataSource = fieldGroup.getItemDataSource();
-
- // the input field is always a TextField with a String as value
- // we need to make a hard assumtion that the input field value is a Float
-
- // we check to see if the model property is numeric
- // the model at this point will not be updated, so we only check type
- final Property> igraValueProp = beanItemDataSource.getItemProperty(igraInputFieldId);
-
- if (!Number.class.isAssignableFrom(igraValueProp.getType())) {
- // we will not deal with non-numeric values
- return;
- }
-
- // we know that the model property is numeric
- // we could get the original value with: igraValueProp.getValue();
-
- // we need to convert the value to number
- // and we need to do it locale aware and need to finagle with types
-
- Number igraNewValue = null;
-
- try {
- igraNewValue = igraInputField.getValue() == null
- ? null
- : (Number) ConverterUtil
- .getConverter(igraInputField.getType(), (Class) igraValueProp.getType(), null /* current session */)
- .convertToModel(igraInputField.getValue(), igraValueProp.getType(), igraInputField.getLocale());
- } catch (ConversionException e) {
- if (igraInputField instanceof AbstractComponent) {
- ((AbstractComponent) igraInputField).setComponentError(new UserError(I18nProperties.getString(Strings.errorInvalidValue)));
- }
- return;
- }
-
- final Boolean checked = igraNewValue == null ? null : igraNewValue.floatValue() > 10;
-
- // now we need to set the value of the GT10 field
- @SuppressWarnings("unchecked")
- final Field igraGT10Field = (Field) fieldGroup.getField(igraGT10FieldId);
- if (igraGT10Field == null) {
- // if we can't find the field, we don't care
- return;
- }
-
- // lets make sure the property is a boolean
- final Property> igraGT10Prop = beanItemDataSource.getItemProperty(igraGT10FieldId);
- if (igraGT10Prop == null || !Boolean.class.isAssignableFrom(igraGT10Prop.getType())) {
- // if we can't find the property, or we can't set it we don't care
- return;
- }
-
- // now field is supposed to be a boolean
- // booleans come in two flavors: collection based and primitive
- final boolean isCollection = Collection.class.isAssignableFrom(igraGT10Field.getType());
-
- if (!isCollection) {
- // primitive booleans are easy
- final boolean currentChecked = Boolean.TRUE.equals(igraGT10Field.getValue());
- if (checked != null && checked.booleanValue() != currentChecked) {
- igraGT10Field.setValue(checked);
- }
- } else {
- // well have to do it the hard way
- final Collection> currentSet = (Collection>) igraGT10Field.getValue();
- final boolean currentChecked = currentSet != null && !currentSet.isEmpty() && currentSet.contains(Boolean.TRUE);
- if (checked != null && checked.booleanValue() != currentChecked) {
- final HashSet set = new HashSet<>();
- set.add(checked);
- igraGT10Field.setValue(Collections.unmodifiableSet(set));
- }
- }
- }
+ public void addTestDateAfterSampleDateValidator(Supplier sampleDateSupplier, String errorMessage) {
+ testMethodComponent.addTestDateAfterSampleDateValidator(sampleDateSupplier, errorMessage);
}
- /**
- * This class is to be used for the Tuberculosis IGRA greater than 10 checkboxes value change listeners.
- * It will clear the associated input field if the checkbox is checked and the
- * value is less than or equal to 10.
- * In reverse if the value is greater than 10 and the checkbox is not checked it will clear the input field.
- *
- * Note: ideally a custom component should be used for both fields, to avoid potential race conditions between the two listeners.
- */
- protected static class TuberculosisIGRAGT10InputValueChangeListener implements ValueChangeListener {
+ /** Sets the VIA_LIMS checkbox value. */
+ public void setViaLims(boolean checked) {
+ testIdentificationComponent.getViaLimsField().setValue(checked);
+ }
- private final String igraInputFieldId;
- private final String igraGT10FieldId;
- private final BeanFieldGroup fieldGroup;
+ /** Shows deletion reason fields for a deleted record. */
+ public void showDeletionInfo(DeletionReason reason) {
+ deletionComponent.showForDeletedRecord(reason);
+ }
- public TuberculosisIGRAGT10InputValueChangeListener(BeanFieldGroup fg, String igraGT10FieldId, String igraInputFieldId) {
- this.igraInputFieldId = igraInputFieldId;
- this.igraGT10FieldId = igraGT10FieldId;
- this.fieldGroup = fg;
+ @Override
+ public void detach() {
+ for (Registration reg : eventRegistrations) {
+ reg.remove();
}
+ eventRegistrations.clear();
- @SuppressWarnings({
- "rawtypes",
- "unchecked" })
- @Override
- public void valueChange(Property.ValueChangeEvent event) {
-
- // let's try to get the numeric input field and converted value
- final Field> igraInputField = fieldGroup.getField(igraInputFieldId);
- if (igraInputField == null) {
- return;
- }
-
- if (igraInputField instanceof AbstractComponent) {
- ((AbstractComponent) igraInputField).setComponentError(null);
- }
-
- final BeanItem> beanItemDataSource = fieldGroup.getItemDataSource();
-
- final Property> igraValueProp = beanItemDataSource.getItemProperty(igraInputFieldId);
- if (igraValueProp == null || !Number.class.isAssignableFrom(igraValueProp.getType())) {
- return;
- }
-
- // lets make sure the GT10 property is a boolean
- final Property> igraGT10Prop = beanItemDataSource.getItemProperty(igraGT10FieldId);
- if (igraGT10Prop == null || !Boolean.class.isAssignableFrom(igraGT10Prop.getType())) {
- // if we can't find the property, or we can't set it we don't care
- return;
- }
-
- Number igraNewValue = null;
-
- try {
- igraNewValue = igraInputField.getValue() == null
- ? null
- : (Number) ConverterUtil
- .getConverter(igraInputField.getType(), (Class) igraValueProp.getType(), null /* current session */)
- .convertToModel(igraInputField.getValue(), igraValueProp.getType(), igraInputField.getLocale() /* current locale */);
- } catch (ConversionException e) {
- if (igraInputField instanceof AbstractComponent) {
- ((AbstractComponent) igraInputField).setComponentError(new UserError(I18nProperties.getString(Strings.errorInvalidValue)));
- }
- return;
- }
-
- // now let's try to determine if the checkbox is checked (we know it's a boolean)
- @SuppressWarnings("unchecked")
- final Field igraGT10Field = (Field) fieldGroup.getField(igraGT10FieldId);
- if (igraGT10Field == null) {
- // if we can't find the field, we don't care
- return;
- }
-
- // booleans come in two flavors: collection based and primitive
- final boolean isCollection = Collection.class.isAssignableFrom(igraGT10Field.getType());
-
- Boolean checked = false;
-
- // value can be true or false/null(presumed false)
- if (!isCollection) {
- // primitive booleans are easy
- checked = igraGT10Field.getValue() == null ? null : Boolean.TRUE.equals(igraGT10Field.getValue());
- } else {
- Collection> set = (Collection>) igraGT10Field.getValue();
- checked = set == null || set.isEmpty() ? null : set.contains(Boolean.TRUE);
- }
-
- if (checked == null) { // the checbox is neither checked nor unchecked
- checked = igraNewValue != null && igraNewValue.floatValue() > 10;
-
- if (!isCollection) {
- // primitive booleans are easy
- igraGT10Field.setValue(checked);
- } else {
- final HashSet set = new HashSet<>();
- set.add(checked);
- igraGT10Field.setValue(Collections.unmodifiableSet(set));
- }
-
- // don't need to clear anything else because there was no check/uncheck before
- return;
- }
+ super.detach();
+ }
- if ((checked && igraNewValue != null && igraNewValue.floatValue() <= 10) // checked but value is filled in and less than 10
- || (!checked && igraNewValue != null && igraNewValue.floatValue() > 10) // not checked but value is filled in an greater than 10
- ) {
- try {
- igraInputField.clear();
- } catch (ReadOnlyException ex) {
- // ignore read-only
- }
+ @Override
+ public void forEachComponent(java.util.function.Consumer componentConsumer) {
+ Component c = getContent().getComponent(COMPONENT_CONTAINER_LOC);
+ if (c instanceof VerticalLayout) {
+ VerticalLayout vl = (VerticalLayout) c;
+ for (int i = 0; i < vl.getComponentCount(); i++) {
+ componentConsumer.accept(vl.getComponent(i));
}
-
}
}
}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/AdditionalTestInfoComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/AdditionalTestInfoComponent.java
new file mode 100644
index 00000000000..e50c9c28a12
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/AdditionalTestInfoComponent.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import com.vaadin.ui.RadioButtonGroup;
+
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.ui.utils.FormComponent;
+
+/**
+ * Additional test information fields: performed by reference laboratory and retest requested.
+ * Applicable to all diseases.
+ */
+public class AdditionalTestInfoComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private RadioButtonGroup performedByReferenceLaboratory;
+ private RadioButtonGroup retestRequested;
+
+ public AdditionalTestInfoComponent() {
+ super(PathogenTestDto.class);
+ buildLayout();
+ bindFields();
+ }
+
+ private void buildLayout() {
+ performedByReferenceLaboratory =
+ createBooleanRadioGroup(PathogenTestDto.PERFORMED_BY_REFERENCE_LABORATORY, PathogenTestDto.I18N_PREFIX);
+ retestRequested =
+ createBooleanRadioGroup(PathogenTestDto.RETEST_REQUESTED, PathogenTestDto.I18N_PREFIX);
+ addRow(performedByReferenceLaboratory, retestRequested);
+ }
+
+ private void bindFields() {
+ binder.forField(performedByReferenceLaboratory)
+ .bind(PathogenTestDto::getPerformedByReferenceLaboratory, PathogenTestDto::setPerformedByReferenceLaboratory);
+ binder.forField(retestRequested)
+ .bind(PathogenTestDto::getRetestRequested, PathogenTestDto::setRetestRequested);
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/CtCqValueComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/CtCqValueComponent.java
new file mode 100644
index 00000000000..9dd103ab652
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/CtCqValueComponent.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import com.vaadin.data.ValueProvider;
+import com.vaadin.server.Setter;
+import com.vaadin.shared.ui.ValueChangeMode;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.StringToFloatNullableConverter;
+
+/**
+ * CQ value and CT values (E, N, RdRp, S, ORF1, RdRp/S) as a standalone composable component.
+ */
+public class CtCqValueComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private final boolean isLuxembourg;
+
+ private TextField cqValueField;
+ private TextField ctValueE;
+ private TextField ctValueN;
+ private TextField ctValueRdrp;
+ private TextField ctValueS;
+ private TextField ctValueOrf1;
+ private TextField ctValueRdrpS;
+
+ private HorizontalLayout cqRow;
+
+ public CtCqValueComponent(boolean isLuxembourg) {
+ super(PathogenTestDto.class);
+ this.isLuxembourg = isLuxembourg;
+ buildLayout();
+ bindFields();
+ }
+
+ private void buildLayout() {
+ // CQ value
+ cqValueField = createTextField(PathogenTestDto.CQ_VALUE, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ if (!isLuxembourg) {
+ cqValueField.setVisible(false);
+ }
+ cqRow = addRow(cqValueField);
+
+ // CT values
+ ctValueE = createTextField(PathogenTestDto.CT_VALUE_E, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ ctValueN = createTextField(PathogenTestDto.CT_VALUE_N, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ addRow(ctValueE, ctValueN);
+
+ ctValueRdrp = createTextField(PathogenTestDto.CT_VALUE_RDRP, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ ctValueS = createTextField(PathogenTestDto.CT_VALUE_S, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ addRow(ctValueRdrp, ctValueS);
+
+ ctValueOrf1 = createTextField(PathogenTestDto.CT_VALUE_ORF_1, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ ctValueRdrpS = createTextField(PathogenTestDto.CT_VALUE_RDRP_S, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ addRow(ctValueOrf1, ctValueRdrpS);
+
+ // Initially hide CT fields
+ setCtFieldsVisible(false);
+ }
+
+ private void bindFields() {
+ bindFloatField(cqValueField, PathogenTestDto::getCqValue, PathogenTestDto::setCqValue);
+ bindFloatField(ctValueE, PathogenTestDto::getCtValueE, PathogenTestDto::setCtValueE);
+ bindFloatField(ctValueN, PathogenTestDto::getCtValueN, PathogenTestDto::setCtValueN);
+ bindFloatField(ctValueRdrp, PathogenTestDto::getCtValueRdrp, PathogenTestDto::setCtValueRdrp);
+ bindFloatField(ctValueS, PathogenTestDto::getCtValueS, PathogenTestDto::setCtValueS);
+ bindFloatField(ctValueOrf1, PathogenTestDto::getCtValueOrf1, PathogenTestDto::setCtValueOrf1);
+ bindFloatField(ctValueRdrpS, PathogenTestDto::getCtValueRdrpS, PathogenTestDto::setCtValueRdrpS);
+ }
+
+ private void bindFloatField(TextField field, ValueProvider getter, Setter setter) {
+ binder.forField(field).withConverter(new StringToFloatNullableConverter(field.getCaption())).bind(getter, setter);
+ }
+
+ public void updateCtVisibility(Disease disease, PathogenTestType testType) {
+ if (disease == null || !java.util.Arrays.asList(Disease.TUBERCULOSIS).contains(disease)) {
+ setCtFieldsVisible(PathogenTestType.PCR_RT_PCR == testType);
+ } else {
+ setCtFieldsVisible(false);
+ }
+ }
+
+ public void updateCqVisibility(Disease disease, PathogenTestType testType, PathogenTestResultType testResult) {
+ if (disease == null || !java.util.Arrays.asList(Disease.TUBERCULOSIS).contains(disease)) {
+ if ((testType == PathogenTestType.PCR_RT_PCR && testResult == PathogenTestResultType.POSITIVE)
+ || testType == PathogenTestType.CQ_VALUE_DETECTION
+ || (disease == Disease.MALARIA && testType == PathogenTestType.Q_PCR)) {
+ cqValueField.setVisible(true);
+ } else {
+ cqValueField.setVisible(false);
+ cqValueField.clear();
+ }
+ } else {
+ cqValueField.setVisible(false);
+ cqValueField.clear();
+ }
+ updateRowVisibility(cqRow);
+ updateRowAndSelfVisibility();
+ }
+
+ private void setCtFieldsVisible(boolean visible) {
+ TextField[] ctFields = {
+ ctValueE,
+ ctValueN,
+ ctValueRdrp,
+ ctValueS,
+ ctValueOrf1,
+ ctValueRdrpS };
+ for (TextField f : ctFields) {
+ if (!visible) {
+ f.clear();
+ }
+ f.setVisible(visible);
+ }
+ updateRowAndSelfVisibility();
+ }
+
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DeletionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DeletionComponent.java
new file mode 100644
index 00000000000..22dc8e464e2
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DeletionComponent.java
@@ -0,0 +1,80 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.TextArea;
+
+import de.symeda.sormas.api.common.DeletionReason;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.ui.utils.FormComponent;
+
+/**
+ * Deletion reason fields using Vaadin 8 components with own Binder.
+ * Self-manages visibility of otherDeletionReason based on deletionReason value.
+ */
+public class DeletionComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private ComboBox deletionReasonField;
+ private TextArea otherReasonField;
+
+ public DeletionComponent() {
+ super(PathogenTestDto.class);
+ buildLayout();
+ bindFields();
+ }
+
+ private void buildLayout() {
+ deletionReasonField = createComboBox(PathogenTestDto.DELETION_REASON, PathogenTestDto.I18N_PREFIX);
+ deletionReasonField.setItems(DeletionReason.values());
+ deletionReasonField.setItemCaptionGenerator(DeletionReason::toString);
+ addFullWidthRow(deletionReasonField);
+
+ otherReasonField = createTextArea(PathogenTestDto.OTHER_DELETION_REASON, PathogenTestDto.I18N_PREFIX);
+ otherReasonField.setRows(3);
+ addFullWidthRow(otherReasonField);
+
+ // Hidden by default
+ deletionReasonField.setVisible(false);
+ otherReasonField.setVisible(false);
+
+ // Self-managed visibility: show otherReason only when OTHER_REASON selected
+ track(deletionReasonField.addValueChangeListener(e -> {
+ boolean showOther = e.getValue() == DeletionReason.OTHER_REASON;
+ otherReasonField.setVisible(showOther);
+ if (!showOther) {
+ otherReasonField.clear();
+ }
+ updateRowAndSelfVisibility();
+ }));
+ }
+
+ private void bindFields() {
+ binder.forField(deletionReasonField).bind(PathogenTestDto::getDeletionReason, PathogenTestDto::setDeletionReason);
+ binder.forField(otherReasonField).bind(PathogenTestDto::getOtherDeletionReason, PathogenTestDto::setOtherDeletionReason);
+ }
+
+ /** Shows the deletion reason field (and other reason if applicable) for a deleted record. */
+ public void showForDeletedRecord(DeletionReason reason) {
+ deletionReasonField.setVisible(true);
+ otherReasonField.setVisible(reason == DeletionReason.OTHER_REASON);
+ updateRowAndSelfVisibility();
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DiseaseSelectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DiseaseSelectionComponent.java
new file mode 100644
index 00000000000..4b50531295f
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DiseaseSelectionComponent.java
@@ -0,0 +1,159 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import java.util.List;
+
+import com.vaadin.shared.ui.ValueChangeMode;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.FacadeProvider;
+import de.symeda.sormas.api.customizableenum.CustomizableEnumType;
+import de.symeda.sormas.api.environment.environmentsample.Pathogen;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.ui.samples.events.DiseaseChangedEvent;
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.FormEventBus;
+
+/**
+ * Disease/pathogen selection.
+ * Vaadin 8 components with own Binder, self-managed visibility.
+ * Disease variant fields are in {@link DiseaseVariantComponent}.
+ */
+public class DiseaseSelectionComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private final FormEventBus eventBus;
+ private final Disease initialDisease;
+ private final boolean create;
+ private final boolean isEnvironmentSample;
+
+ private ComboBox diseaseField;
+ private TextField diseaseDetailsField;
+ private Label diseaseDetailsSpacer;
+ private ComboBox testedPathogenField;
+ private TextField testedPathogenDetailsField;
+ private Label pathogenDetailsSpacer;
+
+ public DiseaseSelectionComponent(FormEventBus eventBus, Disease initialDisease, boolean create, boolean isEnvironmentSample) {
+ super(PathogenTestDto.class);
+ this.eventBus = eventBus;
+ this.initialDisease = initialDisease;
+ this.create = create;
+ this.isEnvironmentSample = isEnvironmentSample;
+ buildLayout();
+ bindFields();
+ wireEvents();
+ }
+
+ private void buildLayout() {
+ // Disease
+ diseaseField = createComboBox(PathogenTestDto.TESTED_DISEASE, PathogenTestDto.I18N_PREFIX);
+ diseaseField.setItemCaptionGenerator(Disease::toString);
+
+ List activeDiseases = FacadeProvider.getDiseaseConfigurationFacade().getAllDiseases(true, true, true);
+ List nonPrimary = FacadeProvider.getDiseaseConfigurationFacade().getAllDiseases(true, false, true);
+ activeDiseases.addAll(nonPrimary);
+ diseaseField.setItems(activeDiseases);
+
+ diseaseDetailsField = createTextField(PathogenTestDto.TESTED_DISEASE_DETAILS, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ diseaseDetailsField.setVisible(false);
+
+ diseaseDetailsSpacer = createSpacer();
+ addToggleRow(diseaseField, diseaseDetailsField, diseaseDetailsSpacer);
+
+ // Pathogen
+ testedPathogenField = createComboBox(PathogenTestDto.TESTED_PATHOGEN, PathogenTestDto.I18N_PREFIX);
+ testedPathogenField.setItemCaptionGenerator(Pathogen::getCaption);
+ testedPathogenField.setItems(FacadeProvider.getCustomizableEnumFacade().getEnumValues(CustomizableEnumType.PATHOGEN, initialDisease));
+
+ testedPathogenDetailsField = createTextField(PathogenTestDto.TESTED_PATHOGEN_DETAILS, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ testedPathogenDetailsField.setVisible(false);
+
+ pathogenDetailsSpacer = createSpacer();
+ addToggleRow(testedPathogenField, testedPathogenDetailsField, pathogenDetailsSpacer);
+
+ // Environment vs human sample visibility
+ if (isEnvironmentSample) {
+ diseaseField.setVisible(false);
+ diseaseDetailsField.setVisible(false);
+ testedPathogenField.setVisible(true);
+ } else {
+ diseaseField.setVisible(true);
+ testedPathogenField.setVisible(false);
+ }
+
+ // Auto-select default disease on creation when server has a single active disease
+ if (create && !isEnvironmentSample) {
+ Disease defaultDisease = FacadeProvider.getDiseaseConfigurationFacade().getDefaultDisease();
+ if (defaultDisease != null) {
+ diseaseField.setValue(defaultDisease);
+ }
+ }
+ }
+
+ private void bindFields() {
+ if (isEnvironmentSample) {
+ binder.forField(diseaseField).bind(PathogenTestDto::getTestedDisease, PathogenTestDto::setTestedDisease);
+ } else {
+ binder.forField(diseaseField).asRequired().bind(PathogenTestDto::getTestedDisease, PathogenTestDto::setTestedDisease);
+ }
+ binder.forField(diseaseDetailsField).bind(PathogenTestDto::getTestedDiseaseDetails, PathogenTestDto::setTestedDiseaseDetails);
+ if (isEnvironmentSample) {
+ binder.forField(testedPathogenField).asRequired().bind(PathogenTestDto::getTestedPathogen, PathogenTestDto::setTestedPathogen);
+ } else {
+ binder.forField(testedPathogenField).bind(PathogenTestDto::getTestedPathogen, PathogenTestDto::setTestedPathogen);
+ }
+ binder.forField(testedPathogenDetailsField).bind(PathogenTestDto::getTestedPathogenDetails, PathogenTestDto::setTestedPathogenDetails);
+ }
+
+ private void wireEvents() {
+ // Disease change: show/hide details, fire event
+ track(diseaseField.addValueChangeListener(e -> {
+ Disease newDisease = e.getValue();
+
+ boolean showDiseaseDetails = newDisease == Disease.OTHER;
+ diseaseDetailsField.setVisible(showDiseaseDetails);
+ diseaseDetailsSpacer.setVisible(!showDiseaseDetails);
+ if (!showDiseaseDetails) {
+ diseaseDetailsField.clear();
+ }
+
+ eventBus.fire(new DiseaseChangedEvent(newDisease));
+ }));
+
+ // Pathogen details visibility
+ track(testedPathogenField.addValueChangeListener(e -> {
+ Pathogen pathogen = e.getValue();
+ boolean showPathogenDetails = pathogen != null && pathogen.isHasDetails();
+ testedPathogenDetailsField.setVisible(showPathogenDetails);
+ pathogenDetailsSpacer.setVisible(!showPathogenDetails);
+ if (!showPathogenDetails) {
+ testedPathogenDetailsField.clear();
+ }
+ }));
+ }
+
+ public ComboBox getDiseaseField() {
+ return diseaseField;
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DiseaseVariantComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DiseaseVariantComponent.java
new file mode 100644
index 00000000000..b2bf71fb274
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DiseaseVariantComponent.java
@@ -0,0 +1,164 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.collections4.CollectionUtils;
+
+import com.vaadin.shared.ui.ValueChangeMode;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.DiseaseHelper;
+import de.symeda.sormas.api.FacadeProvider;
+import de.symeda.sormas.api.customizableenum.CustomizableEnumType;
+import de.symeda.sormas.api.disease.DiseaseVariant;
+import de.symeda.sormas.api.i18n.Captions;
+import de.symeda.sormas.api.i18n.I18nProperties;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.ui.samples.events.DiseaseChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.FormEventBus;
+
+/**
+ * Disease variant selection with details support.
+ * as a standalone composable component
+ */
+public class DiseaseVariantComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final Set VARIANT_ALLOWED_TEST_TYPES = new HashSet<>(
+ Arrays.asList(
+ PathogenTestType.SEQUENCING,
+ PathogenTestType.WHOLE_GENOME_SEQUENCING,
+ PathogenTestType.PCR_RT_PCR,
+ PathogenTestType.ISOLATION,
+ PathogenTestType.OTHER));
+
+ private final FormEventBus eventBus;
+ private Disease currentDisease;
+ private PathogenTestType currentTestType;
+ private List currentVariants;
+
+ private ComboBox diseaseVariantField;
+ private TextField diseaseVariantDetailsField;
+ private Label variantDetailsSpacer;
+ private HorizontalLayout variantRow;
+
+ public DiseaseVariantComponent(FormEventBus eventBus, Disease initialDisease) {
+ super(PathogenTestDto.class);
+ this.eventBus = eventBus;
+ this.currentDisease = initialDisease;
+ buildLayout();
+ bindFields();
+ wireEvents();
+ }
+
+ private void buildLayout() {
+ diseaseVariantField = createComboBox(PathogenTestDto.TESTED_DISEASE_VARIANT, PathogenTestDto.I18N_PREFIX);
+ diseaseVariantField.setItemCaptionGenerator(DiseaseVariant::getCaption);
+ diseaseVariantField.setEmptySelectionAllowed(true);
+ diseaseVariantField.setVisible(false);
+
+ diseaseVariantDetailsField =
+ createTextField(PathogenTestDto.TESTED_DISEASE_VARIANT_DETAILS, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ diseaseVariantDetailsField.setVisible(false);
+
+ updateCaptions(currentDisease);
+
+ variantDetailsSpacer = createSpacer();
+ variantRow = addToggleRow(diseaseVariantField, diseaseVariantDetailsField, variantDetailsSpacer);
+
+ refreshVariantItems();
+ updateVisibility();
+ }
+
+ private void bindFields() {
+ binder.forField(diseaseVariantField).bind(PathogenTestDto::getTestedDiseaseVariant, PathogenTestDto::setTestedDiseaseVariant);
+ binder.forField(diseaseVariantDetailsField)
+ .bind(PathogenTestDto::getTestedDiseaseVariantDetails, PathogenTestDto::setTestedDiseaseVariantDetails);
+ }
+
+ private void wireEvents() {
+ // Variant details visibility
+ track(diseaseVariantField.addValueChangeListener(e -> {
+ DiseaseVariant variant = e.getValue();
+ boolean showVariantDetails = variant != null && variant.matchPropertyValue(DiseaseVariant.HAS_DETAILS, true);
+ diseaseVariantDetailsField.setVisible(showVariantDetails);
+ variantDetailsSpacer.setVisible(!showVariantDetails);
+ }));
+
+ // Disease change -> update variant list, captions, and visibility
+ track(eventBus.on(DiseaseChangedEvent.class, event -> {
+ currentDisease = event.getDisease();
+ refreshVariantItems();
+ updateCaptions(currentDisease);
+ updateVisibility();
+ }));
+
+ // Test type change -> update visibility
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ currentTestType = event.getTestType();
+ updateVisibility();
+ }));
+ }
+
+ private void refreshVariantItems() {
+ currentVariants = FacadeProvider.getCustomizableEnumFacade().getEnumValues(CustomizableEnumType.DISEASE_VARIANT, currentDisease);
+ diseaseVariantField.setItems(currentVariants);
+ }
+
+ private void updateVisibility() {
+ boolean visible = currentDisease != null
+ && DiseaseHelper.SUBTYPE_ALLOWED_DISEASES.contains(currentDisease)
+ && VARIANT_ALLOWED_TEST_TYPES.contains(currentTestType)
+ && CollectionUtils.isNotEmpty(currentVariants);
+ diseaseVariantField.setVisible(visible);
+ if (!visible) {
+ diseaseVariantField.clear();
+ diseaseVariantDetailsField.clear();
+ }
+ updateRowVisibility(variantRow);
+ setVisible(variantRow.isVisible());
+ }
+
+ private void updateCaptions(Disease disease) {
+ if (DiseaseHelper.SUBTYPE_ALLOWED_DISEASES.contains(disease)) {
+ diseaseVariantField.setCaption(I18nProperties.getCaption(Captions.PathogenTest_rsv_testedDiseaseVariant));
+ diseaseVariantDetailsField.setCaption(I18nProperties.getCaption(Captions.PathogenTest_rsv_testedDiseaseVariantDetails));
+ }
+ }
+
+ public ComboBox getDiseaseVariantField() {
+ return diseaseVariantField;
+ }
+
+ public TextField getDiseaseVariantDetailsField() {
+ return diseaseVariantDetailsField;
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/FourFoldCtCqComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/FourFoldCtCqComponent.java
new file mode 100644
index 00000000000..eaeb6a8bb7a
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/FourFoldCtCqComponent.java
@@ -0,0 +1,172 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import com.vaadin.ui.CheckBox;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.i18n.Captions;
+import de.symeda.sormas.api.i18n.I18nProperties;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.api.utils.fieldaccess.UiFieldAccessCheckers;
+import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers;
+import de.symeda.sormas.ui.samples.events.DiseaseChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestResultChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+import de.symeda.sormas.ui.utils.CssStyles;
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.FormEventBus;
+
+/**
+ * Four-fold antibody increase checkbox and CT/CQ value fields.
+ * as a standalone composable component
+ */
+public class FourFoldCtCqComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private final FormEventBus eventBus;
+ private final int caseSampleCount;
+ private final CtCqValueComponent ctCqValueComponent;
+
+ private CheckBox fourFoldIncrease;
+
+ private Disease currentDisease;
+ private PathogenTestType currentTestType;
+ private PathogenTestResultType currentTestResult;
+
+ public FourFoldCtCqComponent(FormEventBus eventBus, int caseSampleCount, boolean isLuxembourg, Disease initialDisease) {
+ super(PathogenTestDto.class);
+ this.eventBus = eventBus;
+ this.caseSampleCount = caseSampleCount;
+ this.currentDisease = initialDisease;
+ this.ctCqValueComponent = new CtCqValueComponent(isLuxembourg);
+ buildLayout();
+ bindFields();
+ wireEvents();
+ }
+
+ private void buildLayout() {
+ fourFoldIncrease = createCheckBox(PathogenTestDto.FOUR_FOLD_INCREASE_ANTIBODY_TITER, PathogenTestDto.I18N_PREFIX);
+ CssStyles.style(fourFoldIncrease, CssStyles.VSPACE_3, CssStyles.VSPACE_TOP_4);
+ fourFoldIncrease.setVisible(false);
+ fourFoldIncrease.setEnabled(false);
+ addComponent(fourFoldIncrease);
+
+ addComponent(ctCqValueComponent);
+ }
+
+ private void bindFields() {
+ binder.forField(fourFoldIncrease).bind(PathogenTestDto::isFourFoldIncreaseAntibodyTiter, PathogenTestDto::setFourFoldIncreaseAntibodyTiter);
+ }
+
+ private void wireEvents() {
+ // Listen for test type changes
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ PathogenTestType testType = event.getTestType();
+ currentTestType = testType;
+
+ if (testType != null) {
+ updateFourFoldIncrease(testType);
+
+ ctCqValueComponent.updateCtVisibility(currentDisease, testType);
+ } else {
+ ctCqValueComponent.updateCtVisibility(currentDisease, null);
+ }
+
+ ctCqValueComponent.updateCqVisibility(currentDisease, currentTestType, currentTestResult);
+ syncSelfVisibility();
+ }));
+
+ // Listen for test result changes
+ track(eventBus.on(TestResultChangedEvent.class, event -> {
+ currentTestResult = event.getTestResult();
+ ctCqValueComponent.updateCqVisibility(currentDisease, currentTestType, currentTestResult);
+ syncSelfVisibility();
+ }));
+
+ // Listen for disease changes
+ track(eventBus.on(DiseaseChangedEvent.class, event -> {
+ currentDisease = event.getDisease();
+ if (currentTestType != null) {
+ updateFourFoldIncrease(currentTestType);
+ }
+ ctCqValueComponent.updateCtVisibility(currentDisease, currentTestType);
+ ctCqValueComponent.updateCqVisibility(currentDisease, currentTestType, currentTestResult);
+ syncSelfVisibility();
+ }));
+ }
+
+ private void updateFourFoldIncrease(PathogenTestType testType) {
+ if (currentDisease == Disease.DENGUE && testType == PathogenTestType.IGG_SERUM_ANTIBODY) {
+ fourFoldIncrease.setCaption(I18nProperties.getCaption(Captions.PathogenTest_fourFoldIncreaseAntibodyTiter_DENGUE));
+ fourFoldIncrease.setVisible(true);
+ fourFoldIncrease.setEnabled(true);
+ } else if (testType == PathogenTestType.IGM_SERUM_ANTIBODY || testType == PathogenTestType.IGG_SERUM_ANTIBODY) {
+ fourFoldIncrease.setCaption(
+ I18nProperties.getPrefixCaption(PathogenTestDto.I18N_PREFIX, PathogenTestDto.FOUR_FOLD_INCREASE_ANTIBODY_TITER));
+ fourFoldIncrease.setVisible(true);
+ fourFoldIncrease.setEnabled(caseSampleCount >= 2);
+ } else {
+ fourFoldIncrease.setCaption(
+ I18nProperties.getPrefixCaption(PathogenTestDto.I18N_PREFIX, PathogenTestDto.FOUR_FOLD_INCREASE_ANTIBODY_TITER));
+ fourFoldIncrease.setVisible(false);
+ fourFoldIncrease.setEnabled(false);
+ }
+ }
+
+ private void syncSelfVisibility() {
+ setVisible(fourFoldIncrease.isVisible() || ctCqValueComponent.isVisible());
+ }
+
+ @Override
+ public void setDto(PathogenTestDto dto) {
+ super.setDto(dto);
+ ctCqValueComponent.setDto(dto);
+ // Sync visibility to the loaded DTO values, since events are not fired on initial load
+ currentTestType = dto != null ? dto.getTestType() : null;
+ currentTestResult = dto != null ? dto.getTestResult() : null;
+ ctCqValueComponent.updateCtVisibility(currentDisease, currentTestType);
+ ctCqValueComponent.updateCqVisibility(currentDisease, currentTestType, currentTestResult);
+ if (currentTestType != null) {
+ updateFourFoldIncrease(currentTestType);
+ }
+ syncSelfVisibility();
+ }
+
+ @Override
+ public void validate() {
+ super.validate();
+ ctCqValueComponent.validate();
+ }
+
+ @Override
+ public void applyVisibility(FieldVisibilityCheckers checkers, Class> dtoClass) {
+ super.applyVisibility(checkers, dtoClass);
+ ctCqValueComponent.applyVisibility(checkers, dtoClass);
+ syncSelfVisibility();
+ }
+
+ @Override
+ public void applyAccess(UiFieldAccessCheckers checkers, Class> dtoClass) {
+ super.applyAccess(checkers, dtoClass);
+ ctCqValueComponent.applyAccess(checkers, dtoClass);
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/PrescriberComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/PrescriberComponent.java
new file mode 100644
index 00000000000..8c7e5e37afa
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/PrescriberComponent.java
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import static de.symeda.sormas.ui.utils.CssStyles.H3;
+
+import com.vaadin.shared.ui.ValueChangeMode;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.FacadeProvider;
+import de.symeda.sormas.api.i18n.Captions;
+import de.symeda.sormas.api.i18n.I18nProperties;
+import de.symeda.sormas.api.i18n.Validations;
+import de.symeda.sormas.api.infrastructure.country.CountryReferenceDto;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.utils.DataHelper;
+import de.symeda.sormas.ui.utils.FormComponent;
+
+/**
+ * Prescriber fields section using Vaadin 8 components with own Binder.
+ * as a standalone composable component
+ */
+public class PrescriberComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private Label heading;
+ private TextField physicianCode;
+ private TextField firstName;
+ private TextField lastName;
+ private TextField phone;
+ private TextField address;
+ private TextField postalCode;
+ private TextField city;
+ private ComboBox country;
+
+ public PrescriberComponent() {
+ super(PathogenTestDto.class);
+ buildLayout();
+ bindFields();
+ }
+
+ private void buildLayout() {
+ heading = new Label(I18nProperties.getCaption(Captions.PathogenTest_prescriber));
+ heading.addStyleName(H3);
+ addComponent(heading);
+
+ physicianCode = createTextField(PathogenTestDto.PRESCRIBER_PHYSICIAN_CODE, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ addRow(physicianCode);
+
+ firstName = createTextField(PathogenTestDto.PRESCRIBER_FIRST_NAME, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ lastName = createTextField(PathogenTestDto.PRESCRIBER_LAST_NAME, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ addRow(firstName, lastName);
+
+ phone = createTextField(PathogenTestDto.PRESCRIBER_PHONE_NUMBER, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ addRow(phone);
+
+ address = createTextField(PathogenTestDto.PRESCRIBER_ADDRESS, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ postalCode = createTextField(PathogenTestDto.PRESCRIBER_POSTAL_CODE, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ addRow(address, postalCode);
+
+ city = createTextField(PathogenTestDto.PRESCRIBER_CITY, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ country = createComboBox(PathogenTestDto.PRESCRIBER_COUNTRY, PathogenTestDto.I18N_PREFIX);
+ country.setItems(FacadeProvider.getCountryFacade().getAllActiveAsReference());
+ country.setItemCaptionGenerator(CountryReferenceDto::getCaption);
+ addRow(city, country);
+ }
+
+ private void bindFields() {
+ binder.forField(physicianCode).bind(PathogenTestDto::getPrescriberPhysicianCode, PathogenTestDto::setPrescriberPhysicianCode);
+ binder.forField(firstName).bind(PathogenTestDto::getPrescriberFirstName, PathogenTestDto::setPrescriberFirstName);
+ binder.forField(lastName).bind(PathogenTestDto::getPrescriberLastName, PathogenTestDto::setPrescriberLastName);
+ binder.forField(phone)
+ .withValidator(DataHelper::isValidPhoneNumber, I18nProperties.getValidationError(Validations.validPhoneNumber, phone.getCaption()))
+ .bind(PathogenTestDto::getPrescriberPhoneNumber, PathogenTestDto::setPrescriberPhoneNumber);
+ binder.forField(address).bind(PathogenTestDto::getPrescriberAddress, PathogenTestDto::setPrescriberAddress);
+ binder.forField(postalCode).bind(PathogenTestDto::getPrescriberPostalCode, PathogenTestDto::setPrescriberPostalCode);
+ binder.forField(city).bind(PathogenTestDto::getPrescriberCity, PathogenTestDto::setPrescriberCity);
+ binder.forField(country).bind(PathogenTestDto::getPrescriberCountry, PathogenTestDto::setPrescriberCountry);
+ }
+
+ @Override
+ protected void updateRowAndSelfVisibility() {
+ super.updateRowAndSelfVisibility();
+ heading.setVisible(this.isVisible());
+ }
+
+ public boolean isHeadingVisible() {
+ return heading.isVisible();
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/ResultTextComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/ResultTextComponent.java
new file mode 100644
index 00000000000..81ad3c367f2
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/ResultTextComponent.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import com.vaadin.ui.TextArea;
+
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.ui.utils.FormComponent;
+
+/**
+ * Test result free-text field.
+ * Vaadin 8 components with own Binder, self-managed visibility.
+ */
+public class ResultTextComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private TextArea resultText;
+
+ public ResultTextComponent() {
+ super(PathogenTestDto.class);
+ buildLayout();
+ bindFields();
+ }
+
+ private void buildLayout() {
+ resultText = createTextArea(PathogenTestDto.TEST_RESULT_TEXT, PathogenTestDto.I18N_PREFIX);
+ resultText.setRows(6);
+ addFullWidthRow(resultText);
+ }
+
+ private void bindFields() {
+ binder.forField(resultText).bind(PathogenTestDto::getTestResultText, PathogenTestDto::setTestResultText);
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestIdentificationComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestIdentificationComponent.java
new file mode 100644
index 00000000000..561ee80b986
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestIdentificationComponent.java
@@ -0,0 +1,111 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Date;
+
+import com.vaadin.data.Converter;
+import com.vaadin.data.Result;
+import com.vaadin.data.ValueContext;
+import com.vaadin.shared.ui.ValueChangeMode;
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.DateField;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.ui.samples.events.ViaLimsChangedEvent;
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.FormEventBus;
+
+/**
+ * Report date, VIA_LIMS, external ID, external order ID.
+ * as a standalone composable component
+ */
+public class TestIdentificationComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private final FormEventBus eventBus;
+
+ private DateField reportDate;
+ private CheckBox viaLims;
+ private TextField externalId;
+ private TextField externalOrderId;
+
+ public TestIdentificationComponent(FormEventBus eventBus) {
+ super(PathogenTestDto.class);
+ this.eventBus = eventBus;
+ buildLayout();
+ bindFields();
+ wireEvents();
+ }
+
+ private void buildLayout() {
+ reportDate = createDateField(PathogenTestDto.REPORT_DATE, PathogenTestDto.I18N_PREFIX);
+ viaLims = createCheckBox(PathogenTestDto.VIA_LIMS, PathogenTestDto.I18N_PREFIX);
+ addRow(reportDate, viaLims);
+
+ externalId = createTextField(PathogenTestDto.EXTERNAL_ID, PathogenTestDto.I18N_PREFIX);
+ externalId.setValueChangeMode(ValueChangeMode.BLUR);
+ externalOrderId = createTextField(PathogenTestDto.EXTERNAL_ORDER_ID, PathogenTestDto.I18N_PREFIX);
+ externalOrderId.setValueChangeMode(ValueChangeMode.BLUR);
+ addRow(externalId, externalOrderId);
+ }
+
+ private void bindFields() {
+ binder.forField(reportDate)
+ .withConverter(new LocalDateToDateConverter())
+ .bind(PathogenTestDto::getReportDate, PathogenTestDto::setReportDate);
+
+ binder.forField(viaLims).bind(PathogenTestDto::isViaLims, PathogenTestDto::setViaLims);
+ binder.forField(externalId).bind(PathogenTestDto::getExternalId, PathogenTestDto::setExternalId);
+ binder.forField(externalOrderId).bind(PathogenTestDto::getExternalOrderId, PathogenTestDto::setExternalOrderId);
+ }
+
+ private void wireEvents() {
+ track(viaLims.addValueChangeListener(e -> eventBus.fire(new ViaLimsChangedEvent(Boolean.TRUE.equals(e.getValue())))));
+ }
+
+ public CheckBox getViaLimsField() {
+ return viaLims;
+ }
+
+ /**
+ * Converter between Vaadin 8 LocalDate and java.util.Date used by the DTO.
+ */
+ private static class LocalDateToDateConverter implements Converter {
+
+ @Override
+ public Result convertToModel(LocalDate value, ValueContext context) {
+ if (value == null) {
+ return Result.ok(null);
+ }
+ return Result.ok(Date.from(value.atStartOfDay(ZoneId.systemDefault()).toInstant()));
+ }
+
+ @Override
+ public LocalDate convertToPresentation(Date value, ValueContext context) {
+ if (value == null) {
+ return null;
+ }
+ return value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+ }
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestMethodComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestMethodComponent.java
new file mode 100644
index 00000000000..b6d8bd0d14f
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestMethodComponent.java
@@ -0,0 +1,351 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import com.vaadin.shared.ui.ValueChangeMode;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.DateField;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.FacadeProvider;
+import de.symeda.sormas.api.i18n.I18nProperties;
+import de.symeda.sormas.api.infrastructure.facility.FacilityDto;
+import de.symeda.sormas.api.infrastructure.facility.FacilityReferenceDto;
+import de.symeda.sormas.api.sample.PCRTestSpecification;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.api.utils.fieldaccess.UiFieldAccessCheckers;
+import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers;
+import de.symeda.sormas.ui.samples.events.DiseaseChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+import de.symeda.sormas.ui.utils.CssStyles;
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.FormEventBus;
+
+/**
+ * Test type, typing ID, test date/time, lab, lab details.
+ * as a standalone composable component
+ */
+public class TestMethodComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private final FormEventBus eventBus;
+ private final Supplier sampleDateSupplier;
+ private final Map timeItems = new LinkedHashMap<>();
+
+ private ComboBox testTypeField;
+ private TextField testTypeTextField;
+ private Label testTypeTextSpacer;
+ private ComboBox pcrTestSpecField;
+ private HorizontalLayout pcrTestSpecRow;
+ private TextField typingIdField;
+ private DateField testDateField;
+ private ComboBox testTimeField;
+ private ComboBox labField;
+ private TextField labDetailsField;
+
+ private PathogenTestDto currentDto;
+ private Disease currentDisease;
+
+ private HorizontalLayout typingIdRow;
+ private HorizontalLayout labDetailsRow;
+
+ private Supplier testDateSampleDateSupplier;
+ private String testDateValidationError;
+
+ public TestMethodComponent(FormEventBus eventBus, Supplier sampleDateSupplier, Disease initialDisease) {
+ super(PathogenTestDto.class);
+ this.eventBus = eventBus;
+ this.sampleDateSupplier = sampleDateSupplier;
+ this.currentDisease = initialDisease;
+ buildLayout();
+ bindFields();
+ wireEvents();
+ }
+
+ private void buildLayout() {
+ // Test type
+ testTypeField = createComboBox(PathogenTestDto.TEST_TYPE, PathogenTestDto.I18N_PREFIX);
+ updateComboBoxByDisease(testTypeField, PathogenTestType.class, currentDisease);
+ testTypeField.setItemCaptionGenerator(PathogenTestType::toString);
+
+ testTypeTextField = createTextField(PathogenTestDto.TEST_TYPE_TEXT, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ testTypeTextField.setVisible(false);
+
+ testTypeTextSpacer = createSpacer();
+ addToggleRow(testTypeField, testTypeTextField, testTypeTextSpacer);
+
+ // PCR test specification (Coronavirus-specific, below test type)
+ pcrTestSpecField = createComboBox(PathogenTestDto.PCR_TEST_SPECIFICATION, PathogenTestDto.I18N_PREFIX);
+ pcrTestSpecField.setItems(PCRTestSpecification.values());
+ pcrTestSpecField.setItemCaptionGenerator(PCRTestSpecification::toString);
+ pcrTestSpecField.setVisible(false);
+ pcrTestSpecRow = addRow(pcrTestSpecField);
+
+ // Typing ID
+ typingIdField = createTextField(PathogenTestDto.TYPING_ID, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ typingIdField.setVisible(false);
+ typingIdRow = addRow(typingIdField);
+
+ // Test date/time — custom IDs so created manually and tracked
+ testDateField = new DateField();
+ testDateField.setId(PathogenTestDto.TEST_DATE_TIME + "_date");
+ testDateField.setCaption(I18nProperties.getPrefixCaption(PathogenTestDto.I18N_PREFIX, PathogenTestDto.TEST_DATE_TIME));
+ CssStyles.style(testDateField, CssStyles.CAPTION_ON_TOP);
+ testDateField.setWidth(100, Unit.PERCENTAGE);
+
+ testTimeField = new ComboBox<>();
+ testTimeField.setId(PathogenTestDto.TEST_DATE_TIME + "_time");
+ testTimeField.setCaption("");
+ CssStyles.style(testTimeField, CssStyles.CAPTION_ON_TOP);
+ testTimeField.setWidth(100, Unit.PERCENTAGE);
+ testTimeField.setEmptySelectionAllowed(true);
+ for (int hours = 0; hours <= 23; hours++) {
+ for (int minutes = 0; minutes <= 59; minutes += 15) {
+ int totalMinutes = hours * 60 + minutes;
+ timeItems.put(totalMinutes, String.format("%02d:%02d", hours, minutes));
+ }
+ }
+ testTimeField.setItems(timeItems.keySet());
+ testTimeField.setItemCaptionGenerator(timeItems::get);
+
+ HorizontalLayout testDateTimeGroup = new HorizontalLayout();
+ testDateTimeGroup.setWidth(100, Unit.PERCENTAGE);
+ testDateTimeGroup.setSpacing(true);
+ testDateTimeGroup.addComponent(testDateField);
+ testDateTimeGroup.setExpandRatio(testDateField, 1);
+ testDateTimeGroup.addComponent(testTimeField);
+ testDateTimeGroup.setExpandRatio(testTimeField, 1);
+
+ // Lab
+ labField = createComboBox(PathogenTestDto.LAB, PathogenTestDto.I18N_PREFIX);
+ labField.setItems(FacadeProvider.getFacilityFacade().getAllActiveLaboratories(true));
+ labField.setItemCaptionGenerator(FacilityReferenceDto::getCaption);
+
+ addRow(testDateTimeGroup, labField);
+
+ // Lab details
+ labDetailsField = createTextField(PathogenTestDto.LAB_DETAILS, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ labDetailsField.setVisible(false);
+ labDetailsRow = addRowWithLeadingSpacer(labDetailsField);
+ }
+
+ private void bindFields() {
+ binder.forField(testTypeField).asRequired().bind(PathogenTestDto::getTestType, PathogenTestDto::setTestType);
+ binder.forField(testTypeTextField).bind(PathogenTestDto::getTestTypeText, PathogenTestDto::setTestTypeText);
+ binder.forField(pcrTestSpecField).bind(PathogenTestDto::getPcrTestSpecification, PathogenTestDto::setPcrTestSpecification);
+ binder.forField(typingIdField).bind(PathogenTestDto::getTypingId, PathogenTestDto::setTypingId);
+ // testDateField and testTimeField are managed manually (both map to testDateTime)
+ binder.forField(labField).bind(PathogenTestDto::getLab, PathogenTestDto::setLab);
+ binder.forField(labDetailsField).bind(PathogenTestDto::getLabDetails, PathogenTestDto::setLabDetails);
+ }
+
+ private void wireEvents() {
+ // Self-managed visibility: testTypeText visible for PCR_RT_PCR or OTHER
+ track(testTypeField.addValueChangeListener(e -> {
+ PathogenTestType type = e.getValue();
+
+ boolean showTestTypeText = type == PathogenTestType.PCR_RT_PCR || type == PathogenTestType.OTHER;
+ testTypeTextField.setVisible(showTestTypeText);
+ testTypeTextSpacer.setVisible(!showTestTypeText);
+ if (!showTestTypeText) {
+ testTypeTextField.clear();
+ }
+
+ boolean showTypingId =
+ type == PathogenTestType.PCR_RT_PCR || type == PathogenTestType.DNA_MICROARRAY || type == PathogenTestType.SEQUENCING;
+ typingIdField.setVisible(showTypingId);
+ if (!showTypingId) {
+ typingIdField.clear();
+ }
+ updateRowVisibility(typingIdRow);
+
+ updatePcrTestSpecVisibility(type);
+
+ eventBus.fire(new TestTypeChangedEvent(type));
+ }));
+
+ // Self-managed: lab details visible when "Other" lab selected
+ track(labField.addValueChangeListener(e -> {
+ FacilityReferenceDto lab = e.getValue();
+ boolean showDetails = lab != null && FacilityDto.OTHER_FACILITY_UUID.equals(lab.getUuid());
+ labDetailsField.setVisible(showDetails);
+ if (!showDetails) {
+ labDetailsField.clear();
+ }
+ updateRowVisibility(labDetailsRow);
+ }));
+
+ // Sync date/time fields back to DTO on value change
+ track(testDateField.addValueChangeListener(e -> syncTestDateTimeToDto()));
+ track(testTimeField.addValueChangeListener(e -> syncTestDateTimeToDto()));
+
+ // Listen for disease changes to update test type items and PCR spec visibility
+ track(eventBus.on(DiseaseChangedEvent.class, event -> {
+ currentDisease = event.getDisease();
+ updateTestTypeItems(currentDisease);
+ updatePcrTestSpecVisibility(testTypeField.getValue());
+ }));
+ }
+
+ private void syncTestDateTimeToDto() {
+ if (currentDto == null) {
+ return;
+ }
+ LocalDate date = testDateField.getValue();
+ if (date == null) {
+ currentDto.setTestDateTime(null);
+ return;
+ }
+ Integer totalMinutes = testTimeField.getValue();
+ LocalDateTime dateTime;
+ if (totalMinutes != null) {
+ dateTime = date.atTime(totalMinutes / 60, totalMinutes % 60);
+ } else {
+ dateTime = date.atStartOfDay();
+ }
+ currentDto.setTestDateTime(Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant()));
+ }
+
+ private void populateTestDateTimeFields(PathogenTestDto dto) {
+ Date testDateTime = dto != null ? dto.getTestDateTime() : null;
+ if (testDateTime == null) {
+ testDateField.setValue(null);
+ testTimeField.setValue(null);
+ return;
+ }
+ LocalDateTime ldt = testDateTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+ testDateField.setValue(ldt.toLocalDate());
+ LocalTime time = ldt.toLocalTime();
+ int totalMinutes = time.getHour() * 60 + time.getMinute();
+ if (!timeItems.containsKey(totalMinutes)) {
+ timeItems.put(totalMinutes, String.format("%02d:%02d", time.getHour(), time.getMinute()));
+ testTimeField.setItems(timeItems.keySet());
+ testTimeField.setItemCaptionGenerator(timeItems::get);
+ }
+ testTimeField.setValue(totalMinutes);
+ }
+
+ private void updateTestTypeItems(Disease disease) {
+ updateComboBoxByDisease(testTypeField, PathogenTestType.class, disease);
+ }
+
+ private void updatePcrTestSpecVisibility(PathogenTestType testType) {
+ boolean visible = currentDisease == Disease.CORONAVIRUS && testType == PathogenTestType.PCR_RT_PCR;
+ pcrTestSpecField.setVisible(visible);
+ if (!visible) {
+ pcrTestSpecField.clear();
+ }
+ updateRowVisibility(pcrTestSpecRow);
+ }
+
+ /** Forces typingId field visible when a preset value exists, regardless of current test type. */
+ public void showTypingIdIfPreset(String typingId) {
+ if (typingId != null && !typingId.trim().isEmpty()) {
+ typingIdField.setVisible(true);
+ updateRowVisibility(typingIdRow);
+ }
+ }
+
+ /**
+ * Registers a validator ensuring test date is not before sample date.
+ * Evaluated during {@link #validate()}.
+ */
+ public void addTestDateAfterSampleDateValidator(Supplier sampleDateSupplier, String errorMessage) {
+ this.testDateSampleDateSupplier = sampleDateSupplier;
+ this.testDateValidationError = errorMessage;
+ }
+
+ @Override
+ public void validate() {
+ super.validate();
+ if (testDateSampleDateSupplier != null && testDateField.getValue() != null) {
+ Date sampleDate = testDateSampleDateSupplier.get();
+ if (sampleDate != null) {
+ Integer totalMinutes = testTimeField.getValue();
+ LocalDateTime ldt = totalMinutes != null
+ ? testDateField.getValue().atTime(totalMinutes / 60, totalMinutes % 60)
+ : testDateField.getValue().atStartOfDay();
+ Date testDate = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
+ if (testDate.before(sampleDate)) {
+ throw new com.vaadin.v7.data.Validator.InvalidValueException(testDateValidationError);
+ }
+ }
+ }
+ }
+
+ public ComboBox getTestTypeField() {
+ return testTypeField;
+ }
+
+ public ComboBox getLabField() {
+ return labField;
+ }
+
+ public void setLabRequired(boolean required) {
+ binder.removeBinding(labField);
+ if (required) {
+ binder.forField(labField).asRequired().bind(PathogenTestDto::getLab, PathogenTestDto::setLab);
+ } else {
+ binder.forField(labField).bind(PathogenTestDto::getLab, PathogenTestDto::setLab);
+ }
+ }
+
+ @Override
+ public void setDto(PathogenTestDto dto) {
+ this.currentDto = dto;
+ populateTestDateTimeFields(dto);
+ super.setDto(dto);
+ }
+
+ @Override
+ public void applyVisibility(FieldVisibilityCheckers checkers, Class> dtoClass) {
+ super.applyVisibility(checkers, dtoClass);
+ // testDateField/testTimeField have custom IDs that don't match the DTO property
+ if (!checkers.isVisible(dtoClass, PathogenTestDto.TEST_DATE_TIME)) {
+ testDateField.setVisible(false);
+ testTimeField.setVisible(false);
+ }
+ updateRowAndSelfVisibility();
+ }
+
+ @Override
+ public void applyAccess(UiFieldAccessCheckers checkers, Class> dtoClass) {
+ super.applyAccess(checkers, dtoClass);
+ // testDateField/testTimeField have custom IDs that don't match the DTO property
+ if (!checkers.isAccessible(dtoClass, PathogenTestDto.TEST_DATE_TIME)) {
+ testDateField.setEnabled(false);
+ testDateField.addStyleName(CssStyles.INACCESSIBLE_FIELD);
+ testTimeField.setEnabled(false);
+ testTimeField.addStyleName(CssStyles.INACCESSIBLE_FIELD);
+ }
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestResultComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestResultComponent.java
new file mode 100644
index 00000000000..e2c5b5af17c
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestResultComponent.java
@@ -0,0 +1,154 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.components;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.RadioButtonGroup;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.ui.samples.events.DiseaseChangedEvent;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestResultChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+import de.symeda.sormas.ui.samples.events.ViaLimsChangedEvent;
+import de.symeda.sormas.ui.utils.CssStyles;
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.FormEventBus;
+
+/**
+ * Test result, verified, and preliminary fields.
+ * as a standalone composable component
+ */
+public class TestResultComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private final FormEventBus eventBus;
+ private final boolean isLuxembourg;
+
+ private ComboBox testResultField;
+ private RadioButtonGroup testResultVerifiedField;
+ private RadioButtonGroup preliminaryField;
+
+ private Disease currentDisease;
+ private PathogenTestType currentTestType;
+
+ public TestResultComponent(FormEventBus eventBus, boolean isLuxembourg, Disease initialDisease) {
+ super(PathogenTestDto.class);
+ this.eventBus = eventBus;
+ this.isLuxembourg = isLuxembourg;
+ this.currentDisease = initialDisease;
+ buildLayout();
+ bindFields();
+ wireEvents();
+ }
+
+ private void buildLayout() {
+ testResultField = createComboBox(PathogenTestDto.TEST_RESULT, PathogenTestDto.I18N_PREFIX);
+ List resultTypes = new ArrayList<>(java.util.Arrays.asList(PathogenTestResultType.values()));
+ resultTypes.remove(PathogenTestResultType.NOT_DONE);
+ if (!isLuxembourg) {
+ resultTypes.remove(PathogenTestResultType.NOT_APPLICABLE);
+ }
+ testResultField.setItems(resultTypes);
+ testResultField.setItemCaptionGenerator(PathogenTestResultType::toString);
+
+ testResultVerifiedField = createBooleanRadioGroup(PathogenTestDto.TEST_RESULT_VERIFIED, PathogenTestDto.I18N_PREFIX);
+
+ preliminaryField = createBooleanRadioGroup(PathogenTestDto.PRELIMINARY, PathogenTestDto.I18N_PREFIX);
+ preliminaryField.removeStyleName(CssStyles.CAPTION_ON_TOP);
+ CssStyles.style(preliminaryField, CssStyles.VSPACE_4);
+
+ addRow(
+ new float[] {
+ 6.4f,
+ 4.1f,
+ 2 },
+ testResultField,
+ testResultVerifiedField,
+ preliminaryField);
+ }
+
+ private void bindFields() {
+ binder.forField(testResultField).asRequired().bind(PathogenTestDto::getTestResult, PathogenTestDto::setTestResult);
+ binder.forField(testResultVerifiedField).bind(PathogenTestDto::getTestResultVerified, PathogenTestDto::setTestResultVerified);
+ binder.forField(preliminaryField).bind(PathogenTestDto::getPreliminary, PathogenTestDto::setPreliminary);
+ }
+
+ private void wireEvents() {
+ // Test result changed -> fire event
+ track(testResultField.addValueChangeListener(e -> {
+ eventBus.fire(new TestResultChangedEvent(e.getValue()));
+ }));
+
+ // Listen for test type changes -> clear result when type is cleared
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ currentTestType = event.getTestType();
+ if (currentTestType == null) {
+ testResultField.clear();
+ testResultField.setEnabled(true);
+ }
+ }));
+
+ // Disease sections fire this to request a specific test result value
+ track(eventBus.on(SetTestResultEvent.class, event -> {
+ if (event.getTestResult() != null) {
+ testResultField.setValue(event.getTestResult());
+ } else {
+ testResultField.clear();
+ }
+ }));
+
+ // Listen for disease changes -> clear result if disease changed
+ track(eventBus.on(DiseaseChangedEvent.class, event -> {
+ Disease oldDisease = currentDisease;
+ currentDisease = event.getDisease();
+ if (currentDisease != oldDisease && currentTestType != null) {
+ testResultField.clear();
+ }
+ }));
+
+ // VIA LIMS -> required state on test result verified
+ track(eventBus.on(ViaLimsChangedEvent.class, event -> setTestResultVerifiedRequired(event.isViaLims())));
+ }
+
+ public ComboBox getTestResultField() {
+ return testResultField;
+ }
+
+ public RadioButtonGroup getTestResultVerifiedField() {
+ return testResultVerifiedField;
+ }
+
+ private void setTestResultVerifiedRequired(boolean required) {
+ binder.removeBinding(testResultVerifiedField);
+ if (required) {
+ binder.forField(testResultVerifiedField)
+ .asRequired()
+ .bind(PathogenTestDto::getTestResultVerified, PathogenTestDto::setTestResultVerified);
+ } else {
+ binder.forField(testResultVerifiedField).bind(PathogenTestDto::getTestResultVerified, PathogenTestDto::setTestResultVerified);
+ }
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/AbstractDiseaseSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/AbstractDiseaseSectionComponent.java
new file mode 100644
index 00000000000..d26228bff41
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/AbstractDiseaseSectionComponent.java
@@ -0,0 +1,146 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import java.util.function.Consumer;
+
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.RadioButtonGroup;
+import com.vaadin.ui.TextField;
+import com.vaadin.v7.data.fieldgroup.FieldGroup;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.FormEventBus;
+
+/**
+ * Base class for disease-specific sections in the pathogen test form.
+ * Extends {@link FormComponent} and adds disease-section-specific lifecycle:
+ *
+ * Deferred initialization via {@link #initialize}
+ * Mid-lifecycle {@link #cleanup()} that clears owned DTO fields before unbinding
+ * Legacy {@link FieldGroup} support for DrugSusceptibilityForm
+ * Drug susceptibility field management
+ *
+ */
+public abstract class AbstractDiseaseSectionComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ protected FormEventBus eventBus;
+ protected PathogenTestFormConfig config;
+ protected Disease disease;
+
+ /** Kept only for DrugSusceptibilityForm legacy binding */
+ protected FieldGroup fieldGroup;
+
+ private Component drugSusceptibilityField;
+ private Consumer visibilityCallback;
+
+ protected AbstractDiseaseSectionComponent() {
+ super(PathogenTestDto.class);
+ }
+
+ public void setVisibilityCallback(Consumer callback) {
+ this.visibilityCallback = callback;
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visibilityCallback != null) {
+ visibilityCallback.accept(visible);
+ }
+ }
+
+ public void initialize(FieldGroup fieldGroup, FormEventBus eventBus, PathogenTestFormConfig config, Disease disease) {
+ this.fieldGroup = fieldGroup;
+ this.eventBus = eventBus;
+ this.config = config;
+ this.disease = disease;
+
+ buildLayout();
+ wireVisibility();
+ }
+
+ protected abstract void buildLayout();
+
+ protected abstract void wireVisibility();
+
+ /**
+ * Clears owned DTO fields and unbinds. Called explicitly before section swap
+ * to prevent stale values from being persisted when the disease changes.
+ */
+ public void cleanup() {
+ removeRegistrations();
+ clearOwnedFields();
+ binder.removeBean();
+ unbindLegacyFields();
+ }
+
+ /**
+ * Subclasses null out all DTO properties they own.
+ * Called during cleanup() while the binder still holds the bean.
+ */
+ protected abstract void clearOwnedFields();
+
+ /** Override to unbind any FieldGroup-bound legacy fields (e.g. DrugSusceptibilityForm) */
+ protected void unbindLegacyFields() {
+ }
+
+ protected void addDrugSusceptibilityField(Component field) {
+ this.drugSusceptibilityField = field;
+ field.setVisible(false);
+ addComponent(field);
+ }
+
+ protected void setDrugSusceptibilityRowVisible(boolean visible) {
+ if (drugSusceptibilityField != null) {
+ drugSusceptibilityField.setVisible(visible);
+ updateRowAndSelfVisibility();
+ }
+ }
+
+ @Override
+ protected boolean hasVisibleContent() {
+ return drugSusceptibilityField != null && drugSusceptibilityField.isVisible();
+ }
+
+ protected ComboBox createComboBox(String propertyId) {
+ return createComboBox(propertyId, PathogenTestDto.I18N_PREFIX);
+ }
+
+ protected TextField createTextField(String propertyId) {
+ return createTextField(propertyId, PathogenTestDto.I18N_PREFIX);
+ }
+
+ protected CheckBox createCheckBox(String propertyId) {
+ return createCheckBox(propertyId, PathogenTestDto.I18N_PREFIX);
+ }
+
+ protected RadioButtonGroup createRadioButtonGroup(String propertyId) {
+ return createBooleanRadioGroup(propertyId, PathogenTestDto.I18N_PREFIX);
+ }
+
+ protected > RadioButtonGroup createEnumRadioButtonGroup(String propertyId, Class enumClass) {
+ return createEnumRadioGroup(propertyId, PathogenTestDto.I18N_PREFIX, enumClass);
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/CryptosporidiosisSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/CryptosporidiosisSectionComponent.java
new file mode 100644
index 00000000000..fd3d2c4f5ca
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/CryptosporidiosisSectionComponent.java
@@ -0,0 +1,114 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.sample.GenoType;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestResultChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+
+public class CryptosporidiosisSectionComponent extends AbstractDiseaseSectionComponent {
+
+ private ComboBox genoTypeField;
+ private TextField genoTypeTextField;
+ private Label genoTypeTextFieldSpacer;
+
+ private PathogenTestType currentTestType;
+ private PathogenTestResultType currentResult;
+
+ @Override
+ protected void buildLayout() {
+ genoTypeField = createComboBox(PathogenTestDto.GENOTYPE);
+ genoTypeField.setItemCaptionGenerator(GenoType::toString);
+ genoTypeField.setVisible(false);
+ updateGenoTypeItems();
+
+ genoTypeTextField = createTextField(PathogenTestDto.GENOTYPE_TEXT);
+ genoTypeTextField.setVisible(false);
+
+ genoTypeTextFieldSpacer = createSpacer();
+ addToggleRow(genoTypeField, genoTypeTextField, genoTypeTextFieldSpacer);
+
+ binder.forField(genoTypeField).bind(PathogenTestDto::getGenoType, PathogenTestDto::setGenoType);
+ binder.forField(genoTypeTextField).bind(PathogenTestDto::getGenoTypeText, PathogenTestDto::setGenoTypeText);
+ }
+
+ @Override
+ protected void wireVisibility() {
+ track(genoTypeField.addValueChangeListener(e -> {
+ boolean showText = e.getValue() == GenoType.OTHER && genoTypeField.isVisible();
+ genoTypeTextField.setVisible(showText);
+ genoTypeTextFieldSpacer.setVisible(!showText);
+ if (!showText) {
+ genoTypeTextField.clear();
+ }
+ }));
+
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ currentTestType = event.getTestType();
+ updateVisibility();
+ updateGenoTypeItems();
+
+ // Auto-set test result
+ if (currentTestType == PathogenTestType.GENOTYPING) {
+ eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE));
+ } else if (currentTestType != null) {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ }));
+
+ track(eventBus.on(TestResultChangedEvent.class, event -> {
+ currentResult = event.getTestResult();
+ updateVisibility();
+ }));
+
+ }
+
+ private void updateVisibility() {
+ boolean visible = currentTestType == PathogenTestType.GENOTYPING && currentResult == PathogenTestResultType.POSITIVE;
+ genoTypeField.setVisible(visible);
+ if (!visible) {
+ genoTypeField.clear();
+ genoTypeTextField.setVisible(false);
+ genoTypeTextField.clear();
+ }
+ updateRowAndSelfVisibility();
+ }
+
+ @Override
+ protected void clearOwnedFields() {
+ PathogenTestDto dto = binder.getBean();
+ if (dto == null) {
+ return;
+ }
+ dto.setGenoType(null);
+ dto.setGenoTypeText(null);
+ }
+
+ private void updateGenoTypeItems() {
+ updateComboBoxByDisease(genoTypeField, GenoType.class, disease);
+ }
+
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/CsmSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/CsmSectionComponent.java
new file mode 100644
index 00000000000..0e47c99275e
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/CsmSectionComponent.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestResultChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+
+public class CsmSectionComponent extends AbstractDiseaseSectionComponent {
+
+ private TextField serotypeField;
+ private PathogenTestResultType currentResult;
+
+ @Override
+ protected void buildLayout() {
+ serotypeField = createTextField(PathogenTestDto.SEROTYPE_TEXT);
+ serotypeField.setVisible(false);
+ addRow(serotypeField, createSpacer());
+
+ binder.forField(serotypeField).bind(PathogenTestDto::getSerotypeText, PathogenTestDto::setSerotypeText);
+ }
+
+ @Override
+ protected void wireVisibility() {
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ PathogenTestType testType = event.getTestType();
+
+ // Clear test result on test type change (no auto-set for CSM)
+ if (testType != null) {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ }));
+
+ track(eventBus.on(TestResultChangedEvent.class, event -> {
+ currentResult = event.getTestResult();
+ updateVisibility();
+ }));
+ }
+
+ @Override
+ protected void clearOwnedFields() {
+ PathogenTestDto dto = binder.getBean();
+ if (dto == null) {
+ return;
+ }
+ dto.setSerotypeText(null);
+ }
+
+ private void updateVisibility() {
+ boolean visible = currentResult == PathogenTestResultType.POSITIVE;
+ serotypeField.setVisible(visible);
+ if (!visible) {
+ serotypeField.clear();
+ }
+ updateRowAndSelfVisibility();
+ }
+
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/DefaultSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/DefaultSectionComponent.java
new file mode 100644
index 00000000000..e277d684d8c
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/DefaultSectionComponent.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+
+/**
+ * Disease section for diseases with no extra fields.
+ * Handles auto-set test result for diseases that don't have a dedicated section component.
+ */
+public class DefaultSectionComponent extends AbstractDiseaseSectionComponent {
+
+ private static final Map> AUTO_POSITIVE_TYPES = buildAutoPositiveTypes();
+
+ private static Map> buildAutoPositiveTypes() {
+ Map> map = new HashMap<>();
+ map.put(
+ Disease.RESPIRATORY_SYNCYTIAL_VIRUS,
+ Collections.unmodifiableList(Arrays.asList(PathogenTestType.SEQUENCING, PathogenTestType.WHOLE_GENOME_SEQUENCING)));
+ map.put(Disease.INFLUENZA, Collections.singletonList(PathogenTestType.ISOLATION));
+ return Collections.unmodifiableMap(map);
+ }
+
+ @Override
+ protected void buildLayout() {
+ }
+
+ @Override
+ protected void wireVisibility() {
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ PathogenTestType testType = event.getTestType();
+ if (testType == null) {
+ return;
+ }
+
+ List positiveTypes = AUTO_POSITIVE_TYPES.get(disease);
+ if (positiveTypes != null && positiveTypes.contains(testType)) {
+ eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE));
+ } else {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ }));
+ }
+
+ @Override
+ protected void clearOwnedFields() {
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/DengueSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/DengueSectionComponent.java
new file mode 100644
index 00000000000..3b50a627ec3
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/DengueSectionComponent.java
@@ -0,0 +1,133 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.api.sample.Serotype;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestResultChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+
+public class DengueSectionComponent extends AbstractDiseaseSectionComponent {
+
+ private static final List SEROTYPE_VISIBLE_TYPES = Arrays.asList(
+ PathogenTestType.NAAT,
+ PathogenTestType.PCR_RT_PCR,
+ PathogenTestType.NEUTRALIZING_ANTIBODIES);
+
+ private static final List AUTO_POSITIVE_TYPES = Arrays.asList(
+ PathogenTestType.NAAT,
+ PathogenTestType.NEUTRALIZING_ANTIBODIES,
+ PathogenTestType.PCR_RT_PCR);
+
+ private ComboBox serotypeField;
+ private TextField serotypeTextField;
+ private Label serotypeTextSpacer;
+ private TextField antibodyTitreField;
+
+ private PathogenTestType currentTestType;
+ private PathogenTestResultType currentResult;
+
+ @Override
+ protected void buildLayout() {
+ serotypeField = createComboBox(PathogenTestDto.SEROTYPE);
+ serotypeField.setItemCaptionGenerator(Serotype::toString);
+ serotypeField.setVisible(false);
+ updateComboBoxByDisease(serotypeField, Serotype.class, disease);
+
+ serotypeTextField = createTextField(PathogenTestDto.SEROTYPE_TEXT);
+ serotypeTextField.setVisible(false);
+
+ serotypeTextSpacer = createSpacer();
+ addToggleRow(serotypeField, serotypeTextField, serotypeTextSpacer);
+
+ antibodyTitreField = createTextField(PathogenTestDto.ANTIBODY_TITRE);
+ antibodyTitreField.setVisible(false);
+ addRow(antibodyTitreField, createSpacer());
+
+ binder.forField(serotypeField).bind(PathogenTestDto::getSerotype, PathogenTestDto::setSerotype);
+ binder.forField(serotypeTextField).bind(PathogenTestDto::getSerotypeText, PathogenTestDto::setSerotypeText);
+ binder.forField(antibodyTitreField).bind(PathogenTestDto::getAntibodyTitre, PathogenTestDto::setAntibodyTitre);
+ }
+
+ @Override
+ protected void wireVisibility() {
+ track(serotypeField.addValueChangeListener(e -> {
+ boolean showText = e.getValue() == Serotype.OTHER && serotypeField.isVisible();
+ serotypeTextField.setVisible(showText);
+ serotypeTextSpacer.setVisible(!showText);
+ if (!showText) {
+ serotypeTextField.clear();
+ }
+ }));
+
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ currentTestType = event.getTestType();
+ updateVisibility();
+
+ if (currentTestType != null && AUTO_POSITIVE_TYPES.contains(currentTestType)) {
+ eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE));
+ } else if (currentTestType != null) {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ }));
+
+ track(eventBus.on(TestResultChangedEvent.class, event -> {
+ currentResult = event.getTestResult();
+ updateVisibility();
+ }));
+ }
+
+ private void updateVisibility() {
+ boolean showSerotype = SEROTYPE_VISIBLE_TYPES.contains(currentTestType);
+ serotypeField.setVisible(showSerotype);
+ if (!showSerotype) {
+ serotypeField.clear();
+ serotypeTextField.setVisible(false);
+ serotypeTextField.clear();
+ }
+
+ boolean showAntibodyTitre = currentTestType == PathogenTestType.NEUTRALIZING_ANTIBODIES;
+ antibodyTitreField.setVisible(showAntibodyTitre);
+ if (!showAntibodyTitre) {
+ antibodyTitreField.clear();
+ }
+
+ updateRowAndSelfVisibility();
+ }
+
+ @Override
+ protected void clearOwnedFields() {
+ PathogenTestDto dto = binder.getBean();
+ if (dto == null) {
+ return;
+ }
+ dto.setSerotype(null);
+ dto.setSerotypeText(null);
+ dto.setAntibodyTitre(null);
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/DiseaseSectionFactory.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/DiseaseSectionFactory.java
new file mode 100644
index 00000000000..493fcfc1d6f
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/DiseaseSectionFactory.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import de.symeda.sormas.api.Disease;
+
+/**
+ * Factory for component-based disease sections.
+ */
+public final class DiseaseSectionFactory {
+
+ private DiseaseSectionFactory() {
+ }
+
+ public static AbstractDiseaseSectionComponent forDisease(Disease disease) {
+ if (disease == null) {
+ return new DefaultSectionComponent();
+ }
+ switch (disease) {
+ case TUBERCULOSIS:
+ case LATENT_TUBERCULOSIS:
+ return new TuberculosisSectionComponent();
+ case MEASLES:
+ return new MeaslesSectionComponent();
+ case CRYPTOSPORIDIOSIS:
+ return new CryptosporidiosisSectionComponent();
+ case INVASIVE_MENINGOCOCCAL_INFECTION:
+ return new ImiSectionComponent();
+ case INVASIVE_PNEUMOCOCCAL_INFECTION:
+ return new IpiSectionComponent();
+ case CSM:
+ return new CsmSectionComponent();
+ case DENGUE:
+ return new DengueSectionComponent();
+ case MALARIA:
+ return new MalariaSectionComponent();
+ default:
+ return new DefaultSectionComponent();
+ }
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/ImiSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/ImiSectionComponent.java
new file mode 100644
index 00000000000..e79bdebb0d7
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/ImiSectionComponent.java
@@ -0,0 +1,155 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.FacadeProvider;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.api.sample.SeroGroupSpecification;
+import de.symeda.sormas.api.utils.fieldaccess.UiFieldAccessCheckers;
+import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestResultChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+import de.symeda.sormas.ui.therapy.DrugSusceptibilityForm;
+
+public class ImiSectionComponent extends AbstractDiseaseSectionComponent {
+
+ private static final List IMI_TEST_TYPES = Arrays.asList(
+ PathogenTestType.SEROGROUPING,
+ PathogenTestType.MULTILOCUS_SEQUENCE_TYPING,
+ PathogenTestType.SLIDE_AGGLUTINATION,
+ PathogenTestType.WHOLE_GENOME_SEQUENCING);
+
+ private static final List AUTO_POSITIVE_TYPES = Arrays.asList(
+ PathogenTestType.SEROGROUPING,
+ PathogenTestType.MULTILOCUS_SEQUENCE_TYPING,
+ PathogenTestType.SLIDE_AGGLUTINATION,
+ PathogenTestType.WHOLE_GENOME_SEQUENCING,
+ PathogenTestType.SEQUENCING,
+ PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY);
+
+ private ComboBox seroGroupSpecField;
+ private TextField seroGroupSpecTextField;
+ private Label seroGroupSpecTextSpacer;
+ private DrugSusceptibilityForm drugSusceptibilityField;
+
+ private PathogenTestType currentTestType;
+ private PathogenTestResultType currentResult;
+
+ @Override
+ protected void buildLayout() {
+
+ seroGroupSpecField = createComboBox(PathogenTestDto.SERO_GROUP_SPECIFICATION);
+ seroGroupSpecField.setItems(SeroGroupSpecification.values());
+ seroGroupSpecField.setItemCaptionGenerator(SeroGroupSpecification::toString);
+ seroGroupSpecField.setVisible(false);
+
+ seroGroupSpecTextField = createTextField(PathogenTestDto.SERO_GROUP_SPECIFICATION_TEXT);
+ seroGroupSpecTextField.setVisible(false);
+
+ seroGroupSpecTextSpacer = createSpacer();
+ addToggleRow(seroGroupSpecField, seroGroupSpecTextField, seroGroupSpecTextSpacer);
+
+ binder.forField(seroGroupSpecField).bind(PathogenTestDto::getSeroGroupSpecification, PathogenTestDto::setSeroGroupSpecification);
+ binder.forField(seroGroupSpecTextField).bind(PathogenTestDto::getSeroGroupSpecificationText, PathogenTestDto::setSeroGroupSpecificationText);
+
+ // DrugSusceptibilityForm — legacy v7, bound via parent FieldGroup
+ drugSusceptibilityField = new DrugSusceptibilityForm(
+ FieldVisibilityCheckers.getNoop(),
+ UiFieldAccessCheckers.getDefault(true, FacadeProvider.getConfigFacade().getCountryLocale()));
+ drugSusceptibilityField.setCaption(null);
+ fieldGroup.bind(drugSusceptibilityField, PathogenTestDto.DRUG_SUSCEPTIBILITY);
+ addDrugSusceptibilityField(drugSusceptibilityField);
+ }
+
+ @Override
+ protected void wireVisibility() {
+ // seroGroupSpecText visible only when OTHER
+ track(seroGroupSpecField.addValueChangeListener(e -> {
+ boolean showText = e.getValue() == SeroGroupSpecification.OTHER;
+ seroGroupSpecTextField.setVisible(showText);
+ seroGroupSpecTextSpacer.setVisible(!showText);
+ if (!showText) {
+ seroGroupSpecTextField.clear();
+ }
+ }));
+
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ currentTestType = event.getTestType();
+ updateVisibility();
+
+ // Drug susceptibility visibility
+ if (drugSusceptibilityField != null) {
+ boolean visible = drugSusceptibilityField.updateFieldsVisibility(disease, currentTestType);
+ setDrugSusceptibilityRowVisible(visible);
+ }
+
+ if (currentTestType != null && AUTO_POSITIVE_TYPES.contains(currentTestType)) {
+ eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE));
+ } else if (currentTestType != null) {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ }));
+
+ track(eventBus.on(TestResultChangedEvent.class, event -> {
+ currentResult = event.getTestResult();
+ updateVisibility();
+ }));
+
+ }
+
+ private void updateVisibility() {
+ boolean visible = currentResult == PathogenTestResultType.POSITIVE && IMI_TEST_TYPES.contains(currentTestType);
+ seroGroupSpecField.setVisible(visible);
+ if (!visible) {
+ seroGroupSpecField.clear();
+ seroGroupSpecTextField.setVisible(false);
+ seroGroupSpecTextField.clear();
+ }
+ updateRowAndSelfVisibility();
+ }
+
+ @Override
+ protected void clearOwnedFields() {
+ PathogenTestDto dto = binder.getBean();
+ if (dto == null) {
+ return;
+ }
+ dto.setSeroGroupSpecification(null);
+ dto.setSeroGroupSpecificationText(null);
+ dto.setDrugSusceptibility(null);
+ }
+
+ @Override
+ protected void unbindLegacyFields() {
+ if (drugSusceptibilityField != null) {
+ fieldGroup.unbind(drugSusceptibilityField);
+ drugSusceptibilityField = null;
+ }
+ }
+
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/IpiSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/IpiSectionComponent.java
new file mode 100644
index 00000000000..06831d4b346
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/IpiSectionComponent.java
@@ -0,0 +1,166 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.FacadeProvider;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.api.sample.SerotypingMethod;
+import de.symeda.sormas.api.utils.fieldaccess.UiFieldAccessCheckers;
+import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestResultChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+import de.symeda.sormas.ui.therapy.DrugSusceptibilityForm;
+
+public class IpiSectionComponent extends AbstractDiseaseSectionComponent {
+
+ private static final List SEROTYPE_EXTENDED_TYPES = Arrays.asList(
+ PathogenTestType.WHOLE_GENOME_SEQUENCING,
+ PathogenTestType.SLIDE_AGGLUTINATION,
+ PathogenTestType.MULTILOCUS_SEQUENCE_TYPING,
+ PathogenTestType.SEROGROUPING);
+
+ private static final List AUTO_POSITIVE_TYPES = Arrays.asList(
+ PathogenTestType.SEROGROUPING,
+ PathogenTestType.MULTILOCUS_SEQUENCE_TYPING,
+ PathogenTestType.SLIDE_AGGLUTINATION,
+ PathogenTestType.WHOLE_GENOME_SEQUENCING,
+ PathogenTestType.SEQUENCING,
+ PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY);
+
+ private TextField serotypeField;
+ private ComboBox serotypingMethodField;
+ private TextField serotypingMethodTextField;
+ private DrugSusceptibilityForm drugSusceptibilityField;
+
+ private PathogenTestType currentTestType;
+ private PathogenTestResultType currentResult;
+
+ @Override
+ protected void buildLayout() {
+ serotypeField = createTextField(PathogenTestDto.SEROTYPE_TEXT);
+ serotypeField.setVisible(false);
+
+ serotypingMethodField = createComboBox(PathogenTestDto.SEROTYPING_METHOD);
+ serotypingMethodField.setItems(SerotypingMethod.values());
+ serotypingMethodField.setItemCaptionGenerator(SerotypingMethod::toString);
+ serotypingMethodField.setVisible(false);
+
+ addRow(serotypeField, serotypingMethodField);
+
+ serotypingMethodTextField = createTextField(PathogenTestDto.SERO_TYPING_METHOD_TEXT);
+ serotypingMethodTextField.setVisible(false);
+ addRow(serotypingMethodTextField);
+
+ binder.forField(serotypeField).bind(PathogenTestDto::getSerotypeText, PathogenTestDto::setSerotypeText);
+ binder.forField(serotypingMethodField).bind(PathogenTestDto::getSeroTypingMethod, PathogenTestDto::setSeroTypingMethod);
+ binder.forField(serotypingMethodTextField).bind(PathogenTestDto::getSeroTypingMethodText, PathogenTestDto::setSeroTypingMethodText);
+
+ // DrugSusceptibilityForm — legacy v7, bound via parent FieldGroup
+ drugSusceptibilityField = new DrugSusceptibilityForm(
+ FieldVisibilityCheckers.getNoop(),
+ UiFieldAccessCheckers.getDefault(true, FacadeProvider.getConfigFacade().getCountryLocale()));
+ drugSusceptibilityField.setCaption(null);
+ fieldGroup.bind(drugSusceptibilityField, PathogenTestDto.DRUG_SUSCEPTIBILITY);
+ addDrugSusceptibilityField(drugSusceptibilityField);
+ }
+
+ @Override
+ protected void wireVisibility() {
+ // serotypingMethodText visible only when OTHER
+ track(serotypingMethodField.addValueChangeListener(e -> {
+ boolean showText = e.getValue() == SerotypingMethod.OTHER;
+ serotypingMethodTextField.setVisible(showText);
+ if (!showText) {
+ serotypingMethodTextField.clear();
+ }
+ }));
+
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ currentTestType = event.getTestType();
+ updateVisibility();
+
+ // Drug susceptibility visibility
+ if (drugSusceptibilityField != null) {
+ boolean visible = drugSusceptibilityField.updateFieldsVisibility(disease, currentTestType);
+ setDrugSusceptibilityRowVisible(visible);
+ }
+
+ if (currentTestType != null && AUTO_POSITIVE_TYPES.contains(currentTestType)) {
+ eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE));
+ } else if (currentTestType != null) {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ }));
+
+ track(eventBus.on(TestResultChangedEvent.class, event -> {
+ currentResult = event.getTestResult();
+ updateVisibility();
+ }));
+
+ }
+
+ private void updateVisibility() {
+ boolean isPositive = currentResult == PathogenTestResultType.POSITIVE;
+
+ // Serotype visible on extended test types + positive
+ boolean showSerotype = isPositive && SEROTYPE_EXTENDED_TYPES.contains(currentTestType);
+ serotypeField.setVisible(showSerotype);
+ if (!showSerotype) {
+ serotypeField.clear();
+ }
+
+ // Serotyping method visible on SEROGROUPING + positive
+ boolean showMethod = isPositive && currentTestType == PathogenTestType.SEROGROUPING;
+ serotypingMethodField.setVisible(showMethod);
+ if (!showMethod) {
+ serotypingMethodField.clear();
+ serotypingMethodTextField.setVisible(false);
+ serotypingMethodTextField.clear();
+ }
+ updateRowAndSelfVisibility();
+ }
+
+ @Override
+ protected void clearOwnedFields() {
+ PathogenTestDto dto = binder.getBean();
+ if (dto == null) {
+ return;
+ }
+ dto.setSerotypeText(null);
+ dto.setSeroTypingMethod(null);
+ dto.setSeroTypingMethodText(null);
+ dto.setDrugSusceptibility(null);
+ }
+
+ @Override
+ protected void unbindLegacyFields() {
+ if (drugSusceptibilityField != null) {
+ fieldGroup.unbind(drugSusceptibilityField);
+ drugSusceptibilityField = null;
+ }
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/MalariaSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/MalariaSectionComponent.java
new file mode 100644
index 00000000000..407ea9c08ed
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/MalariaSectionComponent.java
@@ -0,0 +1,146 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.sample.PathogenSpecie;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+
+public class MalariaSectionComponent extends AbstractDiseaseSectionComponent {
+
+ private static final List SPECIE_VISIBLE_TYPES = Arrays.asList(
+ PathogenTestType.THIN_BLOOD_SMEAR,
+ PathogenTestType.ANTIGEN_DETECTION,
+ PathogenTestType.RAPID_TEST,
+ PathogenTestType.PCR_RT_PCR,
+ PathogenTestType.Q_PCR,
+ PathogenTestType.LAMP,
+ PathogenTestType.INDIRECT_FLUORESCENT_ANTIBODY,
+ PathogenTestType.OTHER_MOLECULAR_ASSAY,
+ PathogenTestType.OTHER_SEROLOGICAL_TEST,
+ PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST,
+ PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY);
+
+ private static final List AUTO_POSITIVE_TYPES = Arrays.asList(
+ PathogenTestType.ANTIGEN_DETECTION,
+ PathogenTestType.THIN_BLOOD_SMEAR,
+ PathogenTestType.RAPID_TEST,
+ PathogenTestType.INDIRECT_FLUORESCENT_ANTIBODY,
+ PathogenTestType.PCR_RT_PCR,
+ PathogenTestType.Q_PCR,
+ PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY,
+ PathogenTestType.LAMP,
+ PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST,
+ PathogenTestType.OTHER_SEROLOGICAL_TEST,
+ PathogenTestType.OTHER_MOLECULAR_ASSAY);
+
+ private static final List RESULT_DETAILS_VISIBLE_TYPES =
+ Arrays.asList(PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.Q_PCR);
+
+ private ComboBox specieField;
+ private TextField specieTextField;
+ private Label specieTextSpacer;
+ private TextField resultDetailsField;
+
+ private PathogenTestType currentTestType;
+
+ @Override
+ protected void buildLayout() {
+ specieField = createComboBox(PathogenTestDto.SPECIE);
+ specieField.setItemCaptionGenerator(PathogenSpecie::toString);
+ specieField.setVisible(false);
+ updateComboBoxByDiseaseAndTestType(specieField, PathogenSpecie.class, disease, currentTestType);
+
+ specieTextField = createTextField(PathogenTestDto.SPECIE_TEXT);
+ specieTextField.setVisible(false);
+
+ specieTextSpacer = createSpacer();
+ addToggleRow(specieField, specieTextField, specieTextSpacer);
+
+ resultDetailsField = createTextField(PathogenTestDto.RESULT_DETAILS);
+ resultDetailsField.setVisible(false);
+ addRow(resultDetailsField, createSpacer());
+
+ binder.forField(specieField).bind(PathogenTestDto::getSpecie, PathogenTestDto::setSpecie);
+ binder.forField(specieTextField).bind(PathogenTestDto::getSpecieText, PathogenTestDto::setSpecieText);
+ binder.forField(resultDetailsField).bind(PathogenTestDto::getResultDetails, PathogenTestDto::setResultDetails);
+ }
+
+ @Override
+ protected void wireVisibility() {
+ track(specieField.addValueChangeListener(e -> {
+ boolean showText = e.getValue() == PathogenSpecie.OTHER && specieField.isVisible();
+ specieTextField.setVisible(showText);
+ specieTextSpacer.setVisible(!showText);
+ if (!showText) {
+ specieTextField.clear();
+ }
+ }));
+
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ currentTestType = event.getTestType();
+ updateComboBoxByDiseaseAndTestType(specieField, PathogenSpecie.class, disease, currentTestType);
+ updateVisibility();
+
+ if (currentTestType != null && AUTO_POSITIVE_TYPES.contains(currentTestType)) {
+ eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE));
+ } else if (currentTestType != null) {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ }));
+ }
+
+ private void updateVisibility() {
+ boolean showSpecie = SPECIE_VISIBLE_TYPES.contains(currentTestType);
+ specieField.setVisible(showSpecie);
+ if (!showSpecie) {
+ specieField.clear();
+ specieTextField.setVisible(false);
+ specieTextField.clear();
+ }
+
+ boolean showResultDetails = RESULT_DETAILS_VISIBLE_TYPES.contains(currentTestType);
+ resultDetailsField.setVisible(showResultDetails);
+ if (!showResultDetails) {
+ resultDetailsField.clear();
+ }
+
+ updateRowAndSelfVisibility();
+ }
+
+ @Override
+ protected void clearOwnedFields() {
+ PathogenTestDto dto = binder.getBean();
+ if (dto == null) {
+ return;
+ }
+ dto.setSpecie(null);
+ dto.setSpecieText(null);
+ dto.setResultDetails(null);
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/MeaslesSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/MeaslesSectionComponent.java
new file mode 100644
index 00000000000..feba82c32a6
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/MeaslesSectionComponent.java
@@ -0,0 +1,115 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.sample.GenoType;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestResultChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+
+public class MeaslesSectionComponent extends AbstractDiseaseSectionComponent {
+
+ private ComboBox genoTypeField;
+ private TextField genoTypeTextField;
+ private Label genoTypeTextSpacer;
+
+ private PathogenTestType currentTestType;
+ private PathogenTestResultType currentResult;
+
+ @Override
+ protected void buildLayout() {
+ genoTypeField = createComboBox(PathogenTestDto.GENOTYPE);
+ genoTypeField.setItemCaptionGenerator(GenoType::toString);
+ genoTypeField.setVisible(false);
+ updateGenoTypeItems();
+
+ genoTypeTextField = createTextField(PathogenTestDto.GENOTYPE_TEXT);
+ genoTypeTextField.setVisible(false);
+
+ genoTypeTextSpacer = createSpacer();
+ addToggleRow(genoTypeField, genoTypeTextField, genoTypeTextSpacer);
+
+ binder.forField(genoTypeField).bind(PathogenTestDto::getGenoType, PathogenTestDto::setGenoType);
+ binder.forField(genoTypeTextField).bind(PathogenTestDto::getGenoTypeText, PathogenTestDto::setGenoTypeText);
+ }
+
+ @Override
+ protected void wireVisibility() {
+ // genoTypeResultText visible only when OTHER selected
+ track(genoTypeField.addValueChangeListener(e -> {
+ boolean showText = e.getValue() == GenoType.OTHER && genoTypeField.isVisible();
+ genoTypeTextField.setVisible(showText);
+ genoTypeTextSpacer.setVisible(!showText);
+ if (!showText) {
+ genoTypeTextField.clear();
+ }
+ }));
+
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ currentTestType = event.getTestType();
+ updateVisibility();
+ updateGenoTypeItems();
+
+ // Auto-set test result
+ if (currentTestType == PathogenTestType.GENOTYPING) {
+ eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE));
+ } else if (currentTestType != null) {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ }));
+
+ track(eventBus.on(TestResultChangedEvent.class, event -> {
+ currentResult = event.getTestResult();
+ updateVisibility();
+ }));
+
+ }
+
+ private void updateVisibility() {
+ boolean visible = currentTestType == PathogenTestType.GENOTYPING && currentResult == PathogenTestResultType.POSITIVE;
+ genoTypeField.setVisible(visible);
+ if (!visible) {
+ genoTypeField.clear();
+ genoTypeTextField.setVisible(false);
+ genoTypeTextField.clear();
+ }
+ updateRowAndSelfVisibility();
+ }
+
+ @Override
+ protected void clearOwnedFields() {
+ PathogenTestDto dto = binder.getBean();
+ if (dto == null) {
+ return;
+ }
+ dto.setGenoType(null);
+ dto.setGenoTypeText(null);
+ }
+
+ private void updateGenoTypeItems() {
+ updateComboBoxByDisease(genoTypeField, GenoType.class, disease);
+ }
+
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/PathogenTestFormConfig.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/PathogenTestFormConfig.java
new file mode 100644
index 00000000000..3e3b7c5be3b
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/PathogenTestFormConfig.java
@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import de.symeda.sormas.api.CountryHelper;
+import de.symeda.sormas.api.FacadeProvider;
+
+/**
+ * Immutable snapshot of country-specific feature flags for PathogenTestForm.
+ * Computed once at form construction time so section classes never call
+ * FacadeProvider directly for country checks.
+ */
+public class PathogenTestFormConfig {
+
+ public final boolean isLuxembourg;
+
+ private PathogenTestFormConfig(boolean isLuxembourg) {
+ this.isLuxembourg = isLuxembourg;
+ }
+
+ public static PathogenTestFormConfig fromCurrentConfig() {
+ return new PathogenTestFormConfig(
+ FacadeProvider.getConfigFacade().isConfiguredCountry(CountryHelper.COUNTRY_CODE_LUXEMBOURG));
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/TuberculosisSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/TuberculosisSectionComponent.java
new file mode 100644
index 00000000000..8bc535d62a3
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/TuberculosisSectionComponent.java
@@ -0,0 +1,376 @@
+
+/*******************************************************************************
+ * SORMAS® - Surveillance Outbreak Response Management & Analysis System
+ * Copyright © 2016-2018 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *******************************************************************************/
+package de.symeda.sormas.ui.samples.diseasesection;
+
+import java.util.Locale;
+
+import com.vaadin.data.ValueProvider;
+import com.vaadin.server.Setter;
+import com.vaadin.shared.ui.ValueChangeMode;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.RadioButtonGroup;
+import com.vaadin.ui.TextField;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.FacadeProvider;
+import de.symeda.sormas.api.sample.PathogenSpecie;
+import de.symeda.sormas.api.sample.PathogenStrainCallStatus;
+import de.symeda.sormas.api.sample.PathogenTestDto;
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+import de.symeda.sormas.api.sample.PathogenTestScale;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.api.utils.YesNoUnknown;
+import de.symeda.sormas.api.utils.fieldaccess.UiFieldAccessCheckers;
+import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers;
+import de.symeda.sormas.ui.samples.events.DiseaseChangedEvent;
+import de.symeda.sormas.ui.samples.events.SetTestResultEvent;
+import de.symeda.sormas.ui.samples.events.TestResultChangedEvent;
+import de.symeda.sormas.ui.samples.events.TestTypeChangedEvent;
+import de.symeda.sormas.ui.therapy.DrugSusceptibilityForm;
+import de.symeda.sormas.ui.utils.StringToFloatNullableConverter;
+
+public class TuberculosisSectionComponent extends AbstractDiseaseSectionComponent {
+
+ private RadioButtonGroup rifampicinResistant;
+ private RadioButtonGroup isoniazidResistant;
+ private ComboBox testScale;
+ private ComboBox strainCallStatus;
+ private ComboBox specie;
+ private TextField patternProfile;
+ private DrugSusceptibilityForm drugSusceptibilityField;
+
+ // Tube fields
+ private TextField tubeNil;
+ private RadioButtonGroup tubeNilGT10;
+ private TextField tubeAgTb1;
+ private RadioButtonGroup tubeAgTb1GT10;
+ private TextField tubeAgTb2;
+ private RadioButtonGroup tubeAgTb2GT10;
+ private TextField tubeMitogene;
+ private RadioButtonGroup tubeMitogeneGT10;
+
+ private PathogenTestType currentTestType;
+ private PathogenTestResultType currentResult;
+
+ @Override
+ protected void buildLayout() {
+
+ // Rifampicin / Isoniazid resistant
+ rifampicinResistant = createEnumRadioButtonGroup(PathogenTestDto.RIFAMPICIN_RESISTANT, YesNoUnknown.class);
+ rifampicinResistant.setVisible(false);
+
+ isoniazidResistant = createEnumRadioButtonGroup(PathogenTestDto.ISONIAZID_RESISTANT, YesNoUnknown.class);
+ isoniazidResistant.setVisible(false);
+
+ addRow(rifampicinResistant, isoniazidResistant);
+
+ // Test scale
+ testScale = createComboBox(PathogenTestDto.TEST_SCALE);
+ testScale.setItems(PathogenTestScale.values());
+ testScale.setItemCaptionGenerator(PathogenTestScale::toString);
+ testScale.setVisible(false);
+ addRow(testScale);
+
+ // Strain call status
+ strainCallStatus = createComboBox(PathogenTestDto.STRAIN_CALL_STATUS);
+ strainCallStatus.setItemCaptionGenerator(PathogenStrainCallStatus::toString);
+ strainCallStatus.setVisible(false);
+ updateStrainCallStatusItems(disease);
+ addRow(strainCallStatus);
+
+ // Specie
+ specie = createComboBox(PathogenTestDto.SPECIE);
+ specie.setItemCaptionGenerator(PathogenSpecie::toString);
+ specie.setVisible(false);
+ updateComboBoxByDiseaseAndTestType(specie, PathogenSpecie.class, disease, currentTestType);
+ addRow(specie);
+
+ // Pattern profile
+ patternProfile = createTextField(PathogenTestDto.PATTERN_PROFILE);
+ patternProfile.setVisible(false);
+ addRow(patternProfile);
+
+ // Bind non-tube fields
+ binder.forField(rifampicinResistant).bind(PathogenTestDto::getRifampicinResistant, PathogenTestDto::setRifampicinResistant);
+ binder.forField(isoniazidResistant).bind(PathogenTestDto::getIsoniazidResistant, PathogenTestDto::setIsoniazidResistant);
+ binder.forField(testScale).bind(PathogenTestDto::getTestScale, PathogenTestDto::setTestScale);
+ binder.forField(strainCallStatus).bind(PathogenTestDto::getStrainCallStatus, PathogenTestDto::setStrainCallStatus);
+ binder.forField(specie).bind(PathogenTestDto::getSpecie, PathogenTestDto::setSpecie);
+ binder.forField(patternProfile).bind(PathogenTestDto::getPatternProfile, PathogenTestDto::setPatternProfile);
+
+ // DrugSusceptibilityForm — legacy v7
+ drugSusceptibilityField = new DrugSusceptibilityForm(
+ FieldVisibilityCheckers.getNoop(),
+ UiFieldAccessCheckers.getDefault(true, FacadeProvider.getConfigFacade().getCountryLocale()));
+ drugSusceptibilityField.setCaption(null);
+ fieldGroup.bind(drugSusceptibilityField, PathogenTestDto.DRUG_SUSCEPTIBILITY);
+ addDrugSusceptibilityField(drugSusceptibilityField);
+
+ // Tube fields
+ buildTubeFields();
+ }
+
+ private void buildTubeFields() {
+ tubeNil = createTubeTextField(PathogenTestDto.TUBE_NIL);
+ tubeNilGT10 = createRadioButtonGroup(PathogenTestDto.TUBE_NIL_GT10);
+ tubeNilGT10.setVisible(false);
+ tubeNil.setVisible(false);
+ addRow(tubeNil, tubeNilGT10);
+
+ tubeAgTb1 = createTubeTextField(PathogenTestDto.TUBE_AG_TB1);
+ tubeAgTb1GT10 = createRadioButtonGroup(PathogenTestDto.TUBE_AG_TB1_GT10);
+ tubeAgTb1GT10.setVisible(false);
+ tubeAgTb1.setVisible(false);
+ addRow(tubeAgTb1, tubeAgTb1GT10);
+
+ tubeAgTb2 = createTubeTextField(PathogenTestDto.TUBE_AG_TB2);
+ tubeAgTb2GT10 = createRadioButtonGroup(PathogenTestDto.TUBE_AG_TB2_GT10);
+ tubeAgTb2GT10.setVisible(false);
+ tubeAgTb2.setVisible(false);
+ addRow(tubeAgTb2, tubeAgTb2GT10);
+
+ tubeMitogene = createTubeTextField(PathogenTestDto.TUBE_MITOGENE);
+ tubeMitogeneGT10 = createRadioButtonGroup(PathogenTestDto.TUBE_MITOGENE_GT10);
+ tubeMitogeneGT10.setVisible(false);
+ tubeMitogene.setVisible(false);
+ addRow(tubeMitogene, tubeMitogeneGT10);
+
+ // Bind and sync tube fields
+ bindTubePair(
+ tubeNil,
+ PathogenTestDto::getTubeNil,
+ PathogenTestDto::setTubeNil,
+ tubeNilGT10,
+ PathogenTestDto::getTubeNilGT10,
+ PathogenTestDto::setTubeNilGT10);
+ bindTubePair(
+ tubeAgTb1,
+ PathogenTestDto::getTubeAgTb1,
+ PathogenTestDto::setTubeAgTb1,
+ tubeAgTb1GT10,
+ PathogenTestDto::getTubeAgTb1GT10,
+ PathogenTestDto::setTubeAgTb1GT10);
+ bindTubePair(
+ tubeAgTb2,
+ PathogenTestDto::getTubeAgTb2,
+ PathogenTestDto::setTubeAgTb2,
+ tubeAgTb2GT10,
+ PathogenTestDto::getTubeAgTb2GT10,
+ PathogenTestDto::setTubeAgTb2GT10);
+ bindTubePair(
+ tubeMitogene,
+ PathogenTestDto::getTubeMitogene,
+ PathogenTestDto::setTubeMitogene,
+ tubeMitogeneGT10,
+ PathogenTestDto::getTubeMitogeneGT10,
+ PathogenTestDto::setTubeMitogeneGT10);
+ }
+
+ private TextField createTubeTextField(String propertyId) {
+ TextField field = createTextField(propertyId);
+ field.setValueChangeMode(ValueChangeMode.LAZY);
+ field.setValueChangeTimeout(1000);
+ return field;
+ }
+
+ @Override
+ protected void wireVisibility() {
+ // Drug susceptibility, tube fields, and auto-set apply regardless of Luxembourg
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ currentTestType = event.getTestType();
+
+ if (config.isLuxembourg) {
+ updateFieldVisibility();
+ }
+
+ updateComboBoxByDiseaseAndTestType(specie, PathogenSpecie.class, disease, currentTestType);
+ updateDrugSusceptibility(currentTestType);
+ setTubeFieldsVisible(currentTestType);
+
+ // Auto-set test result (Luxembourg only)
+ if (currentTestType != null && config.isLuxembourg) {
+ if (currentTestType == PathogenTestType.BEIJINGGENOTYPING
+ || currentTestType == PathogenTestType.MIRU_PATTERN_CODE
+ || currentTestType == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY) {
+ eventBus.fire(new SetTestResultEvent(PathogenTestResultType.NOT_APPLICABLE));
+ } else if (currentTestType == PathogenTestType.SPOLIGOTYPING) {
+ eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE));
+ } else {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ } else if (currentTestType != null) {
+ eventBus.fire(new SetTestResultEvent(null));
+ }
+ }));
+
+ track(eventBus.on(TestResultChangedEvent.class, event -> {
+ currentResult = event.getTestResult();
+ if (config.isLuxembourg) {
+ updateFieldVisibility();
+ }
+ }));
+
+ track(eventBus.on(DiseaseChangedEvent.class, event -> {
+ disease = event.getDisease();
+ if (config.isLuxembourg) {
+ updateFieldVisibility();
+ updateStrainCallStatusItems(disease);
+ }
+ setTubeFieldsVisible(currentTestType);
+ updateDrugSusceptibility(currentTestType);
+ }));
+ }
+
+ private void updateFieldVisibility() {
+ // Rifampicin resistant: PCR_RT_PCR + POSITIVE
+ boolean showRifampicin = currentTestType == PathogenTestType.PCR_RT_PCR && currentResult == PathogenTestResultType.POSITIVE;
+ rifampicinResistant.setVisible(showRifampicin);
+ if (!showRifampicin) {
+ rifampicinResistant.clear();
+ }
+
+ // Isoniazid follows same conditions as rifampicin
+ isoniazidResistant.setVisible(showRifampicin);
+ if (!showRifampicin) {
+ isoniazidResistant.clear();
+ }
+
+ // Test scale: MICROSCOPY
+ boolean showTestScale = currentTestType == PathogenTestType.MICROSCOPY;
+ testScale.setVisible(showTestScale);
+ if (!showTestScale) {
+ testScale.clear();
+ }
+
+ // Strain call status: BEIJINGGENOTYPING
+ boolean showStrain = currentTestType == PathogenTestType.BEIJINGGENOTYPING;
+ strainCallStatus.setVisible(showStrain);
+ if (!showStrain) {
+ strainCallStatus.clear();
+ }
+
+ // Specie: SPOLIGOTYPING + POSITIVE
+ boolean showSpecie = currentTestType == PathogenTestType.SPOLIGOTYPING && currentResult == PathogenTestResultType.POSITIVE;
+ specie.setVisible(showSpecie);
+ if (!showSpecie) {
+ specie.clear();
+ }
+
+ // Pattern profile: MIRU_PATTERN_CODE
+ boolean showPattern = currentTestType == PathogenTestType.MIRU_PATTERN_CODE;
+ patternProfile.setVisible(showPattern);
+ if (!showPattern) {
+ patternProfile.clear();
+ }
+ updateRowAndSelfVisibility();
+ }
+
+ private void setTubeFieldsVisible(PathogenTestType testType) {
+ boolean showTubes = testType == PathogenTestType.IGRA && config.isLuxembourg;
+ TextField[] numericFields = {
+ tubeNil,
+ tubeAgTb1,
+ tubeAgTb2,
+ tubeMitogene };
+ @SuppressWarnings("unchecked")
+ RadioButtonGroup[] gt10Fields = new RadioButtonGroup[] {
+ tubeNilGT10,
+ tubeAgTb1GT10,
+ tubeAgTb2GT10,
+ tubeMitogeneGT10 };
+
+ for (int i = 0; i < numericFields.length; i++) {
+ numericFields[i].setVisible(showTubes);
+ gt10Fields[i].setVisible(showTubes);
+ if (!showTubes) {
+ numericFields[i].clear();
+ gt10Fields[i].clear();
+ }
+ }
+ updateRowAndSelfVisibility();
+ }
+
+ private void updateStrainCallStatusItems(Disease disease) {
+ updateComboBoxByDisease(strainCallStatus, PathogenStrainCallStatus.class, disease);
+ }
+
+ private void updateDrugSusceptibility(PathogenTestType testType) {
+ if (drugSusceptibilityField != null) {
+ boolean visible = drugSusceptibilityField.updateFieldsVisibility(disease, testType);
+ setDrugSusceptibilityRowVisible(visible);
+ }
+ }
+
+ @Override
+ protected void clearOwnedFields() {
+ PathogenTestDto dto = binder.getBean();
+ if (dto == null) {
+ return;
+ }
+ dto.setRifampicinResistant(null);
+ dto.setIsoniazidResistant(null);
+ dto.setTestScale(null);
+ dto.setStrainCallStatus(null);
+ dto.setSpecie(null);
+ dto.setPatternProfile(null);
+ dto.setTubeNil(null);
+ dto.setTubeNilGT10(null);
+ dto.setTubeAgTb1(null);
+ dto.setTubeAgTb1GT10(null);
+ dto.setTubeAgTb2(null);
+ dto.setTubeAgTb2GT10(null);
+ dto.setTubeMitogene(null);
+ dto.setTubeMitogeneGT10(null);
+ dto.setDrugSusceptibility(null);
+ }
+
+ @Override
+ protected void unbindLegacyFields() {
+ if (drugSusceptibilityField != null) {
+ fieldGroup.unbind(drugSusceptibilityField);
+ drugSusceptibilityField = null;
+ }
+ }
+
+ private void bindTubePair(
+ TextField tubeField,
+ ValueProvider tubeGetter,
+ Setter tubeSetter,
+ RadioButtonGroup gt10Field,
+ ValueProvider gt10Getter,
+ Setter gt10Setter) {
+
+ binder.forField(tubeField).withConverter(new StringToFloatNullableConverter(tubeField.getCaption())).bind(tubeGetter, tubeSetter);
+ binder.forField(gt10Field).bind(gt10Getter, gt10Setter);
+
+ track(tubeField.addValueChangeListener(e -> {
+ Locale locale = tubeField.getLocale() != null ? tubeField.getLocale() : Locale.getDefault();
+ StringToFloatNullableConverter.parseLocaleAware(e.getValue(), locale).ifPresent(f -> gt10Field.setValue(f > 10));
+ }));
+
+ track(gt10Field.addValueChangeListener(e -> {
+ Locale locale = tubeField.getLocale() != null ? tubeField.getLocale() : Locale.getDefault();
+ if (Boolean.TRUE.equals(e.getValue())) {
+ StringToFloatNullableConverter.parseLocaleAware(tubeField.getValue(), locale).filter(f -> f <= 10).ifPresent(f -> tubeField.clear());
+ } else if (Boolean.FALSE.equals(e.getValue())) {
+ StringToFloatNullableConverter.parseLocaleAware(tubeField.getValue(), locale).filter(f -> f > 10).ifPresent(f -> tubeField.clear());
+ }
+ }));
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/DiseaseChangedEvent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/DiseaseChangedEvent.java
new file mode 100644
index 00000000000..a9ffdc711c1
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/DiseaseChangedEvent.java
@@ -0,0 +1,16 @@
+package de.symeda.sormas.ui.samples.events;
+
+import de.symeda.sormas.api.Disease;
+
+public class DiseaseChangedEvent {
+
+ private final Disease disease;
+
+ public DiseaseChangedEvent(Disease disease) {
+ this.disease = disease;
+ }
+
+ public Disease getDisease() {
+ return disease;
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/SetTestResultEvent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/SetTestResultEvent.java
new file mode 100644
index 00000000000..a667249b7c9
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/SetTestResultEvent.java
@@ -0,0 +1,21 @@
+package de.symeda.sormas.ui.samples.events;
+
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+
+/**
+ * Command event: requests that the test result field be set to a specific value.
+ * Distinct from {@link TestResultChangedEvent}, which notifies that the value has already changed.
+ * A {@code null} test result means "clear the field".
+ */
+public class SetTestResultEvent {
+
+ private final PathogenTestResultType testResult;
+
+ public SetTestResultEvent(PathogenTestResultType testResult) {
+ this.testResult = testResult;
+ }
+
+ public PathogenTestResultType getTestResult() {
+ return testResult;
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/TestResultChangedEvent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/TestResultChangedEvent.java
new file mode 100644
index 00000000000..7d35dfd0007
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/TestResultChangedEvent.java
@@ -0,0 +1,16 @@
+package de.symeda.sormas.ui.samples.events;
+
+import de.symeda.sormas.api.sample.PathogenTestResultType;
+
+public class TestResultChangedEvent {
+
+ private final PathogenTestResultType testResult;
+
+ public TestResultChangedEvent(PathogenTestResultType testResult) {
+ this.testResult = testResult;
+ }
+
+ public PathogenTestResultType getTestResult() {
+ return testResult;
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/TestTypeChangedEvent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/TestTypeChangedEvent.java
new file mode 100644
index 00000000000..716ab99d247
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/TestTypeChangedEvent.java
@@ -0,0 +1,16 @@
+package de.symeda.sormas.ui.samples.events;
+
+import de.symeda.sormas.api.sample.PathogenTestType;
+
+public class TestTypeChangedEvent {
+
+ private final PathogenTestType testType;
+
+ public TestTypeChangedEvent(PathogenTestType testType) {
+ this.testType = testType;
+ }
+
+ public PathogenTestType getTestType() {
+ return testType;
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/ViaLimsChangedEvent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/ViaLimsChangedEvent.java
new file mode 100644
index 00000000000..d379ce975d7
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/events/ViaLimsChangedEvent.java
@@ -0,0 +1,14 @@
+package de.symeda.sormas.ui.samples.events;
+
+public class ViaLimsChangedEvent {
+
+ private final boolean viaLims;
+
+ public ViaLimsChangedEvent(boolean viaLims) {
+ this.viaLims = viaLims;
+ }
+
+ public boolean isViaLims() {
+ return viaLims;
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/humansample/SampleController.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/humansample/SampleController.java
index 3e4135575a5..c1c893e8c57 100644
--- a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/humansample/SampleController.java
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/humansample/SampleController.java
@@ -22,7 +22,6 @@
import java.util.function.Consumer;
import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.StringUtils;
import com.vaadin.navigator.Navigator;
import com.vaadin.server.Sizeable.Unit;
@@ -40,9 +39,6 @@
import com.vaadin.ui.themes.ValoTheme;
import com.vaadin.v7.data.Buffered.SourceException;
import com.vaadin.v7.data.Validator.InvalidValueException;
-import com.vaadin.v7.ui.CheckBox;
-import com.vaadin.v7.ui.ComboBox;
-import com.vaadin.v7.ui.Field;
import de.symeda.sormas.api.Disease;
import de.symeda.sormas.api.FacadeProvider;
@@ -89,7 +85,6 @@
import de.symeda.sormas.ui.utils.CommitDiscardWrapperComponent;
import de.symeda.sormas.ui.utils.ConfirmationComponent;
import de.symeda.sormas.ui.utils.CssStyles;
-import de.symeda.sormas.ui.utils.DateComparisonValidator;
import de.symeda.sormas.ui.utils.DateFormatHelper;
import de.symeda.sormas.ui.utils.DateTimeField;
import de.symeda.sormas.ui.utils.DeleteRestoreHandlers;
@@ -221,39 +216,29 @@ private CollapsiblePathogenTestForm addPathogenTestComponent(
if (pathogenTest != null) {
pathogenTestForm.setValue(pathogenTest);
// show typingId field when it has a preset value
- if (StringUtils.isNotBlank(pathogenTest.getTypingId())) {
- pathogenTestForm.getField(PathogenTestDto.TYPING_ID).setVisible(true);
- }
+ pathogenTestForm.showTypingIdIfPreset(pathogenTest.getTypingId());
} else {
pathogenTestForm.setValue(PathogenTestDto.build(sampleComponent.getWrappedComponent().getValue(), UiUtil.getUser()));
- // remove value invalid for newly created pathogen tests
- ComboBox pathogenTestResultField = pathogenTestForm.getField(PathogenTestDto.TEST_RESULT);
- pathogenTestResultField.removeItem(PathogenTestResultType.NOT_DONE);
- pathogenTestResultField.setValue(PathogenTestResultType.PENDING);
- ComboBox testDiseaseField = pathogenTestForm.getField(PathogenTestDto.TESTED_DISEASE);
- // setting the disease field value is only necessary if the disease is not null
- testDiseaseField.setValue(pathogenTestFormDisease);
+ // set initial result and disease for newly created pathogen tests
+ pathogenTestForm.initializeForNewTest(pathogenTestFormDisease);
}
// setup field updates
- Field testLabField = pathogenTestForm.getField(PathogenTestDto.LAB);
NullableOptionGroup samplePurposeField = sampleComponent.getWrappedComponent().getField(SampleDto.SAMPLE_PURPOSE);
- Runnable updateTestLabFieldRequired = () -> testLabField.setRequired(!SamplePurpose.INTERNAL.equals(samplePurposeField.getValue()));
- updateTestLabFieldRequired.run();
- samplePurposeField.addValueChangeListener(e -> updateTestLabFieldRequired.run());
+ Runnable updateLabRequired = () -> pathogenTestForm.setLabRequired(!SamplePurpose.INTERNAL.equals(samplePurposeField.getValue()));
+ updateLabRequired.run();
+ samplePurposeField.addValueChangeListener(e -> updateLabRequired.run());
// validate pathogen test create component before saving the sample
sampleComponent.addFieldGroups(pathogenTestForm.getFieldGroup());
- // Sample creation specific configuration
+ // Sample creation specific configuration: test date must not be before sample date
final DateTimeField sampleDateField = sampleComponent.getWrappedComponent().getField(SampleDto.SAMPLE_DATE_TIME);
- final DateTimeField testDateField = pathogenTestForm.getField(PathogenTestDto.TEST_DATE_TIME);
- testDateField.addValidator(
- new DateComparisonValidator(
- testDateField,
- sampleDateField,
- false,
- false,
- I18nProperties.getValidationError(Validations.afterDate, testDateField.getCaption(), sampleDateField.getCaption())));
+ pathogenTestForm.addTestDateAfterSampleDateValidator(
+ sampleDateField::getValue,
+ I18nProperties.getValidationError(
+ Validations.afterDate,
+ I18nProperties.getPrefixCaption(PathogenTestDto.I18N_PREFIX, PathogenTestDto.TEST_DATE_TIME),
+ sampleDateField.getCaption()));
if (viaLims) {
setViaLimsFieldChecked(pathogenTestForm);
@@ -345,8 +330,7 @@ public void addPathogenTestButton(
}
public void setViaLimsFieldChecked(PathogenTestForm pathogenTestForm) {
- CheckBox viaLimsCheckbox = pathogenTestForm.getField(PathogenTestDto.VIA_LIMS);
- viaLimsCheckbox.setValue(Boolean.TRUE);
+ pathogenTestForm.setViaLims(true);
}
public void createReferral(SampleDto existingSample, Disease disease) {
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/therapy/DrugSusceptibilityForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/therapy/DrugSusceptibilityForm.java
index fef8d0ee77b..2c003c559f9 100644
--- a/sormas-ui/src/main/java/de/symeda/sormas/ui/therapy/DrugSusceptibilityForm.java
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/therapy/DrugSusceptibilityForm.java
@@ -185,8 +185,9 @@ private ComboBox addResistanceResultField(String fieldId) {
return field;
}
+ @Override
public void markAsDirty() {
-
+ super.markAsDirty();
}
public void forceUpdateDrugSusceptibilityFields() {
@@ -297,38 +298,43 @@ private void forceUpdateDrugSusceptibilityMicField(String fieldId, Optional applicableFieldIds =
AnnotationFieldHelper.getFieldNamesWithMatchingDiseaseAndTestAnnotations(DrugSusceptibilityDto.class, disease, pathogenTestType);
- formHeadingLabel.setVisible(!applicableFieldIds.isEmpty());
+ boolean hasVisibleFields = !applicableFieldIds.isEmpty();
+ formHeadingLabel.setVisible(hasVisibleFields);
- if (!applicableFieldIds.isEmpty()) {
- FieldHelper.showOnlyFields(getFieldGroup(), applicableFieldIds, true);
+ if (hasVisibleFields) {
+ FieldHelper.showOnlyFields(getFieldGroup(), applicableFieldIds, false);
}
+
+ return hasVisibleFields;
}
}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FieldHelper.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FieldHelper.java
index 38d7db5f1c5..113d351b37c 100644
--- a/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FieldHelper.java
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FieldHelper.java
@@ -313,6 +313,7 @@ public static void setCaptionWhen(Field> sourceField, Field> targetField, Ob
}
}
+ @SuppressWarnings("rawtypes")
public static void setVisibleWhen(
final FieldGroup fieldGroup,
List targetPropertyIds,
@@ -344,6 +345,7 @@ public static void setVisibleWhen(final Field targetField, Map onValueChangedSetVisible(targetField, sourceFieldsAndValues, clearOnHidden)));
}
+ @SuppressWarnings("rawtypes")
private static void onValueChangedSetVisible(
final FieldGroup fieldGroup,
List targetPropertyIds,
@@ -907,7 +909,8 @@ private static void onValueChangedSetRequired(
true };
sourcePropertyIdsAndValues.forEach((sourcePropertyId, sourceValues) -> {
- if (!sourceValues.contains(fieldGroup.getField(sourcePropertyId).getValue()))
+ Field sourceField = fieldGroup.getField(sourcePropertyId);
+ if (sourceField == null || !sourceValues.contains(sourceField.getValue()))
requiredArray[0] = false;
});
@@ -1039,7 +1042,8 @@ private static void onValueChangedSetValue(
true };
sourcePropertyIdsAndValues.forEach((sourcePropertyId, sourceValues) -> {
- if (!sourceValues.contains(fieldGroup.getField(sourcePropertyId).getValue()))
+ Field sourceField = fieldGroup.getField(sourcePropertyId);
+ if (sourceField == null || !sourceValues.contains(sourceField.getValue()))
shouldSetValueArray[0] = false;
});
@@ -1110,6 +1114,10 @@ private static void onValueChangedSetReadOnly(
sourcePropertyIdsAndValues.forEach((sourcePropertyId, sourceValues) -> {
Field sourceField = fieldGroup.getField(sourcePropertyId);
+ if (sourceField == null) {
+ readOnlyArray[0] = false;
+ return;
+ }
Object sourceValue = getNullableSourceFieldValue(sourceField);
boolean matches;
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FormComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FormComponent.java
new file mode 100644
index 00000000000..822a60ec976
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FormComponent.java
@@ -0,0 +1,431 @@
+package de.symeda.sormas.ui.utils;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.vaadin.data.Binder;
+import com.vaadin.data.BinderValidationStatus;
+import com.vaadin.data.BindingValidationStatus;
+import com.vaadin.data.ValidationResult;
+import com.vaadin.shared.Registration;
+import com.vaadin.shared.ui.ValueChangeMode;
+import com.vaadin.ui.AbstractOrderedLayout;
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.DateField;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.RadioButtonGroup;
+import com.vaadin.ui.TextArea;
+import com.vaadin.ui.TextField;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.themes.ValoTheme;
+import com.vaadin.v7.data.Validator.InvalidValueException;
+
+import de.symeda.sormas.api.Disease;
+import de.symeda.sormas.api.i18n.I18nProperties;
+import de.symeda.sormas.api.sample.PathogenTestType;
+import de.symeda.sormas.api.utils.ApplicableToPathogenTests;
+import de.symeda.sormas.api.utils.Diseases;
+import de.symeda.sormas.api.utils.fieldaccess.UiFieldAccessCheckers;
+import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers;
+
+/**
+ * Base class for composable form components that participate in the parent form lifecycle.
+ * Extends {@link VerticalLayout} so that Vaadin's {@link #detach()} is called automatically
+ * when the parent form is removed — ensuring binder unbinding and listener cleanup.
+ *
+ * Provides optional field factory methods ({@link #createComboBox}, {@link #createTextField}, etc.)
+ * that auto-track fields for visibility/access application. Subclasses that build fields manually
+ * can override {@link #applyVisibility} and {@link #applyAccess} directly.
+ *
+ * @param
+ * the DTO type managed by the parent form
+ */
+public abstract class FormComponent extends VerticalLayout {
+
+ private static final long serialVersionUID = 1L;
+
+ protected final Binder binder;
+
+ private final List registrations = new ArrayList<>();
+ private final List trackedFields = new ArrayList<>();
+ private final List trackedRows = new ArrayList<>();
+
+ protected FormComponent(Class dtoClass) {
+ binder = new Binder<>(dtoClass);
+ setWidth(100, Unit.PERCENTAGE);
+ setMargin(false);
+ setSpacing(true);
+ }
+
+ /** Tracks a registration for automatic removal on detach. */
+ protected void track(Registration registration) {
+ registrations.add(registration);
+ }
+
+ /** Removes all tracked registrations. Called by detach() automatically, but can also be called explicitly for mid-lifecycle cleanup. */
+ protected void removeRegistrations() {
+ for (Registration reg : registrations) {
+ reg.remove();
+ }
+ registrations.clear();
+ }
+
+ /** Tracks a field for automatic visibility/access application. */
+ protected void trackField(Component field) {
+ trackedFields.add(field);
+ }
+
+ protected DateField createDateField(String propertyId, String i18nPrefix) {
+ DateField field = new DateField();
+ field.setId(propertyId);
+ field.setCaption(I18nProperties.getPrefixCaption(i18nPrefix, propertyId));
+ CssStyles.style(field, CssStyles.CAPTION_ON_TOP);
+ field.setWidth(100, Unit.PERCENTAGE);
+ trackField(field);
+ return field;
+ }
+
+ protected ComboBox createComboBox(String propertyId, String i18nPrefix) {
+ ComboBox field = new ComboBox<>();
+ field.setId(propertyId);
+ field.setCaption(I18nProperties.getPrefixCaption(i18nPrefix, propertyId));
+ CssStyles.style(field, CssStyles.CAPTION_ON_TOP);
+ field.setWidth(100, Unit.PERCENTAGE);
+ trackField(field);
+ return field;
+ }
+
+ protected TextField createTextField(String propertyId, String i18nPrefix) {
+ TextField field = new TextField();
+ field.setId(propertyId);
+ field.setCaption(I18nProperties.getPrefixCaption(i18nPrefix, propertyId));
+ CssStyles.style(field, CssStyles.CAPTION_ON_TOP);
+ field.setWidth(100, Unit.PERCENTAGE);
+ trackField(field);
+ return field;
+ }
+
+ protected TextField createTextField(String propertyId, String i18nPrefix, ValueChangeMode mode) {
+ TextField field = createTextField(propertyId, i18nPrefix);
+ field.setValueChangeMode(mode);
+ return field;
+ }
+
+ protected TextArea createTextArea(String propertyId, String i18nPrefix) {
+ TextArea field = new TextArea();
+ field.setId(propertyId);
+ field.setCaption(I18nProperties.getPrefixCaption(i18nPrefix, propertyId));
+ CssStyles.style(field, CssStyles.CAPTION_ON_TOP);
+ field.setWidth(100, Unit.PERCENTAGE);
+ trackField(field);
+ return field;
+ }
+
+ protected CheckBox createCheckBox(String propertyId, String i18nPrefix) {
+ CheckBox field = new CheckBox();
+ field.setId(propertyId);
+ field.setCaption(I18nProperties.getPrefixCaption(i18nPrefix, propertyId));
+ CssStyles.style(field, CssStyles.CAPTION_ON_TOP);
+ field.setWidth(100, Unit.PERCENTAGE);
+ trackField(field);
+ return field;
+ }
+
+ protected RadioButtonGroup createBooleanRadioGroup(String propertyId, String i18nPrefix) {
+ RadioButtonGroup field = new RadioButtonGroup<>();
+ field.setId(propertyId);
+ field.setCaption(I18nProperties.getPrefixCaption(i18nPrefix, propertyId));
+ CssStyles.style(field, CssStyles.CAPTION_ON_TOP, ValoTheme.OPTIONGROUP_HORIZONTAL);
+ field.setItems(Boolean.TRUE, Boolean.FALSE);
+ field.setItemCaptionGenerator(
+ b -> b
+ ? I18nProperties.getEnumCaption(de.symeda.sormas.api.utils.YesNoUnknown.YES)
+ : I18nProperties.getEnumCaption(de.symeda.sormas.api.utils.YesNoUnknown.NO));
+ trackField(field);
+ return field;
+ }
+
+ protected > RadioButtonGroup createEnumRadioGroup(String propertyId, String i18nPrefix, Class enumClass) {
+ RadioButtonGroup field = new RadioButtonGroup<>();
+ field.setId(propertyId);
+ field.setCaption(I18nProperties.getPrefixCaption(i18nPrefix, propertyId));
+ CssStyles.style(field, CssStyles.CAPTION_ON_TOP, ValoTheme.OPTIONGROUP_HORIZONTAL);
+ field.setItems(enumClass.getEnumConstants());
+ field.setItemCaptionGenerator(I18nProperties::getEnumCaption);
+ field.setWidth(100, Unit.PERCENTAGE);
+ trackField(field);
+ return field;
+ }
+
+ /**
+ * Creates a row layout with the given fields, adds it to this component, and tracks it.
+ * If only one field (or two with the second null), a spacer is added for alignment.
+ */
+ protected HorizontalLayout addRow(Component... fields) {
+ HorizontalLayout row = new HorizontalLayout();
+ row.setWidth(100, Unit.PERCENTAGE);
+ row.setSpacing(true);
+ for (Component field : fields) {
+ if (field != null) {
+ row.addComponent(field);
+ row.setExpandRatio(field, 1);
+ }
+ }
+ if (fields.length == 1 || (fields.length == 2 && fields[1] == null)) {
+ Label spacer = new Label();
+ spacer.setWidth(100, Unit.PERCENTAGE);
+ row.addComponent(spacer);
+ row.setExpandRatio(spacer, 1);
+ }
+ addComponent(row);
+ trackedRows.add(row);
+ return row;
+ }
+
+ /** Creates a row with custom expand ratios per field. */
+ protected HorizontalLayout addRow(float[] expandRatios, Component... fields) {
+ HorizontalLayout row = new HorizontalLayout();
+ row.setWidth(100, Unit.PERCENTAGE);
+ row.setSpacing(true);
+ for (int i = 0; i < fields.length; i++) {
+ if (fields[i] != null) {
+ row.addComponent(fields[i]);
+ row.setExpandRatio(fields[i], expandRatios[i]);
+ }
+ }
+ addComponent(row);
+ trackedRows.add(row);
+ return row;
+ }
+
+ /** Creates a 3-component row for field + detail + spacer toggle pattern. */
+ protected HorizontalLayout addToggleRow(Component main, Component detail, Label spacer) {
+ HorizontalLayout row = new HorizontalLayout();
+ row.setWidth(100, Unit.PERCENTAGE);
+ row.setSpacing(true);
+ row.addComponent(main);
+ row.setExpandRatio(main, 1);
+ row.addComponent(detail);
+ row.setExpandRatio(detail, 1);
+ row.addComponent(spacer);
+ row.setExpandRatio(spacer, 1);
+ addComponent(row);
+ trackedRows.add(row);
+ return row;
+ }
+
+ /** Creates a row with a leading spacer for right-alignment. */
+ protected HorizontalLayout addRowWithLeadingSpacer(Component field) {
+ HorizontalLayout row = new HorizontalLayout();
+ row.setWidth(100, Unit.PERCENTAGE);
+ row.setSpacing(true);
+ Label spacer = new Label();
+ spacer.setWidth(100, Unit.PERCENTAGE);
+ row.addComponent(spacer);
+ row.setExpandRatio(spacer, 1);
+ row.addComponent(field);
+ row.setExpandRatio(field, 1);
+ addComponent(row);
+ trackedRows.add(row);
+ return row;
+ }
+
+ /** Creates a single-field row without spacer (full width). */
+ protected HorizontalLayout addFullWidthRow(Component field) {
+ HorizontalLayout row = new HorizontalLayout();
+ row.setWidth(100, Unit.PERCENTAGE);
+ row.setSpacing(true);
+ row.addComponent(field);
+ row.setExpandRatio(field, 1);
+ addComponent(row);
+ trackedRows.add(row);
+ return row;
+ }
+
+ /**
+ * Updates a ComboBox's items to only those enum values visible for the given disease
+ * (based on {@link Diseases} annotations). Preserves the current selection if still valid.
+ */
+ protected static > void updateComboBoxByDisease(ComboBox comboBox, Class enumClass, Disease disease) {
+ E currentValue = comboBox.getValue();
+ List filtered = Diseases.DiseasesConfiguration.getVisibleValues(enumClass, disease);
+ filtered.sort(Comparator.comparing(Object::toString));
+ comboBox.setItems(filtered);
+ if (currentValue != null && filtered.contains(currentValue)) {
+ comboBox.setValue(currentValue);
+ }
+ }
+
+ /**
+ * Updates a ComboBox's items to only those enum values visible for the given disease
+ * AND applicable to the given pathogen test type (based on {@link Diseases} and
+ * {@link ApplicableToPathogenTests} annotations). Preserves the current selection if still valid.
+ */
+ protected static > void updateComboBoxByDiseaseAndTestType(
+ ComboBox comboBox,
+ Class enumClass,
+ Disease disease,
+ PathogenTestType testType) {
+
+ E currentValue = comboBox.getValue();
+ List filtered = Diseases.DiseasesConfiguration.getVisibleValues(enumClass, disease).stream().filter(value -> {
+ if (testType == null) {
+ return true;
+ }
+ try {
+ java.lang.reflect.Field enumField = enumClass.getField(value.name());
+ ApplicableToPathogenTests ann = enumField.getAnnotation(ApplicableToPathogenTests.class);
+ return ann == null || java.util.Arrays.asList(ann.value()).contains(testType);
+ } catch (NoSuchFieldException e) {
+ return true;
+ }
+ }).sorted(Comparator.comparing(Object::toString)).collect(Collectors.toList());
+ comboBox.setItems(filtered);
+ if (currentValue != null && filtered.contains(currentValue)) {
+ comboBox.setValue(currentValue);
+ }
+ }
+
+ protected Label createSpacer() {
+ Label spacer = new Label();
+ spacer.setWidth(100, Unit.PERCENTAGE);
+ return spacer;
+ }
+
+ /** Called when the parent form receives a new DTO value. */
+ public void setDto(T dto) {
+ binder.setBean(dto);
+ }
+
+ /** Validates this component's fields; throws {@link InvalidValueException} on failure. */
+ public void validate() {
+ validateBinder(binder.validate());
+ }
+
+ /**
+ * Applies annotation-based field visibility rules.
+ * Default implementation uses tracked fields; subclasses may override for manual control.
+ */
+ public void applyVisibility(FieldVisibilityCheckers checkers, Class> dtoClass) {
+ for (Component field : trackedFields) {
+ String id = field.getId();
+ if (id != null && !checkers.isVisible(dtoClass, id)) {
+ field.setVisible(false);
+ }
+ }
+ updateRowAndSelfVisibility();
+ }
+
+ /**
+ * Applies annotation-based field access/editability rules.
+ * Default implementation uses tracked fields; subclasses may override for manual control.
+ */
+ public void applyAccess(UiFieldAccessCheckers checkers, Class> dtoClass) {
+ for (Component field : trackedFields) {
+ String id = field.getId();
+ if (id != null && !checkers.isAccessible(dtoClass, id)) {
+ field.setEnabled(false);
+ field.addStyleName(CssStyles.INACCESSIBLE_FIELD);
+ }
+ }
+ }
+
+ /** Subclasses perform any additional cleanup beyond binder unbinding. */
+ protected void onCleanup() {
+ }
+
+ @Override
+ public void detach() {
+ for (Registration reg : registrations) {
+ reg.remove();
+ }
+ registrations.clear();
+ binder.removeBean();
+ onCleanup();
+ super.detach();
+ }
+
+ /**
+ * Updates visibility of tracked rows and this component itself.
+ * Hides rows where all fields are hidden, and hides this section entirely
+ * if no rows are visible.
+ */
+ protected void updateRowAndSelfVisibility() {
+ for (HorizontalLayout row : trackedRows) {
+ updateRowVisibility(row);
+ }
+
+ boolean anyVisible = hasVisibleContent();
+ for (HorizontalLayout row : trackedRows) {
+ if (row.isVisible()) {
+ anyVisible = true;
+ break;
+ }
+ }
+ setVisible(anyVisible);
+ }
+
+ /**
+ * Subclasses override to indicate additional visible content beyond tracked rows
+ * (e.g. drug susceptibility fields). Default returns false.
+ */
+ protected boolean hasVisibleContent() {
+ return false;
+ }
+
+ /**
+ * Validates a Binder and throws an {@link InvalidValueException} if validation fails.
+ * Creates a structured exception hierarchy so that field captions appear in the error notification.
+ */
+ public static void validateBinder(BinderValidationStatus status) {
+ if (status.hasErrors()) {
+ List fieldCauses = new ArrayList<>();
+
+ for (BindingValidationStatus> fieldStatus : status.getFieldValidationErrors()) {
+ String caption = null;
+ if (fieldStatus.getField() instanceof Component) {
+ caption = ((Component) fieldStatus.getField()).getCaption();
+ }
+ String errorMsg = fieldStatus.getMessage().orElse(caption != null ? caption : "");
+ fieldCauses.add(new InvalidValueException(caption != null ? caption : errorMsg));
+ }
+
+ for (ValidationResult beanError : status.getBeanValidationErrors()) {
+ fieldCauses.add(new InvalidValueException(beanError.getErrorMessage()));
+ }
+
+ if (fieldCauses.isEmpty()) {
+ String message = status.getValidationErrors().stream().map(ValidationResult::getErrorMessage).collect(Collectors.joining("; "));
+ throw new InvalidValueException(message);
+ }
+
+ String joinedCaptions = fieldCauses.stream().map(InvalidValueException::getMessage).collect(Collectors.joining(", "));
+ throw new InvalidValueException(joinedCaptions, fieldCauses.toArray(new InvalidValueException[0]));
+ }
+ }
+
+ /**
+ * Hides a layout row if none of its actual field children are visible (ignores spacer Labels).
+ */
+ public static void updateRowVisibility(AbstractOrderedLayout row) {
+ boolean anyChildVisible = false;
+ for (int i = 0; i < row.getComponentCount(); i++) {
+ Component child = row.getComponent(i);
+ if (child instanceof Label) {
+ continue;
+ }
+ if (child instanceof AbstractOrderedLayout) {
+ updateRowVisibility((AbstractOrderedLayout) child);
+ }
+ if (child.isVisible()) {
+ anyChildVisible = true;
+ }
+ }
+ row.setVisible(anyChildVisible);
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FormEventBus.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FormEventBus.java
new file mode 100644
index 00000000000..b58b6a0bed1
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/FormEventBus.java
@@ -0,0 +1,36 @@
+package de.symeda.sormas.ui.utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Lightweight, type-safe event bus scoped to a single form instance.
+ * Components fire and listen to typed events without direct references to each other.
+ */
+public class FormEventBus {
+
+ private final Map, List>> listeners = new HashMap<>();
+
+ public com.vaadin.shared.Registration on(Class eventType, Consumer handler) {
+ listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(handler);
+ return () -> {
+ List> list = listeners.get(eventType);
+ if (list != null) {
+ list.remove(handler);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ public void fire(E event) {
+ List> list = listeners.get(event.getClass());
+ if (list != null) {
+ for (Consumer> handler : new ArrayList<>(list)) {
+ ((Consumer) handler).accept(event);
+ }
+ }
+ }
+}
diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/StringToFloatNullableConverter.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/StringToFloatNullableConverter.java
new file mode 100644
index 00000000000..dcfb1331ee8
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/utils/StringToFloatNullableConverter.java
@@ -0,0 +1,62 @@
+package de.symeda.sormas.ui.utils;
+
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.util.Locale;
+import java.util.Optional;
+
+import com.vaadin.data.Converter;
+import com.vaadin.data.Result;
+import com.vaadin.data.ValueContext;
+
+import de.symeda.sormas.api.i18n.I18nProperties;
+import de.symeda.sormas.api.i18n.Validations;
+
+public class StringToFloatNullableConverter implements Converter {
+
+ private final String fieldCaption;
+
+ public StringToFloatNullableConverter(String fieldCaption) {
+ this.fieldCaption = fieldCaption;
+ }
+
+ @Override
+ public Result convertToModel(String value, ValueContext context) {
+ if (value == null || value.trim().isEmpty()) {
+ return Result.ok(null);
+ }
+ try {
+ NumberFormat numberFormat = NumberFormat.getInstance(context.getLocale().orElse(Locale.getDefault()));
+ Number number = numberFormat.parse(value.trim());
+ return Result.ok(number.floatValue());
+ } catch (ParseException e) {
+ return Result.error(I18nProperties.getValidationError(Validations.onlyNumbersAllowed, fieldCaption));
+ }
+ }
+
+ /**
+ * Parses a string to a float using the given locale's number format.
+ * Returns empty if the value is null, blank, or not a valid number.
+ */
+ public static Optional parseLocaleAware(String value, Locale locale) {
+ if (value == null || value.trim().isEmpty()) {
+ return Optional.empty();
+ }
+ try {
+ NumberFormat numberFormat = NumberFormat.getInstance(locale);
+ Number number = numberFormat.parse(value.trim());
+ return Optional.of(number.floatValue());
+ } catch (ParseException e) {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public String convertToPresentation(Float value, ValueContext context) {
+ if (value == null) {
+ return "";
+ }
+ NumberFormat numberFormat = NumberFormat.getInstance(context.getLocale().orElse(Locale.getDefault()));
+ return numberFormat.format(value);
+ }
+}