From ff8c8fc1efe980417e136177582a759ac84ca2db Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Mon, 27 Apr 2026 15:09:13 -0400 Subject: [PATCH 1/5] Well list: lead with name, site name, monitoring, created; column hints Reorder DataGrid columns so Name, Site name, Monitoring, and Created At appear first. Add site_name column with empty-string fallback. Set description on each column for MUI header accessibility and columns panel. --- src/pages/ocotillo/thing/list.tsx | 57 ++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index d846452e..b52c8252 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -116,31 +116,58 @@ 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.', type: 'string', - width: 150, + minWidth: 140, + flex: 0.9, + valueGetter: (_: unknown, row: IWell) => row.site_name ?? '', }, { 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: + 'Date and time this well record was first created in Ocotillo.', + width: 180, + valueGetter: (v: string) => formatAppDateTime(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.', minWidth: 180, flex: 1, sortable: false, @@ -155,12 +182,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 +200,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,12 +210,16 @@ 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.', minWidth: 180, flex: 1, sortable: false, @@ -222,12 +259,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 +274,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 +287,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 +300,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 +310,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), - }, ], [] ) From a74c0c3ee4bdd3dd351dbf1778f26a0ad01b7a51 Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Mon, 27 Apr 2026 15:32:24 -0400 Subject: [PATCH 2/5] Wells list: show Created At as date only without time Use formatAppDate instead of formatAppDateTime for the Created At column and tighten column width. --- src/pages/ocotillo/thing/list.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index b52c8252..1b2d193e 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -144,9 +144,9 @@ export const WellList: React.FC = () => { field: 'created_at', headerName: 'Created At', description: - 'Date and time this well record was first created in Ocotillo.', - width: 180, - valueGetter: (v: string) => formatAppDateTime(v), + 'Calendar date when this well record was first added to Ocotillo.', + width: 130, + valueGetter: (v: string) => formatAppDate(v), }, { field: 'well_status', From 5fe0f682786b123235005bc899925e907e61dad0 Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Mon, 27 Apr 2026 15:45:06 -0400 Subject: [PATCH 3/5] Add displayWellSiteName helper for well list and contacts card Use API site_name with NMBGMR alternate_id fallback to match backend Thing.site_name. Apply on wells list column and well show ContactsCard. --- src/pages/ocotillo/thing/list.tsx | 4 ++-- src/pages/ocotillo/thing/well-show.tsx | 3 ++- src/utils/index.ts | 1 + src/utils/wellSiteName.ts | 18 ++++++++++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 src/utils/wellSiteName.ts diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index 1b2d193e..e4ca8731 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({ @@ -130,7 +130,7 @@ export const WellList: React.FC = () => { type: 'string', minWidth: 140, flex: 0.9, - valueGetter: (_: unknown, row: IWell) => row.site_name ?? '', + valueGetter: (_: unknown, row: IWell) => displayWellSiteName(row), }, { field: 'monitoring_status', diff --git a/src/pages/ocotillo/thing/well-show.tsx b/src/pages/ocotillo/thing/well-show.tsx index 1e33c9b4..307aff07 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[] = [] @@ -385,7 +386,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 ?? '' +} From d93489dc90848bbedfa6c1f18ae86bb639b3faad Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Tue, 28 Apr 2026 10:12:50 -0400 Subject: [PATCH 4/5] Align wells and contacts grids with API virtual sorts and document filter limits Enable sorting on Associated Sites, Aquifers, and Contacts where the API defines rules and add column descriptions for sort semantics. Update data-tables.md for MUI X Community single column filter in the panel, multi-filter capability on the API side, and refresh sortability in the column tables. Clarify site name description and simplify README deployment hosting examples. --- README.md | 3 +-- docs/data-tables.md | 17 +++++++++++------ src/pages/ocotillo/contact/list.tsx | 3 ++- src/pages/ocotillo/thing/list.tsx | 8 +++----- 4 files changed, 17 insertions(+), 14 deletions(-) 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 8b0abc72..7469ce87 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -126,7 +126,7 @@ export const WellList: React.FC = () => { field: 'site_name', headerName: 'Site name', description: - 'Name of the monitoring site or facility associated with this well when one is recorded.', + 'Name of the monitoring site or facility associated with this well when one is recorded (NMBGMR alternate ID when present).', type: 'string', minWidth: 140, flex: 0.9, @@ -167,10 +167,9 @@ export const WellList: React.FC = () => { field: 'aquifers', headerName: 'Aquifers', description: - 'Aquifer systems linked to this well, summarized from association data.', + '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( @@ -219,10 +218,9 @@ export const WellList: React.FC = () => { field: 'contacts', headerName: 'Contacts', description: - 'People or organizations linked to this well; open a contact from the link.', + '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) => { From 23f1f13eeb58dfccfaaeacbeb4eee4408fda3de1 Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Tue, 28 Apr 2026 10:49:37 -0400 Subject: [PATCH 5/5] Log well details API payload to the console in all environments After each successful thing/water-well details fetch, log a JSON snapshot and pretty-printed copy so the full response is inspectable in DevTools in dev, staging, and production. --- src/pages/ocotillo/thing/well-show.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pages/ocotillo/thing/well-show.tsx b/src/pages/ocotillo/thing/well-show.tsx index 613441e1..ff43dbba 100644 --- a/src/pages/ocotillo/thing/well-show.tsx +++ b/src/pages/ocotillo/thing/well-show.tsx @@ -81,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()