Skip to content

Commit

Permalink
Update find periods to allow arrays of sample site and technique ids.
Browse files Browse the repository at this point in the history
Update create/edit periods to enforce site, technique, and period information.
UI tweaks to create/edit periods pages.
Enforce site/technique/period validation on observations table.
Update sample sites left list, to show technique/period details.
  • Loading branch information
NickPhura committed Jan 10, 2025
1 parent 551f619 commit 0b230b3
Show file tree
Hide file tree
Showing 23 changed files with 384 additions and 449 deletions.
4 changes: 2 additions & 2 deletions api/src/models/period-view.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface IPeriodAdvancedFilters {
survey_id?: number;
sample_site_id?: number;
method_technique_id?: number;
sample_site_id?: number[];
method_technique_id?: number[];
system_user_id?: number;
}
18 changes: 12 additions & 6 deletions api/src/paths/periods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ GET.apiDoc = {
name: 'sample_site_id',
required: false,
schema: {
type: 'integer',
minimum: 1,
type: 'array',
items: {
type: 'integer',
minimum: 1
},
nullable: true
}
},
Expand All @@ -63,8 +66,11 @@ GET.apiDoc = {
name: 'method_technique_id',
required: false,
schema: {
type: 'integer',
minimum: 1,
type: 'array',
items: {
type: 'integer',
minimum: 1
},
nullable: true
}
},
Expand Down Expand Up @@ -276,8 +282,8 @@ export function findPeriods(): RequestHandler {
function parseQueryParams(req: Request<unknown, unknown, unknown, IPeriodAdvancedFilters>): IPeriodAdvancedFilters {
return {
survey_id: (req.query.survey_id && Number(req.query.survey_id)) ?? undefined,
sample_site_id: (req.query.sample_site_id && Number(req.query.sample_site_id)) ?? undefined,
method_technique_id: (req.query.method_technique_id && Number(req.query.method_technique_id)) ?? undefined,
sample_site_id: (req.query.sample_site_id && req.query.sample_site_id.map(Number)) ?? [],
method_technique_id: (req.query.method_technique_id && req.query.method_technique_id.map(Number)) ?? [],
system_user_id: req.query.system_user_id ?? undefined
};
}
1 change: 1 addition & 0 deletions api/src/repositories/analytics-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export class AnalyticsRepository extends BaseRepository {
groupByQualitativeMeasurements
)
)
.select('w_observations.survey_sample_period_id')
.from('w_observations');

