Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 11 additions & 6 deletions docs/data-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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 |
Expand Down Expand Up @@ -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 |
3 changes: 2 additions & 1 deletion src/pages/ocotillo/contact/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
61 changes: 49 additions & 12 deletions src/pages/ocotillo/thing/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ISpring>({
Expand Down Expand Up @@ -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(
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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) => {
Expand Down Expand Up @@ -222,19 +257,23 @@ 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,
},
{
field: 'latitude',
headerName: 'Latitude',
description:
'Latitude of the current mapped location in decimal degrees (WGS84).',
type: 'number',
width: 110,
sortable: false,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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),
},
],
[]
)
Expand Down
20 changes: 18 additions & 2 deletions src/pages/ocotillo/thing/well-show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
MonitoringInfoCard,
WaterLevelObservationRow,
} from '@/components'
import { displayWellSiteName } from '@/utils'

const EMPTY_ASSETS: IAsset[] = []
const EMPTY_CONTACTS: IContact[] = []
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -398,7 +414,7 @@ export const WellShow = () => {
<ContactsCard
contacts={contacts}
isLoading={isDetailsLoading}
siteName={well?.site_name}
siteName={well ? displayWellSiteName(well) : undefined}
/>
<MonitoringInfoCard
well={well}
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export * from './Unit'
export * from './UpdateMapView'
export * from './UtmToLonLat'
export * from './WellBatchExport'
export * from './wellSiteName'
export * from './BuildBugReportUrl'
export * from './docsSearch'
export * from './searchModal'
18 changes: 18 additions & 0 deletions src/utils/wellSiteName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { IWell } from '@/interfaces/ocotillo'

/**
* Site name shown in lists and exports: uses API `site_name` when set, otherwise
* the same rule as OcotilloAPI `Thing.site_name` (lowest id among NMBGMR links).
*/
export function displayWellSiteName(
well: Pick<IWell, 'site_name' | 'alternate_ids'>
): 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 ?? ''
}
Loading