diff --git a/README.md b/README.md index 21aed4b9..3081c4da 100644 --- a/README.md +++ b/README.md @@ -180,8 +180,7 @@ npm run start ## Deployment -Deploy the contents of the `dist/` folder to any static hosting provider (e.g., Netlify, Vercel, AWS S3, GitHub Pages, -GCP). Ensure environment variables are configured on the hosting platform. +Deploy the contents of the `dist/` folder to any static hosting provider (e.g. GCP). Ensure environment variables are configured on the hosting platform. ## License diff --git a/docs/data-tables.md b/docs/data-tables.md index 96733d9e..715e4bcc 100644 --- a/docs/data-tables.md +++ b/docs/data-tables.md @@ -31,9 +31,11 @@ Limitation: it does not search your entire database. If the record you are looki ### Column filters (server-side filters) -The **Filters** button opens a panel where you can add one or more conditions to narrow down results across the entire dataset. This sends a request to the server and returns only the matching records. +The **Filters** button opens a panel where you set conditions that narrow results across the **entire** dataset. Each change sends a request to the server. -Currently supported filter operators: +**Important:** The app uses **MUI X Data Grid (Community)**. In this edition, the filter panel only supports **one active column filter at a time**. If you add a filter on a second column, it **replaces** the previous one rather than stacking. The Ocotillo API and Refine can in principle accept **multiple** filter parameters in one request, but the current grid UI does not let you combine several column filters without upgrading to a commercial Data Grid tier or building a custom filter UI. + +Currently supported filter operators (when the API implements them for that column): - **contains** -- finds records where the field includes the search text - **equals** -- finds records where the field exactly matches @@ -45,9 +47,11 @@ Other operators shown in the dropdown (such as "does not contain" or "does not e ### Sorting -Clicking any column header sorts the table by that column. Clicking again reverses the sort order. Sorting is applied server-side, so it sorts across all records, not just the current page. +Clicking any column header sorts the table by that column when the column is sortable. Clicking again reverses the sort order. Sorting is applied server-side, so it sorts across all records, not just the current page. + +For columns that aggregate multiple links (for example **Aquifers**, **Contacts** on wells, or **Associated Sites** on contacts), the server uses a fixed rule such as ordering by the alphabetically first linked value. That order may not match how labels are joined with commas in the cell. -Columns marked as not sortable (such as Aquifers and Associated Sites) cannot be sorted because they contain computed or multi-value data that doesn't translate well to a single sort order. +Some columns stay non-sortable by design (for example **Primary Phone** and **Primary Email** on Contacts), usually because sorting would require extra API work or privacy rules. ### Column visibility @@ -79,6 +83,7 @@ The following improvements would require changes to the OcotilloAPI server in ad - **Additional filter operators** -- "does not contain," "does not equal," and "is any of" would need to be implemented in the API filter logic. - **Additional columns** -- several useful fields (owner name, county, latitude/longitude, site name) exist in the database but are not currently returned by the API in the list endpoints. Adding them requires API and potentially database changes. - **Saved views or presets** -- letting users save a filter configuration and return to it later would need storage on the server or in user preferences. +- **Multiple column filters visible at once in the filter panel** -- the Ocotillo API can accept more than one `filter` query parameter, but **MUI X Data Grid (Community)** only allows **one** active column filter in the built-in panel. Offering several simultaneous column filters in that panel would need a **custom filter UI** (or a different table stack), not only an API change. ## Column reference by page @@ -89,7 +94,7 @@ The following improvements would require changes to the OcotilloAPI server in ad | Name | Well identifier (e.g. WELL-0001) | Yes | | Well Status | Current operational status | Yes | | Monitoring | Whether the well is actively monitored | Yes | -| Aquifers | Aquifer systems the well is associated with | No | +| Aquifers | Aquifer systems the well is associated with | Yes (first aquifer name alphabetically) | | Release Status | Whether the record is public | Yes | | Well Depth (ft) | Total depth of the well casing | Yes | | Hole Depth (ft) | Total drilled depth | Yes | @@ -124,5 +129,5 @@ The following improvements would require changes to the OcotilloAPI server in ad | Contact Type | Classification of the contact | Yes | | Primary Phone | First phone number on file | No | | Primary Email | First email address on file | No | -| Associated Sites | Wells or springs linked to this contact | No | +| Associated Sites | Wells or springs linked to this contact | Yes (first site name alphabetically) | | Created At | When the record was added to the system | Yes | diff --git a/src/pages/ocotillo/contact/list.tsx b/src/pages/ocotillo/contact/list.tsx index 18c1d456..1041548c 100644 --- a/src/pages/ocotillo/contact/list.tsx +++ b/src/pages/ocotillo/contact/list.tsx @@ -114,10 +114,11 @@ export const ContactList: React.FC = () => { { field: 'things', headerName: 'Associated Sites', + description: + 'Monitoring sites linked to this contact. Sort uses the alphabetically first linked site name.', type: 'string', minWidth: 180, flex: 1, - sortable: false, valueGetter: (_: unknown, row: IContact) => row.things?.map((thing) => thing.name).join('; ') ?? '', renderCell: (params) => { diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index 093ab667..7469ce87 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -7,7 +7,7 @@ import { Button } from '@mui/material' import { PictureAsPdf } from '@mui/icons-material' import { ListPage } from '@/components/ListPage' import { ISpring, IWell } from '@/interfaces/ocotillo' -import { formatAppDate, formatAppDateTime } from '@/utils' +import { displayWellSiteName, formatAppDate, formatAppDateTime } from '@/utils' export const SpringList: React.FC = () => { const { dataGridProps } = useDataGrid({ @@ -116,34 +116,60 @@ export const WellList: React.FC = () => { { field: 'name', headerName: 'Name', + description: + 'Official well identifier used in bureau records (for example county prefix and local ID).', type: 'string', minWidth: 100, flex: 1, }, { - field: 'well_status', - headerName: 'Well Status', + field: 'site_name', + headerName: 'Site name', + description: + 'Name of the monitoring site or facility associated with this well when one is recorded (NMBGMR alternate ID when present).', type: 'string', - width: 150, + minWidth: 140, + flex: 0.9, + valueGetter: (_: unknown, row: IWell) => displayWellSiteName(row), }, { field: 'monitoring_status', headerName: 'Monitoring', + description: + 'Whether the well is actively monitored or how monitoring is categorized in the current record.', type: 'string', width: 160, }, + { + field: 'created_at', + headerName: 'Created At', + description: + 'Calendar date when this well record was first added to Ocotillo.', + width: 130, + valueGetter: (v: string) => formatAppDate(v), + }, + { + field: 'well_status', + headerName: 'Well Status', + description: 'Operational or administrative status of the well.', + type: 'string', + width: 150, + }, { field: 'thing_type', headerName: 'Type', + description: + 'Infrastructure type from the controlled vocabulary (for example water well or geothermal well).', type: 'string', width: 130, }, { field: 'aquifers', headerName: 'Aquifers', + description: + 'Aquifer systems linked to this well, summarized from association data. Sort uses the first aquifer name alphabetically among linked systems.', minWidth: 180, flex: 1, - sortable: false, valueGetter: (_: unknown, row: IWell) => row.aquifers ?.map( @@ -155,12 +181,16 @@ export const WellList: React.FC = () => { { field: 'release_status', headerName: 'Release Status', + description: + 'Whether the record is released for public viewing under data release rules.', type: 'string', width: 130, }, { field: 'well_depth', headerName: 'Well Depth (ft)', + description: + 'Completed well depth from ground surface to bottom of the well in feet.', type: 'number', width: 130, align: 'right', @@ -169,6 +199,8 @@ export const WellList: React.FC = () => { { field: 'hole_depth', headerName: 'Hole Depth (ft)', + description: + 'Total drilled hole depth from ground surface to bottom of the borehole in feet.', type: 'number', width: 130, align: 'right', @@ -177,15 +209,18 @@ export const WellList: React.FC = () => { { field: 'first_visit_date', headerName: 'First Visit', + description: + 'Date of the bureau first recorded visit to this well when available.', width: 130, valueGetter: (v: string) => formatAppDate(v), }, { field: 'contacts', headerName: 'Contacts', + description: + 'People or organizations linked to this well; open a contact from the link. Sort uses the alphabetically first linked contact name.', minWidth: 180, flex: 1, - sortable: false, valueGetter: (_: unknown, row: IWell) => row.contacts?.map((c) => c.name ?? '').join(', ') ?? '', renderCell: (params) => { @@ -222,12 +257,14 @@ export const WellList: React.FC = () => { { field: 'well_completion_date', headerName: 'Completed', + description: 'Reported date the well construction was completed.', width: 130, valueGetter: (v: string) => formatAppDate(v), }, { field: 'well_driller_name', headerName: 'Driller', + description: 'Drilling company name when it was recorded for this well.', type: 'string', minWidth: 150, flex: 1, @@ -235,6 +272,8 @@ export const WellList: React.FC = () => { { field: 'latitude', headerName: 'Latitude', + description: + 'Latitude of the current mapped location in decimal degrees (WGS84).', type: 'number', width: 110, sortable: false, @@ -246,6 +285,8 @@ export const WellList: React.FC = () => { { field: 'longitude', headerName: 'Longitude', + description: + 'Longitude of the current mapped location in decimal degrees (WGS84).', type: 'number', width: 110, sortable: false, @@ -257,6 +298,8 @@ export const WellList: React.FC = () => { { field: 'alternate_ids', headerName: 'Alternate IDs', + description: + 'Identifiers from other agencies or programs that cross reference this well.', minWidth: 160, flex: 1, sortable: false, @@ -265,12 +308,6 @@ export const WellList: React.FC = () => { ?.map((a) => `${a.alternate_organization}: ${a.alternate_id}`) .join(', ') ?? '', }, - { - field: 'created_at', - headerName: 'Created At', - width: 180, - valueGetter: (v: string) => formatAppDateTime(v), - }, ], [] ) diff --git a/src/pages/ocotillo/thing/well-show.tsx b/src/pages/ocotillo/thing/well-show.tsx index 50be1c27..ff43dbba 100644 --- a/src/pages/ocotillo/thing/well-show.tsx +++ b/src/pages/ocotillo/thing/well-show.tsx @@ -43,6 +43,7 @@ import { MonitoringInfoCard, WaterLevelObservationRow, } from '@/components' +import { displayWellSiteName } from '@/utils' const EMPTY_ASSETS: IAsset[] = [] const EMPTY_CONTACTS: IContact[] = [] @@ -80,7 +81,22 @@ export const WellShow = () => { method: 'get', }) - return response.data as IWellDetails + const data = response.data as IWellDetails + // Log full details payload in every environment (dev, staging, prod) for + // debugging. Visible only in the browser console when it is open. + const label = `[ocotillo] GET thing/water-well/${id}/details` + try { + const plain = JSON.parse( + JSON.stringify(data) + ) as IWellDetails + console.log(label, plain) + } catch { + console.log(label, data) + } + console.log( + `${label} (full JSON, scroll or copy this if the object above will not expand)\n${JSON.stringify(data, null, 2)}` + ) + return data }, }) const { canManageAmp } = useAccessCapabilities() @@ -398,7 +414,7 @@ export const WellShow = () => { +): string { + const fromApi = well.site_name?.trim() + if (fromApi) return fromApi + + const links = [...(well.alternate_ids ?? [])].sort((a, b) => a.id - b.id) + const nmbgmr = links.find( + (link) => link.alternate_organization?.toUpperCase() === 'NMBGMR' + ) + return nmbgmr?.alternate_id ?? '' +}