if (groupByColumns.length) {
Expand Down
8 changes: 4 additions & 4 deletions api/src/repositories/sample-period-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,14 +407,14 @@ export class SamplePeriodRepository extends BaseRepository {
getSamplingPeriodsQuery.andWhere('survey_sample_period.survey_id', filterFields.survey_id);
}

if (filterFields.sample_site_id) {
if (filterFields.sample_site_id?.length) {
// Filter by a specific sample site id
getSamplingPeriodsQuery.andWhere('survey_sample_period.survey_sample_site_id', filterFields.sample_site_id);
getSamplingPeriodsQuery.whereIn('survey_sample_period.survey_sample_site_id', filterFields.sample_site_id);
}

if (filterFields.method_technique_id) {
if (filterFields.method_technique_id?.length) {
// Filter by a specific sample method id
getSamplingPeriodsQuery.andWhere('survey_sample_period.method_technique_id', filterFields.method_technique_id);
getSamplingPeriodsQuery.whereIn('survey_sample_period.method_technique_id', filterFields.method_technique_id);
}

return getSamplingPeriodsQuery;
Expand Down
21 changes: 12 additions & 9 deletions api/src/services/observation-services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,27 @@ export function pullSamplingDataFromWorksheetRowObject(
periodsMatchingTechnique,
periodsMatchingPeriod
);

if (matchingPeriods.length === 1) {
// Found exactly one period that matches some or all of the filters above
// Found exactly one period record that uniquely matches some or all of the filters above
return _formatMatchingPeriod(matchingPeriods[0]);
}

// If no single period was matched above, then attempt to find a suitable period based on the observation date and time
// If no single period record was matched above, then attempt to find a suitable unique period based on the
// observation date and time
const observationDate = getColumnCellValue(row, 'DATE').cell as string | null;
const observationTime = getColumnCellValue(row, 'TIME').cell as string | null;

const suitablePeriods = _findSamplePeriodFromWorksheetDateAndTime(observationDate, observationTime, samplingPeriods);

if (suitablePeriods.length) {
// If at least one period is found, return the first one
// If at least one period record is found that satisfies the observation date and time, then return the first one
return _formatMatchingPeriod(suitablePeriods[0]);
}

// TODO Nick: If no period record is found by date/time, but multiple were found by site, technique, or period,
// should we return the first one here? Or should we return null to indicate that the observation record is invalid?

// Unable to match this observation record to any existing period
return null;
}
Expand All @@ -95,7 +100,7 @@ function _findSamplePeriodFromWorksheetSite(
siteName: string,
samplingPeriods: SurveySamplePeriodDetails[]
): SurveySamplePeriodDetails[] {
return samplingPeriods.filter((period: any) => period.survey_sample_site.name === siteName);
return samplingPeriods.filter((period) => period?.survey_sample_site?.name === siteName);
}

/**
Expand All @@ -111,7 +116,7 @@ function _findSamplePeriodFromWorksheetTechnique(
techniqueName: string,
samplingPeriods: SurveySamplePeriodDetails[]
): SurveySamplePeriodDetails[] {
return samplingPeriods.filter((period: any) => period.method_technique.name === techniqueName);
return samplingPeriods.filter((period) => period?.method_technique?.name === techniqueName);
}

/**
Expand All @@ -135,7 +140,7 @@ function _findSamplePeriodFromWorksheetPeriod(

// Find matching periods by date
let matchingPeriods = samplingPeriods.filter(
(period: any) => period.start_date === startDate && period.end_date === endDate
(period) => period.start_date === startDate && period.end_date === endDate
);

// Return if exactly one period matches by date
Expand All @@ -144,9 +149,7 @@ function _findSamplePeriodFromWorksheetPeriod(
}

// If multiple periods match by date, try matching additionally by time
matchingPeriods = matchingPeriods.filter(
(period: any) => period.start_time === startTime && period.end_time === endTime
);
matchingPeriods = matchingPeriods.filter((period) => period.start_time === startTime && period.end_time === endTime);

return matchingPeriods;
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/services/technique-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class TechniqueService extends DBService {
const samplePeriodService = new SamplePeriodService(this.connection);
await samplePeriodService
.findSamplePeriodsCount(false, this.connection.systemUserId(), {
method_technique_id: methodTechniqueId,
method_technique_id: [methodTechniqueId],
survey_id: surveyId
})
.then((count) => {
Expand Down
2 changes: 2 additions & 0 deletions app/src/contexts/dialogContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ export const DialogContextProvider: React.FC<React.PropsWithChildren> = (props)
setScoreDialogProps((prev) => ({ ...prev, ...partialProps }));
};

console.log(snackbarProps);

return (
<DialogContext.Provider
value={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,15 @@ export const findMissingSamplingColumns = (
tableColumns: GridColDef[]
): ObservationRowValidationError[] => {
const errors: ObservationRowValidationError[] = [];
// if this row has survey_sample_site_id we need to validate that the other 2 sampling columns are also present
if (row['method_technique_id']) {
// If the row has any of the sampling columns set, then all of them must be set.
// Technically, only the period is required, but a period can only be selected if the site and technique are
// selected as well.
if (row['survey_sample_site_id'] || row['method_technique_id'] || row['survey_sample_period_id']) {
if (!row['survey_sample_site_id']) {
const header = tableColumns.find((tc) => tc.field === 'survey_sample_site_id')?.headerName;
errors.push({ field: 'survey_sample_site_id', message: `Missing column: ${header}` });
}

if (!row['method_technique_id']) {
const header = tableColumns.find((tc) => tc.field === 'method_technique_id')?.headerName;
errors.push({ field: 'method_technique_id', message: `Missing column: ${header}` });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Box from '@mui/material/Box';
import List from '@mui/material/List';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
import { SamplingSiteListPeriodContainer } from 'features/surveys/observations/sampling-sites/site/accordion-details/period/SamplingSiteListPeriodContainer';
import { SamplingStratumChips } from 'features/surveys/sampling-information/sites/edit/form/SamplingStratumChips';
import { useBiohubApi } from 'hooks/useBioHubApi';
import { useSurveyContext } from 'hooks/useContext';
Expand Down Expand Up @@ -29,12 +30,25 @@ export const SamplingSiteListContent = (props: ISamplingSiteListContentProps) =>
biohubApi.samplingSite.getSampleSiteById(projectId, surveyId, surveySampleSiteId)
);

const samplePeriodDataLoader = useDataLoader(() =>
biohubApi.samplingPeriod.findSamplePeriods({
survey_id: surveyId,
sample_site_id: [surveySampleSiteId]
})
);

useEffect(() => {
sampleSiteDataLoader.load();
}, [sampleSiteDataLoader]);

useEffect(() => {
samplePeriodDataLoader.load();
}, [samplePeriodDataLoader]);

const sampleSite = sampleSiteDataLoader.data;

const samplePeriods = samplePeriodDataLoader.data?.periods ?? [];

if (!sampleSite) {
return (
<Stack gap={1} px={1} flex="1 1 auto">
Expand All @@ -47,34 +61,18 @@ export const SamplingSiteListContent = (props: ISamplingSiteListContentProps) =>
}

return (
<>
<Box mb={2} mx={2}>
{sampleSite.stratums && sampleSite.stratums.length > 0 && (
<Box display="flex" alignItems="center" color="textSecondary" py={1} px={1.5}>
<Box display="flex" alignItems="center" color="textSecondary" pb={1}>
<SamplingStratumChips stratums={sampleSite.stratums} />
</Box>
)}
<List
disablePadding
sx={{
mx: 1.5,
'& .MuiListItemText-primary': {
typography: 'body2',
pt: 1
}
}}>
{/* // TODO Nick */}
{/* {sampleSite.sample_methods?.map((sampleMethod, index) => {
return (
<SamplingSiteListMethod
sampleMethod={sampleMethod}
key={`${sampleMethod.survey_sample_site_id}-${sampleMethod.survey_sample_method_id}-${index}`}
/>
);
})} */}
<List disablePadding sx={{ '& .MuiListItemText-primary': { typography: 'body2' }, pb: 1 }}>
<SamplingSiteListPeriodContainer samplePeriods={samplePeriods} />
</List>
<Box height="250px" flex="1 1 auto" mx={1} m={2}>
<Box height="250px" flex="1 1 auto">
<SamplingSiteListMap sampleSite={sampleSite} />
</Box>
</>
</Box>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { IObservationsContext } from 'contexts/observationsContext';
import { IObservationsPageContext } from 'contexts/observationsPageContext';
import dayjs from 'dayjs';
import { ImportObservationsButton } from 'features/surveys/observations/sampling-sites/components/ImportObservationsButton';
// import { ISurveySamplePeriodFormData } from 'features/surveys/sampling-information/periods/form/components/periods/SamplePeriodPeriodForm';
import { GetSamplingPeriod } from 'interfaces/useSamplingPeriodApi.interface';

interface ISamplingSiteListPeriodProps {
Expand All @@ -17,14 +16,17 @@ interface ISamplingSiteListPeriodProps {
observationsContext?: IObservationsContext;
}
/**
* Renders sampling periods for a sampling method
* Renders a timeline of sampling period dates.
*
* Includes an import observations button if the observationsPageContext and observationsContext are provided.
*
* @param props {ISamplingSiteListPeriodProps}
* @returns
*/
export const SamplingSiteListPeriod = (props: ISamplingSiteListPeriodProps) => {
const formatDate = (dt: Date, time: boolean) => dayjs(dt).format(time ? 'MMM D, YYYY h:mm A' : 'MMM D, YYYY');

const { observationsPageContext, observationsContext } = props;
const { samplePeriods, observationsPageContext, observationsContext } = props;

const dateSx = {
fontSize: '0.85rem',
Expand All @@ -36,7 +38,7 @@ export const SamplingSiteListPeriod = (props: ISamplingSiteListPeriodProps) => {
color: 'text.secondary'
};

const sortedSamplePeriods = props.samplePeriods.sort((a, b) => {
const sortedSamplePeriods = samplePeriods.sort((a, b) => {
if (!a.start_date && !b.start_date) {
return 0;
}
Expand Down Expand Up @@ -75,12 +77,12 @@ export const SamplingSiteListPeriod = (props: ISamplingSiteListPeriodProps) => {
m: 0,
p: 0
}}
key={`${samplePeriod.survey_sample_period_id}-${index}`}>
key={`sample-period-${samplePeriod.survey_sample_period_id}`}>
<TimelineSeparator sx={{ minWidth: 0, ml: 1, mr: 0.5 }}>
{props.samplePeriods.length > 1 ? (
{samplePeriods.length > 1 ? (
<Box display="flex" justifyContent="center">
<TimelineDot sx={{ bgcolor: grey[400], boxShadow: 'none' }} />
{index < props.samplePeriods.length - 1 && (
{index < samplePeriods.length - 1 && (
<TimelineConnector
sx={{
bgcolor: grey[400],
Expand All @@ -99,6 +101,7 @@ export const SamplingSiteListPeriod = (props: ISamplingSiteListPeriodProps) => {
</TimelineSeparator>
<TimelineContent
sx={{
pr: 0,
'& .MuiTimelineItem-root': {
width: '100%',
flex: '1 1 auto'
Expand Down
Loading

0 comments on commit 0b230b3

Please sign in to comment.