Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observations: Add indicators in Zones and Plots containing 25m sq plots #3307

Merged
merged 4 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 6 additions & 0 deletions src/redux/features/observations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,9 @@ const mergeSpecies = (
})
);
};

export const has25mPlots = (subzones: ObservationPlantingSubzoneResults[]) => {
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 @@ -134,6 +134,14 @@ export default function ObservationDetails(props: ObservationDetailsProps): JSX.
}
}, [zoneNames, searchProps.filtersProps]);

const has25mPlots = details?.plantingZones
.flatMap((zone) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the has25mPlots function here?

zone.plantingSubzones?.flatMap((subzone: { monitoringPlots: any[] }) =>
subzone.monitoringPlots.flatMap((plot) => plot.sizeMeters)
)
)
.some((size: number) => size.toString() === '25');

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={has25mPlots ? 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
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down