Skip to content

Commit

Permalink
Observations: Add indicators in Zones and Plots containing 25m sq plo…
Browse files Browse the repository at this point in the history
…ts (#3307)
  • Loading branch information
constanzauanini authored Nov 1, 2024
1 parent b8b9da0 commit 082f591
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 33 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/api/types/generated-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/redux/features/observations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
};
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -21,6 +22,7 @@ const ObservationDetailsRenderer =
return (
<Link fontSize='16px' to={url}>
{name as React.ReactNode}
{has25mPlots(row.plantingSubzones) ? '*' : ''}
</Link>
);
};
Expand Down
9 changes: 9 additions & 0 deletions src/scenes/ObservationsRouter/details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<DetailsPage title={title} plantingSiteId={plantingSiteId}>
<ObservationStatusSummaryMessage
Expand All @@ -155,6 +163,7 @@ export default function ObservationDetails(props: ObservationDetailsProps): JSX.
rows={details?.plantingZones ?? []}
orderBy='plantingZoneName'
Renderer={ObservationDetailsRenderer(plantingSiteId, observationId)}
tableComments={has25mPlotsZones() ? strings.PLOTS_SIZE_NOTE : undefined}
/>
</Box>
</Card>
Expand Down
55 changes: 55 additions & 0 deletions src/scenes/ObservationsRouter/plot/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string, any>[] = useMemo(() => {
Expand Down Expand Up @@ -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 (
<Link
key={`plot-link-${index}`}
to={APP_PATHS.OBSERVATION_MONITORING_PLOT_DETAILS.replace(
':plantingSiteId',
Number(plantingSiteId).toString()
)
.replace(':observationId', Number(observationId).toString())
.replace(
':plantingZoneId',
Number(plantingZoneId).toString().replace(':monitoringPlotId', found.monitoringPlotId.toString())
)}
>
{found.monitoringPlotName}
</Link>
);
}
return undefined;
}) || [];

const elements = (names ?? []).filter((element): element is JSX.Element => element !== undefined);
return elements;
};

return (
<DetailsPage
title={monitoringPlot?.monitoringPlotName ?? ''}
Expand All @@ -129,6 +176,14 @@ export default function ObservationMonitoringPlot(): JSX.Element {
/>
</Grid>
))}
{monitoringPlot?.overlapsWithPlotIds && monitoringPlot.overlapsWithPlotIds.length > 0 && (
<Grid item xs={gridSize} marginTop={2}>
<Typography fontSize='14px' fontWeight={400} color={theme.palette.TwClrTxtSecondary}>
{strings.PLOT_REPLACES_THIS_25M_PLOT}
</Typography>
{getReplacedPlotsNames()}
</Grid>
)}
</Grid>
{title(strings.NUMBER_OF_LIVE_PLANTS_PER_SPECIES)}
<Box height='360px'>
Expand Down
146 changes: 118 additions & 28 deletions src/scenes/ObservationsRouter/schedule/ObservationSubzoneSelector.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,6 +19,58 @@ const ObservationSubzoneSelector = ({ onChangeSelectedSubzones, plantingSite }:
const theme = useTheme();

const [selectedSubzones, setSelectedSubzones] = useState(new Map<number, boolean>());
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
Expand Down Expand Up @@ -63,32 +119,66 @@ const ObservationSubzoneSelector = ({ onChangeSelectedSubzones, plantingSite }:

return (
<Grid container spacing={3}>
{plantingSite.plantingZones?.map((zone, index) => (
<Grid item xs={12} key={index}>
<Checkbox
id={`observation-zone-${zone.id}`}
indeterminate={isZonePartiallySelected(zone)}
label={zone.name}
name='Limit Observation to Zone'
onChange={(value) => onChangeZoneCheckbox(zone, value)}
value={isZoneFullySelected(zone)}
/>

<Box sx={{ columnCount: 2, columnGap: theme.spacing(3), paddingLeft: `${theme.spacing(4)}` }}>
{zone.plantingSubzones.map((subzone, _index) => (
<Box sx={{ display: 'inline-block', width: '100%' }} key={_index}>
<Checkbox
id={`observation-subzone-${zone.id}`}
label={subzone.totalPlants ? subzone.name : <Box>⚠️ {subzone.name}</Box>}
name='Limit Observation to Subzone'
onChange={(value) => onChangeSubzoneCheckbox(subzone.id, value)}
value={selectedSubzones.get(subzone.id)}
/>
</Box>
))}
</Box>
</Grid>
))}
{plantingSite.plantingZones?.map((zone, index) => {
const lastZoneOb = lastZoneObservation(zoneObservations?.[zone.id]);
return (
<Grid item xs={12} key={index}>
<Checkbox
id={`observation-zone-${zone.id}`}
indeterminate={isZonePartiallySelected(zone)}
label={zone.name}
name='Limit Observation to Zone'
onChange={(value) => onChangeZoneCheckbox(zone, value)}
value={isZoneFullySelected(zone)}
/>
<Typography
sx={{
display: 'inline',
fontWeight: 500,
color: theme.palette.TwClrTxtSecondary,
verticalAlign: 'bottom',
paddingLeft: 1,
}}
>
{lastZoneOb
? strings.formatString(strings.LAST_OBSERVATION, lastZoneOb.startDate || '')
: strings.NO_OBSERVATIONS_HAVE_BEEN_SCHEDULED}
</Typography>

<Box sx={{ columnGap: theme.spacing(3), paddingLeft: `${theme.spacing(4)}` }}>
{zone.plantingSubzones.map((subzone, _index) => {
const lastSubzoneOb = lastSubZoneObservation(zone.id, subzone.id);
return (
<Box sx={{ display: 'inline-block', width: '100%' }} key={_index}>
<Checkbox
id={`observation-subzone-${zone.id}`}
label={subzone.totalPlants ? subzone.name : <Box>⚠️ {subzone.name}</Box>}
name='Limit Observation to Subzone'
onChange={(value) => onChangeSubzoneCheckbox(subzone.id, value)}
value={selectedSubzones.get(subzone.id)}
/>
{lastZoneOb && (
<Typography
sx={{
display: 'inline',
fontWeight: 500,
color: theme.palette.TwClrTxtSecondary,
verticalAlign: 'bottom',
paddingLeft: 1,
}}
>
{lastSubzoneOb
? strings.formatString(strings.LAST_OBSERVATION, lastSubzoneOb.startDate || '')
: strings.NO_OBSERVATIONS_HAVE_BEEN_SCHEDULED}
</Typography>
)}
</Box>
);
})}
</Box>
</Grid>
);
})}
<Grid item xs={12}>
<Box>⚠️: {strings.NO_PLANTS}</Box>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const ObservationPlantingZoneRenderer =
return (
<Link fontSize='16px' to={url}>
{name as React.ReactNode}
{row.sizeMeters.toString() === '25' ? '*' : ''}
</Link>
);
};
Expand Down
6 changes: 6 additions & 0 deletions src/scenes/ObservationsRouter/zone/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -184,6 +185,11 @@ export default function ObservationPlantingZone(): JSX.Element {
plantingZoneId,
setReplaceObservationPlot
)}
tableComments={
plantingZone?.plantingSubzones && has25mPlots(plantingZone.plantingSubzones)
? strings.PLOTS_SIZE_NOTE
: undefined
}
/>
</Box>
</Card>
Expand Down
4 changes: 4 additions & 0 deletions src/strings/csv/en.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 082f591

Please sign in to comment.