diff --git a/package.json b/package.json
index 719068ac87d..1529252086c 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
"@mui/styled-engine-sc": "^5.14.12",
"@mui/styles": "^5.15.15",
"@reduxjs/toolkit": "^1.9.3",
- "@terraware/web-components": "^3.4.3",
+ "@terraware/web-components": "^3.4.4",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.3.0",
"@testing-library/user-event": "^14.4.3",
diff --git a/src/api/types/generated-schema.ts b/src/api/types/generated-schema.ts
index f21b2770210..a2a5bd74b65 100644
--- a/src/api/types/generated-schema.ts
+++ b/src/api/types/generated-schema.ts
@@ -3720,6 +3720,10 @@ export interface components {
*/
mortalityRate?: number;
notes?: string;
+ /** @description IDs of any newer monitoring plots that overlap with this one. */
+ overlappedByPlotIds: number[];
+ /** @description IDs of any older monitoring plots this one overlaps with. */
+ overlapsWithPlotIds: number[];
photos: components["schemas"]["ObservationMonitoringPlotPhotoPayload"][];
/**
* Format: int32
diff --git a/src/redux/features/observations/utils.ts b/src/redux/features/observations/utils.ts
index ba598b2f557..7a5f011f8be 100644
--- a/src/redux/features/observations/utils.ts
+++ b/src/redux/features/observations/utils.ts
@@ -270,3 +270,11 @@ const mergeSpecies = (
})
);
};
+
+export const has25mPlots = (
+ subzones: ObservationPlantingSubzoneResults[] | ObservationPlantingSubzoneResultsPayload[]
+) => {
+ return subzones
+ ?.flatMap((subzone: { monitoringPlots: any[] }) => subzone.monitoringPlots.flatMap((plot) => plot.sizeMeters))
+ .some((size: number) => size.toString() === '25');
+};
diff --git a/src/scenes/ObservationsRouter/details/ObservationDetailsRenderer.tsx b/src/scenes/ObservationsRouter/details/ObservationDetailsRenderer.tsx
index 3aa65c94c97..14174d4a805 100644
--- a/src/scenes/ObservationsRouter/details/ObservationDetailsRenderer.tsx
+++ b/src/scenes/ObservationsRouter/details/ObservationDetailsRenderer.tsx
@@ -4,6 +4,7 @@ import Link from 'src/components/common/Link';
import CellRenderer, { TableRowType } from 'src/components/common/table/TableCellRenderer';
import { RendererProps } from 'src/components/common/table/types';
import { APP_PATHS } from 'src/constants';
+import { has25mPlots } from 'src/redux/features/observations/utils';
import { MonitoringPlotStatus, getPlotStatus } from 'src/types/Observations';
const NO_DATA_FIELDS = ['totalPlants', 'totalSpecies', 'mortalityRate'];
@@ -21,6 +22,7 @@ const ObservationDetailsRenderer =
return (
{name as React.ReactNode}
+ {has25mPlots(row.plantingSubzones) ? '*' : ''}
);
};
diff --git a/src/scenes/ObservationsRouter/details/index.tsx b/src/scenes/ObservationsRouter/details/index.tsx
index 89c41d26615..381438af6b3 100644
--- a/src/scenes/ObservationsRouter/details/index.tsx
+++ b/src/scenes/ObservationsRouter/details/index.tsx
@@ -14,6 +14,7 @@ import {
selectDetailsZoneNames,
} from 'src/redux/features/observations/observationDetailsSelectors';
import { selectObservation } from 'src/redux/features/observations/observationsSelectors';
+import { has25mPlots } from 'src/redux/features/observations/utils';
import { selectPlantingSite } from 'src/redux/features/tracking/trackingSelectors';
import { useAppSelector } from 'src/redux/store';
import AggregatedPlantsStats from 'src/scenes/ObservationsRouter/common/AggregatedPlantsStats';
@@ -134,6 +135,13 @@ export default function ObservationDetails(props: ObservationDetailsProps): JSX.
}
}, [zoneNames, searchProps.filtersProps]);
+ const has25mPlotsZones = () => {
+ const allSubzones = details?.plantingZones.flatMap((zone) => zone.plantingSubzones.flatMap((subzone) => subzone));
+ if (allSubzones) {
+ return has25mPlots(allSubzones);
+ }
+ };
+
return (
diff --git a/src/scenes/ObservationsRouter/plot/index.tsx b/src/scenes/ObservationsRouter/plot/index.tsx
index 5452e181198..c499d165846 100644
--- a/src/scenes/ObservationsRouter/plot/index.tsx
+++ b/src/scenes/ObservationsRouter/plot/index.tsx
@@ -5,8 +5,10 @@ import { Box, Grid, Typography, useTheme } from '@mui/material';
import { Textfield } from '@terraware/web-components';
import Card from 'src/components/common/Card';
+import Link from 'src/components/common/Link';
import { APP_PATHS } from 'src/constants';
import { useLocalization } from 'src/providers';
+import { searchObservationDetails } from 'src/redux/features/observations/observationDetailsSelectors';
import { selectObservationMonitoringPlot } from 'src/redux/features/observations/observationMonitoringPlotSelectors';
import { selectPlantingSite } from 'src/redux/features/tracking/trackingSelectors';
import { useAppSelector } from 'src/redux/store';
@@ -48,6 +50,19 @@ export default function ObservationMonitoringPlot(): JSX.Element {
const plantingSite = useAppSelector((state) => selectPlantingSite(state, Number(plantingSiteId)));
+ const details = useAppSelector((state) =>
+ searchObservationDetails(
+ state,
+ {
+ plantingSiteId: Number(plantingSiteId),
+ observationId: Number(observationId),
+ search: '',
+ zoneNames: [],
+ },
+ defaultTimeZone.get().id
+ )
+ );
+
const gridSize = isMobile ? 12 : 4;
const data: Record[] = useMemo(() => {
@@ -105,6 +120,38 @@ export default function ObservationMonitoringPlot(): JSX.Element {
}
}, [navigate, monitoringPlot, observationId, plantingZoneId, plantingSiteId]);
+ const getReplacedPlotsNames = (): JSX.Element[] => {
+ const names =
+ monitoringPlot?.overlapsWithPlotIds.map((plotId, index) => {
+ const allPlots = details?.plantingZones?.flatMap((pz) =>
+ pz.plantingSubzones.flatMap((subzone) => subzone.monitoringPlots.flatMap((plot) => plot))
+ );
+ const found = allPlots?.find((plot) => plot.monitoringPlotId === plotId);
+ if (found) {
+ return (
+
+ {found.monitoringPlotName}
+
+ );
+ }
+ return undefined;
+ }) || [];
+
+ const elements = (names ?? []).filter((element): element is JSX.Element => element !== undefined);
+ return elements;
+ };
+
return (
))}
+ {monitoringPlot?.overlapsWithPlotIds && monitoringPlot.overlapsWithPlotIds.length > 0 && (
+
+
+ {strings.PLOT_REPLACES_THIS_25M_PLOT}
+
+ {getReplacedPlotsNames()}
+
+ )}
{title(strings.NUMBER_OF_LIVE_PLANTS_PER_SPECIES)}
diff --git a/src/scenes/ObservationsRouter/schedule/ObservationSubzoneSelector.tsx b/src/scenes/ObservationsRouter/schedule/ObservationSubzoneSelector.tsx
index 8d544d44dcb..539c0fa6f7b 100644
--- a/src/scenes/ObservationsRouter/schedule/ObservationSubzoneSelector.tsx
+++ b/src/scenes/ObservationsRouter/schedule/ObservationSubzoneSelector.tsx
@@ -1,10 +1,14 @@
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
-import { Box, Grid, useTheme } from '@mui/material';
+import { Box, Grid, Typography, useTheme } from '@mui/material';
import { Checkbox } from '@terraware/web-components';
+import { selectObservations, selectObservationsResults } from 'src/redux/features/observations/observationsSelectors';
+import { useAppSelector } from 'src/redux/store';
import strings from 'src/strings';
+import { ObservationResultsPayload } from 'src/types/Observations';
import { PlantingSiteWithReportedPlants, PlantingZone } from 'src/types/Tracking';
+import { isAfter } from 'src/utils/dateUtils';
interface ObservationSubzoneSelectorProps {
onChangeSelectedSubzones: (requestedSubzoneIds: number[]) => void;
@@ -15,6 +19,58 @@ const ObservationSubzoneSelector = ({ onChangeSelectedSubzones, plantingSite }:
const theme = useTheme();
const [selectedSubzones, setSelectedSubzones] = useState(new Map());
+ const observationsData = useAppSelector(selectObservations);
+
+ const allObservationsResults = useAppSelector(selectObservationsResults);
+ const plantingSiteObservations = allObservationsResults?.filter(
+ (observation) => observation.plantingSiteId === plantingSite.id
+ );
+ const zoneObservations: ObservationResultsPayload[][] = [];
+ plantingSiteObservations?.forEach((observation) => {
+ observation.plantingZones.forEach((pz) => {
+ zoneObservations[pz.plantingZoneId]
+ ? zoneObservations[pz.plantingZoneId].push(observation)
+ : (zoneObservations[pz.plantingZoneId] = [observation]);
+ });
+ });
+
+ const lastZoneObservation = (observationsList: ObservationResultsPayload[]) => {
+ const observationsToProcess = observationsList;
+ if (observationsToProcess && observationsToProcess.length > 0) {
+ let lastObs = observationsToProcess[0];
+ observationsToProcess.forEach((obs) => {
+ if (isAfter(obs.startDate, lastObs.startDate)) {
+ lastObs = obs;
+ }
+ });
+ return lastObs;
+ }
+ };
+
+ const lastSubZoneObservation = useCallback(
+ (
+ zoneId: number,
+ subzoneId: number,
+ observationsList?: ObservationResultsPayload[]
+ ): ObservationResultsPayload | undefined => {
+ const observationsToProcess = observationsList ? observationsList : zoneObservations?.[zoneId];
+ const lastZoneObs = lastZoneObservation(observationsToProcess);
+ const foundObs = observationsData?.find((ob) => ob.id === lastZoneObs?.observationId);
+
+ if (foundObs) {
+ if (!foundObs.requestedSubzoneIds || foundObs.requestedSubzoneIds.includes(subzoneId)) {
+ return lastZoneObs;
+ } else {
+ const newZoneObservations = observationsToProcess.filter(
+ (ob) => ob.observationId !== lastZoneObs?.observationId
+ );
+
+ return lastSubZoneObservation(zoneId, subzoneId, newZoneObservations);
+ }
+ }
+ },
+ [zoneObservations, observationsData]
+ );
useEffect(() => {
// Initialize all subzone selections with subzoneId -> false unless they have totalPlants > 0
@@ -63,32 +119,66 @@ const ObservationSubzoneSelector = ({ onChangeSelectedSubzones, plantingSite }:
return (
- {plantingSite.plantingZones?.map((zone, index) => (
-
- onChangeZoneCheckbox(zone, value)}
- value={isZoneFullySelected(zone)}
- />
-
-
- {zone.plantingSubzones.map((subzone, _index) => (
-
- ⚠️ {subzone.name}}
- name='Limit Observation to Subzone'
- onChange={(value) => onChangeSubzoneCheckbox(subzone.id, value)}
- value={selectedSubzones.get(subzone.id)}
- />
-
- ))}
-
-
- ))}
+ {plantingSite.plantingZones?.map((zone, index) => {
+ const lastZoneOb = lastZoneObservation(zoneObservations?.[zone.id]);
+ return (
+
+ onChangeZoneCheckbox(zone, value)}
+ value={isZoneFullySelected(zone)}
+ />
+
+ {lastZoneOb
+ ? strings.formatString(strings.LAST_OBSERVATION, lastZoneOb.startDate || '')
+ : strings.NO_OBSERVATIONS_HAVE_BEEN_SCHEDULED}
+
+
+
+ {zone.plantingSubzones.map((subzone, _index) => {
+ const lastSubzoneOb = lastSubZoneObservation(zone.id, subzone.id);
+ return (
+
+ ⚠️ {subzone.name}}
+ name='Limit Observation to Subzone'
+ onChange={(value) => onChangeSubzoneCheckbox(subzone.id, value)}
+ value={selectedSubzones.get(subzone.id)}
+ />
+ {lastZoneOb && (
+
+ {lastSubzoneOb
+ ? strings.formatString(strings.LAST_OBSERVATION, lastSubzoneOb.startDate || '')
+ : strings.NO_OBSERVATIONS_HAVE_BEEN_SCHEDULED}
+
+ )}
+
+ );
+ })}
+
+
+ );
+ })}
⚠️: {strings.NO_PLANTS}
diff --git a/src/scenes/ObservationsRouter/zone/ObservationPlantingZoneRenderer.tsx b/src/scenes/ObservationsRouter/zone/ObservationPlantingZoneRenderer.tsx
index 00f000cf1ac..068e9745556 100644
--- a/src/scenes/ObservationsRouter/zone/ObservationPlantingZoneRenderer.tsx
+++ b/src/scenes/ObservationsRouter/zone/ObservationPlantingZoneRenderer.tsx
@@ -29,6 +29,7 @@ const ObservationPlantingZoneRenderer =
return (
{name as React.ReactNode}
+ {row.sizeMeters.toString() === '25' ? '*' : ''}
);
};
diff --git a/src/scenes/ObservationsRouter/zone/index.tsx b/src/scenes/ObservationsRouter/zone/index.tsx
index 4732991b5a5..0e6eb85e228 100644
--- a/src/scenes/ObservationsRouter/zone/index.tsx
+++ b/src/scenes/ObservationsRouter/zone/index.tsx
@@ -11,6 +11,7 @@ import Table from 'src/components/common/table';
import { APP_PATHS } from 'src/constants';
import { useLocalization, useOrganization } from 'src/providers';
import { searchObservationPlantingZone } from 'src/redux/features/observations/observationPlantingZoneSelectors';
+import { has25mPlots } from 'src/redux/features/observations/utils';
import { selectPlantingSite } from 'src/redux/features/tracking/trackingSelectors';
import { useAppSelector } from 'src/redux/store';
import AggregatedPlantsStats from 'src/scenes/ObservationsRouter/common/AggregatedPlantsStats';
@@ -184,6 +185,11 @@ export default function ObservationPlantingZone(): JSX.Element {
plantingZoneId,
setReplaceObservationPlot
)}
+ tableComments={
+ plantingZone?.plantingSubzones && has25mPlots(plantingZone.plantingSubzones)
+ ? strings.PLOTS_SIZE_NOTE
+ : undefined
+ }
/>
diff --git a/src/strings/csv/en.csv b/src/strings/csv/en.csv
index b937dc8bd93..44ec5b96141 100644
--- a/src/strings/csv/en.csv
+++ b/src/strings/csv/en.csv
@@ -752,6 +752,7 @@ LAST_EDITED_BY,Last Edited By,
LAST_MODIFIED_BY,Last modified by,
LAST_MODIFIED_ON,Last modified on,
LAST_NAME,Last Name,
+LAST_OBSERVATION,Last Observation: {0},
LAST_OBSERVED,Last Observed,
LAST_UPDATED,Last Updated,
LATITUDE_LONGITUDE,"Latitude, Longitude",
@@ -930,6 +931,7 @@ NO_LOCATION_FOUND,Location not found,
NO_MAP_DATA,No map data to show for this planting site.,
NO_NOTIFICATIONS,No notifications to show.,
NO_NURSERIES_NON_ADMIN_MSG,"Before you can add and manage your nursery inventory, you’ll need to have a nursery and species within Terraware. Only admins can add nurseries and species, so please reach out to yours for assistance.",
+NO_OBSERVATIONS_HAVE_BEEN_SCHEDULED,No observations have been scheduled,
NO_PARTICIPANT,No Participant,
NO_PLANTING_SITES_DESCRIPTION,Start by adding a planting site. Adding a planting site will allow you to track where seedlings go after they leave the nursery.,
NO_PLANTING_SITES_TITLE,"Just a moment, let’s add a planting site first.",
@@ -1195,8 +1197,10 @@ PLANTS,Plants,
PLANTS_CARD_DESCRIPTION,They’ve taken root! View all your plants in one place.,
PLANTS_PER_HECTARE,plants/ha,
PLEASE_TRY_AGAIN,Please try again.,
+PLOT_REPLACES_THIS_25M_PLOT,Plot replaces this 25x25m plot:
PLOT_TYPE,Plot Type,
PLOTS_PERMANENT,Permanent Plots,
+PLOTS_SIZE_NOTE,*Data collected with 25x25m plots,
PLOTS_TEMPORARY,Temporary Plots,
POSITION,Position,
POUNDS,Pounds,
diff --git a/yarn.lock b/yarn.lock
index 4c11523b9a8..50fb850afb2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3998,10 +3998,10 @@
dependencies:
defer-to-connect "^2.0.1"
-"@terraware/web-components@^3.4.3":
- version "3.4.3"
- resolved "https://registry.yarnpkg.com/@terraware/web-components/-/web-components-3.4.3.tgz#1027b0bddbd502eafe209191a3fdfe86030ed886"
- integrity sha512-UhXXLt9p2piN05y7ritNG5HADryPGg7dJ3oUwKyPcZEEJYKY5Ze+w0TRu3gP8WhlVUlzYzDc0Xrg7eVct89Qxg==
+"@terraware/web-components@^3.4.4":
+ version "3.4.4"
+ resolved "https://registry.yarnpkg.com/@terraware/web-components/-/web-components-3.4.4.tgz#d58ee7ad87f9c766618ca78a9b5b32e40d0c8602"
+ integrity sha512-9PvOr6Phl9NiGZuhwFb/padRRuphJtJaZZtHg2P7OR8AJjC0qlGAx2sUnEsRP/KHPdqkgmy7FtSqOXtb2uQvbg==
dependencies:
"@date-io/luxon" "^3.0.0"
"@dnd-kit/core" "^6.0.7"