diff --git a/.github/workflows/build_api.yml b/.github/workflows/build_api.yml index bf6293639..7ced1440e 100644 --- a/.github/workflows/build_api.yml +++ b/.github/workflows/build_api.yml @@ -47,9 +47,9 @@ jobs: with: path: | **/node_modules - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + key: ${{ runner.os }}-yarn-api-${{ hashFiles('yarn.lock') }} restore-keys: | - ${{ runner.os }}-yarn- + ${{ runner.os }}-yarn-api- - name: Install dependencies if needed. id: install diff --git a/packages/api/src/sites/sites.entity.ts b/packages/api/src/sites/sites.entity.ts index b12e129fc..e69de22b0 100644 --- a/packages/api/src/sites/sites.entity.ts +++ b/packages/api/src/sites/sites.entity.ts @@ -167,6 +167,8 @@ export class Site { maskedSpotterApiToken?: string; + waterQualitySources?: string[]; + @Expose() get applied(): boolean { return !!this.siteApplication?.permitRequirements; diff --git a/packages/api/src/sites/sites.service.ts b/packages/api/src/sites/sites.service.ts index 96025653c..044f71a13 100644 --- a/packages/api/src/sites/sites.service.ts +++ b/packages/api/src/sites/sites.service.ts @@ -25,6 +25,7 @@ import { filterMetricDataByDate, getConflictingExclusionDates, hasHoboDataSubQuery, + getWaterQualityDataSubQuery, getLatestData, getSite, createSite, @@ -199,11 +200,16 @@ export class SitesService { const hasHoboDataSet = await hasHoboDataSubQuery(this.sourceRepository); + const waterQualityDataSet = await getWaterQualityDataSubQuery( + this.latestDataRepository, + ); + return res.map((site) => ({ ...site, applied: site.applied, collectionData: mappedSiteData[site.id], hasHobo: hasHoboDataSet.has(site.id), + waterQualitySources: waterQualityDataSet.get(site.id), })); } diff --git a/packages/api/src/utils/site.utils.ts b/packages/api/src/utils/site.utils.ts index 089121437..c6eefba15 100644 --- a/packages/api/src/utils/site.utils.ts +++ b/packages/api/src/utils/site.utils.ts @@ -10,7 +10,7 @@ import { NotFoundException, } from '@nestjs/common'; import { ObjectLiteral, Repository } from 'typeorm'; -import { mapValues, some } from 'lodash'; +import { groupBy, mapValues, some } from 'lodash'; import geoTz from 'geo-tz'; import { Region } from '../regions/regions.entity'; import { ExclusionDates } from '../sites/exclusion-dates.entity'; @@ -23,6 +23,7 @@ import { LatestData } from '../time-series/latest-data.entity'; import { SiteSurveyPoint } from '../site-survey-points/site-survey-points.entity'; import { getHistoricalMonthlyMeans, getMMM } from './temperature'; import { HistoricalMonthlyMean } from '../sites/historical-monthly-mean.entity'; +import { Metric } from '../time-series/metrics.enum'; const googleMapsClient = new Client({}); const logger = new Logger('Site Utils'); @@ -278,6 +279,52 @@ export const hasHoboDataSubQuery = async ( return hasHoboDataSet; }; +export const getWaterQualityDataSubQuery = async ( + latestDataRepository: Repository, +): Promise> => { + const latestData: LatestData[] = await latestDataRepository + .createQueryBuilder('water_quality_data') + .select('site_id', 'siteId') + .addSelect('metric') + .addSelect('source') + .where(`source in ('${SourceType.HUI}', '${SourceType.SONDE}')`) + .getRawMany(); + + const sondeMetrics = [ + Metric.ODO_CONCENTRATION, + Metric.CHOLOROPHYLL_CONCENTRATION, + Metric.PH, + Metric.SALINITY, + Metric.TURBIDITY, + ]; + + const waterQualityDataSet = new Map(); + + Object.entries(groupBy(latestData, (o) => o.siteId)).forEach( + ([siteId, data]) => { + let sondeMetricsCount = 0; + const id = Number(siteId); + waterQualityDataSet.set(id, []); + data.forEach((siteData) => { + if (siteData.source === 'hui') { + // eslint-disable-next-line fp/no-mutating-methods + waterQualityDataSet.get(id)!.push('hui'); + } + if (sondeMetrics.includes(siteData.metric)) { + // eslint-disable-next-line fp/no-mutation + sondeMetricsCount += 1; + if (sondeMetricsCount >= 3) { + // eslint-disable-next-line fp/no-mutating-methods + waterQualityDataSet.get(id)!.push('sonde'); + } + } + }); + }, + ); + + return waterQualityDataSet; +}; + export const getLatestData = async ( site: Site, latestDataRepository: Repository, diff --git a/packages/website/src/common/SiteDetails/WaterSampling/index.tsx b/packages/website/src/common/SiteDetails/WaterSampling/index.tsx index 7a8639fd8..ae3027b23 100644 --- a/packages/website/src/common/SiteDetails/WaterSampling/index.tsx +++ b/packages/website/src/common/SiteDetails/WaterSampling/index.tsx @@ -78,7 +78,11 @@ function WaterSamplingCard({ siteId, source }: WaterSamplingCardProps) { Object.entries(timeSeriesData || {}) .map(([key, val]) => { const values = val - .find((x) => x.type === source) + .find( + (x) => + // hui is specific type of sonde, look for hui as well when looking for sonde + x.type === source || (source === 'sonde' && x.type === 'hui'), + ) ?.data.map((x) => x.value); if (!values) return [undefined, undefined]; return [key, getMeanCalculationFunction(source)(values)]; diff --git a/packages/website/src/common/SiteDetails/WaterSampling/utils.ts b/packages/website/src/common/SiteDetails/WaterSampling/utils.ts index 0804276aa..9a4aed27d 100644 --- a/packages/website/src/common/SiteDetails/WaterSampling/utils.ts +++ b/packages/website/src/common/SiteDetails/WaterSampling/utils.ts @@ -176,8 +176,12 @@ export async function getCardData( ); const uploads = - uploadHistory.filter((x) => x.dataUpload.sensorTypes.includes(source)) || - []; + uploadHistory.filter( + (x) => + x.dataUpload.sensorTypes.includes(source) || + // hui is specific type of sonde, look for hui as well when looking for sonde + (source === 'sonde' && x.dataUpload.sensorTypes.includes('hui')), + ) || []; if (uploads.length < 1) { return {}; } @@ -196,10 +200,13 @@ export async function getCardData( return currMin < min ? currMin : min; }, new Date().toISOString()); - const maxDate = inLastYear.reduce((max, curr) => { - const currMax = curr.maxDate || curr.dataUpload.maxDate; - return currMax > max ? currMax : max; - }, new Date(0).toISOString()); + const maxDate = + inLastYear.length > 0 + ? inLastYear.reduce((max, curr) => { + const currMax = curr.maxDate || curr.dataUpload.maxDate; + return currMax > max ? currMax : max; + }, new Date(0).toISOString()) + : new Date().toISOString(); const [data] = await timeSeriesRequest({ siteId, @@ -209,7 +216,7 @@ export async function getCardData( hourly: true, }); - const pointId = inLastYear[0].surveyPoint; + const pointId = inLastYear[0]?.surveyPoint; const samePoint = pointId !== null ? inLastYear.reduce( diff --git a/packages/website/src/common/SiteDetails/index.tsx b/packages/website/src/common/SiteDetails/index.tsx index 6532f7956..a918ad9c9 100644 --- a/packages/website/src/common/SiteDetails/index.tsx +++ b/packages/website/src/common/SiteDetails/index.tsx @@ -51,8 +51,9 @@ import LoadingSkeleton from '../LoadingSkeleton'; import playIcon from '../../assets/play-icon.svg'; import { TemperatureChange } from './TemperatureChange'; +/** Show only the last year of HUI data, should match with {@link getCardData} */ const acceptHUIInterval = Interval.fromDateTimes( - DateTime.now().minus({ years: 2 }), + DateTime.now().minus({ years: 1 }), DateTime.now(), ); @@ -158,7 +159,11 @@ const SiteDetails = ({ MINIMUM_SONDE_METRICS_TO_SHOW_CARD; const hasHUI = - latestData.some((x) => x.source === 'hui') || + latestData.some( + (x) => + x.source === 'hui' && + acceptHUIInterval.contains(DateTime.fromISO(x.timestamp)), + ) || sourceWithinDataRangeInterval( acceptHUIInterval, 'hui', diff --git a/packages/website/src/helpers/siteUtils.ts b/packages/website/src/helpers/siteUtils.ts index 4bbbb2179..05af09646 100644 --- a/packages/website/src/helpers/siteUtils.ts +++ b/packages/website/src/helpers/siteUtils.ts @@ -152,6 +152,8 @@ export const sitesFilterFn = ( return hasDeployedSpotter(s); case 'HOBO loggers': return s?.hasHobo; + case 'Water quality': + return s?.waterQualitySources?.length; case 'Reef Check': return !!s?.reefCheckSite; default: diff --git a/packages/website/src/routes/HomeMap/SiteTable/__snapshots__/index.test.tsx.snap b/packages/website/src/routes/HomeMap/SiteTable/__snapshots__/index.test.tsx.snap index e2b785a59..4a7c29dba 100644 --- a/packages/website/src/routes/HomeMap/SiteTable/__snapshots__/index.test.tsx.snap +++ b/packages/website/src/routes/HomeMap/SiteTable/__snapshots__/index.test.tsx.snap @@ -146,6 +146,18 @@ exports[`SiteTable should render with given state from Redux store 1`] = ` HOBO loggers + + + Water quality + +