diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java index 2bae8d48046..0a8fa7dbd4b 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java @@ -928,13 +928,11 @@ public interface Captions { String caseLabResultsDateCollected = "caseLabResultsDateCollected"; String caseLabResultsDrugSusceptibilityHeading = "caseLabResultsDrugSusceptibilityHeading"; String caseLabResultsMethod = "caseLabResultsMethod"; - String caseLabResultsMicValue = "caseLabResultsMicValue"; String caseLabResultsSample = "caseLabResultsSample"; String caseLabResultsSamplesHeading = "caseLabResultsSamplesHeading"; - String caseLabResultsSurveillanceInterpretation = "caseLabResultsSurveillanceInterpretation"; String caseLabResultsTestsHeading = "caseLabResultsTestsHeading"; String caseLabResultsTestsPerformed = "caseLabResultsTestsPerformed"; - String caseLabResultsZoneDiameter = "caseLabResultsZoneDiameter"; + String caseLabResultsValue = "caseLabResultsValue"; String caseLinkToSamples = "caseLinkToSamples"; String caseMergeDuplicates = "caseMergeDuplicates"; String caseMinusDays = "caseMinusDays"; @@ -2424,7 +2422,6 @@ public interface Captions { String PathogenTest_prescriberPhysicianCode = "PathogenTest.prescriberPhysicianCode"; String PathogenTest_prescriberPostalCode = "PathogenTest.prescriberPostalCode"; String PathogenTest_quantitativeBoolean = "PathogenTest.quantitativeBoolean"; - String PathogenTest_quantitativeText = "PathogenTest.quantitativeText"; String PathogenTest_quantitativeUnit = "PathogenTest.quantitativeUnit"; String PathogenTest_quantitativeValue = "PathogenTest.quantitativeValue"; String PathogenTest_reportDate = "PathogenTest.reportDate"; @@ -3304,6 +3301,7 @@ public interface Captions { String Symptoms_uproariousness = "Symptoms.uproariousness"; String Symptoms_urinaryRetention = "Symptoms.urinaryRetention"; String Symptoms_vomiting = "Symptoms.vomiting"; + String Symptoms_wateryDiarrhea = "Symptoms.wateryDiarrhea"; String Symptoms_weakness = "Symptoms.weakness"; String Symptoms_weight = "Symptoms.weight"; String Symptoms_weightLoss = "Symptoms.weightLoss"; diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenSpecie.java b/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenSpecie.java index 754e0f61007..344e5ca629e 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenSpecie.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenSpecie.java @@ -57,6 +57,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -70,6 +73,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -83,6 +89,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -96,6 +105,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -109,6 +121,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -122,6 +137,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -135,6 +153,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -148,6 +169,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -161,6 +185,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -174,35 +201,40 @@ public enum PathogenSpecie { @ApplicableToPathogenTests(value = { PathogenTestType.SEROGROUPING, PathogenTestType.SEROTYPING, - PathogenTestType.BACTERIAL_CULTURE }) + PathogenTestType.BACTERIAL_CULTURE, + PathogenTestType.CULTURE }) BOYDII, @Diseases(value = { Disease.SHIGELLOSIS }) @ApplicableToPathogenTests(value = { PathogenTestType.SEROGROUPING, PathogenTestType.SEROTYPING, - PathogenTestType.BACTERIAL_CULTURE }) + PathogenTestType.BACTERIAL_CULTURE, + PathogenTestType.CULTURE }) DYSENTERIAE, @Diseases(value = { Disease.SHIGELLOSIS }) @ApplicableToPathogenTests(value = { PathogenTestType.SEROGROUPING, PathogenTestType.SEROTYPING, - PathogenTestType.BACTERIAL_CULTURE }) + PathogenTestType.BACTERIAL_CULTURE, + PathogenTestType.CULTURE }) FLEXNERI, @Diseases(value = { Disease.SHIGELLOSIS }) @ApplicableToPathogenTests(value = { PathogenTestType.SEROGROUPING, PathogenTestType.SEROTYPING, - PathogenTestType.BACTERIAL_CULTURE }) + PathogenTestType.BACTERIAL_CULTURE, + PathogenTestType.CULTURE }) SONNEI, @Diseases(value = { Disease.SHIGELLOSIS }) @ApplicableToPathogenTests(value = { PathogenTestType.SEROGROUPING, PathogenTestType.SEROTYPING, - PathogenTestType.BACTERIAL_CULTURE }) + PathogenTestType.BACTERIAL_CULTURE, + PathogenTestType.CULTURE }) SHIGELLA_SPP, @Diseases({ Disease.MALARIA, @@ -211,6 +243,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -218,6 +253,7 @@ public enum PathogenSpecie { PathogenTestType.OTHER_MOLECULAR_ASSAY, PathogenTestType.OTHER_SEROLOGICAL_TEST, PathogenTestType.BACTERIAL_CULTURE, + PathogenTestType.CULTURE, PathogenTestType.SEROGROUPING, PathogenTestType.SEROTYPING }) OTHER, @@ -231,6 +267,9 @@ public enum PathogenSpecie { PathogenTestType.THIN_BLOOD_SMEAR, PathogenTestType.RAPID_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, @@ -238,6 +277,7 @@ public enum PathogenSpecie { PathogenTestType.OTHER_MOLECULAR_ASSAY, PathogenTestType.OTHER_SEROLOGICAL_TEST, PathogenTestType.BACTERIAL_CULTURE, + PathogenTestType.CULTURE, PathogenTestType.SEROGROUPING, PathogenTestType.SEROTYPING }) UNKNOWN, diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenTestDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenTestDto.java index 75020137f16..bd109edc54d 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenTestDto.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenTestDto.java @@ -131,7 +131,6 @@ public class PathogenTestDto extends PseudonymizableDto { public static final String RESULT_DETAILS = "resultDetails"; public static final String QUANTITATIVE_VALUE = "quantitativeValue"; public static final String QUANTITATIVE_UNIT = "quantitativeUnit"; - public static final String QUANTITATIVE_TEXT = "quantitativeText"; public static final String QUANTITATIVE_BOOLEAN = "quantitativeBoolean"; public static final String SMEAR_GRADE = "smearGrade"; public static final String WESTERN_BLOT_INTERPRETATION = "westernBlotInterpretation"; @@ -346,9 +345,6 @@ public class PathogenTestDto extends PseudonymizableDto { private Float quantitativeValue; @Size(max = FieldConstraints.CHARACTER_LIMIT_SMALL, message = Validations.textTooLong) private String quantitativeUnit; - @Size(max = FieldConstraints.CHARACTER_LIMIT_DEFAULT, message = Validations.textTooLong) - @SensitiveData - private String quantitativeText; private YesNoUnknown quantitativeBoolean; private SmearGrade smearGrade; private WesternBlotInterpretation westernBlotInterpretation; @@ -1014,14 +1010,6 @@ public void setQuantitativeUnit(String quantitativeUnit) { this.quantitativeUnit = quantitativeUnit; } - public String getQuantitativeText() { - return quantitativeText; - } - - public void setQuantitativeText(String quantitativeText) { - this.quantitativeText = quantitativeText; - } - public YesNoUnknown getQuantitativeBoolean() { return quantitativeBoolean; } diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenTestType.java b/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenTestType.java index fb6fa5b1080..802383b6fdb 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenTestType.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/sample/PathogenTestType.java @@ -47,60 +47,52 @@ public enum PathogenTestType { @NotSelectableForNewTests ANTIGEN_DETECTION, - @Diseases(value = { - Disease.GIARDIASIS, - Disease.DENGUE, - Disease.MALARIA }, hide = true) + // Merged Culture entry (bacterial + fungal) — supersedes BACTERIAL_CULTURE / FUNGAL_CULTURE for new + // tests (#13951). The qualitative result is the Pos/Neg outcome; TEXT carries the organism identifier + // and NUMERIC carries the CFU/mL count. The legacy constants remain @NotSelectableForNewTests so + // historic records and case-classification rules keep working. @PathogenTestCategoryRel(PathogenTestCategory.CULTURE_AND_ISOLATION) - @NotSelectableForNewTests + @ResultValueTypeRel({ + ResultValueType.QUALITATIVE, + ResultValueType.TEXT, + ResultValueType.NUMERIC }) CULTURE, - @Diseases(value = { - Disease.RESPIRATORY_SYNCYTIAL_VIRUS, - Disease.GIARDIASIS, - Disease.CRYPTOSPORIDIOSIS, - Disease.MALARIA }, hide = true) + // Merged Isolation entry (bacterial + viral) — supersedes VIRAL_ISOLATION for new tests (#13951). + // Result is qualitative (Pos/Neg/Indet/Pending). Legacy VIRAL_ISOLATION stays @NotSelectableForNewTests + // so historic records still render and case-classification rules (EVD/Lassa/Cholera/…) keep firing. @PathogenTestCategoryRel(PathogenTestCategory.CULTURE_AND_ISOLATION) - @NotSelectableForNewTests + @ResultValueTypeRel(ResultValueType.QUALITATIVE) ISOLATION, + // Resurrected ELISA Ig-class variants (#13951): split the legacy single ENZYME_LINKED_IMMUNOSORBENT_ASSAY + // entry into per-Ig-class methods so the form can offer the IgM/IgG/IgA distinction that serology + // workflows already need. Result is qualitative (Pos/Neg) + numeric (titre). No @Diseases — visible + // for every disease per #13951. The legacy ENZYME_LINKED_IMMUNOSORBENT_ASSAY is now + // @NotSelectableForNewTests so historic records still render and case-classification rules + // referencing IGM_/IGG_SERUM_ANTIBODY continue to fire (they bind to the same enum constants). @Diseases(value = { - Disease.RESPIRATORY_SYNCYTIAL_VIRUS, - Disease.INVASIVE_MENINGOCOCCAL_INFECTION, - Disease.INVASIVE_PNEUMOCOCCAL_INFECTION, - Disease.GIARDIASIS, - Disease.CRYPTOSPORIDIOSIS, - Disease.MALARIA, - Disease.SHIGELLOSIS }, hide = true) + Disease.SALMONELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.SEROLOGICAL_TESTS) - @NotSelectableForNewTests + @ResultValueTypeRel({ + ResultValueType.QUALITATIVE, + ResultValueType.NUMERIC }) IGM_SERUM_ANTIBODY, @Diseases(value = { - Disease.RESPIRATORY_SYNCYTIAL_VIRUS, - Disease.INVASIVE_MENINGOCOCCAL_INFECTION, - Disease.INVASIVE_PNEUMOCOCCAL_INFECTION, - Disease.GIARDIASIS, - Disease.CRYPTOSPORIDIOSIS, - Disease.MALARIA, - Disease.SHIGELLOSIS }, hide = true) + Disease.SALMONELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.SEROLOGICAL_TESTS) - @NotSelectableForNewTests + @ResultValueTypeRel({ + ResultValueType.QUALITATIVE, + ResultValueType.NUMERIC }) IGG_SERUM_ANTIBODY, @Diseases(value = { - Disease.RESPIRATORY_SYNCYTIAL_VIRUS, - Disease.INVASIVE_MENINGOCOCCAL_INFECTION, - Disease.INVASIVE_PNEUMOCOCCAL_INFECTION, - Disease.MEASLES, - Disease.GIARDIASIS, - Disease.CRYPTOSPORIDIOSIS, - Disease.DENGUE, - Disease.MALARIA, - Disease.SALMONELLOSIS, - Disease.SHIGELLOSIS }, hide = true) + Disease.SALMONELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.SEROLOGICAL_TESTS) - @NotSelectableForNewTests + @ResultValueTypeRel({ + ResultValueType.QUALITATIVE, + ResultValueType.NUMERIC }) IGA_SERUM_ANTIBODY, @Diseases(value = { @@ -193,8 +185,6 @@ public enum PathogenTestType { // Molecular Assays // ---------------------------------------------------------------------------------------------- - @Diseases(value = { - Disease.SALMONELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.MOLECULAR_ASSAYS) @RevealsTestTypeText @ResultValueTypeRel({ @@ -380,6 +370,8 @@ public enum PathogenTestType { // Serological Tests // ---------------------------------------------------------------------------------------------- + // Superseded by IGM_/IGG_/IGA_SERUM_ANTIBODY for new tests (#13951). Kept here so historic records + // still render and so case-classification logic (which binds to this constant) keeps working. @Diseases(value = { Disease.RESPIRATORY_SYNCYTIAL_VIRUS, Disease.MALARIA }) @@ -387,9 +379,11 @@ public enum PathogenTestType { @ResultValueTypeRel({ ResultValueType.QUALITATIVE, ResultValueType.NUMERIC }) + @NotSelectableForNewTests ENZYME_LINKED_IMMUNOSORBENT_ASSAY, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.SEROLOGICAL_TESTS) @ResultValueTypeRel({ @@ -431,6 +425,7 @@ public enum PathogenTestType { DIRECT_FLUORESCENT_ANTIBODY, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.SEROLOGICAL_TESTS) @ResultValueTypeRel(ResultValueType.QUALITATIVE) @@ -493,22 +488,28 @@ public enum PathogenTestType { // Culture & Isolation // ---------------------------------------------------------------------------------------------- + // Superseded by the merged CULTURE entry for new tests (#13951). Kept for historic records. @PathogenTestCategoryRel(PathogenTestCategory.CULTURE_AND_ISOLATION) @ResultValueTypeRel({ ResultValueType.TEXT, ResultValueType.NUMERIC }) + @NotSelectableForNewTests BACTERIAL_CULTURE, + // Superseded by the merged ISOLATION entry for new tests (#13951). Kept for historic records. @Diseases(value = { Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.CULTURE_AND_ISOLATION) @ResultValueTypeRel(ResultValueType.QUALITATIVE) + @NotSelectableForNewTests VIRAL_ISOLATION, + // Superseded by the merged CULTURE entry for new tests (#13951). Kept for historic records. @Diseases(value = { Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.CULTURE_AND_ISOLATION) @ResultValueTypeRel(ResultValueType.TEXT) + @NotSelectableForNewTests FUNGAL_CULTURE, @Diseases(value = { @@ -521,6 +522,14 @@ public enum PathogenTestType { // Microscopy & Staining // ---------------------------------------------------------------------------------------------- + // Direct microscopy (#13951): qualitative-only entry (Pos/Neg/Indeterminate/Pending) for visual + // detection of a pathogen without staining or fluorescence. Sibling of MICROSCOPY (legacy generic). + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) + @PathogenTestCategoryRel(PathogenTestCategory.MICROSCOPY_AND_STAINING) + @ResultValueTypeRel(ResultValueType.QUALITATIVE) + DIRECT_MICROSCOPY, + @Diseases(value = { Disease.CORONAVIRUS, Disease.RESPIRATORY_SYNCYTIAL_VIRUS, @@ -536,18 +545,21 @@ public enum PathogenTestType { GRAM_STAIN, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.MICROSCOPY_AND_STAINING) @ResultValueTypeRel(ResultValueType.SMEAR_GRADE) ACID_FAST_STAIN, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.MICROSCOPY_AND_STAINING) @ResultValueTypeRel(ResultValueType.QUALITATIVE) DARK_FIELD_MICROSCOPY, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.MICROSCOPY_AND_STAINING) @ResultValueTypeRel({ @@ -571,6 +583,7 @@ public enum PathogenTestType { HISTOPATHOLOGY, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.MICROSCOPY_AND_STAINING) @ResultValueTypeRel({ @@ -579,18 +592,21 @@ public enum PathogenTestType { FISH, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.MICROSCOPY_AND_STAINING) @ResultValueTypeRel(ResultValueType.QUALITATIVE) IMMUNOHISTOCHEMISTRY, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.MICROSCOPY_AND_STAINING) @ResultValueTypeRel(ResultValueType.QUALITATIVE) ELECTRON_MICROSCOPY, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.MICROSCOPY_AND_STAINING) @ResultValueTypeRel({ @@ -626,9 +642,14 @@ public enum PathogenTestType { Disease.INVASIVE_PNEUMOCOCCAL_INFECTION, Disease.SHIGELLOSIS }) @PathogenTestCategoryRel(PathogenTestCategory.ANTIMICROBIAL_SUSCEPTIBILITY_TESTING) + // AST has no result value type of its own — its result is the drug-susceptibility grid, not a + // Positive/Negative/numeric/text value. The empty set hides the Test result selector and all quantitative + // result fields; the result is kept as Not applicable. + @ResultValueTypeRel({}) ANTIBIOTIC_SUSCEPTIBILITY, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @PathogenTestCategoryRel(PathogenTestCategory.ANTIMICROBIAL_SUSCEPTIBILITY_TESTING) @ResultValueTypeRel(ResultValueType.BOOLEAN) @@ -655,6 +676,7 @@ public enum PathogenTestType { @PathogenTestCategoryRel(PathogenTestCategory.FUNCTIONAL_IMMUNE_ASSAYS) @ResultValueTypeRel(ResultValueType.NUMERIC) @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) FLOW_CYTOMETRY, @@ -739,23 +761,49 @@ public static PathogenTestCategory getCategory(PathogenTestType testType) { } /** - * @return the {@link ResultValueType}(s) this method produces, declared via - * {@link ResultValueTypeRel}; drives which result fields the pathogen-test form shows. - * A method without the annotation (e.g. legacy/hidden values and {@code OTHER}) is treated - * as {@link ResultValueType#QUALITATIVE} only, preserving the long-standing behaviour. + * Cached, reflection-free map of {@code PathogenTestType -> Set}. Built once at class + * initialization from each constant's {@link ResultValueTypeRel} annotation. The returned sets are + * unmodifiable so callers cannot accidentally mutate the shared instance. */ - public static java.util.Set getResultValueTypes(PathogenTestType testType) { - if (testType != null) { + private static final java.util.Map> RESULT_VALUE_TYPES_BY_METHOD; + + /** Default for {@code null} and unannotated test types (preserves long-standing behaviour). */ + private static final java.util.Set DEFAULT_RESULT_VALUE_TYPES = + java.util.Collections.unmodifiableSet(java.util.EnumSet.of(ResultValueType.QUALITATIVE)); + + static { + java.util.EnumMap> map = new java.util.EnumMap<>(PathogenTestType.class); + for (PathogenTestType type : values()) { + java.util.Set valueTypes = DEFAULT_RESULT_VALUE_TYPES; try { - ResultValueTypeRel annotation = PathogenTestType.class.getField(testType.name()).getAnnotation(ResultValueTypeRel.class); + ResultValueTypeRel annotation = PathogenTestType.class.getField(type.name()).getAnnotation(ResultValueTypeRel.class); if (annotation != null) { - return java.util.EnumSet.copyOf(java.util.Arrays.asList(annotation.value())); + // An explicit empty set (e.g. Antibiotic Susceptibility, whose result is the drug-susceptibility + // grid) means the method has no result value type of its own. + java.util.EnumSet set = java.util.EnumSet.noneOf(ResultValueType.class); + java.util.Collections.addAll(set, annotation.value()); + valueTypes = java.util.Collections.unmodifiableSet(set); } } catch (NoSuchFieldException e) { // fall through to the default } + map.put(type, valueTypes); + } + RESULT_VALUE_TYPES_BY_METHOD = java.util.Collections.unmodifiableMap(map); + } + + /** + * @return the {@link ResultValueType}(s) this method produces, declared via + * {@link ResultValueTypeRel}; drives which result fields the pathogen-test form shows. + * A method without the annotation (e.g. legacy/hidden values and {@code OTHER}) is treated + * as {@link ResultValueType#QUALITATIVE} only, preserving the long-standing behaviour. The + * returned set is unmodifiable. + */ + public static java.util.Set getResultValueTypes(PathogenTestType testType) { + if (testType == null) { + return DEFAULT_RESULT_VALUE_TYPES; } - return java.util.EnumSet.of(ResultValueType.QUALITATIVE); + return RESULT_VALUE_TYPES_BY_METHOD.getOrDefault(testType, DEFAULT_RESULT_VALUE_TYPES); } /** @@ -774,4 +822,25 @@ public static boolean isSelectableForNewTests(PathogenTestType testType) { return true; } } + + /** + * Single source of truth for the disease + method combinations that show the Cq value input (the existing + * {@code CtCqValueComponent}). Used by both that component and by {@code TestResultComponent} to suppress + * the generic numeric value/unit fields when the Cq input applies. Both call sites must use this method so + * the rule cannot drift. + * + *

The Cq input applies for {@code PCR_RT_PCR}, {@code CQ_VALUE_DETECTION}, or {@code Q_PCR} on Malaria + * — except for Tuberculosis, which historically does not offer a Cq value. + * + * @param disease the tested disease (may be {@code null}) + * @param testType the test method (may be {@code null}) + */ + public static boolean cqInputApplies(Disease disease, PathogenTestType testType) { + if (disease == Disease.TUBERCULOSIS) { + return false; + } + return testType == PCR_RT_PCR + || testType == CQ_VALUE_DETECTION + || (disease == Disease.MALARIA && testType == Q_PCR); + } } diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/sample/ResultValueTypeRel.java b/sormas-api/src/main/java/de/symeda/sormas/api/sample/ResultValueTypeRel.java index d5b0aa8058f..f05a7ccd2fb 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/sample/ResultValueTypeRel.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/sample/ResultValueTypeRel.java @@ -24,6 +24,13 @@ * Declares which {@link ResultValueType}(s) a {@link PathogenTestType} method produces, driving the * result fields shown on the pathogen-test form. Read at runtime by * {@link PathogenTestType#getResultValueTypes(PathogenTestType)}. + * + *

An explicit empty array ({@code @ResultValueTypeRel({})}) means the method has no result + * value type of its own — the qualitative selector and every quantitative field are hidden, and the + * stored result is coerced to {@link PathogenTestResultType#NOT_APPLICABLE}. Use this only when the + * method's real result is captured by a dedicated component elsewhere (e.g. Antibiotic Susceptibility's + * drug-susceptibility grid). It is not a placeholder for "to be filled in later" — leave the + * annotation off entirely if you want the method to fall back to the default qualitative behaviour. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/sample/SampleMaterial.java b/sormas-api/src/main/java/de/symeda/sormas/api/sample/SampleMaterial.java index 2be6cbf09d6..f0060028c13 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/sample/SampleMaterial.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/sample/SampleMaterial.java @@ -46,6 +46,7 @@ public enum SampleMaterial { Disease.INVASIVE_PNEUMOCOCCAL_INFECTION, Disease.GIARDIASIS, Disease.CRYPTOSPORIDIOSIS, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) DRY_BLOOD, @@ -54,6 +55,7 @@ public enum SampleMaterial { Disease.INVASIVE_PNEUMOCOCCAL_INFECTION, Disease.GIARDIASIS, Disease.CRYPTOSPORIDIOSIS, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) SERA, @@ -111,6 +113,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) NP_SWAB, @@ -145,6 +148,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) CRUST, @@ -156,6 +160,7 @@ public enum SampleMaterial { Disease.GIARDIASIS, Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) TISSUE, @@ -216,6 +221,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) @@ -223,7 +229,9 @@ public enum SampleMaterial { @Diseases({ Disease.GIARDIASIS, - Disease.CRYPTOSPORIDIOSIS }) + Disease.CRYPTOSPORIDIOSIS, + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) BIOPSY, @@ -236,6 +244,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) SPUTUM, @@ -248,6 +257,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) @@ -261,6 +271,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) BRONCHOALVEOLAR_LAVAGE, @@ -274,6 +285,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) @@ -288,6 +300,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) @@ -302,6 +315,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) @@ -314,6 +328,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) @@ -327,6 +342,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) PLEURAL_FLUID, @@ -343,6 +359,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) OROPHARYNGEAL_SWAB, @@ -353,6 +370,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) @@ -370,7 +388,8 @@ public enum SampleMaterial { Disease.GIARDIASIS, Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, - Disease.DENGUE }, hide = true) + Disease.DENGUE, + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) PERITONEAL_FLUID, @@ -382,6 +401,7 @@ public enum SampleMaterial { Disease.CRYPTOSPORIDIOSIS, Disease.MALARIA, Disease.DENGUE, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) SYNOVIAL_FLUID, @@ -389,23 +409,29 @@ public enum SampleMaterial { Disease.RESPIRATORY_SYNCYTIAL_VIRUS, Disease.GIARDIASIS, Disease.CRYPTOSPORIDIOSIS, + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) EDTA_WHOLE_BLOOD, @Diseases(value = { - Disease.CRYPTOSPORIDIOSIS }) + Disease.CRYPTOSPORIDIOSIS, + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) INTESTINAL_FLUID, @Diseases(value = { - Disease.GIARDIASIS }) + Disease.GIARDIASIS, + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) DUODENUM_FLUID, @Diseases({ - Disease.SALMONELLOSIS }) + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) ASPIRATE, @Diseases({ @@ -414,36 +440,44 @@ public enum SampleMaterial { BONE_AND_JOINT, @Diseases({ - Disease.SALMONELLOSIS }) + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) CATHETER_EXIT_SITE, @Diseases({ - Disease.SALMONELLOSIS }) + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) EYE, @Diseases({ - Disease.SALMONELLOSIS }) + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) GASTRIC_FLUID, @Diseases({ - Disease.SALMONELLOSIS }) + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) GENITAL_SWAB, @Diseases({ - Disease.SALMONELLOSIS }) + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) LOWER_RESPIRATORY_TRACT, @Diseases({ - Disease.SALMONELLOSIS, - Disease.SHIGELLOSIS }) + Disease.SHIGELLOSIS, + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) PUS, @Diseases({ - Disease.SALMONELLOSIS }) + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) SEMEN, @Diseases({ - Disease.SALMONELLOSIS }) + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) SKIN, @Diseases({ @@ -452,14 +486,18 @@ public enum SampleMaterial { SOFT_TISSUE, @Diseases({ - Disease.SALMONELLOSIS }) + Disease.TUBERCULOSIS, + Disease.LATENT_TUBERCULOSIS }) WOUND, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) ABSCESS_SWAB, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) BONE, @@ -467,42 +505,61 @@ public enum SampleMaterial { Disease.SHIGELLOSIS }, hide = true) BONE_MARROW, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) CONJUNCTIVAL_SWAB, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) MIDDLE_EAR_FLUID, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) PLASMA, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) SWAB_UNSPECIFIED, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) TEARS, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) CORD_BLOOD, @Diseases(value = { + Disease.SALMONELLOSIS, Disease.SHIGELLOSIS }, hide = true) LUNG_TISSUE, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) PLACENTA, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) @HideForCountries(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) ULCER_SWAB, + @Diseases(value = { + Disease.SALMONELLOSIS }, hide = true) UNKNOWN, @Deprecated diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/symptoms/SymptomsDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/symptoms/SymptomsDto.java index d6d364edd2c..dadabc3c1f3 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/symptoms/SymptomsDto.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/symptoms/SymptomsDto.java @@ -280,6 +280,7 @@ public class SymptomsDto extends PseudonymizableDto { public static final String UNILATERAL_CATARACTS = "unilateralCataracts"; public static final String UPROARIOUSNESS = "uproariousness"; public static final String VOMITING = "vomiting"; + public static final String WATERY_DIARRHEA = "wateryDiarrhea"; public static final String WHEEZING = "wheezing"; public static final String WHOOP_SOUND = "whoopSound"; public static final String NOCTURNAL_COUGH = "nocturnalCough"; @@ -804,8 +805,7 @@ public static SymptomsDto build() { @SymptomGrouping(SymptomGroup.OTHER) private SymptomState eyesBleeding; - @Diseases({ - SALMONELLOSIS }) + @Diseases({}) @HideForCountriesExcept(countries = { CountryHelper.COUNTRY_CODE_LUXEMBOURG }) @SymptomGrouping(SymptomGroup.OTHER) @@ -1473,6 +1473,7 @@ public static SymptomsDto build() { PERTUSSIS, GIARDIASIS, CRYPTOSPORIDIOSIS, + SALMONELLOSIS, SHIGELLOSIS }) @HideForCountries @Outbreaks @@ -1539,6 +1540,7 @@ public static SymptomsDto build() { INVASIVE_PNEUMOCOCCAL_INFECTION, FHA, PERTUSSIS, + SALMONELLOSIS, SHIGELLOSIS }) @HideForCountries @Size(max = FieldConstraints.CHARACTER_LIMIT_DEFAULT, message = Validations.textTooLong) @@ -3200,6 +3202,16 @@ public static SymptomsDto build() { SHIGELLOSIS }) private SymptomState bloodyDiarrhea; + @Diseases({ + CHOLERA, + GIARDIASIS, + CRYPTOSPORIDIOSIS, + SALMONELLOSIS, + SHIGELLOSIS }) + @Outbreaks + @SymptomGrouping(SymptomGroup.GASTROINTESTINAL) + private SymptomState wateryDiarrhea; + @Order(0) public Float getTemperature() { return temperature; @@ -5409,4 +5421,13 @@ public void setBloodyDiarrhea(SymptomState bloodyDiarrhea) { this.bloodyDiarrhea = bloodyDiarrhea; } + @Order(370) + public SymptomState getWateryDiarrhea() { + return wateryDiarrhea; + } + + public void setWateryDiarrhea(SymptomState wateryDiarrhea) { + this.wateryDiarrhea = wateryDiarrhea; + } + } diff --git a/sormas-api/src/main/resources/captions.properties b/sormas-api/src/main/resources/captions.properties index c0f56544f38..f79a88d1aef 100644 --- a/sormas-api/src/main/resources/captions.properties +++ b/sormas-api/src/main/resources/captions.properties @@ -367,10 +367,8 @@ caseLabResultsDateCollected=Date collected caseLabResultsTestsPerformed=Tests performed caseLabResultsAntibiotic=Antibiotic caseLabResultsMethod=Method -caseLabResultsMicValue=MIC value (mg/l) -caseLabResultsZoneDiameter=Zone (mm) +caseLabResultsValue=Value caseLabResultsClinicalInterpretation=Clinical interpretation -caseLabResultsSurveillanceInterpretation=Surveillance interpretation caseLabResultsComments=Comments caseCloneCaseWithNewDisease=Generate new case for caseContacts=Contacts @@ -2012,7 +2010,6 @@ PathogenTest.retestRequested=Retest requested PathogenTest.resultDetails=Result details PathogenTest.quantitativeValue=Value PathogenTest.quantitativeUnit=Unit -PathogenTest.quantitativeText=Value (text) PathogenTest.quantitativeBoolean=Detected PathogenTest.smearGrade=Smear grade PathogenTest.westernBlotInterpretation=Interpretation @@ -3144,6 +3141,7 @@ Symptoms.eyeIrritation=Eye irritation Symptoms.tenesmus=Tenesmus Symptoms.haemolyticUremicSyndrome=Haemolytic uremic syndrome Symptoms.bloodyDiarrhea=Bloody diarrhea +Symptoms.wateryDiarrhea=Watery diarrhoea titleComplications=Complications titleNoComplications=No complications diff --git a/sormas-api/src/main/resources/enum.properties b/sormas-api/src/main/resources/enum.properties index 69254948dc7..097aa6e9f66 100644 --- a/sormas-api/src/main/resources/enum.properties +++ b/sormas-api/src/main/resources/enum.properties @@ -1237,11 +1237,12 @@ PathogenTestType.DENGUE_FEVER_ANTIBODIES = Dengue fever neutralizing antibodies PathogenTestType.DENGUE_FEVER_IGM = Dengue fever IgM serum antibody PathogenTestType.DNA_MICROARRAY = DNA Microarray PathogenTestType.HISTOPATHOLOGY = Histopathology -PathogenTestType.IGG_SERUM_ANTIBODY = IgG serum antibody -PathogenTestType.IGM_SERUM_ANTIBODY = IgM serum antibody -PathogenTestType.IGA_SERUM_ANTIBODY = IgA serum antibody +PathogenTestType.IGG_SERUM_ANTIBODY = ELISA IgG +PathogenTestType.IGM_SERUM_ANTIBODY = ELISA IgM +PathogenTestType.IGA_SERUM_ANTIBODY = ELISA IgA PathogenTestType.ISOLATION = Isolation PathogenTestType.MICROSCOPY = Microscopy +PathogenTestType.DIRECT_MICROSCOPY = Direct microscopy PathogenTestType.NEUTRALIZING_ANTIBODIES = Neutralizing antibodies PathogenTestType.OTHER = Other PathogenTestType.PCR_RT_PCR = PCR / RT-PCR diff --git a/sormas-api/src/test/java/de/symeda/sormas/api/sample/PathogenTestTypeTest.java b/sormas-api/src/test/java/de/symeda/sormas/api/sample/PathogenTestTypeTest.java index 4b06c5d0aa3..42a8d7f7b3e 100644 --- a/sormas-api/src/test/java/de/symeda/sormas/api/sample/PathogenTestTypeTest.java +++ b/sormas-api/src/test/java/de/symeda/sormas/api/sample/PathogenTestTypeTest.java @@ -31,16 +31,19 @@ public class PathogenTestTypeTest { private static final Set EXPECTED_LEGACY = EnumSet.of( PathogenTestType.ANTIBODY_DETECTION, PathogenTestType.ANTIGEN_DETECTION, - PathogenTestType.CULTURE, - PathogenTestType.ISOLATION, - PathogenTestType.IGM_SERUM_ANTIBODY, - PathogenTestType.IGG_SERUM_ANTIBODY, - PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.INCUBATION_TIME, PathogenTestType.MICROSCOPY, PathogenTestType.LATEX_AGGLUTINATION, PathogenTestType.CQ_VALUE_DETECTION, PathogenTestType.SEQUENCING, + // Superseded by the per-Ig-class ELISA split (#13951); kept so historic records still render and + // case-classification rules referencing this constant keep working. + PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, + // Superseded by the merged CULTURE entry (#13951); kept so historic records still render. + PathogenTestType.BACTERIAL_CULTURE, + PathogenTestType.FUNGAL_CULTURE, + // Superseded by the merged ISOLATION entry (#13951); kept so historic records still render. + PathogenTestType.VIRAL_ISOLATION, // "Other " placeholders: superseded but kept hidden so existing records still load PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, PathogenTestType.OTHER_MOLECULAR_ASSAY, @@ -88,7 +91,12 @@ public void newMethodsAreSelectable() { PathogenTestType.RAPID_ANTIBODY_TEST, PathogenTestType.LATERAL_FLOW_ASSAY, PathogenTestType.RDT, - PathogenTestType.BACTERIAL_CULTURE, + PathogenTestType.CULTURE, + PathogenTestType.ISOLATION, + PathogenTestType.DIRECT_MICROSCOPY, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.MALDI_TOF, PathogenTestType.ACID_FAST_STAIN, PathogenTestType.GENOTYPIC_RESISTANCE_TEST, @@ -113,8 +121,11 @@ public void allSevenCategoriesAreRepresentedBySelectableMethods() { public void categoryMatchesAcrossKnownAnchors() { assertThat(PathogenTestType.getCategory(PathogenTestType.PCR_RT_PCR), is(PathogenTestCategory.MOLECULAR_ASSAYS)); assertThat(PathogenTestType.getCategory(PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY), is(PathogenTestCategory.SEROLOGICAL_TESTS)); + assertThat(PathogenTestType.getCategory(PathogenTestType.IGG_SERUM_ANTIBODY), is(PathogenTestCategory.SEROLOGICAL_TESTS)); assertThat(PathogenTestType.getCategory(PathogenTestType.RDT), is(PathogenTestCategory.ANTIGEN_DETECTION)); - assertThat(PathogenTestType.getCategory(PathogenTestType.BACTERIAL_CULTURE), is(PathogenTestCategory.CULTURE_AND_ISOLATION)); + assertThat(PathogenTestType.getCategory(PathogenTestType.CULTURE), is(PathogenTestCategory.CULTURE_AND_ISOLATION)); + assertThat(PathogenTestType.getCategory(PathogenTestType.ISOLATION), is(PathogenTestCategory.CULTURE_AND_ISOLATION)); + assertThat(PathogenTestType.getCategory(PathogenTestType.DIRECT_MICROSCOPY), is(PathogenTestCategory.MICROSCOPY_AND_STAINING)); assertThat(PathogenTestType.getCategory(PathogenTestType.ACID_FAST_STAIN), is(PathogenTestCategory.MICROSCOPY_AND_STAINING)); assertThat( PathogenTestType.getCategory(PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY), @@ -124,10 +135,16 @@ public void categoryMatchesAcrossKnownAnchors() { @Test public void everyMethodResolvesToAtLeastOneResultValueType() { - // getResultValueTypes never returns null/empty: an unannotated value defaults to QUALITATIVE. + // getResultValueTypes never returns null/empty (an unannotated value defaults to QUALITATIVE), with one + // documented exception: Antibiotic Susceptibility Testing has no result value type of its own (its result + // is the drug-susceptibility grid), so it is explicitly annotated with an empty set. for (PathogenTestType type : PathogenTestType.values()) { Set valueTypes = PathogenTestType.getResultValueTypes(type); - assertFalse(valueTypes.isEmpty(), "no result value type for " + type.name()); + if (type == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY) { + assertTrue(valueTypes.isEmpty(), "ANTIBIOTIC_SUSCEPTIBILITY must have no result value type"); + } else { + assertFalse(valueTypes.isEmpty(), "no result value type for " + type.name()); + } } assertThat(PathogenTestType.getResultValueTypes(null), is(EnumSet.of(ResultValueType.QUALITATIVE))); // A value with no annotation (e.g. OTHER) falls back to qualitative. @@ -176,6 +193,8 @@ public void everyMethodMapsToItsExpectedResultValueTypes() { PathogenTestType.QUELLUNG_REACTION, PathogenTestType.RDT, PathogenTestType.VIRAL_ISOLATION, + PathogenTestType.ISOLATION, + PathogenTestType.DIRECT_MICROSCOPY, PathogenTestType.DARK_FIELD_MICROSCOPY, PathogenTestType.IMMUNOHISTOCHEMISTRY, PathogenTestType.ELECTRON_MICROSCOPY, @@ -213,12 +232,18 @@ public void everyMethodMapsToItsExpectedResultValueTypes() { PathogenTestType.DIGITAL_PCR, PathogenTestType.NAAT, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.GIEMSA_STAIN }) { expected.put(t, EnumSet.of(ResultValueType.QUALITATIVE, ResultValueType.NUMERIC)); } expected.put(PathogenTestType.LINE_PROBE_ASSAY, EnumSet.of(ResultValueType.BOOLEAN)); expected.put(PathogenTestType.GENOTYPIC_RESISTANCE_TEST, EnumSet.of(ResultValueType.BOOLEAN)); + // Antibiotic Susceptibility Testing has no result value type of its own (its result is the + // drug-susceptibility grid), so it maps to an empty set. + expected.put(PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY, EnumSet.noneOf(ResultValueType.class)); expected.put(PathogenTestType.FISH, EnumSet.of(ResultValueType.QUALITATIVE, ResultValueType.TEXT)); expected.put(PathogenTestType.THICK_BLOOD_SMEAR, EnumSet.of(ResultValueType.QUALITATIVE, ResultValueType.TEXT)); expected.put(PathogenTestType.WESTERN_BLOT, EnumSet.of(ResultValueType.TEXT, ResultValueType.WESTERN_BLOT)); @@ -226,6 +251,8 @@ public void everyMethodMapsToItsExpectedResultValueTypes() { expected.put(PathogenTestType.ACID_FAST_STAIN, EnumSet.of(ResultValueType.SMEAR_GRADE)); expected .put(PathogenTestType.QUANTITATIVE_BUFFY_COAT, EnumSet.of(ResultValueType.QUALITATIVE, ResultValueType.TEXT, ResultValueType.NUMERIC)); + // Merged Culture entry: organism text + CFU count + Pos/Neg qualitative result. + expected.put(PathogenTestType.CULTURE, EnumSet.of(ResultValueType.QUALITATIVE, ResultValueType.TEXT, ResultValueType.NUMERIC)); expected.put(PathogenTestType.THIN_BLOOD_SMEAR, EnumSet.of(ResultValueType.NUMERIC, ResultValueType.TEXT)); expected.put(PathogenTestType.FLOW_CYTOMETRY, EnumSet.of(ResultValueType.NUMERIC)); diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/sample/PathogenTest.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/sample/PathogenTest.java index 1f9a213fdfe..219ec45f04c 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/sample/PathogenTest.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/sample/PathogenTest.java @@ -192,7 +192,6 @@ public class PathogenTest extends DeletableAdo { private String resultDetails; private Float quantitativeValue; private String quantitativeUnit; - private String quantitativeText; private YesNoUnknown quantitativeBoolean; private SmearGrade smearGrade; private WesternBlotInterpretation westernBlotInterpretation; @@ -847,14 +846,6 @@ public void setQuantitativeUnit(String quantitativeUnit) { this.quantitativeUnit = quantitativeUnit; } - @Column(columnDefinition = "text") - public String getQuantitativeText() { - return quantitativeText; - } - - public void setQuantitativeText(String quantitativeText) { - this.quantitativeText = quantitativeText; - } @Enumerated(EnumType.STRING) public YesNoUnknown getQuantitativeBoolean() { diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/sample/PathogenTestFacadeEjb.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/sample/PathogenTestFacadeEjb.java index cf758a18757..0630476e67f 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/sample/PathogenTestFacadeEjb.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/sample/PathogenTestFacadeEjb.java @@ -351,7 +351,6 @@ public static PathogenTestDto toDto(PathogenTest source) { target.setResultDetails(source.getResultDetails()); target.setQuantitativeValue(source.getQuantitativeValue()); target.setQuantitativeUnit(source.getQuantitativeUnit()); - target.setQuantitativeText(source.getQuantitativeText()); target.setQuantitativeBoolean(source.getQuantitativeBoolean()); target.setSmearGrade(source.getSmearGrade()); target.setWesternBlotInterpretation(source.getWesternBlotInterpretation()); @@ -682,7 +681,6 @@ public PathogenTest fillOrBuildEntity(@NotNull PathogenTestDto source, PathogenT target.setResultDetails(source.getResultDetails()); target.setQuantitativeValue(source.getQuantitativeValue()); target.setQuantitativeUnit(source.getQuantitativeUnit()); - target.setQuantitativeText(source.getQuantitativeText()); target.setQuantitativeBoolean(source.getQuantitativeBoolean()); target.setSmearGrade(source.getSmearGrade()); target.setWesternBlotInterpretation(source.getWesternBlotInterpretation()); @@ -694,9 +692,6 @@ public PathogenTest fillOrBuildEntity(@NotNull PathogenTestDto source, PathogenT target.setQuantitativeValue(null); target.setQuantitativeUnit(null); } - if (!resultValueTypes.contains(ResultValueType.TEXT)) { - target.setQuantitativeText(null); - } if (!resultValueTypes.contains(ResultValueType.BOOLEAN)) { target.setQuantitativeBoolean(null); } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/symptoms/Symptoms.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/symptoms/Symptoms.java index 191b1eb8ed7..18e90e0f6a5 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/symptoms/Symptoms.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/symptoms/Symptoms.java @@ -322,6 +322,7 @@ public class Symptoms extends AbstractDomainObject { private SymptomState tenesmus; private SymptomState haemolyticUremicSyndrome; private SymptomState bloodyDiarrhea; + private SymptomState wateryDiarrhea; // when adding new fields make sure to extend toHumanString @@ -2578,4 +2579,13 @@ public void setBloodyDiarrhea(SymptomState bloodyDiarrhea) { this.bloodyDiarrhea = bloodyDiarrhea; } + @Enumerated(EnumType.STRING) + public SymptomState getWateryDiarrhea() { + return wateryDiarrhea; + } + + public void setWateryDiarrhea(SymptomState wateryDiarrhea) { + this.wateryDiarrhea = wateryDiarrhea; + } + } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/symptoms/SymptomsFacadeEjb.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/symptoms/SymptomsFacadeEjb.java index a02477b9628..abf8a6a1506 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/symptoms/SymptomsFacadeEjb.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/symptoms/SymptomsFacadeEjb.java @@ -287,6 +287,7 @@ public Symptoms fillOrBuildEntity(SymptomsDto source, Symptoms target, boolean c target.setTenesmus(source.getTenesmus()); target.setHaemolyticUremicSyndrome(source.getHaemolyticUremicSyndrome()); target.setBloodyDiarrhea(source.getBloodyDiarrhea()); + target.setWateryDiarrhea(source.getWateryDiarrhea()); return target; } @@ -556,6 +557,7 @@ public static SymptomsDto toSymptomsDto(Symptoms symptoms) { target.setTenesmus(source.getTenesmus()); target.setHaemolyticUremicSyndrome(source.getHaemolyticUremicSyndrome()); target.setBloodyDiarrhea(source.getBloodyDiarrhea()); + target.setWateryDiarrhea(source.getWateryDiarrhea()); return target; } diff --git a/sormas-backend/src/main/resources/sql/sormas_schema.sql b/sormas-backend/src/main/resources/sql/sormas_schema.sql index 72689e4bb3c..498d33a724a 100644 --- a/sormas-backend/src/main/resources/sql/sormas_schema.sql +++ b/sormas-backend/src/main/resources/sql/sormas_schema.sql @@ -16349,4 +16349,62 @@ BEGIN END $$; INSERT INTO schema_version (version_number, comment) VALUES (639, '#13965 - Shigellosis Lab messages'); +-- Drop the generic quantitative free-text result column: typing methods use their disease-section fields and +-- everything else falls back to "Test result details" (#13948). Before dropping, copy any existing value into +-- testresulttext (appending to it when it already holds a value) so no historical free-text result is lost. +-- Both pathogentest and pathogentest_history are migrated. +-- +-- versioning_trigger on pathogentest mirrors every UPDATE into pathogentest_history that would inject a +-- phantom audit row (a copy of the OLD pathogentest, still carrying its quantitativetext) for every preserved +-- value, in addition to the rows users actually edited. Disable the trigger for the migration window so the +-- history table only keeps the rows users wrote, and re-enable when done. +-- +-- testresulttext is varchar(4096) while quantitativetext is unbounded text: LEFT(..., 4096) truncates the +-- concatenated value so a long quantitativetext (or near-full testresulttext) cannot overflow the column and +-- abort the migration. Practical impact is minimal because the source feature is unreleased, but the guard +-- keeps the migration safe for staging/test instances that seeded the field. +ALTER TABLE pathogentest DISABLE TRIGGER versioning_trigger; +DO $$ +DECLARE + truncated_count integer; +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'pathogentest' AND column_name = 'quantitativetext') THEN + SELECT count(*) INTO truncated_count FROM pathogentest + WHERE quantitativetext IS NOT NULL AND quantitativetext <> '' + AND length(CASE WHEN testresulttext IS NULL OR testresulttext = '' THEN quantitativetext ELSE testresulttext || E'\n' || quantitativetext END) > 4096; + IF truncated_count > 0 THEN + RAISE NOTICE 'Migration 640: % pathogentest row(s) had their preserved quantitativetext truncated to fit varchar(4096).', truncated_count; + END IF; + UPDATE pathogentest + SET testresulttext = LEFT(CASE + WHEN testresulttext IS NULL OR testresulttext = '' THEN quantitativetext + ELSE testresulttext || E'\n' || quantitativetext + END, 4096) + WHERE quantitativetext IS NOT NULL AND quantitativetext <> ''; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'pathogentest_history' AND column_name = 'quantitativetext') THEN + SELECT count(*) INTO truncated_count FROM pathogentest_history + WHERE quantitativetext IS NOT NULL AND quantitativetext <> '' + AND length(CASE WHEN testresulttext IS NULL OR testresulttext = '' THEN quantitativetext ELSE testresulttext || E'\n' || quantitativetext END) > 4096; + IF truncated_count > 0 THEN + RAISE NOTICE 'Migration 640: % pathogentest_history row(s) had their preserved quantitativetext truncated to fit varchar(4096).', truncated_count; + END IF; + UPDATE pathogentest_history + SET testresulttext = LEFT(CASE + WHEN testresulttext IS NULL OR testresulttext = '' THEN quantitativetext + ELSE testresulttext || E'\n' || quantitativetext + END, 4096) + WHERE quantitativetext IS NOT NULL AND quantitativetext <> ''; + END IF; +END $$; +ALTER TABLE pathogentest DROP COLUMN IF EXISTS quantitativetext; +ALTER TABLE pathogentest_history DROP COLUMN IF EXISTS quantitativetext; +ALTER TABLE pathogentest ENABLE TRIGGER versioning_trigger; +INSERT INTO schema_version (version_number, comment) VALUES (640, 'Remove generic quantitative free-text pathogen test result column, preserving values into testresulttext issue #13952 (epic #13948)'); + +-- 2026-06-19 Add Salmonellosis-specific watery diarrhoea symptom (testing salmo.xlsx) +ALTER TABLE symptoms ADD COLUMN IF NOT EXISTS waterydiarrhea varchar(255); +ALTER TABLE symptoms_history ADD COLUMN IF NOT EXISTS waterydiarrhea varchar(255); +INSERT INTO schema_version (version_number, comment) VALUES (641, 'Add Salmonellosis watery diarrhoea symptom issue #13918'); + -- *** Insert new sql commands BEFORE this line. Remember to always consider _history tables. *** diff --git a/sormas-backend/src/test/java/de/symeda/sormas/backend/sample/PathogenTestFacadeEjbTest.java b/sormas-backend/src/test/java/de/symeda/sormas/backend/sample/PathogenTestFacadeEjbTest.java index 160fa18c779..1dc1d2165ec 100644 --- a/sormas-backend/src/test/java/de/symeda/sormas/backend/sample/PathogenTestFacadeEjbTest.java +++ b/sormas-backend/src/test/java/de/symeda/sormas/backend/sample/PathogenTestFacadeEjbTest.java @@ -89,33 +89,30 @@ public void testQuantitativeResultFieldsRoundTrip() { final CaseDataDto caze = creator.createCase(user.toReference(), person.toReference(), rdcf); final SampleDto sample = creator.createSample(caze.toReference(), user.toReference(), rdcf.facility); - // QUANTITATIVE_BUFFY_COAT produces QUALITATIVE + NUMERIC + TEXT, so those fields must round-trip. + // QUANTITATIVE_BUFFY_COAT produces QUALITATIVE + NUMERIC, so those fields must round-trip. final PathogenTestDto test = creator.buildPathogenTestDto(rdcf, user, sample, caze.getDisease(), testDateTime); test.setTestType(PathogenTestType.QUANTITATIVE_BUFFY_COAT); test.setTestResult(PathogenTestResultType.POSITIVE); test.setQuantitativeValue(24.0f); test.setQuantitativeUnit("copies/mL"); - test.setQuantitativeText("Trophozoites seen"); final PathogenTestDto reloaded = getPathogenTestFacade().savePathogenTest(test); assertEquals(24.0f, reloaded.getQuantitativeValue()); assertEquals("copies/mL", reloaded.getQuantitativeUnit()); - assertEquals("Trophozoites seen", reloaded.getQuantitativeText()); - - // Partially-populated test (only a text result, as for Sanger/WGS) round-trips with the rest null. - final PathogenTestDto textOnly = creator.buildPathogenTestDto(rdcf, user, sample, caze.getDisease(), testDateTime); - textOnly.setTestType(PathogenTestType.SANGER_SEQUENCING); - textOnly.setTestResult(PathogenTestResultType.NOT_APPLICABLE); - textOnly.setQuantitativeText("Measles genotype B3"); - - final PathogenTestDto reloadedTextOnly = getPathogenTestFacade().savePathogenTest(textOnly); - assertEquals("Measles genotype B3", reloadedTextOnly.getQuantitativeText()); - assertNull(reloadedTextOnly.getQuantitativeValue()); - assertNull(reloadedTextOnly.getQuantitativeUnit()); - assertNull(reloadedTextOnly.getQuantitativeBoolean()); - assertNull(reloadedTextOnly.getSmearGrade()); - assertNull(reloadedTextOnly.getWesternBlotInterpretation()); + + // Partially-populated test (only a smear grade, as for Acid-Fast Stain) round-trips with the rest null. + final PathogenTestDto smearOnly = creator.buildPathogenTestDto(rdcf, user, sample, caze.getDisease(), testDateTime); + smearOnly.setTestType(PathogenTestType.ACID_FAST_STAIN); + smearOnly.setTestResult(PathogenTestResultType.NOT_APPLICABLE); + smearOnly.setSmearGrade(de.symeda.sormas.api.sample.SmearGrade.THREE_PLUS); + + final PathogenTestDto reloadedSmearOnly = getPathogenTestFacade().savePathogenTest(smearOnly); + assertEquals(de.symeda.sormas.api.sample.SmearGrade.THREE_PLUS, reloadedSmearOnly.getSmearGrade()); + assertNull(reloadedSmearOnly.getQuantitativeValue()); + assertNull(reloadedSmearOnly.getQuantitativeUnit()); + assertNull(reloadedSmearOnly.getQuantitativeBoolean()); + assertNull(reloadedSmearOnly.getWesternBlotInterpretation()); } @Test @@ -127,42 +124,71 @@ public void testQuantitativeResultFieldsClearedOnSaveForMismatchedTestType() { final CaseDataDto caze = creator.createCase(user.toReference(), person.toReference(), rdcf); final SampleDto sample = creator.createSample(caze.toReference(), user.toReference(), rdcf.facility); - // A method declaring only TEXT (Genotyping) must not persist numeric/boolean/smear/Western-Blot + // A method declaring only SMEAR_GRADE (Acid-Fast Stain) must not persist numeric/boolean/Western-Blot // values, even when the DTO carries them - e.g. left over after changing the test type. - final PathogenTestDto textType = creator.buildPathogenTestDto(rdcf, user, sample, caze.getDisease(), testDateTime); - textType.setTestType(PathogenTestType.GENOTYPING); - textType.setTestResult(PathogenTestResultType.NOT_APPLICABLE); - textType.setQuantitativeText("M. tuberculosis L4 Euro-American"); - textType.setQuantitativeValue(24.0f); - textType.setQuantitativeUnit("copies/mL"); - textType.setQuantitativeBoolean(de.symeda.sormas.api.utils.YesNoUnknown.YES); - textType.setSmearGrade(de.symeda.sormas.api.sample.SmearGrade.THREE_PLUS); - textType.setWesternBlotInterpretation(de.symeda.sormas.api.sample.WesternBlotInterpretation.POSITIVE); - - final PathogenTestDto reloaded = getPathogenTestFacade().savePathogenTest(textType); - - assertEquals("M. tuberculosis L4 Euro-American", reloaded.getQuantitativeText()); + final PathogenTestDto smearType = creator.buildPathogenTestDto(rdcf, user, sample, caze.getDisease(), testDateTime); + smearType.setTestType(PathogenTestType.ACID_FAST_STAIN); + smearType.setTestResult(PathogenTestResultType.NOT_APPLICABLE); + smearType.setSmearGrade(de.symeda.sormas.api.sample.SmearGrade.THREE_PLUS); + smearType.setQuantitativeValue(24.0f); + smearType.setQuantitativeUnit("copies/mL"); + smearType.setQuantitativeBoolean(de.symeda.sormas.api.utils.YesNoUnknown.YES); + smearType.setWesternBlotInterpretation(de.symeda.sormas.api.sample.WesternBlotInterpretation.POSITIVE); + + final PathogenTestDto reloaded = getPathogenTestFacade().savePathogenTest(smearType); + + assertEquals(de.symeda.sormas.api.sample.SmearGrade.THREE_PLUS, reloaded.getSmearGrade()); assertNull(reloaded.getQuantitativeValue()); assertNull(reloaded.getQuantitativeUnit()); assertNull(reloaded.getQuantitativeBoolean()); - assertNull(reloaded.getSmearGrade()); assertNull(reloaded.getWesternBlotInterpretation()); - // Changing the test type to one that no longer produces TEXT clears the previously stored text too. - reloaded.setTestType(PathogenTestType.ACID_FAST_STAIN); - reloaded.setTestResult(PathogenTestResultType.NOT_APPLICABLE); - reloaded.setSmearGrade(de.symeda.sormas.api.sample.SmearGrade.TWO_PLUS); + // Changing the test type to one that no longer produces a smear grade clears the previously stored value. + reloaded.setTestType(PathogenTestType.PCR_RT_PCR); + reloaded.setTestResult(PathogenTestResultType.POSITIVE); + reloaded.setQuantitativeValue(12.0f); + reloaded.setQuantitativeUnit("IU/mL"); final PathogenTestDto retyped = getPathogenTestFacade().savePathogenTest(reloaded); - assertEquals(de.symeda.sormas.api.sample.SmearGrade.TWO_PLUS, retyped.getSmearGrade()); - assertNull(retyped.getQuantitativeText()); - assertNull(retyped.getQuantitativeValue()); - assertNull(retyped.getQuantitativeUnit()); + assertEquals(12.0f, retyped.getQuantitativeValue()); + assertNull(retyped.getSmearGrade()); assertNull(retyped.getQuantitativeBoolean()); assertNull(retyped.getWesternBlotInterpretation()); } + @Test + public void testAntibioticSusceptibilityHasNoResultValueType() { + // ANTIBIOTIC_SUSCEPTIBILITY's real result is the drug-susceptibility grid, not a value-type-bearing + // field. Its @ResultValueTypeRel is explicitly empty, so saving it with NOT_APPLICABLE must round-trip + // cleanly and the empty-set branch in fillOrBuildEntity must null every quantitative field. + + final RDCF rdcf = creator.createRDCF("Region", "District", "Community", "Facility"); + final UserDto user = creator.createSurveillanceSupervisor(rdcf); + final PersonDto person = creator.createPerson(); + final CaseDataDto caze = creator.createCase(user.toReference(), person.toReference(), rdcf); + final SampleDto sample = creator.createSample(caze.toReference(), user.toReference(), rdcf.facility); + + final PathogenTestDto ast = creator.buildPathogenTestDto(rdcf, user, sample, caze.getDisease(), testDateTime); + ast.setTestType(PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY); + ast.setTestResult(PathogenTestResultType.NOT_APPLICABLE); + // Carry stale values to verify the empty-ResultValueTypeRel set clears every quantitative field on save. + ast.setQuantitativeValue(99.0f); + ast.setQuantitativeUnit("copies/mL"); + ast.setQuantitativeBoolean(de.symeda.sormas.api.utils.YesNoUnknown.YES); + ast.setSmearGrade(de.symeda.sormas.api.sample.SmearGrade.THREE_PLUS); + ast.setWesternBlotInterpretation(de.symeda.sormas.api.sample.WesternBlotInterpretation.POSITIVE); + + final PathogenTestDto reloaded = getPathogenTestFacade().savePathogenTest(ast); + + assertEquals(PathogenTestResultType.NOT_APPLICABLE, reloaded.getTestResult()); + assertNull(reloaded.getQuantitativeValue()); + assertNull(reloaded.getQuantitativeUnit()); + assertNull(reloaded.getQuantitativeBoolean()); + assertNull(reloaded.getSmearGrade()); + assertNull(reloaded.getWesternBlotInterpretation()); + } + @Test public void testSaveAndUpdatePathogenTestAssociatedToContact() { diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseLabResultsView.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseLabResultsView.java index 02271b14dea..fffd8523814 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseLabResultsView.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseLabResultsView.java @@ -43,7 +43,6 @@ import de.symeda.sormas.api.therapy.DrugSusceptibilityDto; import de.symeda.sormas.api.therapy.DrugSusceptibilityType; import de.symeda.sormas.api.therapy.SusceptibilityMethod; -import de.symeda.sormas.api.therapy.SusceptibilitySurveillanceType; import de.symeda.sormas.api.utils.AnnotationFieldHelper; import de.symeda.sormas.api.utils.DateHelper; import de.symeda.sormas.ui.ControllerProvider; @@ -248,11 +247,10 @@ private Grid buildAstGrid(List astTests) { grid.addColumn(AstRow::getAntibiotic).setCaption(I18nProperties.getCaption(Captions.caseLabResultsAntibiotic)); grid.addColumn(AstRow::getMethod).setCaption(I18nProperties.getCaption(Captions.caseLabResultsMethod)); - grid.addColumn(AstRow::getMic).setCaption(I18nProperties.getCaption(Captions.caseLabResultsMicValue)); - grid.addColumn(AstRow::getZoneDiameter).setCaption(I18nProperties.getCaption(Captions.caseLabResultsZoneDiameter)); + // "Value" merges MIC value (mg/l) and zone diameter (mm) into one column — a given antibiotic+method + // combination typically reports one or the other (#13955). + grid.addColumn(AstRow::getValue).setCaption(I18nProperties.getCaption(Captions.caseLabResultsValue)); grid.addColumn(AstRow::getClinicalInterpretation).setCaption(I18nProperties.getCaption(Captions.caseLabResultsClinicalInterpretation)); - grid.addColumn(AstRow::getSurveillanceInterpretation) - .setCaption(I18nProperties.getCaption(Captions.caseLabResultsSurveillanceInterpretation)); grid.setItems(rows); return grid; @@ -261,8 +259,8 @@ private Grid buildAstGrid(List astTests) { /** * Flattens the single, flat {@link DrugSusceptibilityDto} of an AST test into one row per antibiotic * that applies to the tested disease (per the {@code @Diseases}/{@code @ApplicableToPathogenTests} - * annotations), reading the MIC, zone diameter, method, clinical (S/I/R) and surveillance (WT/NWT) - * values reflectively. + * annotations), reading the method, MIC, zone diameter and clinical (S/I/R) values reflectively. The + * MIC and zone diameter are combined into a single "Value" cell (#13955). */ private List flattenDrugSusceptibility(PathogenTestDto test) { @@ -291,22 +289,35 @@ private List flattenDrugSusceptibility(PathogenTestDto test) { SusceptibilityMethod method = readProperty(ds, capitalized + "Method", SusceptibilityMethod.class); Float mic = readProperty(ds, capitalized + "Mic", Float.class); Float zoneDiameter = readProperty(ds, capitalized + "ZoneDiameter", Float.class); - SusceptibilitySurveillanceType surveillance = readProperty(ds, capitalized + "Surveillance", SusceptibilitySurveillanceType.class); Drug drug = resolveDrug(base); rows.add( new AstRow( drug != null ? I18nProperties.getEnumCaption(drug) : base, method != null ? I18nProperties.getEnumCaption(method) : null, - mic, - zoneDiameter, - clinical != null ? I18nProperties.getEnumCaption(clinical) : null, - surveillance != null ? I18nProperties.getEnumCaption(surveillance) : null)); + formatAstValue(mic, zoneDiameter), + clinical != null ? I18nProperties.getEnumCaption(clinical) : null)); } return rows; } + /** + * Builds the merged "Value" cell from the MIC (mg/L) and zone diameter (mm). A given antibiotic+method + * combination typically reports one or the other, but both are surfaced when present so no data is lost. + */ + private static String formatAstValue(Float mic, Float zoneDiameter) { + boolean hasMic = mic != null; + boolean hasZone = zoneDiameter != null; + if (!hasMic && !hasZone) { + return null; + } + if (hasMic && hasZone) { + return String.format("MIC: %s mg/l; Zone: %s mm", mic, zoneDiameter); + } + return hasMic ? String.format("%s mg/l", mic) : String.format("%s mm", zoneDiameter); + } + /** * Reads a {@link DrugSusceptibilityDto} property by its getter name, returning {@code null} when the * getter does not exist (drugs vary in which measurements they record) or cannot be read. @@ -353,24 +364,20 @@ private void reload() { } /** - * View-model for one antibiotic row of the AST table. + * View-model for one antibiotic row of the AST table (#13955). */ public static final class AstRow { private final String antibiotic; private final String method; - private final Float mic; - private final Float zoneDiameter; + private final String value; private final String clinicalInterpretation; - private final String surveillanceInterpretation; - AstRow(String antibiotic, String method, Float mic, Float zoneDiameter, String clinicalInterpretation, String surveillanceInterpretation) { + AstRow(String antibiotic, String method, String value, String clinicalInterpretation) { this.antibiotic = antibiotic; this.method = method; - this.mic = mic; - this.zoneDiameter = zoneDiameter; + this.value = value; this.clinicalInterpretation = clinicalInterpretation; - this.surveillanceInterpretation = surveillanceInterpretation; } public String getAntibiotic() { @@ -381,20 +388,12 @@ public String getMethod() { return method; } - public Float getMic() { - return mic; - } - - public Float getZoneDiameter() { - return zoneDiameter; + public String getValue() { + return value; } public String getClinicalInterpretation() { return clinicalInterpretation; } - - public String getSurveillanceInterpretation() { - return surveillanceInterpretation; - } } } diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/configuration/DevModeView.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/configuration/DevModeView.java index 334f857a439..518f7daffda 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/configuration/DevModeView.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/configuration/DevModeView.java @@ -1334,14 +1334,13 @@ private void createPathogenTests(SampleDto sample, Disease disease, Date date, L test.setTestResult(random(PATHOGEN_TEST_RESULT_POOL)); test.setTestDateTime(date); - // Populate a quantitative value for half the tests so the inline quantitative display has data. + // Populate a quantitative value for half the numeric-result tests so the inline quantitative display + // has data. if (randomPercent(50)) { Set valueTypes = PathogenTestType.getResultValueTypes(test.getTestType()); if (valueTypes.contains(ResultValueType.NUMERIC)) { test.setQuantitativeValue(random().nextFloat() * 1000); test.setQuantitativeUnit("copies/mL"); - } else if (valueTypes.contains(ResultValueType.TEXT)) { - test.setQuantitativeText("Generated result"); } } diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/epidata/EpiDataForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/epidata/EpiDataForm.java index 16b5a460468..3e033d79a0b 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/epidata/EpiDataForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/epidata/EpiDataForm.java @@ -67,6 +67,7 @@ import de.symeda.sormas.api.epidata.ClusterType; import de.symeda.sormas.api.epidata.EpiDataDto; import de.symeda.sormas.api.exposure.ExposureDto; +import de.symeda.sormas.api.exposure.ExposureType; import de.symeda.sormas.api.exposure.InfectionSource; import de.symeda.sormas.api.exposure.ModeOfTransmission; import de.symeda.sormas.api.exposure.ProphylaxisAdherence; @@ -118,8 +119,14 @@ public class EpiDataForm extends AbstractEditForm { private static final String PROPHYLAXIS_LAYOUT = fluidRowLocs(3, "PROPHYLAXIS_LABEL", 3, "PROPHYLAXIS_VALUE", 6, ""); private static final String LOC_OTHER_INFORMATION_HEADING = "locOtherInformationHeading"; - private static final List CONCLUSION_ALLOWED_DISEASES = Collections - .unmodifiableList(Arrays.asList(Disease.CRYPTOSPORIDIOSIS, Disease.GIARDIASIS, Disease.MALARIA, Disease.DENGUE, Disease.SHIGELLOSIS)); + private static final List CONCLUSION_ALLOWED_DISEASES = Collections.unmodifiableList( + Arrays.asList( + Disease.CRYPTOSPORIDIOSIS, + Disease.GIARDIASIS, + Disease.MALARIA, + Disease.DENGUE, + Disease.SALMONELLOSIS, + Disease.SHIGELLOSIS)); //@formatter:off private static final String MAIN_HTML_LAYOUT = @@ -306,6 +313,27 @@ protected void addFields() { exposuresField.addValueChangeListener(e -> ogExposureDetailsKnown.setEnabled(CollectionUtils.isEmpty(exposuresField.getValue()))); + // Salmonellosis: when a Travel exposure carries a country in its location, propose it as the + // probable country of infection on the conclusion. Gated on country.isVisible() so we never + // write a hidden field (visibility tracks importedCase==YES per setVisibleWhen above) and on + // isAttached() so initial bean-binding propagation does not overwrite a saved-null country. + if (disease == Disease.SALMONELLOSIS) { + exposuresField.addValueChangeListener(e -> { + if (!isAttached() || !country.isVisible() || country.getValue() != null) { + return; + } + java.util.Collection exposures = exposuresField.getValue(); + if (CollectionUtils.isEmpty(exposures)) { + return; + } + exposures.stream() + .filter(ex -> ex.getExposureType() == ExposureType.TRAVEL && ex.getLocation() != null && ex.getLocation().getCountry() != null) + .map(ex -> ex.getLocation().getCountry()) + .findFirst() + .ifPresent(country::setValue); + }); + } + TextArea additionalDetails = addField(EpiDataDto.OTHER_DETAILS, TextArea.class); additionalDetails.setRows(6); additionalDetails.setDescription( 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 index 57f16a868e8..6a065f7882a 100644 --- 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 @@ -101,19 +101,12 @@ public void updateCtVisibility(Disease disease, PathogenTestType testType) { } public void updateCqVisibility(Disease disease, PathogenTestType testType, PathogenTestResultType testResult) { - // CQ value is only meaningful for a positive result, regardless of test type/disease + // CQ value is only meaningful for a positive result; the disease+method rule lives on PathogenTestType so + // it stays in lockstep with TestResultComponent's generic-numeric suppression. boolean positive = testResult == PathogenTestResultType.POSITIVE; - if (positive && (disease == null || !java.util.Arrays.asList(Disease.TUBERCULOSIS).contains(disease))) { - if (testType == PathogenTestType.PCR_RT_PCR - || 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); + boolean show = positive && PathogenTestType.cqInputApplies(disease, testType); + cqValueField.setVisible(show); + if (!show) { cqValueField.clear(); } updateRowVisibility(cqRow); 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 index f9e67b0e578..4a2c44f99b8 100644 --- 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 @@ -118,7 +118,9 @@ private void wireEvents() { } private void updateFourFoldIncrease(PathogenTestType testType) { - boolean antibodyTest = testType == PathogenTestType.IGM_SERUM_ANTIBODY || testType == PathogenTestType.IGG_SERUM_ANTIBODY; + boolean antibodyTest = testType == PathogenTestType.IGM_SERUM_ANTIBODY + || testType == PathogenTestType.IGG_SERUM_ANTIBODY + || testType == PathogenTestType.IGA_SERUM_ANTIBODY; boolean positive = currentTestResult == PathogenTestResultType.POSITIVE; if (antibodyTest && positive) { fourFoldIncrease.setVisible(true); 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 index e69237c2e5a..dbb57431bbe 100644 --- 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 @@ -61,13 +61,11 @@ public class TestResultComponent extends FormComponent { private RadioButtonGroup preliminaryField; private TextField quantitativeValueField; private TextField quantitativeUnitField; - private TextField quantitativeTextField; private RadioButtonGroup quantitativeBooleanField; private ComboBox smearGradeField; private ComboBox westernBlotInterpretationField; private HorizontalLayout quantitativeValueRow; private HorizontalLayout quantitativeEnumRow; - private HorizontalLayout quantitativeTextRow; private List resultTypes; @@ -113,7 +111,6 @@ private void buildLayout() { quantitativeValueField = createTextField(PathogenTestDto.QUANTITATIVE_VALUE, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR); quantitativeUnitField = createTextField(PathogenTestDto.QUANTITATIVE_UNIT, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR); - quantitativeTextField = createTextField(PathogenTestDto.QUANTITATIVE_TEXT, PathogenTestDto.I18N_PREFIX, ValueChangeMode.BLUR); quantitativeBooleanField = createEnumRadioGroup(PathogenTestDto.QUANTITATIVE_BOOLEAN, PathogenTestDto.I18N_PREFIX, YesNoUnknown.class); smearGradeField = createComboBox(PathogenTestDto.SMEAR_GRADE, PathogenTestDto.I18N_PREFIX); smearGradeField.setItems(SmearGrade.values()); @@ -146,7 +143,6 @@ private void buildLayout() { smearGradeField, westernBlotInterpretationField, createSpacer()); - quantitativeTextRow = addRow(quantitativeTextField, createSpacer()); updateQuantitativeFieldsVisibility(currentTestType); } @@ -160,7 +156,6 @@ private void bindFields() { .withConverter(new de.symeda.sormas.ui.utils.StringToFloatNullableConverter(quantitativeValueField.getCaption())) .bind(PathogenTestDto::getQuantitativeValue, PathogenTestDto::setQuantitativeValue); binder.forField(quantitativeUnitField).bind(PathogenTestDto::getQuantitativeUnit, PathogenTestDto::setQuantitativeUnit); - binder.forField(quantitativeTextField).bind(PathogenTestDto::getQuantitativeText, PathogenTestDto::setQuantitativeText); binder.forField(quantitativeBooleanField).bind(PathogenTestDto::getQuantitativeBoolean, PathogenTestDto::setQuantitativeBoolean); binder.forField(smearGradeField).bind(PathogenTestDto::getSmearGrade, PathogenTestDto::setSmearGrade); binder.forField(westernBlotInterpretationField) @@ -222,46 +217,35 @@ private void updateQuantitativeFieldsVisibility(PathogenTestType testType) { boolean hasQualitative = testType == null || valueTypes.contains(ResultValueType.QUALITATIVE); boolean hasNumeric = valueTypes.contains(ResultValueType.NUMERIC); - boolean showText = valueTypes.contains(ResultValueType.TEXT); boolean showBoolean = valueTypes.contains(ResultValueType.BOOLEAN); boolean showSmearGrade = valueTypes.contains(ResultValueType.SMEAR_GRADE); boolean showWesternBlot = valueTypes.contains(ResultValueType.WESTERN_BLOT); - // The qualitative selector is also hidden when a disease section has set the result to "Not - // applicable" (e.g. AST, whose real result is the drug-susceptibility grid) — keep the value so - // the stored Not-applicable is preserved, just don't show an irrelevant selector. - boolean resultNotApplicable = testResultField.getValue() == PathogenTestResultType.NOT_APPLICABLE; - boolean showQualitative = hasQualitative && !resultNotApplicable; + boolean showQualitative = computeShowQualitative(testType, hasQualitative); - // Not applicable is only added on demand for methods without a qualitative result (see - // ensureNotApplicableSelectable). Once the method has a qualitative result again, drop it back out of - // the non-Luxembourg combo so it cannot be picked as a stray option, unless it is the current value. + // Once a method with a qualitative result is selected, drop the on-demand "Not applicable" back out of the + // non-Luxembourg combo so it cannot be picked as a stray option, unless it is the current value. + boolean resultNotApplicable = testResultField.getValue() == PathogenTestResultType.NOT_APPLICABLE; if (!isLuxembourg && hasQualitative && !resultNotApplicable) { removeNotApplicableSelectable(); } - // A numeric value only makes sense for a positive result: for a qualitative+numeric method (e.g. - // RT-PCR) the Ct/value appears only once Positive is selected - for a purely numeric method (no - // qualitative component) it always applies. - boolean showNumeric = hasNumeric && (!hasQualitative || testResultField.getValue() == PathogenTestResultType.POSITIVE); + boolean showNumeric = computeShowNumeric(testType, hasQualitative, hasNumeric); // Only toggle the selector's visibility — never clear its value here. Typing methods (Genotyping, - // Serogrouping, ... are value-type TEXT) hide the selector but rely on a disease section having set - // the result to Positive to reveal their own field; clearing it would wipe that and hide them. The - // result is managed by the disease sections and the SetTestResultEvent flow, not here. + // Serogrouping, ...) hide the selector but rely on a disease section having set the result to Positive to + // reveal their own field; clearing it would wipe that and hide them. The result is managed by the disease + // sections and the SetTestResultEvent flow, not here. Free-text results are captured via the disease + // sections' typing fields, or otherwise the "Test result details" field. testResultField.setVisible(showQualitative); setQuantitativeFieldVisible(quantitativeValueField, showNumeric); setQuantitativeFieldVisible(quantitativeUnitField, showNumeric); - setQuantitativeFieldVisible(quantitativeTextField, showText); setQuantitativeFieldVisible(quantitativeBooleanField, showBoolean); setQuantitativeFieldVisible(smearGradeField, showSmearGrade); setQuantitativeFieldVisible(westernBlotInterpretationField, showWesternBlot); - quantitativeTextField.setRequiredIndicatorVisible(resultRequired && showText && !showWesternBlot); - updateRowVisibility(quantitativeValueRow); updateRowVisibility(quantitativeEnumRow); - updateRowVisibility(quantitativeTextRow); } /** @@ -292,6 +276,54 @@ private static boolean hasQualitativeResult(PathogenTestType testType) { return testType == null || PathogenTestType.getResultValueTypes(testType).contains(ResultValueType.QUALITATIVE); } + /** + * @return whether the qualitative Positive/Negative selector should be shown for the selected method. + * Shown for every method that has a qualitative result, with one explicit exception: + * {@link PathogenTestType#ANTIBIOTIC_SUSCEPTIBILITY} is always hidden because its real result is + * captured by the drug-susceptibility grid (the value is kept as "Not applicable"). The visibility + * is driven purely by the method — never by the current value — so a user can never pick a value + * that makes the field disappear mid-edit. Methods without QUALITATIVE in their + * {@code @ResultValueTypeRel} (typing methods such as Genotyping/Serogrouping/...) hide the + * selector implicitly via {@code hasQualitative == false}; their result is driven by the disease + * sections' SetTestResultEvent flow. + */ + private static boolean computeShowQualitative(PathogenTestType testType, boolean hasQualitative) { + if (testType == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY) { + return false; + } + return hasQualitative; + } + + /** + * @return whether the generic numeric value/unit fields should be shown for the selected method. + * A numeric value only makes sense for a positive result: for a qualitative+numeric method + * (e.g. RT-PCR) the value appears only once Positive is selected — for a purely numeric method + * (no qualitative component) it always applies. Methods covered by the existing Ct/Cq fields + * ({@link CtCqValueComponent} / {@link FourFoldCtCqComponent}) suppress the generic fields so + * they are not duplicated; the condition mirrors {@code CtCqValueComponent.updateCqVisibility} + * exactly — including its Tuberculosis exclusion, so TB/PCR keeps the generic numeric input. + */ + private boolean computeShowNumeric(PathogenTestType testType, boolean hasQualitative, boolean hasNumeric) { + if (!hasNumeric) { + return false; + } + if (hasQualitative && testResultField.getValue() != PathogenTestResultType.POSITIVE) { + return false; + } + return !cqFieldShownInstead(testType); + } + + /** + * Delegates to {@link PathogenTestType#cqInputApplies(Disease, PathogenTestType)} — the single source of + * truth for when the Cq input ({@link CtCqValueComponent}) is shown. Suppressing the generic numeric here + * iff the Cq input applies guarantees the two cannot drift (the Tuberculosis exclusion in + * CtCqValueComponent was the source of an earlier gap for TB/PCR_RT_PCR — the shared helper makes that + * class of bug impossible). + */ + private boolean cqFieldShownInstead(PathogenTestType testType) { + return PathogenTestType.cqInputApplies(currentDisease, testType); + } + private void setQuantitativeFieldVisible(com.vaadin.data.HasValue field, boolean visible) { ((com.vaadin.ui.Component) field).setVisible(visible); if (!visible && !field.isEmpty()) { @@ -299,6 +331,25 @@ private void setQuantitativeFieldVisible(com.vaadin.data.HasValue field, bool } } + @Override + public void setDto(PathogenTestDto dto) { + super.setDto(dto); + + // Backwards compatibility for Antibiotic Susceptibility Testing only: AST's real result is the + // drug-susceptibility grid, never a qualitative selector — but old records may carry a stale + // qualitative value (e.g. POSITIVE). Coerce only AST to NOT_APPLICABLE on load. Other methods without + // QUALITATIVE in their @ResultValueTypeRel (WESTERN_BLOT, BACTERIAL_CULTURE, typing methods, ...) + // keep their stored qualitative result so its historical Positive/Negative information is preserved + // and the related disease sections continue to render their typing fields. + if (dto != null && dto.getTestType() == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY) { + PathogenTestResultType current = testResultField.getValue(); + if (current != null && current != PathogenTestResultType.NOT_APPLICABLE) { + ensureNotApplicableSelectable(); + testResultField.setValue(PathogenTestResultType.NOT_APPLICABLE); + } + } + } + @Override public void validate() { super.validate(); @@ -311,9 +362,6 @@ public void validate() { requireIfVisible(quantitativeBooleanField, PathogenTestDto.QUANTITATIVE_BOOLEAN); requireIfVisible(smearGradeField, PathogenTestDto.SMEAR_GRADE); requireIfVisible(westernBlotInterpretationField, PathogenTestDto.WESTERN_BLOT_INTERPRETATION); - if (!westernBlotInterpretationField.isVisible()) { - requireIfVisible(quantitativeTextField, PathogenTestDto.QUANTITATIVE_TEXT); - } } } 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 index e79bdebb0d7..9b03418e394 100644 --- 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 @@ -49,8 +49,7 @@ public class ImiSectionComponent extends AbstractDiseaseSectionComponent { PathogenTestType.MULTILOCUS_SEQUENCE_TYPING, PathogenTestType.SLIDE_AGGLUTINATION, PathogenTestType.WHOLE_GENOME_SEQUENCING, - PathogenTestType.SEQUENCING, - PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY); + PathogenTestType.SEQUENCING); private ComboBox seroGroupSpecField; private TextField seroGroupSpecTextField; @@ -108,7 +107,9 @@ protected void wireVisibility() { setDrugSusceptibilityRowVisible(visible); } - if (currentTestType != null && AUTO_POSITIVE_TYPES.contains(currentTestType)) { + if (currentTestType == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY) { + eventBus.fire(new SetTestResultEvent(PathogenTestResultType.NOT_APPLICABLE)); + } else if (currentTestType != null && AUTO_POSITIVE_TYPES.contains(currentTestType)) { eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE)); } else if (currentTestType != null) { eventBus.fire(new SetTestResultEvent(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 index ca0496de671..1e850139b73 100644 --- 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 @@ -50,8 +50,7 @@ public class IpiSectionComponent extends AbstractDiseaseSectionComponent { PathogenTestType.MULTILOCUS_SEQUENCE_TYPING, PathogenTestType.SLIDE_AGGLUTINATION, PathogenTestType.WHOLE_GENOME_SEQUENCING, - PathogenTestType.SEQUENCING, - PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY); + PathogenTestType.SEQUENCING); private TextField serotypeField; private ComboBox serotypingMethodField; @@ -115,7 +114,9 @@ protected void wireVisibility() { setDrugSusceptibilityRowVisible(visible); } - if (currentTestType != null && AUTO_POSITIVE_TYPES.contains(currentTestType)) { + if (currentTestType == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY) { + eventBus.fire(new SetTestResultEvent(PathogenTestResultType.NOT_APPLICABLE)); + } else if (currentTestType != null && AUTO_POSITIVE_TYPES.contains(currentTestType)) { eventBus.fire(new SetTestResultEvent(PathogenTestResultType.POSITIVE)); } else if (currentTestType != null) { eventBus.fire(new SetTestResultEvent(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 index 1dea9ddce69..c0239482c35 100644 --- 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 @@ -45,7 +45,10 @@ public class MalariaSectionComponent extends AbstractDiseaseSectionComponent { PathogenTestType.OTHER_MOLECULAR_ASSAY, PathogenTestType.OTHER_SEROLOGICAL_TEST, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, - PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY); + PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY); private static final List AUTO_POSITIVE_TYPES = Arrays.asList( PathogenTestType.ANTIGEN_DETECTION, @@ -55,6 +58,9 @@ public class MalariaSectionComponent extends AbstractDiseaseSectionComponent { PathogenTestType.PCR_RT_PCR, PathogenTestType.Q_PCR, PathogenTestType.ENZYME_LINKED_IMMUNOSORBENT_ASSAY, + PathogenTestType.IGM_SERUM_ANTIBODY, + PathogenTestType.IGG_SERUM_ANTIBODY, + PathogenTestType.IGA_SERUM_ANTIBODY, PathogenTestType.LAMP, PathogenTestType.OTHER_ANTIGEN_DETECTION_TEST, PathogenTestType.OTHER_SEROLOGICAL_TEST, diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntry.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntry.java index f19f242f52b..4d057241578 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntry.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntry.java @@ -122,10 +122,13 @@ public PathogenTestListEntry(PathogenTestDto pathogenTest, boolean showTestResul // Quantitative result (issue #13952): combine whichever value-type fields the method populated. String quantitativeResult = formatQuantitativeResult(pathogenTest); - boolean astWithoutQualitativeResult = - pathogenTest.getTestedDisease() == Disease.TUBERCULOSIS && testType == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY; + // Antibiotic susceptibility has no qualitative result, so suppress the result label regardless of the stored + // value — old records may carry a stale POSITIVE. + // Other methods that have no QUALITATIVE in their @ResultValueTypeRel (Western Blot, Bacterial Culture, + // typing methods, ...) keep their qualitative cue. + boolean astWithoutQualitativeResult = testType == PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY; boolean suppressResultLabel = - pathogenTest.getTestResult() == PathogenTestResultType.NOT_APPLICABLE && (quantitativeResult != null || astWithoutQualitativeResult); + astWithoutQualitativeResult || (pathogenTest.getTestResult() == PathogenTestResultType.NOT_APPLICABLE && quantitativeResult != null); if (!suppressResultLabel) { Object resultText = determineSideComponentVariant(pathogenTest); Label labelResult = new Label(DataHelper.toStringNullable(resultText == null ? pathogenTest.getTestResult() : resultText)); @@ -165,9 +168,8 @@ public PathogenTestListEntry(PathogenTestDto pathogenTest, boolean showTestResul /** * @return a short label combining every quantitative result field the test recorded (Western Blot - * interpretation, detected flag, smear grade, numeric value with optional unit, free text), or - * {@code null} when none is set. A method can populate several of these at once (e.g. Western - * Blot stores both an interpretation and a band-pattern text), so all are shown. + * interpretation, detected flag, smear grade, numeric value with optional unit), or {@code null} + * when none is set. A method can populate several of these at once, so all are shown. */ @Nullable static String formatQuantitativeResult(PathogenTestDto test) { @@ -185,9 +187,6 @@ static String formatQuantitativeResult(PathogenTestDto test) { String unit = test.getQuantitativeUnit(); parts.add(DataHelper.isNullOrEmpty(unit) ? test.getQuantitativeValue().toString() : test.getQuantitativeValue() + " " + unit); } - if (StringUtils.isNotBlank(test.getQuantitativeText())) { - parts.add(test.getQuantitativeText()); - } return parts.isEmpty() ? null : String.join(": ", parts); } diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/symptoms/SymptomsForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/symptoms/SymptomsForm.java index 3f0aa88c5fd..154deb16313 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/symptoms/SymptomsForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/symptoms/SymptomsForm.java @@ -368,6 +368,7 @@ public String getFormattedHtmlMessage() { addFields( VOMITING, DIARRHEA, + WATERY_DIARRHEA, BLOOD_IN_STOOL, NAUSEA, ABDOMINAL_PAIN, @@ -725,6 +726,7 @@ public String getFormattedHtmlMessage() { PNEUMONIA_CLINICAL_OR_RADIOLOGIC, VOMITING, DIARRHEA, + WATERY_DIARRHEA, BLOOD_IN_STOOL, NAUSEA, ABDOMINAL_PAIN, diff --git a/sormas-ui/src/test/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntryTest.java b/sormas-ui/src/test/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntryTest.java index 945b042805b..0afe52edb35 100644 --- a/sormas-ui/src/test/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntryTest.java +++ b/sormas-ui/src/test/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntryTest.java @@ -68,20 +68,6 @@ public void unitWithoutValueIsIgnored() { assertNull(PathogenTestListEntry.formatQuantitativeResult(t)); } - @Test - public void textOnly() { - PathogenTestDto t = test(); - t.setQuantitativeText("M. tuberculosis L4 Euro-American"); - assertEquals("M. tuberculosis L4 Euro-American", PathogenTestListEntry.formatQuantitativeResult(t)); - } - - @Test - public void blankTextIsIgnored() { - PathogenTestDto t = test(); - t.setQuantitativeText(" "); - assertNull(PathogenTestListEntry.formatQuantitativeResult(t)); - } - @Test public void booleanOnly() { PathogenTestDto t = test(); @@ -97,42 +83,35 @@ public void smearGradeOnly() { } @Test - public void westernBlotInterpretationAndBandText_combinedInOrder() { - // Western Blot stores both an interpretation and a band-pattern text; both are shown, with the - // interpretation first. + public void westernBlotInterpretationOnly() { PathogenTestDto t = test(); t.setWesternBlotInterpretation(WesternBlotInterpretation.POSITIVE); - t.setQuantitativeText("OspC+, VlsE+"); - assertEquals(WesternBlotInterpretation.POSITIVE + ": OspC+, VlsE+", PathogenTestListEntry.formatQuantitativeResult(t)); + assertEquals(WesternBlotInterpretation.POSITIVE.toString(), PathogenTestListEntry.formatQuantitativeResult(t)); } @Test - public void bacterialCulture_textAndCount_combined() { - // Bacterial Culture = organism text + CFU count. + public void numericValueWithUnitCount() { + // Bacterial Culture = CFU count. PathogenTestDto t = test(); - t.setQuantitativeText("E. coli"); t.setQuantitativeValue(200000.0f); t.setQuantitativeUnit("CFU/mL"); - // Numeric is appended before free text per the fixed field order. - assertEquals("200000.0 CFU/mL: E. coli", PathogenTestListEntry.formatQuantitativeResult(t)); + assertEquals("200000.0 CFU/mL", PathogenTestListEntry.formatQuantitativeResult(t)); } @Test - public void allFieldsPopulated_orderedInterpretationGradeBooleanNumericText() { + public void allFieldsPopulated_orderedInterpretationGradeBooleanNumeric() { PathogenTestDto t = test(); t.setWesternBlotInterpretation(WesternBlotInterpretation.NEGATIVE); t.setSmearGrade(SmearGrade.ONE_PLUS); t.setQuantitativeBoolean(YesNoUnknown.NO); t.setQuantitativeValue(12.0f); t.setQuantitativeUnit("IU/mL"); - t.setQuantitativeText("free text"); String expected = String.join( ": ", WesternBlotInterpretation.NEGATIVE.toString(), SmearGrade.ONE_PLUS.toString(), YesNoUnknown.NO.toString(), - "12.0 IU/mL", - "free text"); + "12.0 IU/mL"); assertEquals(expected, PathogenTestListEntry.formatQuantitativeResult(t)); }