> diseaseConfig = new HashMap<>();
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 7cec6333d6b..b29a4fdebc3 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,180 +18,83 @@
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.List;
-import java.util.Map;
-import java.util.function.Consumer;
-import org.apache.commons.collections4.CollectionUtils;
-
-import com.vaadin.ui.CustomLayout;
+import com.vaadin.shared.Registration;
+import com.vaadin.ui.Component;
import com.vaadin.ui.Label;
+import com.vaadin.ui.VerticalLayout;
+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.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.TextArea;
-import com.vaadin.v7.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.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.Validations;
-import de.symeda.sormas.api.infrastructure.facility.FacilityDto;
-import de.symeda.sormas.api.infrastructure.facility.FacilityReferenceDto;
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.utils.fieldvisibility.FieldVisibilityCheckers;
-import de.symeda.sormas.ui.samples.diseasesection.DefaultDiseaseSectionLayout;
-import de.symeda.sormas.ui.samples.diseasesection.DiseaseSectionLayout;
+import de.symeda.sormas.ui.samples.components.DeletionComponent;
+import de.symeda.sormas.ui.samples.components.DiseaseSelectionComponent;
+import de.symeda.sormas.ui.samples.components.PrescriberComponent;
+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 static final String PATHOGEN_TEST_HEADING_LOC = "pathogenTestHeadingLoc";
-
- private static final String PRESCRIBER_HEADING_LOC = "prescriberHeading";
-
- private static final String DISEASE_SECTION_LOC = "diseaseSectionLoc";
-
- private static final String[] CT_CQ_FIELD_IDS = {
- 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
- 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.TESTED_PATHOGEN, PathogenTestDto.TESTED_PATHOGEN_DETAILS) +
- fluidRowLocs(PathogenTestDto.TYPING_ID, "") +
- fluidRowLocs(PathogenTestDto.TEST_DATE_TIME, PathogenTestDto.LAB) +
- fluidRowLocs("", PathogenTestDto.LAB_DETAILS) +
- fluidRowLocs(6,PathogenTestDto.TEST_RESULT, 4, PathogenTestDto.TEST_RESULT_VERIFIED, 2,PathogenTestDto.PRELIMINARY) +
- fluidRowLocs(PathogenTestDto.TESTED_DISEASE_VARIANT, PathogenTestDto.TESTED_DISEASE_VARIANT_DETAILS) +
- loc(DISEASE_SECTION_LOC) +
- 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.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
-
- // 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)));
- }
- });
+ 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 Label pathogenTestHeadingLabel;
-
- private ComboBox testTypeField;
- private ComboBox diseaseField;
- private ComboBox testResultField;
- private TextField testTypeTextField;
private Disease disease;
- private TextField typingIdField;
-
- // New instance fields promoted from addFields() locals
- private CheckBox viaLimsField;
- private ComboBox lab;
- private TextField labDetails;
- private TextField cqValueField;
- private NullableOptionGroup testResultVerifiedField;
- private CheckBox fourFoldIncrease;
- private Label prescriberHeadingLabel;
- private ComboBox diseaseVariantField;
- private TextField diseaseVariantDetailsField;
- private Consumer updateDiseaseVariantField;
-
- // Disease section swap support
- private DiseaseSectionLayout activeSection = new DefaultDiseaseSectionLayout();
- private CustomLayout diseaseSectionPanel;
private PathogenTestFormConfig formConfig;
+ private final FormEventBus eventBus = new FormEventBus();
+ private final List> formComponents = new ArrayList<>();
+ private final List eventRegistrations = new ArrayList<>();
+
+ private Label headingLabel;
+ private DiseaseSelectionComponent diseaseSelectionComponent;
+ private TestMethodComponent testMethodComponent;
+ private TestResultComponent testResultComponent;
+
+ private AbstractDiseaseSectionComponent activeSection;
+ private VerticalLayout diseaseSectionSlot;
+
public PathogenTestForm(
AbstractSampleForm sampleForm,
boolean create,
@@ -199,6 +102,7 @@ public PathogenTestForm(
boolean isPseudonymized,
boolean inJurisdiction,
Disease disease) {
+
this(create, caseSampleCount, isPseudonymized, inJurisdiction, disease);
this.sampleForm = sampleForm;
this.disease = disease;
@@ -209,7 +113,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;
@@ -220,7 +123,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();
@@ -229,477 +131,228 @@ 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 {
- cqValueField.setVisible(false);
- cqValueField.clear();
- }
- }
- }
-
- private Date getSampleDate() {
- if (sample != null) {
- return sample.getSampleDateTime();
- }
- if (sampleForm != null) {
- return (Date) sampleForm.getField(SampleDto.SAMPLE_DATE_TIME).getValue();
- }
- if (environmentSample != null) {
- return environmentSample.getSampleDateTime();
- }
- return null;
- }
-
- private SamplePurpose getSamplePurpose() {
- if (sample != null) {
- return sample.getSamplePurpose();
- }
- if (sampleForm != null) {
- return (SamplePurpose) sampleForm.getField(SampleDto.SAMPLE_PURPOSE).getValue();
- }
- return null;
- }
-
@Override
protected String createHtmlLayout() {
return HTML_LAYOUT;
}
@Override
- public void setHeading(String heading) {
- pathogenTestHeadingLabel.setValue(heading);
- }
+ protected void addFields() {
+ formConfig = PathogenTestFormConfig.fromCurrentConfig();
- @Override
- public void setValue(PathogenTestDto newFieldValue) throws ReadOnlyException, Converter.ConversionException {
- super.setValue(newFieldValue);
- testTypeField.setValue(newFieldValue.getTestType());
- testTypeTextField.setValue(newFieldValue.getTestTypeText());
- if (!testResultField.isReadOnly()) {
- testResultField.setValue(newFieldValue.getTestResult());
- }
- typingIdField.setValue(newFieldValue.getTypingId());
- ComboBox specieFieldDynamic = getField(PathogenTestDto.SPECIE);
- if (specieFieldDynamic != null) {
- specieFieldDynamic.setValue(newFieldValue.getSpecie());
- }
- markAsDirty();
- }
+ VerticalLayout container = new VerticalLayout();
+ container.setWidth(100, Unit.PERCENTAGE);
+ container.setMargin(false);
+ container.setSpacing(true);
+ getContent().addComponent(container, COMPONENT_CONTAINER_LOC);
- @Override
- protected void addFields() {
- addHeaderAndLayoutFields();
- addTestDateField();
- addLabFields();
- addDiseaseAndResultFields();
- addCtValueFields();
- addPrescriberFields();
- bindVisibilityRules();
- bindValueChangeListeners();
- finalizeForm();
- }
+ headingLabel = new Label();
+ headingLabel.addStyleName(H3);
+ container.addComponent(headingLabel);
- private void addHeaderAndLayoutFields() {
- formConfig = PathogenTestFormConfig.fromCurrentConfig();
+ TestIdentificationComponent identificationComponent = new TestIdentificationComponent(eventBus);
+ formComponents.add(identificationComponent);
+ container.addComponent(identificationComponent);
- pathogenTestHeadingLabel = new Label();
- pathogenTestHeadingLabel.addStyleName(H3);
- getContent().addComponent(pathogenTestHeadingLabel, PATHOGEN_TEST_HEADING_LOC);
-
- // Install the disease section panel — a nested CustomLayout whose template is swapped on disease change
- diseaseSectionPanel = new CustomLayout();
- diseaseSectionPanel.setTemplateContents(activeSection.getHtmlLayout());
- diseaseSectionPanel.setWidth(100, Unit.PERCENTAGE);
- getContent().addComponent(diseaseSectionPanel, DISEASE_SECTION_LOC);
-
- addDateField(PathogenTestDto.REPORT_DATE, DateField.class, 0);
- 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);
- testTypeTextField = addField(PathogenTestDto.TEST_TYPE_TEXT, TextField.class);
- }
+ diseaseSelectionComponent = new DiseaseSelectionComponent(eventBus, disease, create, environmentSample != null);
+ formComponents.add(diseaseSelectionComponent);
+ container.addComponent(diseaseSelectionComponent);
- private DateTimeField addTestDateField() {
- 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;
- }
+ testMethodComponent = new TestMethodComponent(eventBus, this::getSampleDate);
+ formComponents.add(testMethodComponent);
+ container.addComponent(testMethodComponent);
- 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()))));
+ diseaseSectionSlot = new VerticalLayout();
+ diseaseSectionSlot.setWidth(100, Unit.PERCENTAGE);
+ diseaseSectionSlot.setMargin(false);
+ diseaseSectionSlot.setSpacing(false);
+ container.addComponent(diseaseSectionSlot);
- });
- return testDateField;
- }
+ activeSection = DiseaseSectionFactory.forDisease(disease);
+ activeSection.initialize(getFieldGroup(), eventBus, formConfig, disease);
+ activeSection.setVisibilityCallback(visible -> diseaseSectionSlot.setVisible(visible));
+ diseaseSectionSlot.addComponent(activeSection);
+
+ testResultComponent = new TestResultComponent(eventBus, caseSampleCount, formConfig.isLuxembourg, disease);
+ formComponents.add(testResultComponent);
+ container.addComponent(testResultComponent);
+
+ PrescriberComponent prescriberComponent = new PrescriberComponent();
+ formComponents.add(prescriberComponent);
+ container.addComponent(prescriberComponent);
+
+ DeletionComponent deletionComponent = new DeletionComponent();
+ formComponents.add(deletionComponent);
+ container.addComponent(deletionComponent);
- private void addLabFields() {
- lab = addInfrastructureField(PathogenTestDto.LAB);
- lab.addItems(FacadeProvider.getFacilityFacade().getAllActiveLaboratories(true));
- labDetails = addField(PathogenTestDto.LAB_DETAILS, TextField.class);
- labDetails.setVisible(false);
- typingIdField = addField(PathogenTestDto.TYPING_ID, TextField.class);
- typingIdField.setVisible(false);
+ wireEvents();
+
+ finalizeForm();
}
- private void addDiseaseAndResultFields() {
- // Tested Disease or Tested Pathogen, depending on sample type
- diseaseField = addDiseaseField(PathogenTestDto.TESTED_DISEASE, true, create, false);
- addField(PathogenTestDto.TESTED_DISEASE_DETAILS, TextField.class);
- diseaseVariantField = addCustomizableEnumField(PathogenTestDto.TESTED_DISEASE_VARIANT);
- diseaseVariantField.setNullSelectionAllowed(true);
- diseaseVariantField.setVisible(false);
- 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));
- }
- 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);
+ 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
+ diseaseSelectionComponent.getDiseaseVariantField().addValueChangeListener(e -> {
+ DiseaseVariant variant = e.getValue();
+ if (variant != null) {
+ testResultComponent.getTestResultField().setValue(PathogenTestResultType.POSITIVE);
} else {
- testedPathogenDetailsField.clear();
- testedPathogenDetailsField.setVisible(false);
+ testResultComponent.getTestResultField().clear();
}
});
+ }
- if (environmentSample == null) {
- diseaseField.setVisible(true);
- diseaseField.setRequired(true);
+ private void finalizeForm() {
+ testMethodComponent.setLabRequired(SamplePurpose.INTERNAL.equals(getSamplePurpose()));
- testedPathogenField.setVisible(false);
- testedPathogenField.setRequired(false);
- } else {
- diseaseField.setVisible(false);
- diseaseField.setRequired(false);
+ initializeAccessAndAllowedAccesses();
+ initializeVisibilitiesAndAllowedVisibilities();
- testedPathogenField.setVisible(true);
- testedPathogenField.setRequired(true);
+ for (FormComponent comp : formComponents) {
+ if (fieldVisibilityCheckers != null) {
+ comp.applyVisibility(fieldVisibilityCheckers, PathogenTestDto.class);
+ }
+ if (fieldAccessCheckers != null) {
+ comp.applyAccess(fieldAccessCheckers, PathogenTestDto.class);
+ }
}
- testResultField = addField(PathogenTestDto.TEST_RESULT, ComboBox.class);
- testResultField.removeItem(PathogenTestResultType.NOT_DONE);
-
- if (!formConfig.isLuxembourg) {
- testResultField.removeItem(PathogenTestResultType.NOT_APPLICABLE);
+ if (fieldVisibilityCheckers != null) {
+ activeSection.applyVisibility(fieldVisibilityCheckers, PathogenTestDto.class);
}
- // Bind the initial disease section (default = no-op; swapped via swapDiseaseSection() on disease change)
- activeSection = DiseaseSectionLayout.forDisease(disease);
- diseaseSectionPanel.setTemplateContents(activeSection.getHtmlLayout());
- activeSection.bindFields(getFieldGroup(), diseaseSectionPanel, disease, formConfig);
- }
- private void addCtValueFields() {
- cqValueField = addField(FieldConfiguration.withConversionError(PathogenTestDto.CQ_VALUE, Validations.onlyNumbersAllowed));
- if (!formConfig.isLuxembourg) {
- cqValueField.setVisible(false);
+ if (fieldAccessCheckers != null) {
+ activeSection.applyAccess(fieldAccessCheckers, PathogenTestDto.class);
}
- 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, CT_CQ_FIELD_IDS);
}
- private void addPrescriberFields() {
- 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()));
-
- fourFoldIncrease = addField(PathogenTestDto.FOUR_FOLD_INCREASE_ANTIBODY_TITER, CheckBox.class);
- CssStyles.style(fourFoldIncrease, VSPACE_3, VSPACE_TOP_4);
- fourFoldIncrease.setVisible(false);
- fourFoldIncrease.setEnabled(false);
+ private void swapDiseaseSection(Disease newDisease) {
+ AbstractDiseaseSectionComponent newSection = DiseaseSectionFactory.forDisease(newDisease);
+ if (newSection.getClass() == activeSection.getClass()) {
+ return;
+ }
- addField(PathogenTestDto.TEST_RESULT_TEXT, TextArea.class).setRows(6);
+ activeSection.cleanup();
+ diseaseSectionSlot.removeAllComponents();
- 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())));
+ activeSection = newSection;
+ activeSection.initialize(getFieldGroup(), eventBus, formConfig, newDisease);
+ activeSection.setVisibilityCallback(visible -> diseaseSectionSlot.setVisible(visible));
+ diseaseSectionSlot.addComponent(activeSection);
- addFields(PathogenTestDto.PRESCRIBER_ADDRESS, PathogenTestDto.PRESCRIBER_POSTAL_CODE, PathogenTestDto.PRESCRIBER_CITY);
- ComboBox prescriberCountrField = addInfrastructureField(PathogenTestDto.PRESCRIBER_COUNTRY);
- FieldHelper.updateItems(prescriberCountrField, FacadeProvider.getCountryFacade().getAllActiveAsReference());
+ PathogenTestDto dto = getValue();
+ if (dto != null) {
+ activeSection.setDto(dto);
+ }
- addField(PathogenTestDto.DELETION_REASON);
- addField(PathogenTestDto.OTHER_DELETION_REASON, TextArea.class).setRows(3);
- setVisible(false, PathogenTestDto.DELETION_REASON, PathogenTestDto.OTHER_DELETION_REASON);
+ if (fieldVisibilityCheckers != null) {
+ activeSection.applyVisibility(fieldVisibilityCheckers, PathogenTestDto.class);
+ }
- prescriberHeadingLabel = new Label(I18nProperties.getCaption(Captions.PathogenTest_prescriber));
- prescriberHeadingLabel.addStyleName(H3);
- getContent().addComponent(prescriberHeadingLabel, PRESCRIBER_HEADING_LOC);
+ if (fieldAccessCheckers != null) {
+ activeSection.applyAccess(fieldAccessCheckers, PathogenTestDto.class);
+ }
}
- private void bindVisibilityRules() {
- 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);
-
- //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);
-
- updateDiseaseVariantField = d -> {
- List diseaseVariants = FacadeProvider.getCustomizableEnumFacade().getEnumValues(CustomizableEnumType.DISEASE_VARIANT, d);
- FieldHelper.updateItems(diseaseVariantField, diseaseVariants);
- diseaseVariantField
- .setVisible(d != null && isVisibleAllowed(PathogenTestDto.TESTED_DISEASE_VARIANT) && CollectionUtils.isNotEmpty(diseaseVariants));
- };
-
- updateDiseaseVariantField.accept((Disease) diseaseField.getValue());
+ private Date getSampleDate() {
+ if (sample != null) {
+ return sample.getSampleDateTime();
+ }
+ if (sampleForm != null) {
+ return (Date) sampleForm.getField(SampleDto.SAMPLE_DATE_TIME).getValue();
+ }
+ if (environmentSample != null) {
+ return environmentSample.getSampleDateTime();
+ }
+ return null;
}
- private void bindValueChangeListeners() {
- diseaseField.addValueChangeListener(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);
- swapDiseaseSection(latestDisease);
-
- FieldHelper.updateItems(
- testTypeField,
- Arrays.asList(PathogenTestType.values()),
- FieldVisibilityCheckers.withDisease(disease),
- PathogenTestType.class);
- });
- 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) {
- 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, CT_CQ_FIELD_IDS);
- } else {
- setVisibleClear(false, CT_CQ_FIELD_IDS);
- }
- } else {
- testResultField.clear();
- testResultField.setEnabled(true);
- setVisibleClear(false, CT_CQ_FIELD_IDS);
- }
-
- if (RESULT_FIELD_DECISION_MAP.containsKey(disease) && RESULT_FIELD_DECISION_MAP.get(disease).contains(testType)) {
- testResultField.setValue(PathogenTestResultType.POSITIVE);
- } else {
- testResultField.clear();
- }
-
- activeSection.onTestTypeChanged(
- testType,
- (Disease) diseaseField.getValue(),
- (com.vaadin.v7.ui.AbstractField) (Object) testResultField,
- formConfig);
- });
-
- 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);
- });
+ private SamplePurpose getSamplePurpose() {
+ if (sample != null) {
+ return sample.getSamplePurpose();
+ }
+ if (sampleForm != null) {
+ return (SamplePurpose) sampleForm.getField(SampleDto.SAMPLE_PURPOSE).getValue();
+ }
+ return null;
}
- private void finalizeForm() {
- if (SamplePurpose.INTERNAL.equals(getSamplePurpose())) { // this only works for already saved samples
- setRequired(true, PathogenTestDto.LAB);
+ @Override
+ public void preCommit(CommitEvent commitEvent) throws CommitException {
+ super.preCommit(commitEvent);
+
+ for (FormComponent comp : formComponents) {
+ comp.validate();
}
- setRequired(true, PathogenTestDto.TEST_TYPE, PathogenTestDto.TEST_RESULT);
- initializeAccessAndAllowedAccesses();
- initializeVisibilitiesAndAllowedVisibilities();
+ if (activeSection != null) {
+ activeSection.validate();
+ }
+ }
- // 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));
+ @Override
+ public void setHeading(String heading) {
+ headingLabel.setValue(heading);
}
- /** Replaces the active disease section with the one appropriate for the given disease. */
- private void swapDiseaseSection(Disease newDisease) {
- DiseaseSectionLayout newSection = DiseaseSectionLayout.forDisease(newDisease);
- if (newSection.getClass() == activeSection.getClass()) {
- return; // same section type, nothing to swap
- }
+ @Override
+ public void setValue(PathogenTestDto newFieldValue) throws ReadOnlyException, Converter.ConversionException {
+ super.setValue(newFieldValue);
- removeFromAllowedLists(activeSection.getFieldIds());
- activeSection.unbindFields(getFieldGroup(), diseaseSectionPanel);
- activeSection = newSection;
+ for (FormComponent comp : formComponents) {
+ comp.setDto(newFieldValue);
+ }
- // Vaadin's CustomLayout.setTemplateContents() only works before the component
- // is attached. Replace the entire panel so the new template renders correctly.
- CustomLayout newPanel = new CustomLayout();
- newPanel.setTemplateContents(newSection.getHtmlLayout());
- newPanel.setWidth(100, Unit.PERCENTAGE);
- getContent().addComponent(newPanel, DISEASE_SECTION_LOC);
- diseaseSectionPanel = newPanel;
+ if (activeSection != null) {
+ activeSection.setDto(newFieldValue);
+ }
- newSection.bindFields(getFieldGroup(), diseaseSectionPanel, newDisease, formConfig);
- rebuildSectionFieldAllowances(newSection.getFieldIds());
+ markAsDirty();
}
- @SuppressWarnings("unchecked")
- private void rebuildSectionFieldAllowances(Collection fieldIds) {
- for (String id : fieldIds) {
- com.vaadin.v7.ui.Field> f = getField(id);
- if (f != null) {
- addToVisibleAllowedFields(f);
- if (fieldAccessCheckers == null || fieldAccessCheckers.isAccessible(getType(), id)) {
- addToEditableAllowedFields(f);
- }
- }
+ @Override
+ public void detach() {
+ for (Registration reg : eventRegistrations) {
+ reg.remove();
}
+ eventRegistrations.clear();
+
+ super.detach();
}
- private void removeFromAllowedLists(Collection fieldIds) {
- for (String id : fieldIds) {
- com.vaadin.v7.ui.Field> f = getField(id);
- if (f != null) {
- removeFromVisibleAllowedFields(f);
- removeFromEditableAllowedFields(f);
+ @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/CtCqValueComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/CtCqValueComponent.java
new file mode 100644
index 00000000000..6f7c045f455
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/CtCqValueComponent.java
@@ -0,0 +1,123 @@
+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) {
+ 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..5e23598690a
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DeletionComponent.java
@@ -0,0 +1,56 @@
+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);
+ }
+}
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..a738d34a773
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/DiseaseSelectionComponent.java
@@ -0,0 +1,192 @@
+package de.symeda.sormas.ui.samples.components;
+
+import java.util.List;
+
+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.environment.environmentsample.Pathogen;
+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.ui.samples.events.DiseaseChangedEvent;
+import de.symeda.sormas.ui.utils.FormComponent;
+import de.symeda.sormas.ui.utils.FormEventBus;
+
+/**
+ * Disease/pathogen selection with variant support.
+ * Vaadin 8 components with own Binder, self-managed visibility.
+ */
+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;
+ private ComboBox diseaseVariantField;
+ private TextField diseaseVariantDetailsField;
+ private Label variantDetailsSpacer;
+
+ private HorizontalLayout variantRow;
+
+ 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);
+
+ // Disease variant
+ 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);
+
+ if (DiseaseHelper.SUBTYPE_ALLOWED_DISEASES.contains(initialDisease)) {
+ diseaseVariantField.setCaption(I18nProperties.getCaption(Captions.PathogenTest_rsv_testedDiseaseVariant));
+ diseaseVariantDetailsField.setCaption(I18nProperties.getCaption(Captions.PathogenTest_rsv_testedDiseaseVariantDetails));
+ }
+
+ variantDetailsSpacer = createSpacer();
+ variantRow = addToggleRow(diseaseVariantField, diseaseVariantDetailsField, variantDetailsSpacer);
+
+ // Environment vs human sample visibility
+ if (isEnvironmentSample) {
+ diseaseField.setVisible(false);
+ diseaseDetailsField.setVisible(false);
+ testedPathogenField.setVisible(true);
+ } else {
+ diseaseField.setVisible(true);
+ testedPathogenField.setVisible(false);
+ }
+
+ updateDiseaseVariants(initialDisease);
+ }
+
+ 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);
+ binder.forField(diseaseVariantField).bind(PathogenTestDto::getTestedDiseaseVariant, PathogenTestDto::setTestedDiseaseVariant);
+ binder.forField(diseaseVariantDetailsField)
+ .bind(PathogenTestDto::getTestedDiseaseVariantDetails, PathogenTestDto::setTestedDiseaseVariantDetails);
+ }
+
+ private void wireEvents() {
+ // Disease change: update variants, 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();
+ }
+
+ updateDiseaseVariants(newDisease);
+ 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();
+ }
+ }));
+
+ // Disease 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);
+ }));
+ }
+
+ private void updateDiseaseVariants(Disease disease) {
+ List variants = FacadeProvider.getCustomizableEnumFacade().getEnumValues(CustomizableEnumType.DISEASE_VARIANT, disease);
+ diseaseVariantField.setItems(variants);
+ diseaseVariantField.setVisible(disease != null && CollectionUtils.isNotEmpty(variants));
+ updateRowVisibility(variantRow);
+ }
+
+ public ComboBox getDiseaseField() {
+ return diseaseField;
+ }
+
+ public ComboBox getDiseaseVariantField() {
+ return diseaseVariantField;
+ }
+
+ public TextField getDiseaseVariantDetailsField() {
+ return diseaseVariantDetailsField;
+ }
+}
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..8d046e7e63d
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/PrescriberComponent.java
@@ -0,0 +1,90 @@
+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.
+ */
+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/TestIdentificationComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestIdentificationComponent.java
new file mode 100644
index 00000000000..ff59cde046f
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestIdentificationComponent.java
@@ -0,0 +1,94 @@
+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.
+ * Vaadin 8 components with own Binder.
+ */
+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..d757063755e
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestMethodComponent.java
@@ -0,0 +1,269 @@
+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.Arrays;
+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.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.
+ * Vaadin 8 components with own Binder, self-managed visibility.
+ */
+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 TextField typingIdField;
+ private DateField testDateField;
+ private ComboBox testTimeField;
+ private ComboBox labField;
+ private TextField labDetailsField;
+
+ private PathogenTestDto currentDto;
+
+ private HorizontalLayout typingIdRow;
+ private HorizontalLayout labDetailsRow;
+
+ public TestMethodComponent(FormEventBus eventBus, Supplier sampleDateSupplier) {
+ super(PathogenTestDto.class);
+ this.eventBus = eventBus;
+ this.sampleDateSupplier = sampleDateSupplier;
+ buildLayout();
+ bindFields();
+ wireEvents();
+ }
+
+ private void buildLayout() {
+ // Test type
+ testTypeField = createComboBox(PathogenTestDto.TEST_TYPE, PathogenTestDto.I18N_PREFIX);
+ testTypeField.setItems(Arrays.asList(PathogenTestType.values()));
+ testTypeField.setItemCaptionGenerator(PathogenTestType::toString);
+
+ testTypeTextField = createTextField(PathogenTestDto.TEST_TYPE_TEXT, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR);
+ testTypeTextField.setVisible(false);
+
+ testTypeTextSpacer = createSpacer();
+ addToggleRow(testTypeField, testTypeTextField, testTypeTextSpacer);
+
+ // 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(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);
+
+ 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
+ track(eventBus.on(DiseaseChangedEvent.class, event -> updateTestTypeItems(event.getDisease())));
+ }
+
+ 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);
+ }
+
+ 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..5d43761f5c6
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/components/TestResultComponent.java
@@ -0,0 +1,213 @@
+package de.symeda.sormas.ui.samples.components;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.RadioButtonGroup;
+import com.vaadin.ui.TextArea;
+
+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.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.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, preliminary, 4-fold increase, CQ/CT values, result text.
+ * Vaadin 8 components with own Binder, self-managed visibility.
+ */
+public class TestResultComponent extends FormComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private final FormEventBus eventBus;
+ private final int caseSampleCount;
+ private final boolean isLuxembourg;
+
+ private final CtCqValueComponent ctCqValueComponent;
+
+ private ComboBox testResultField;
+ private RadioButtonGroup testResultVerifiedField;
+ private RadioButtonGroup preliminaryField;
+ private CheckBox fourFoldIncrease;
+ private TextArea resultText;
+
+ private Disease currentDisease;
+ private PathogenTestType currentTestType;
+
+ public TestResultComponent(FormEventBus eventBus, int caseSampleCount, boolean isLuxembourg, Disease initialDisease) {
+ super(PathogenTestDto.class);
+ this.eventBus = eventBus;
+ this.caseSampleCount = caseSampleCount;
+ this.isLuxembourg = isLuxembourg;
+ this.currentDisease = initialDisease;
+ this.ctCqValueComponent = new CtCqValueComponent(isLuxembourg);
+ buildLayout();
+ bindFields();
+ wireEvents();
+ }
+
+ private void buildLayout() {
+ // Test result
+ 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);
+
+ // Test result verified (Yes/No)
+ testResultVerifiedField = createBooleanRadioGroup(PathogenTestDto.TEST_RESULT_VERIFIED, PathogenTestDto.I18N_PREFIX);
+
+ // Preliminary
+ 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);
+
+ // Four-fold increase
+ 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);
+
+ // CT/CQ values (delegated to CtCqValueComponent)
+ addComponent(ctCqValueComponent);
+
+ // Result text
+ resultText = createTextArea(PathogenTestDto.TEST_RESULT_TEXT, PathogenTestDto.I18N_PREFIX);
+ resultText.setRows(6);
+ addFullWidthRow(resultText);
+ }
+
+ 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);
+ binder.forField(fourFoldIncrease).bind(PathogenTestDto::isFourFoldIncreaseAntibodyTiter, PathogenTestDto::setFourFoldIncreaseAntibodyTiter);
+ binder.forField(resultText).bind(PathogenTestDto::getTestResultText, PathogenTestDto::setTestResultText);
+ }
+
+ private void wireEvents() {
+ // Test result changed -> fire event + update CQ visibility
+ track(testResultField.addValueChangeListener(e -> {
+ eventBus.fire(new TestResultChangedEvent(e.getValue()));
+ ctCqValueComponent.updateCqVisibility(currentDisease, currentTestType, e.getValue());
+ }));
+
+ // Listen for test type changes
+ track(eventBus.on(TestTypeChangedEvent.class, event -> {
+ PathogenTestType testType = event.getTestType();
+ currentTestType = testType;
+
+ if (testType != null) {
+ // Four fold increase visibility
+ 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);
+ }
+
+ // CT fields visibility
+ ctCqValueComponent.updateCtVisibility(currentDisease, testType);
+ } else {
+ testResultField.clear();
+ testResultField.setEnabled(true);
+ ctCqValueComponent.updateCtVisibility(currentDisease, null);
+ }
+
+ ctCqValueComponent.updateCqVisibility(currentDisease, currentTestType, testResultField.getValue());
+ }));
+
+ // 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();
+ }
+ ctCqValueComponent.updateCqVisibility(currentDisease, currentTestType, testResultField.getValue());
+ }));
+
+ // Listen for disease changes
+ track(eventBus.on(DiseaseChangedEvent.class, event -> {
+ Disease oldDisease = currentDisease;
+ currentDisease = event.getDisease();
+ if (currentDisease != oldDisease && currentTestType != null) {
+ testResultField.clear();
+ }
+ ctCqValueComponent.updateCtVisibility(currentDisease, currentTestType);
+ ctCqValueComponent.updateCqVisibility(currentDisease, currentTestType, testResultField.getValue());
+ }));
+
+ // 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);
+ }
+ }
+
+ @Override
+ public void setDto(PathogenTestDto dto) {
+ super.setDto(dto);
+ ctCqValueComponent.setDto(dto);
+ }
+
+ @Override
+ public void validate() {
+ super.validate();
+ ctCqValueComponent.validate();
+ }
+
+ @Override
+ public void applyVisibility(FieldVisibilityCheckers checkers, Class> dtoClass) {
+ super.applyVisibility(checkers, dtoClass);
+ ctCqValueComponent.applyVisibility(checkers, dtoClass);
+ }
+
+ @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/diseasesection/AbstractDiseaseSectionComponent.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/AbstractDiseaseSectionComponent.java
new file mode 100644
index 00000000000..6bbe016584b
--- /dev/null
+++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/diseasesection/AbstractDiseaseSectionComponent.java
@@ -0,0 +1,129 @@
+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