Bugfix 13548 dashboard samples error#13577
Conversation
WalkthroughAdds environment-sample support across API, backend, UI, and tests: new criteria field and i18n keys, facade/service environment aggregations, UI filter and data-provider branching, EPI curve restricted to humans, map/dashboard branching between human and environment samples, and test utilities for environment entities. Validation blocks disease+environment searches. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as SampleDashboardView
participant DP as SampleDashboardDataProvider
participant Fac as SampleDashboardFacade (EJB)
participant Svc as SampleDashboardService
participant DB as Database
UI->>DP: refreshData()
DP->>DP: buildDashboardCriteria()
alt disease + environmentMaterial
DP-->>UI: show notification (headingSearchSample / messageSampleSearchWithDisease)
DP-->>DP: abort refresh
else environment-only
DP->>Fac: getEnvironment*Counts(criteria)
Fac->>Svc: delegate environment aggregations
Svc->>DB: query/group EnvironmentSample
Svc-->>Fac: return maps
Fac-->>DP: return maps
else human-only
DP->>Fac: getSample*Counts(criteria)
Fac->>Svc: delegate human aggregations
Svc->>DB: query/group Sample
Svc-->>Fac: return maps
Fac-->>DP: return maps
else mixed (no disease)
DP->>Fac: getSample*Counts + getEnvironment*Counts
Fac->>Svc: delegate both
Svc->>DB: queries
Svc-->>Fac: maps
Fac-->>DP: maps
end
DP-->>UI: merged counts
UI->>UI: update totals, charts, map
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 13
🔭 Outside diff range comments (2)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/components/SampleDashboardMapComponent.java (2)
241-245: Wrong initial value for the environment samples checkboxThe checkbox value is tied to shouldShowEventParticipantSamples(), which is unrelated. It should reflect the current environment visibility toggle.
- showEnvironmentSamplesCheckBox.setValue(shouldShowEventParticipantSamples()); + showEnvironmentSamplesCheckBox.setValue(showEnvironmentalSamples);
259-264: Environment-only legend never shownEarly return when displayedHumanSamples is empty prevents the environment legend from rendering, even if showEnvironmentalSamples is true.
- if (displayedHumanSamples.size() == 0) { - return Collections.emptyList(); - } + if (displayedHumanSamples.size() == 0 && !showEnvironmentalSamples) { + return Collections.emptyList(); + }
🧹 Nitpick comments (12)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/diagram/AbstractEpiCurveComponent.java (1)
25-27: Drop Component import after constructor cleanupComponent is only used for a temporary variable introduced in the new constructor; once simplified (see below), this import becomes unnecessary.
Apply after simplifying the constructor:
-import com.vaadin.ui.Component;sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/surveillance/components/statistics/LaboratoryResultsStatisticsComponent.java (1)
51-55: Nit: Prefer setIcon over HTML content for iconsUsing HTML for icons is unnecessary; Vaadin Label supports setIcon. This avoids ContentMode.HTML and is more accessible.
For example:
-Label infoIcon = new Label(VaadinIcons.INFO_CIRCLE.getHtml(), ContentMode.HTML); +Label infoIcon = new Label(); +infoIcon.setIcon(VaadinIcons.INFO_CIRCLE);sormas-api/src/main/java/de/symeda/sormas/api/dashboard/SampleDashboardCriteria.java (1)
61-70: Clarify/enforce mutual exclusivity between sampleMaterial and environmentSampleMaterialThe criteria now has two material fields. Most downstream code likely expects one or the other to be set. Consider enforcing mutual exclusivity in the setters or document precedence.
Option A: Enforce exclusivity in setters (recommended for consistency):
// in SampleDashboardCriteria public SampleDashboardCriteria sampleMaterial(SampleMaterial sampleMaterial) { this.sampleMaterial = sampleMaterial; if (sampleMaterial != null) { this.environmentSampleMaterial = null; } return self; } public SampleDashboardCriteria environmentSampleMaterial(EnvironmentSampleMaterial environmentSampleMaterial) { this.environmentSampleMaterial = environmentSampleMaterial; if (environmentSampleMaterial != null) { this.sampleMaterial = null; } return self; }Option B: Add Javadoc stating that only one of the two should be set and which one takes precedence if both are non-null.
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardFilterLayout.java (2)
116-123: Redundant sorting and potential de-dupe side effects with TreeSet comparatorYou sort the stream and then collect into a TreeSet with the same comparator, effectively sorting twice. Also, de-duplication is based on toString, which may collapse identically captioned entries across enums (e.g., “Other”).
- Remove the pre-collect sorted(...) step and rely on the TreeSet’s comparator.
- Optionally, document that “Other” is intentionally de-duplicated and handled specially in the value-change listener.
- Set<Enum<?>> combinedSampleMaterials = Stream - .concat(Stream.of(SampleMaterial.values()), Stream.of(EnvironmentSampleMaterial.values())) - .sorted(Comparator.comparing(Enum::toString)) - .collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Enum::toString)))); + Set<Enum<?>> combinedSampleMaterials = Stream + .concat(Stream.of(SampleMaterial.values()), Stream.of(EnvironmentSampleMaterial.values())) + .collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Enum::toString))));
127-140: Listener logic OK; minor robustness/readability improvements
- Cache the selected value to avoid repeated e.getProperty().getValue().
- The comment can be clarified.
- // In Other as the selected sample, all samples should select. - if (e.getProperty().getValue() == SampleMaterial.OTHER || e.getProperty().getValue() == EnvironmentSampleMaterial.OTHER) { + // Selecting "Other" should select both human and environment "Other" + Object value = e.getProperty().getValue(); + if (value == SampleMaterial.OTHER || value == EnvironmentSampleMaterial.OTHER) { dashboardDataProvider.setEnvironmentSampleMaterial(EnvironmentSampleMaterial.OTHER); dashboardDataProvider.setSampleMaterial(SampleMaterial.OTHER); - } else if (e.getProperty().getValue() instanceof EnvironmentSampleMaterial) { - dashboardDataProvider.setEnvironmentSampleMaterial((EnvironmentSampleMaterial) e.getProperty().getValue()); + } else if (value instanceof EnvironmentSampleMaterial) { + dashboardDataProvider.setEnvironmentSampleMaterial((EnvironmentSampleMaterial) value); dashboardDataProvider.setSampleMaterial(null); - } else if (e.getProperty().getValue() instanceof SampleMaterial) { + } else if (value instanceof SampleMaterial) { dashboardDataProvider.setEnvironmentSampleMaterial(null); - dashboardDataProvider.setSampleMaterial((SampleMaterial) e.getProperty().getValue()); + dashboardDataProvider.setSampleMaterial((SampleMaterial) value); } else { dashboardDataProvider.setEnvironmentSampleMaterial(null); dashboardDataProvider.setSampleMaterial(null); }sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleEpiCurveComponent.java (2)
70-99: Epi curve correctly gated to human samples; consider UX for environment modeThe guard prevents computing the curve when environmentSampleMaterial is set. Currently this silently renders all-zero series. Consider hiding the chart or showing a short description that the EPI curve is only available for human samples to avoid confusing users with flat zero lines.
75-83: Potential performance concern: N remote calls inside loopFetching counts per date inside the loop can be chatty over EJB. If this becomes a bottleneck, consider a backend endpoint that aggregates by date bucket (day/week/month) in one call and returns a time series.
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/components/SampleDashboardMapComponent.java (1)
83-101: Helper naming/docs OK; logic aligns with usageThe empty criteria helpers are straightforward and used correctly in the branching. Minor nit: the Javadoc for “WithoutDisease” vs “WithDisease” could be crisper, but not blocking.
If you want, I can tighten the Javadoc wording for clarity.
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (2)
551-552: Nit: unnecessary cb.or with single predicateWrapping a single cb.between(...) inside cb.or(...) is redundant. Simplify for readability.
- case ASSOCIATED_ENTITY_REPORT_DATE: - dateFilter = cb.or(cb.between(joins.getEnvironment().get(Environment.REPORT_DATE), dateFrom, dateTo)); + case ASSOCIATED_ENTITY_REPORT_DATE: + dateFilter = cb.between(joins.getEnvironment().get(Environment.REPORT_DATE), dateFrom, dateTo);
576-581: Comment refers to “cases” in environment filterThe comment “Exclude deleted cases” should say “Exclude deleted samples” to match the context.
- // Exclude deleted cases. Archived cases should stay included + // Exclude deleted samples. Archived samples should stay includedsormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProvider.java (2)
210-212: Naming nit: pluralize to reflect Map return typegetEnvironmentSampleCount returns a Map; consider renaming to getEnvironmentSampleCounts for clarity and consistency with other getters.
If you agree, I can generate a follow-up diff (including call sites).
38-56: Reduce duplication by extracting clear/reset helpersThere’s repeated boilerplate to clear either human or environment maps. Consider small private helpers:
- clearHumanCounts()
- clearEnvironmentCounts()
This improves readability and reduces mistakes when adding/removing metrics.
Example (outside changed lines):
private void clearHumanCounts() { sampleCountsByResultType = Maps.newHashMap(); sampleCountsByPurpose = Maps.newHashMap(); sampleCountsBySpecimenCondition = Maps.newHashMap(); sampleCountsByShipmentStatus = Maps.newHashMap(); testResultCountsByResultType = Maps.newHashMap(); } private void clearEnvironmentCounts() { envSampleCountsBySpecimenCondition = Maps.newHashMap(); envSampleCountsByShipmentStatus = Maps.newHashMap(); envTestResultCountsByResultType = Maps.newHashMap(); envSampleCount = Maps.newHashMap(); }Then use these in refreshData branches and on the early return.
Also applies to: 60-111
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
sormas-api/src/main/java/de/symeda/sormas/api/dashboard/SampleDashboardCriteria.java(2 hunks)sormas-api/src/main/java/de/symeda/sormas/api/dashboard/sample/SampleDashboardFacade.java(2 hunks)sormas-api/src/main/java/de/symeda/sormas/api/i18n/Descriptions.java(1 hunks)sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java(2 hunks)sormas-api/src/main/resources/descriptions.properties(1 hunks)sormas-api/src/main/resources/strings.properties(2 hunks)sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardFacadeEjb.java(2 hunks)sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java(5 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/diagram/AbstractEpiCurveComponent.java(4 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProvider.java(5 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardFilterLayout.java(2 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardView.java(4 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleEpiCurveComponent.java(3 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/components/SampleDashboardMapComponent.java(2 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/surveillance/components/statistics/LaboratoryResultsStatisticsComponent.java(2 hunks)sormas-ui/src/test/java/de/symeda/sormas/backend/AbstractBeanTest.java(2 hunks)sormas-ui/src/test/java/de/symeda/sormas/backend/TestDataCreator.java(3 hunks)sormas-ui/src/test/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProviderTest.java(3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProvider.java (3)
sormas-api/src/main/java/de/symeda/sormas/api/FacadeProvider.java (1)
FacadeProvider(127-594)sormas-api/src/main/java/de/symeda/sormas/api/dashboard/SampleDashboardCriteria.java (1)
SampleDashboardCriteria(22-71)sormas-api/src/main/java/de/symeda/sormas/api/i18n/I18nProperties.java (1)
I18nProperties(39-536)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/diagram/AbstractEpiCurveComponent.java (2)
sormas-ui/src/main/java/de/symeda/sormas/ui/utils/CssStyles.java (1)
CssStyles(26-535)sormas-api/src/main/java/de/symeda/sormas/api/i18n/I18nProperties.java (1)
I18nProperties(39-536)
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (2)
sormas-backend/src/main/java/de/symeda/sormas/backend/common/CriteriaBuilderHelper.java (1)
CriteriaBuilderHelper(30-230)sormas-backend/src/main/java/de/symeda/sormas/backend/util/QueryHelper.java (1)
QueryHelper(22-353)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleEpiCurveComponent.java (1)
sormas-api/src/main/java/de/symeda/sormas/api/FacadeProvider.java (1)
FacadeProvider(127-594)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/surveillance/components/statistics/LaboratoryResultsStatisticsComponent.java (2)
sormas-api/src/main/java/de/symeda/sormas/api/i18n/I18nProperties.java (1)
I18nProperties(39-536)sormas-ui/src/main/java/de/symeda/sormas/ui/utils/CssStyles.java (1)
CssStyles(26-535)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: android app test (26)
- GitHub Check: android app test (28)
- GitHub Check: android app test (27)
- GitHub Check: Lint Code Base
- GitHub Check: SORMAS CI
🔇 Additional comments (26)
sormas-api/src/main/java/de/symeda/sormas/api/i18n/Descriptions.java (1)
144-145: New i18n description key is consistent and well-placedThe new Descriptions.sampleDashboardHumans constant follows the established naming and placement conventions, and the backing property exists in descriptions.properties.
sormas-api/src/main/java/de/symeda/sormas/api/i18n/Strings.java (1)
830-831: i18n constants added with correct prefixes and intent
- headingSearchSample and messageSampleSearchWithDisease align with existing heading*/message* patterns.
- Corresponding entries exist in strings.properties with clear user-facing text.
No further action from API side.
Also applies to: 1561-1562
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/diagram/AbstractEpiCurveComponent.java (3)
56-57: Promoting header widgets to fields enables post-construction customizationMaking infoIcon, epiCurveHeaderLayout and expandEpiCurveButton fields is the right move to allow header updates from the overloaded constructor.
Also applies to: 62-64
116-116: Header layout switched to field – good for later mutationUsing epiCurveHeaderLayout as a field instead of a local variable is required for the new constructor logic to modify the header in place.
130-131: Storing expand button in a field enables toggling after header rebuildAssigning expandEpiCurveButton to a field is necessary for removal/re-adding when customizing the header. Ensure this remains assigned only once.
sormas-api/src/main/resources/descriptions.properties (1)
237-238: New description text is clear and matches the new keyThe description is concise and matches the usage intent on the sample dashboard. No issues.
sormas-api/src/main/resources/strings.properties (1)
519-520: New strings read well and align with UI flows
- headingSearchSample: Clear title for the notification.
- messageSampleSearchWithDisease: Communicates the constraint precisely.
Ensure these are wired where the validation is triggered so users get immediate feedback.
Also applies to: 1265-1266
sormas-ui/src/test/java/de/symeda/sormas/backend/AbstractBeanTest.java (2)
36-38: LGTM: Added environment sample facade imports fit the existing test patternThe new imports align with how other facades are wired in tests and look correct.
1007-1009: Provide EnvironmentSampleFacade via Local EJB — consistent with the restAccessor mirrors the existing facade getters (returning the API type while resolving the Local EJB). Good addition.
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/surveillance/components/statistics/LaboratoryResultsStatisticsComponent.java (1)
5-7: LGTM: Imports required for title layout and info iconThe VaadinIcons/ContentMode/HorizontalLayout and Descriptions imports are appropriate for the new UI elements.
Also applies to: 11-11
sormas-ui/src/test/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProviderTest.java (4)
20-25: Imports for environment entities look correctThe added imports align with the new environment test setup and are used below. No issues.
73-77: Correct precedence when environmentSampleMaterial is setAsserting that environmentSampleMaterial takes precedence when sampleMaterial is null is correct and future-proofs the criteria builder behavior.
108-114: Environment test data setup is fineEnvironment, lab facility, and environment sample creation is valid. This provides a solid basis for the following assertions.
115-124: Good: Blocking environment counts when disease filter is setVerifies that disease filters do not mix with environment samples, and that all counts (both environment and human) are empty under this invalid criterion. This matches the intended behavior.
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleEpiCurveComponent.java (1)
43-45: UI description switch to human samples is appropriateForwarding Descriptions.sampleDashboardHumans to the superclass clarifies the chart’s scope. No issues here.
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardView.java (2)
176-189: Merging environment counts into human is correct and null-safeUse of Optional and Map.merge for specimen condition and shipment status looks good and consistent.
105-113: Descriptions updated to human-focused textSwitching to Descriptions.sampleDashboardHumans for lab results and purpose sections is consistent with the EPI curve gating and overall UX.
sormas-ui/src/test/java/de/symeda/sormas/backend/TestDataCreator.java (1)
1405-1435: Environment creation helper looks goodHelper sets required fields, applies optional config, copies jurisdiction from rdcf, and persists. Solid addition for tests.
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardFacadeEjb.java (1)
90-109: Facade extended with environment-specific aggregations – looks consistentThe new methods delegate cleanly to SampleDashboardService and expose the required environment counts. Naming and types are consistent with the API.
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/components/SampleDashboardMapComponent.java (2)
120-128: Ambiguous semantics for isSampleCriteriaForOtherCurrently, selecting SampleMaterial.OTHER or EnvironmentSampleMaterial.OTHER triggers loading both human and environment samples. If “OTHER” is intended to mean “material equals OTHER” (not “all except”), loading both sets unconditionally may be misleading.
Consider either:
- Restricting to the selected domain (human vs environment), or
- Reclassifying this case into the mixed/both branch only when both materials are explicitly set to OTHER.
Do we intend “OTHER” to broaden to both domains, or to filter within the selected domain only? If the latter, adjust the branching accordingly.
63-65: Inconsistent permission gating for environment samples
- Initialization uses UiUtil.permitted(FeatureType.ENVIRONMENT_MANAGEMENT, UserRight.ENVIRONMENT_SAMPLE_VIEW).
- Layer checkbox uses UiUtil.permitted(UserRight.ENVIRONMENT_VIEW).
These may target different rights/feature flags. Align them to the intended authorization policy so that checkbox visibility and initial state remain consistent.
Would you confirm which right(s) should guard environment sample visibility? I can align both checks and add a utility method if needed.
Also applies to: 240-255
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (3)
225-242: Environment shipment status aggregation mirrors human; LGTMConsistent query structure (left join not needed here) and the merge function in toMap protects against duplicates. Looks good.
298-311: Counts by environment material: good additionUsing createEnvironmentSampleFilter and grouping by EnvironmentSample.SAMPLE_MATERIAL is appropriate. No issues.
572-574: Correct switch to environment material in filterChanging the filter from criteria.getSampleMaterial() to criteria.getEnvironmentSampleMaterial() is correct and necessary for environment domain filtering.
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProvider.java (2)
70-111: Branching and facade calls look consistent with backend; goodHuman-only, environment-only, and mixed branches are handled coherently. Environment domain correctly suppressed when disease is set.
120-121: Criteria builder correctly propagates environment materialIncluding environmentSampleMaterial alongside sampleMaterial is correct.
| showEnvironmentalSamples ? FacadeProvider.getSampleDashboardFacade().getEnvironmentalSamplesForMap(criteria) : Collections.emptyList(); | ||
| } | ||
|
|
||
| List<LeafletMarker> markers = new ArrayList<>(environmentSamples.size() + environmentSamples.size()); |
There was a problem hiding this comment.
Initial capacity uses environment size twice
ArrayList is initialized with environmentSamples.size() + environmentSamples.size(). This should include humanSamples.size().
- List<LeafletMarker> markers = new ArrayList<>(environmentSamples.size() + environmentSamples.size());
+ List<LeafletMarker> markers = new ArrayList<>(humanSamples.size() + environmentSamples.size());📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| List<LeafletMarker> markers = new ArrayList<>(environmentSamples.size() + environmentSamples.size()); | |
| List<LeafletMarker> markers = new ArrayList<>(humanSamples.size() + environmentSamples.size()); |
🤖 Prompt for AI Agents
In
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/components/SampleDashboardMapComponent.java
around line 156, the ArrayList initial capacity incorrectly sums
environmentSamples.size() twice; change the second operand to
humanSamples.size() so the initial capacity is humanSamples.size() +
environmentSamples.size(), ensuring the list is sized to hold both sample sets.
| Map<PathogenTestResultType, Long> sampleCounts = FacadeProvider.getSampleDashboardFacade().getSampleCountsByResultType(criteria); | ||
|
|
||
| Long positiveCount = sampleCounts.get(PathogenTestResultType.POSITIVE); | ||
| Long negativeCount = sampleCounts.get(PathogenTestResultType.NEGATIVE); | ||
| Long pendingCount = sampleCounts.get(PathogenTestResultType.PENDING); | ||
| Long indeterminateCount = sampleCounts.get((PathogenTestResultType.INDETERMINATE)); | ||
| Long notDoneCount = sampleCounts.get(PathogenTestResultType.NOT_DONE); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Defensive null-handling for facade result to avoid NPEs
getSampleCountsByResultType may return null in edge cases; guard against it to prevent NPEs.
Apply this diff:
- Map<PathogenTestResultType, Long> sampleCounts = FacadeProvider.getSampleDashboardFacade().getSampleCountsByResultType(criteria);
+ Map<PathogenTestResultType, Long> sampleCounts = FacadeProvider.getSampleDashboardFacade().getSampleCountsByResultType(criteria);
+ if (sampleCounts == null) {
+ sampleCounts = java.util.Collections.emptyMap();
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Map<PathogenTestResultType, Long> sampleCounts = FacadeProvider.getSampleDashboardFacade().getSampleCountsByResultType(criteria); | |
| Long positiveCount = sampleCounts.get(PathogenTestResultType.POSITIVE); | |
| Long negativeCount = sampleCounts.get(PathogenTestResultType.NEGATIVE); | |
| Long pendingCount = sampleCounts.get(PathogenTestResultType.PENDING); | |
| Long indeterminateCount = sampleCounts.get((PathogenTestResultType.INDETERMINATE)); | |
| Long notDoneCount = sampleCounts.get(PathogenTestResultType.NOT_DONE); | |
| Map<PathogenTestResultType, Long> sampleCounts = FacadeProvider.getSampleDashboardFacade().getSampleCountsByResultType(criteria); | |
| if (sampleCounts == null) { | |
| sampleCounts = java.util.Collections.emptyMap(); | |
| } | |
| Long positiveCount = sampleCounts.get(PathogenTestResultType.POSITIVE); | |
| Long negativeCount = sampleCounts.get(PathogenTestResultType.NEGATIVE); | |
| Long pendingCount = sampleCounts.get(PathogenTestResultType.PENDING); | |
| Long indeterminateCount = sampleCounts.get(PathogenTestResultType.INDETERMINATE); | |
| Long notDoneCount = sampleCounts.get(PathogenTestResultType.NOT_DONE); |
🤖 Prompt for AI Agents
In
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleEpiCurveComponent.java
around lines 84 to 91, the Map returned by getSampleCountsByResultType may be
null which can cause NPEs when calling get(...); guard against null by replacing
a null map with Collections.emptyMap() (or new HashMap<>()), then fetch each
count using map.getOrDefault(PathogenTestResultType.X, 0L) (or check for null
and default to 0L) for POSITIVE, NEGATIVE, PENDING, INDETERMINATE and NOT_DONE
so all counts are non-null longs.
| HorizontalLayout titleLayout = new HorizontalLayout(); | ||
| titleLayout.setMargin(false); | ||
| titleLayout.setSpacing(false); | ||
| addComponent(titleLayout); | ||
|
|
||
| if (subtitleCaption != null) { | ||
| Label subTitleLabel = new Label(I18nProperties.getCaption(subtitleCaption)); | ||
| CssStyles.style(subTitleLabel, CssStyles.H3, CssStyles.VSPACE_TOP_5); | ||
| addComponent(subTitleLabel); | ||
| titleLayout.addComponent(subTitleLabel); | ||
| } | ||
| if (description != null) { | ||
| Label infoIcon = new Label(VaadinIcons.INFO_CIRCLE.getHtml(), ContentMode.HTML); | ||
| infoIcon.setDescription(I18nProperties.getDescription(Descriptions.sampleDashboardHumans)); | ||
| infoIcon.addStyleName(CssStyles.HSPACE_LEFT_4); | ||
| infoIcon.addStyleNames(CssStyles.H3); | ||
| titleLayout.addComponent(infoIcon); | ||
| } |
There was a problem hiding this comment.
Avoid duplicate/wrongly gated info icon; use showInfoIcon and don’t pass info to super
Current logic can render two info icons:
- One possibly added by the super constructor (third arg when showInfoIcon is true).
- Another added here when description != null (regardless of showInfoIcon).
Gate the icon with showInfoIcon and stop passing the info text to super to avoid duplication.
Apply:
- super(titleCaption, description, showInfoIcon ? I18nProperties.getString(Strings.infoDashboardFinalLaboratoryResult) : "");
+ super(titleCaption, description, "");
@@
- if (description != null) {
+ if (showInfoIcon) {
Label infoIcon = new Label(VaadinIcons.INFO_CIRCLE.getHtml(), ContentMode.HTML);
infoIcon.setDescription(I18nProperties.getDescription(Descriptions.sampleDashboardHumans));
infoIcon.addStyleName(CssStyles.HSPACE_LEFT_4);
infoIcon.addStyleNames(CssStyles.H3);
titleLayout.addComponent(infoIcon);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| HorizontalLayout titleLayout = new HorizontalLayout(); | |
| titleLayout.setMargin(false); | |
| titleLayout.setSpacing(false); | |
| addComponent(titleLayout); | |
| if (subtitleCaption != null) { | |
| Label subTitleLabel = new Label(I18nProperties.getCaption(subtitleCaption)); | |
| CssStyles.style(subTitleLabel, CssStyles.H3, CssStyles.VSPACE_TOP_5); | |
| addComponent(subTitleLabel); | |
| titleLayout.addComponent(subTitleLabel); | |
| } | |
| if (description != null) { | |
| Label infoIcon = new Label(VaadinIcons.INFO_CIRCLE.getHtml(), ContentMode.HTML); | |
| infoIcon.setDescription(I18nProperties.getDescription(Descriptions.sampleDashboardHumans)); | |
| infoIcon.addStyleName(CssStyles.HSPACE_LEFT_4); | |
| infoIcon.addStyleNames(CssStyles.H3); | |
| titleLayout.addComponent(infoIcon); | |
| } | |
| // Change the super call to stop passing the info‐text | |
| - super(titleCaption, description, showInfoIcon ? I18nProperties.getString(Strings.infoDashboardFinalLaboratoryResult) : ""); | |
| + super(titleCaption, description, ""); | |
| HorizontalLayout titleLayout = new HorizontalLayout(); | |
| titleLayout.setMargin(false); | |
| titleLayout.setSpacing(false); | |
| addComponent(titleLayout); | |
| if (subtitleCaption != null) { | |
| Label subTitleLabel = new Label(I18nProperties.getCaption(subtitleCaption)); | |
| CssStyles.style(subTitleLabel, CssStyles.H3, CssStyles.VSPACE_TOP_5); | |
| titleLayout.addComponent(subTitleLabel); | |
| } | |
| - if (description != null) { | |
| + if (showInfoIcon) { | |
| Label infoIcon = new Label(VaadinIcons.INFO_CIRCLE.getHtml(), ContentMode.HTML); | |
| infoIcon.setDescription(I18nProperties.getDescription(Descriptions.sampleDashboardHumans)); | |
| infoIcon.addStyleName(CssStyles.HSPACE_LEFT_4); | |
| infoIcon.addStyleNames(CssStyles.H3); | |
| titleLayout.addComponent(infoIcon); | |
| } |
🤖 Prompt for AI Agents
In
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/surveillance/components/statistics/LaboratoryResultsStatisticsComponent.java
around lines 40 to 56, the info icon can be rendered twice because the
superclass may add it when showInfoIcon is true and this constructor always adds
an info icon when description != null; remove the info text argument passed to
super so the superclass does not auto-add the icon, and wrap the local info-icon
creation and addition with a check for showInfoIcon (i.e., only create/add the
Label(VaadinIcons.INFO_CIRCLE...) when showInfoIcon is true and description !=
null) so the icon appears only once and only when explicitly requested.
| public EnvironmentSampleDto createEnvironmentSample( | ||
| EnvironmentReferenceDto environment, | ||
| UserReferenceDto reportingUser, | ||
| RDCF rdcf, | ||
| FacilityReferenceDto lab, | ||
| @Nullable Consumer<EnvironmentSampleDto> extraConfig) { | ||
| EnvironmentSampleDto sample = EnvironmentSampleDto.build(environment, reportingUser); | ||
| sample.setSampleMaterial(EnvironmentSampleMaterial.WATER); | ||
| sample.getLocation().setRegion(rdcf.region); | ||
| sample.getLocation().setLatitude(1.0); | ||
| sample.getLocation().setLongitude(1.0); | ||
| sample.getLocation().setDistrict(rdcf.district); | ||
| sample.setLaboratory(lab); | ||
| if (extraConfig != null) { | ||
| extraConfig.accept(sample); | ||
| } | ||
|
|
||
| return beanTest.getEnvironmentSampleFacade().save(sample); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Guard against null RDCF to prevent NPEs when populating location
createEnvironmentSample unconditionally dereferences rdcf. Either mark rdcf as @NotNull or guard against null like other helpers do. Also keep latitude/longitude assignment outside the guard.
Apply this diff:
- sample.getLocation().setRegion(rdcf.region);
- sample.getLocation().setLatitude(1.0);
- sample.getLocation().setLongitude(1.0);
- sample.getLocation().setDistrict(rdcf.district);
+ if (rdcf != null) {
+ sample.getLocation().setRegion(rdcf.region);
+ sample.getLocation().setDistrict(rdcf.district);
+ }
+ sample.getLocation().setLatitude(1.0);
+ sample.getLocation().setLongitude(1.0);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public EnvironmentSampleDto createEnvironmentSample( | |
| EnvironmentReferenceDto environment, | |
| UserReferenceDto reportingUser, | |
| RDCF rdcf, | |
| FacilityReferenceDto lab, | |
| @Nullable Consumer<EnvironmentSampleDto> extraConfig) { | |
| EnvironmentSampleDto sample = EnvironmentSampleDto.build(environment, reportingUser); | |
| sample.setSampleMaterial(EnvironmentSampleMaterial.WATER); | |
| sample.getLocation().setRegion(rdcf.region); | |
| sample.getLocation().setLatitude(1.0); | |
| sample.getLocation().setLongitude(1.0); | |
| sample.getLocation().setDistrict(rdcf.district); | |
| sample.setLaboratory(lab); | |
| if (extraConfig != null) { | |
| extraConfig.accept(sample); | |
| } | |
| return beanTest.getEnvironmentSampleFacade().save(sample); | |
| } | |
| public EnvironmentSampleDto createEnvironmentSample( | |
| EnvironmentReferenceDto environment, | |
| UserReferenceDto reportingUser, | |
| RDCF rdcf, | |
| FacilityReferenceDto lab, | |
| @Nullable Consumer<EnvironmentSampleDto> extraConfig) { | |
| EnvironmentSampleDto sample = EnvironmentSampleDto.build(environment, reportingUser); | |
| sample.setSampleMaterial(EnvironmentSampleMaterial.WATER); | |
| if (rdcf != null) { | |
| sample.getLocation().setRegion(rdcf.region); | |
| sample.getLocation().setDistrict(rdcf.district); | |
| } | |
| sample.getLocation().setLatitude(1.0); | |
| sample.getLocation().setLongitude(1.0); | |
| sample.setLaboratory(lab); | |
| if (extraConfig != null) { | |
| extraConfig.accept(sample); | |
| } | |
| return beanTest.getEnvironmentSampleFacade().save(sample); | |
| } |
🤖 Prompt for AI Agents
In sormas-ui/src/test/java/de/symeda/sormas/backend/TestDataCreator.java around
lines 1437-1455, the method createEnvironmentSample dereferences rdcf
unconditionally which can cause an NPE; either annotate the rdcf parameter as
@NotNull or (preferred) add a null guard: keep the latitude and longitude
assignments outside the guard, and wrap only the rdcf-dependent calls
(sample.getLocation().setRegion(...); sample.getLocation().setDistrict(...);) in
an if (rdcf != null) { ... } block so the method is safe when rdcf is null.
|
SonarCloud analysis: https://sonarcloud.io/dashboard?id=SORMAS-Project&pullRequest=13577 |
There was a problem hiding this comment.
Actionable comments posted: 2
🔭 Outside diff range comments (1)
sormas-ui/src/test/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProviderTest.java (1)
82-82: Ensure Maven modules compile with Java 11 forvarsupportI searched all POMs and found no
<java.version>,<maven.compiler.source>,<maven.compiler.target>, or<release>settings—Maven defaults to Java 1.5/1.6. Meanwhile, the Gradle modules target Java 11 (source/targetCompatibility). To avoid CI failures on the newvarinSampleDashboardDataProviderTest.java, please add or update your Maven compiler configuration (insormas-ui/pom.xmlor the parent POM):• Location to update:
- sormas-ui/pom.xml (or its parent POM)
• Example snippet:
<properties> <java.version>11</java.version> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <release>${java.version}</release> </configuration> </plugin> </plugins> </build>If you already manage Java toolchains in an external parent POM, confirm it sets the release/source/target to at least 11 for all modules (including tests).
♻️ Duplicate comments (4)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardView.java (1)
163-171: Fix compile-time type mismatch and NPE risk when summing totalsStream.concat over entry sets with different key types won’t compile, and getEnvironmentSampleCount() may be null. Compute human and environment totals independently and guard the environment map.
Apply this diff:
- // combining sample counts of humans and environment samples - long totalSamples = Stream.concat(dataProvider.getSampleCountsByResultType().entrySet().stream(), dataProvider.getEnvironmentSampleCount().entrySet() - .stream()) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue(), Long::sum)) - .values() - .stream() - .mapToLong(Long::longValue).sum(); + // Combine totals from human and environment samples (different key types) + long humanTotal = dataProvider.getSampleCountsByResultType() + .values() + .stream() + .mapToLong(Long::longValue) + .sum(); + long environmentTotal = Optional.ofNullable(dataProvider.getEnvironmentSampleCount()) + .map(m -> m.values().stream().mapToLong(Long::longValue).sum()) + .orElse(0L); + long totalSamples = humanTotal + environmentTotal; heading.updateTotalLabel(String.valueOf(totalSamples)); //Final laboratory results only for human samples.sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/components/SampleDashboardMapComponent.java (2)
154-156: Handle mixed criteria: load both human and environment samplesWith both human and environment materials selected, the current else branch loads nothing. Fetch both sets to mirror DataProvider behavior; gate environment by showEnvironmentalSamples.
Apply this diff:
- } else{ - // In the case of mixed sample criteria, we want to show both human and environment samples; for now this is not supported - } + } else { + // Mixed selection: show both human and environment samples (environment gated by permission/checkbox) + humanSamples = FacadeProvider.getSampleDashboardFacade().getSamplesForMap(criteria, displayedHumanSamples); + environmentSamples = showEnvironmentalSamples + ? FacadeProvider.getSampleDashboardFacade().getEnvironmentalSamplesForMap(criteria) + : Collections.emptyList(); + }
158-158: Fix initial capacity: sum human + environment sizesYou add environmentSamples.size() twice. Use humanSamples.size() + environmentSamples.size() to prevent unnecessary resizes.
Apply this diff:
- List<LeafletMarker> markers = new ArrayList<>(environmentSamples.size() + environmentSamples.size()); + List<LeafletMarker> markers = new ArrayList<>(humanSamples.size() + environmentSamples.size());sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (1)
287-291: Thanks for addressing prior NPE risk with null test resultsFiltering out tuples with a null result ensures toMap doesn’t NPE and aligns with the earlier review feedback.
🧹 Nitpick comments (6)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardView.java (1)
176-182: Optional: Deduplicate map-merging with a small helperYou repeat the same null-safe merge pattern twice. Consider extracting a generic helper to reduce duplication and future mistakes.
Example helper (place it as a private static method in this class):
private static <K> Map<K, Long> mergedCounts(Map<K, Long> human, Map<K, Long> environment) { Map<K, Long> result = new HashMap<>(human); Optional.ofNullable(environment).ifPresent(m -> m.forEach((k, v) -> result.merge(k, v, Long::sum))); return result; }Then replace the current blocks with:
- Map<SpecimenCondition, Long> resultSpecimenConditionMap = new HashMap<>(dataProvider.getSampleCountsBySpecimenCondition()); - Optional.ofNullable(dataProvider.getEnvSampleCountsBySpecimenCondition()).ifPresent(m -> m.forEach((condition, count) -> { - resultSpecimenConditionMap.merge(condition, count, Long::sum); - })); - countsBySpecimenCondition.update(resultSpecimenConditionMap); + countsBySpecimenCondition.update( + mergedCounts(dataProvider.getSampleCountsBySpecimenCondition(), dataProvider.getEnvSampleCountsBySpecimenCondition()) + );- Map<SampleShipmentStatus, Long> resultShipmentStatusMap = new HashMap<>(dataProvider.getSampleCountsByShipmentStatus()); - Optional.ofNullable(dataProvider.getEnvSampleCountsByShipmentStatus()).ifPresent(m -> m.forEach((status, count) -> { - resultShipmentStatusMap.merge(status, count, Long::sum); - })); - countsByShipmentStatus.update(resultShipmentStatusMap); + countsByShipmentStatus.update( + mergedCounts(dataProvider.getSampleCountsByShipmentStatus(), dataProvider.getEnvSampleCountsByShipmentStatus()) + );Also applies to: 183-189
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/components/SampleDashboardMapComponent.java (2)
83-101: Nit: Javadoc wording is inaccurateThe criteria object isn’t “null”; you’re checking that no sample materials are selected. Consider tightening the doc to avoid confusion.
Apply this diff to both comments:
- /** - * Checks if the sample criteria is null, meaning that no human sample material or environment sample material is selected. - * @param criteria - * @return boolean - */ + /** + * Checks whether no human or environment sample material is selected and no disease is set. + */And for the “with disease” variant:
- /** - * Checks if the sample criteria is null, meaning that no human sample material or environment sample material is selected. - * - * @param criteria - * @return boolean - */ + /** + * Checks whether no human or environment sample material is selected and a disease is set. + */
120-129: Nit: Incomplete Javadoc sentenceThe sentence ends abruptly and doesn’t describe the rule clearly.
Apply this diff:
- /** - * Checks if the sample criteria is for other sample materials, meaning that all other(Human+Environment) sample materials are selected except, - * - * @param criteria - * @return - */ + /** + * Checks whether the criteria targets “OTHER” in either human or environment sample materials. + */sormas-ui/src/test/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProviderTest.java (2)
55-58: Rename variable for clarity (typo: toDateDate).Minor readability nit: Rename
toDateDatetotoDate.- Date toDateDate = DateHelper.getDateZero(2023, 2, 25); + Date toDate = DateHelper.getDateZero(2023, 2, 25); @@ - dataProvider.setToDate(toDateDate); + dataProvider.setToDate(toDate);
72-78: Add a targeted test for mutually exclusive materials (human vs environment).Good check that environmentSampleMaterial is passed and sampleMaterial remains null. To prevent regressions, consider adding a dedicated test to assert that both materials cannot be set simultaneously (i.e., provider or criteria enforces mutual exclusivity).
Example test to add:
@Test public void testBuildDashboardCriteria_materialsAreMutuallyExclusive() { dataProvider.setSampleMaterial(SampleMaterial.CRUST); dataProvider.setEnvironmentSampleMaterial(EnvironmentSampleMaterial.AIR); // Depending on intended behavior, assert one of the following: // 1) Environment material wins and sample material is cleared: SampleDashboardCriteria criteria = dataProvider.buildDashboardCriteriaWithDates(); Assertions.assertNull(criteria.getSampleMaterial()); Assertions.assertEquals(EnvironmentSampleMaterial.AIR, criteria.getEnvironmentSampleMaterial()); // OR 2) Provider rejects the invalid combo: // Assertions.assertThrows(IllegalArgumentException.class, () -> dataProvider.buildDashboardCriteriaWithDates()); }sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (1)
267-272: Javadoc mismatch: method counts tests, not samplesThe query groups by PathogenTest.TEST_RESULT and counts PathogenTest rows, i.e., tests-by-result-type, not samples.
Update the Javadoc:
- /** - * Returns a map of counts of environment samples by result type. - * - * @param dashboardCriteria - * @return - */ + /** + * Returns a map of counts of pathogen tests by result type for environment samples. + * Groups by PathogenTest.TEST_RESULT and counts PathogenTest records. + * + * @param dashboardCriteria criteria to filter environment samples/tests + * @return map keyed by PathogenTestResultType with the number of tests + */
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
sormas-api/src/main/java/de/symeda/sormas/api/dashboard/sample/SampleDashboardFacade.java(2 hunks)sormas-api/src/main/resources/strings.properties(2 hunks)sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardFacadeEjb.java(2 hunks)sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java(5 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/diagram/AbstractEpiCurveComponent.java(4 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProvider.java(5 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardView.java(4 hunks)sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/components/SampleDashboardMapComponent.java(2 hunks)sormas-ui/src/test/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProviderTest.java(3 hunks)
✅ Files skipped from review due to trivial changes (1)
- sormas-api/src/main/resources/strings.properties
🚧 Files skipped from review as they are similar to previous changes (4)
- sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardFacadeEjb.java
- sormas-api/src/main/java/de/symeda/sormas/api/dashboard/sample/SampleDashboardFacade.java
- sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProvider.java
- sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/diagram/AbstractEpiCurveComponent.java
🧰 Additional context used
🧬 Code Graph Analysis (2)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/components/SampleDashboardMapComponent.java (1)
sormas-api/src/main/java/de/symeda/sormas/api/FacadeProvider.java (1)
FacadeProvider(127-594)
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (2)
sormas-backend/src/main/java/de/symeda/sormas/backend/common/CriteriaBuilderHelper.java (1)
CriteriaBuilderHelper(30-230)sormas-backend/src/main/java/de/symeda/sormas/backend/util/QueryHelper.java (1)
QueryHelper(22-353)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: android app test (27)
- GitHub Check: Lint Code Base
- GitHub Check: android app test (28)
- GitHub Check: android app test (26)
- GitHub Check: SORMAS CI
🔇 Additional comments (12)
sormas-ui/src/main/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardView.java (3)
106-106: Good: Human-only description is now explicitPassing Descriptions.sampleDashboardHumans clarifies scope for the lab results component.
112-112: Good: Purpose tiles correctly labeled as human-onlyUsing Descriptions.sampleDashboardHumans aligns messaging across widgets.
190-194: Good: Null-safe merge for environment test result countsThanks for guarding getEnvironmentTestResultCountsByResultType() with Optional and merging via Long::sum.
sormas-ui/src/test/java/de/symeda/sormas/ui/dashboard/sample/SampleDashboardDataProviderTest.java (3)
60-63: Verify intent: Disease set while withNoDisease=true.You’re setting both
disease(non-null) andwithNoDisease=true. If the intended semantics are mutual exclusivity, this combination might be misleading in a unit test that primarily validates criteria propagation.If the UI/provider enforces exclusivity in production, consider either:
- Clearing
withNoDiseasewhendiseaseis set, or- Adding a separate test that validates the exclusivity behavior explicitly.
108-125: Good negative coverage: disease filter excludes environment and human samples.Solid checks that combining an active disease filter with environment samples yields zero environment counts and also excludes unrelated human samples (AFP vs created CORONAVIRUS). This guards against mixing domains.
126-130: LGTM; duplicate assertion from earlier review is resolved.The assertions are now unique (environment test result counts, env specimen condition counts, and total environment sample count), addressing the prior duplication note.
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (6)
48-48: LGTM: Required import for environment materialThe new EnvironmentSampleMaterial import is used below and is appropriate.
151-157: Confirm semantics: received-only filter for environment specimen-condition countsHuman samples restrict to received=true for specimen-condition counts. The environment counterpart currently includes all environment samples regardless of received flag. If parity is intended, add a received filter; otherwise, please confirm the divergence is desired.
Example change:
- return getEnvironmentSampleCountsBySpecimenCondition( - EnvironmentSample.SPECIMEN_CONDITION, - SpecimenCondition.class, - dashboardCriteria, - null); + return getEnvironmentSampleCountsBySpecimenCondition( + EnvironmentSample.SPECIMEN_CONDITION, + SpecimenCondition.class, + dashboardCriteria, + (cb, root) -> cb.equal(root.get(EnvironmentSample.RECEIVED), true));
221-223: Good fix: merge duplicate shipment-status keys after normalizationUsing Long::sum prevents duplicate-key exceptions when different (shipped, received) pairs normalize to the same SampleShipmentStatus (e.g., null and false both map to false).
225-242: Verify DISPATCHED/RECEIVED semantics and normalizationImplementation mirrors human shipment counts and aggregates pairs via merge. Please confirm:
- DISPATCHED is the intended “shipped” analog for environment samples.
- Null flags should normalize to false (Boolean.TRUE.equals(...)) and be merged accordingly.
287-291: Mirror the null-result filter in the human counterpart for consistencyThe human variant getTestResultCountsByResultType (Lines 262-265) still collects without filtering null keys; it may NPE for tests without a result. Please align it with this implementation.
Suggested stream ending for the human method:
return QueryHelper.getResultList(em, cq, null, null, Function.identity()) .stream() .filter(t -> t.get(0) != null) .collect(Collectors.toMap(t -> (PathogenTestResultType) t.get(0), t -> (Long) t.get(1)));
573-575: LGTM: environment material filter correctly uses EnvironmentSampleMaterialSwitching to criteria.getEnvironmentSampleMaterial and EnvironmentSample.SAMPLE_MATERIAL is consistent with the new environment-specific criteria.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (1)
268-292: Nice: null-key filter avoids toMap NPE; align the human counterpart for consistencyThe null-key filter here is correct and prevents NPEs when PathogenTest.TEST_RESULT is null (LEFT JOIN). Please apply the same safeguard to the human method getTestResultCountsByResultType for consistency and robustness.
- Option A (stream-level): add
.filter(t -> t.get(0) != null)before collecting.- Option B (DB-level): add
cb.isNotNull(pathogenTestResult)to the WHERE clause and omit the stream filter.Example for the human method (illustrative only; outside this range):
return QueryHelper.getResultList(em, cq, null, null, Function.identity()) .stream() .filter(t -> t.get(0) != null) .collect(Collectors.toMap(t -> (PathogenTestResultType) t.get(0), t -> (Long) t.get(1)));Option B is preferable to avoid transferring null-key rows from the DB.
Run this script to list all toMap usages in this file that may still collect null keys without filtering:
#!/bin/bash set -euo pipefail file="sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java" echo "Potential risky toMap usages (no preceding null-key filter on t.get(0)):" # Find lines with toMap and include a bit of context rg -nA2 -e 'Collectors\.toMap\(' "$file" echo echo "Occurrences of stream null-key filters:" rg -n -e '\.filter\s*\(\s*t\s*->\s*t\.get\(\s*0\s*\)\s*!=\s*null\s*\)' "$file" || true
🧹 Nitpick comments (2)
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (2)
159-179: Filter null grouping keys at the DB level to avoid unnecessary rowsGood call adding a stream-level null-key filter to avoid Collectors.toMap NPEs. You can push this check into the WHERE clause to avoid transferring null-key groups from the DB and drop the stream filter.
Suggested change:
- cq.where(CriteriaBuilderHelper.and(cb, criteriaFilter, additionalFilters != null ? additionalFilters.apply(cb, sample) : null)); + cq.where(CriteriaBuilderHelper.and( + cb, + criteriaFilter, + additionalFilters != null ? additionalFilters.apply(cb, sample) : null, + cb.isNotNull(groupingProperty)));With that in place, the stream
.filter(t -> t.get(0) != null)becomes redundant and can be removed.
300-314: Push null-material filtering into WHERE and simplify the streamCurrent stream-level filter is correct; you can offload it to the database and remove the stream filter.
Apply this diff:
- cq.where(criteriaFilter); + cq.where(CriteriaBuilderHelper.and(cb, criteriaFilter, cb.isNotNull(groupingProperty))); @@ - return QueryHelper.getResultList(em, cq, null, null, Function.identity()) - .stream() - .filter(t -> t.get(0) != null) - .collect(Collectors.toMap(t -> (EnvironmentSampleMaterial) t.get(0), t -> (Long) t.get(1))); + return QueryHelper.getResultList(em, cq, null, null, Function.identity()) + .stream() + .collect(Collectors.toMap(t -> (EnvironmentSampleMaterial) t.get(0), t -> (Long) t.get(1)));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardFacadeEjb.java(3 hunks)sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardFacadeEjb.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (2)
sormas-backend/src/main/java/de/symeda/sormas/backend/common/CriteriaBuilderHelper.java (1)
CriteriaBuilderHelper(30-230)sormas-backend/src/main/java/de/symeda/sormas/backend/util/QueryHelper.java (1)
QueryHelper(22-353)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: SORMAS CI
- GitHub Check: android app test (26)
- GitHub Check: android app test (28)
- GitHub Check: android app test (27)
- GitHub Check: Lint Code Base
🔇 Additional comments (5)
sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java (5)
48-48: Import usage looks correctEnvironmentSampleMaterial is used in the new environment counts method. No issues.
151-157: Parity with human counts: should we restrict to received environment samples?Human specimen-condition counts filter to external + received = true. The environment counterpart currently passes no additional filter, so it counts all env samples regardless of received status. If parity is desired, add a received=true filter.
Apply this diff:
- return getEnvironmentalSampleCountsBySpecimenCondition( - EnvironmentSample.SPECIMEN_CONDITION, - SpecimenCondition.class, - dashboardCriteria, - null); + return getEnvironmentalSampleCountsBySpecimenCondition( + EnvironmentSample.SPECIMEN_CONDITION, + SpecimenCondition.class, + dashboardCriteria, + (cb, root) -> cb.equal(root.get(EnvironmentSample.RECEIVED), true));
221-224: Good fix: merge function prevents duplicate-key errors; confirm null boolean mappingUsing a merge function for Collectors.toMap is the right choice because multiple shipped/received combinations can map to the same SampleShipmentStatus key. Please confirm that treating null booleans as false in getSampleShipmentStatusByFlags (via Boolean.TRUE.equals) is intended. Otherwise consider explicitly filtering or normalizing nulls in the query (e.g., cb.isNotNull on shipped/received) or mapping nulls to a distinct status.
Also applies to: 240-243
388-388: Method addition for environment sample map count looks goodSignature and usage mirror the human counterpart. No issues spotted.
575-577: Correct filter: use environment sample material criteriaSwitching to criteria.getEnvironmentSampleMaterial for environment samples is correct and consistent with the new API.
Fixes #
Summary by CodeRabbit
New Features
Tests