diff --git a/api/routes/well_measurements.py b/api/routes/well_measurements.py index c3fe1a7d..fe7e2fb4 100644 --- a/api/routes/well_measurements.py +++ b/api/routes/well_measurements.py @@ -17,13 +17,19 @@ from api.models.main_models import WellMeasurements, ObservedPropertyTypeLU, Units, Wells from api.session import get_db from api.enums import ScopedUser +from google.cloud import storage from pathlib import Path from jinja2 import Environment, FileSystemLoader, select_autoescape +import json +import os import matplotlib + matplotlib.use("Agg") # Force non-GUI backend +WOODPECKER_BUCKET_NAME = os.getenv("GCP_WOODPECKER_BUCKET_NAME", "") + TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates" templates = Environment( @@ -66,6 +72,46 @@ def add_waterlevel( return well_measurement +@public_well_measurement_router.get( + "/waterlevels/woodpeckers", + response_model=List[well_schemas.WellMeasurementDTO], + tags=["WaterLevels"], +) +def read_woodpecker_waterlevels( + well_id: int = Query(..., description="At least one well ID is required"), +): + SP_JOHNSON_WELL_ID = 2599 + + if well_id != SP_JOHNSON_WELL_ID: + raise HTTPException(status_code=400, detail="Invalid well ID") + + client = storage.Client() + bucket = client.bucket(WOODPECKER_BUCKET_NAME) + + blobs = bucket.list_blobs() + + results = [] + for blob in blobs: + if blob.name.endswith(".json"): + content = blob.download_as_text() + data = json.loads(content) + + measurement = well_schemas.WellMeasurementDTO( + id=data["id"], + timestamp=datetime.fromisoformat(data["timestamp"]), + value=data.get("value"), + submitting_user=well_schemas.WellMeasurementDTO.UserDTO( + full_name=data["submitting_user"]["full_name"] + ), + well=well_schemas.WellMeasurementDTO.WellDTO( + ra_number=data["well"]["ra_number"] + ), + ) + results.append(measurement) + + return results + + @public_well_measurement_router.get( "/waterlevels", response_model=List[well_schemas.WellMeasurementDTO], diff --git a/docker-compose.development.yml b/docker-compose.development.yml index 6f5ec224..3a44c2b6 100644 --- a/docker-compose.development.yml +++ b/docker-compose.development.yml @@ -32,6 +32,7 @@ services: working_dir: /app environment: - GCP_BUCKET_NAME=pvacd + - GCP_WOODPECKER_BUCKET_NAME=pvacd-woodpecker - GCP_BACKUP_PREFIX=pre-prod-db-backups - GCP_PHOTO_PREFIX=pre-prod-meter-activities-photos - BACKUP_RETENTION_DAYS=14 diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 8266cc69..a1edf253 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -32,6 +32,7 @@ services: working_dir: /app environment: - GCP_BUCKET_NAME=pvacd + - GCP_WOODPECKER_BUCKET_NAME=pvacd-woodpecker - GCP_BACKUP_PREFIX=prod-db-backups - GCP_PHOTO_PREFIX=prod-meter-activities-photos - BACKUP_RETENTION_DAYS=90 diff --git a/frontend/src/components/Modals/Region/Create.tsx b/frontend/src/components/Modals/Region/Create.tsx index 871f4cea..6208e47a 100644 --- a/frontend/src/components/Modals/Region/Create.tsx +++ b/frontend/src/components/Modals/Region/Create.tsx @@ -8,6 +8,8 @@ import { InputLabel, Grid, Typography, + FormControlLabel, + Checkbox, } from "@mui/material"; import { useState } from "react"; import { useAuthUser } from "react-auth-kit"; @@ -22,6 +24,7 @@ import timezone from "dayjs/plugin/timezone"; dayjs.extend(utc); dayjs.extend(timezone); import { DatePicker, TimePicker } from "@mui/x-date-pickers"; +import { RadioButtonUnchecked, TaskAlt } from "@mui/icons-material"; import { useGetUserList } from "../../../service/ApiServiceNew"; import { useQuery } from "react-query"; import { useFetchWithAuth } from "../../../hooks/useFetchWithAuth.js"; @@ -70,6 +73,7 @@ export const CreateModal = ({ const userList = useGetUserList(); const [value, setValue] = useState(null); + const [notSampled, setNotSampled] = useState(false); const [selectedUserID, setSelectedUserID] = useState(""); const [selectedWellID, setSelectedWellID] = useState(""); const [date, setDate] = useState(dayjs.utc()); @@ -184,17 +188,42 @@ export const CreateModal = ({ onChange={setTime} /> + + } + checkedIcon={} + checked={notSampled} + onChange={(e) => { + const checked = e.target.checked; + setNotSampled(checked) + + if (checked) { + setValue(null); + } + }} + /> + } + label="Well was visited but NOT SAMPLED" + labelPlacement="end" + /> + - setValue(event.target.value as unknown as number) - } + disabled={notSampled} + value={notSampled ? "" : value ?? ""} + label={notSampled ? "NOT SAMPLED" : "Value"} + onChange={(event) => { + const newValue = event.target.value; + setValue(newValue === "" ? null : Number(newValue)); + }} /> diff --git a/frontend/src/components/Modals/Region/Update.tsx b/frontend/src/components/Modals/Region/Update.tsx index 36cc84d9..b6d01587 100644 --- a/frontend/src/components/Modals/Region/Update.tsx +++ b/frontend/src/components/Modals/Region/Update.tsx @@ -1,3 +1,5 @@ + +import { useState } from "react"; import { Modal, TextField, @@ -8,6 +10,8 @@ import { InputLabel, Grid, Typography, + FormControlLabel, + Checkbox, } from "@mui/material"; import { MonitoredWell, @@ -19,6 +23,7 @@ import dayjs from "dayjs"; dayjs.extend(utc); dayjs.extend(timezone); import { DatePicker, TimePicker } from "@mui/x-date-pickers"; +import { RadioButtonUnchecked, TaskAlt } from "@mui/icons-material"; import { useGetUserList } from "../../../service/ApiServiceNew"; import { useQuery } from "react-query"; import { useFetchWithAuth } from "../../../hooks/useFetchWithAuth.js"; @@ -44,6 +49,10 @@ export const UpdateModal = ({ }) => { const userList = useGetUserList(); const fetchWithAuth = useFetchWithAuth(); + + const [notSampled, setNotSampled] = useState(false); + const [previousValue, setPreviousValue] = useState(null); + const { data: wells, isLoading: isLoadingWells } = useQuery< { items: MonitoredWell[] }, Error, @@ -66,6 +75,21 @@ export const UpdateModal = ({ select: (res) => res.items, }); + const handleToggleNotSampled = (checked: boolean) => { + setNotSampled(checked); + + if (checked) { + // Store previous numeric value and clear backend value + setPreviousValue(measurement.value ?? null); + onUpdateMeasurement({ value: null }); + } else { + // Restore previous numeric value when toggled back + if (previousValue !== null) { + onUpdateMeasurement({ value: previousValue }); + } + } + }; + return ( @@ -127,16 +151,41 @@ export const UpdateModal = ({ } /> + + } + checkedIcon={} + checked={notSampled} + onChange={(e) => handleToggleNotSampled(e.target.checked)} + /> + } + label="Well was visited but NOT SAMPLED" + labelPlacement="end" + /> + onUpdateMeasurement({ - value: event.target.value as unknown as number, + value: + event.target.value === "" + ? null + : Number(event.target.value), }) } /> diff --git a/frontend/src/interfaces.d.ts b/frontend/src/interfaces.d.ts index e00e6131..482b4d2a 100644 --- a/frontend/src/interfaces.d.ts +++ b/frontend/src/interfaces.d.ts @@ -573,7 +573,7 @@ export interface PatchWellMeasurement { export interface NewRegionMeasurement { region_id: number timestamp: string - value: number + value?: number | null submitting_user_id: number well_id: number } @@ -583,7 +583,7 @@ export interface PatchRegionMeasurement { submitting_user_id: number well_id: number timestamp: dayjs.Dayjs - value?: number + value?: number | null } export interface CreateUser {