diff --git a/src/components/ListPage.tsx b/src/components/ListPage.tsx index 51c580dc..0bf906de 100644 --- a/src/components/ListPage.tsx +++ b/src/components/ListPage.tsx @@ -13,14 +13,19 @@ import { GridFilterItem, useGridApiContext, useGridSelector, + GridColDef, } from '@mui/x-data-grid' import { settings } from '@/settings' import React, { useMemo, useState } from 'react' -import { CanAccess, useExport, useNavigation, useResourceParams } from '@refinedev/core' -import { Box, Chip, InputBase, Stack, Tooltip, Typography } from '@mui/material' +import { + CanAccess, + useExport, + useNavigation, + useResourceParams, +} from '@refinedev/core' +import { Box, Chip, InputBase, Stack, Typography } from '@mui/material' import SearchIcon from '@mui/icons-material/Search' - // Shows a dismissible chip for each active column filter. function ActiveFilterChips() { const apiRef = useGridApiContext() @@ -38,11 +43,23 @@ function ActiveFilterChips() { } return ( - + {activeFilters.map((filter) => { const column = columns[filter.field] const fieldLabel = column?.headerName ?? filter.field - const value = filter.value != null && filter.value !== '' ? ` ${filter.operator} "${filter.value}"` : ` ${filter.operator}` + const value = + filter.value != null && filter.value !== '' + ? ` ${filter.operator} "${filter.value}"` + : ` ${filter.operator}` return ( + - - - - + + + + @@ -77,17 +116,22 @@ function ListPageToolbar() { } type ListPageProps = { - title?: string | null - description?: string | null - columns: any + title?: string + description?: string + columns: GridColDef[] dataGridProps: any + getRowId?: (row: any) => string | number exportProps?: any - children?: any + children?: React.ReactNode onSelectionChange?: (selectionModel: any) => void - getRowId?: (row: any) => number - isLoading?: any + isLoading?: boolean headerButtons?: any disableRowClick?: boolean + + searchMode?: 'client' | 'server' + searchValue?: string + onSearchChange?: (value: string) => void + searchPlaceholder?: string } export const ListPage: React.FC = ({ @@ -102,12 +146,18 @@ export const ListPage: React.FC = ({ isLoading, headerButtons, disableRowClick = false, + searchMode = 'client', + searchValue, + onSearchChange, + searchPlaceholder, }) => { if (!exportProps) { exportProps = { pageSize: 1000 } } - const [quickFilter, setQuickFilter] = useState('') + const [localQuickFilter, setLocalQuickFilter] = useState('') + const quickFilter = + searchMode === 'server' ? (searchValue ?? '') : localQuickFilter const { show } = useNavigation() const { resource } = useResourceParams() @@ -133,16 +183,40 @@ export const ListPage: React.FC = ({ } const rowCount = dataGridProps.rowCount as number | undefined - const { rows: allRows, ...restDataGridProps } = dataGridProps + const getSearchableCellValue = (row: any, col: GridColDef) => { + const raw = row[col.field] + + if (raw == null) return '' + if (Array.isArray(raw)) return raw.map((v) => String(v)).join(', ') + if (typeof raw === 'object') return JSON.stringify(raw) + return String(raw) + } + const filteredRows = useMemo(() => { + if (searchMode === 'server') { + return allRows ?? [] + } + if (!quickFilter || !allRows) return allRows ?? [] - const lower = quickFilter.toLowerCase() + + const needle = quickFilter.toLowerCase().trim() + return allRows.filter((row: any) => - Object.values(row).some((val) => String(val ?? '').toLowerCase().includes(lower)) + columns.some((col) => + getSearchableCellValue(row, col).toLowerCase().includes(needle) + ) ) - }, [allRows, quickFilter]) + }, [allRows, quickFilter, columns, searchMode]) + + const handleSearchChange = (value: string) => { + if (searchMode === 'server') { + onSearchChange?.(value) + } else { + setLocalQuickFilter(value) + } + } return ( @@ -168,7 +242,12 @@ export const ListPage: React.FC = ({ breadcrumb={} wrapperProps={{ elevation: 0, - sx: { backgroundColor: 'background.wrapper', boxShadow: 'none', borderRadius: 1, padding: 0 }, + sx: { + backgroundColor: 'background.wrapper', + boxShadow: 'none', + borderRadius: 1, + padding: 0, + }, }} headerProps={{ sx: { @@ -191,8 +270,8 @@ export const ListPage: React.FC = ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', - px: 0.5, - pb: 0.5, + px: 0, + pb: 1.5, }} > = ({ borderRadius: 1, px: 1, py: 0.25, - width: 260, + width: 400, bgcolor: 'background.paper', }} > - + setQuickFilter(e.target.value)} - placeholder="Filter this page..." + onChange={(e) => handleSearchChange(e.target.value)} + placeholder={ + searchPlaceholder ?? + (searchMode === 'server' + ? 'Search all records...' + : 'Filter this page...') + } sx={{ fontSize: 14, flex: 1 }} - inputProps={{ 'aria-label': 'Filter rows on this page' }} + inputProps={{ + 'aria-label': + searchMode === 'server' + ? 'Search all records' + : 'Filter rows on this page', + }} /> {rowCount !== undefined && rowCount > 0 && ( @@ -245,7 +336,9 @@ export const ListPage: React.FC = ({ ? (params) => show(resource.name, params.id as string | number) : undefined } - loading={isLoading !== undefined ? isLoading : restDataGridProps.loading} + loading={ + isLoading !== undefined ? isLoading : restDataGridProps.loading + } columns={columns} sx={{ cursor: disableRowClick ? 'default' : 'pointer' }} /> diff --git a/src/interfaces/ocotillo/IWell.ts b/src/interfaces/ocotillo/IWell.ts index 08fecb40..8555e9f1 100644 --- a/src/interfaces/ocotillo/IWell.ts +++ b/src/interfaces/ocotillo/IWell.ts @@ -1,4 +1,4 @@ -import type { IThing } from '@/interfaces/ocotillo' +import type { IContact, IThing } from '@/interfaces/ocotillo' import { z } from 'zod' import { zWellPurpose } from '@/generated/zod.gen' @@ -59,4 +59,6 @@ export interface IWell extends IThing { start_date: string | null end_date: string | null }[] + + contacts?: Partial[] | null } diff --git a/src/pages/ocotillo/contact/list.tsx b/src/pages/ocotillo/contact/list.tsx index 8f2a8067..18c1d456 100644 --- a/src/pages/ocotillo/contact/list.tsx +++ b/src/pages/ocotillo/contact/list.tsx @@ -15,10 +15,7 @@ import { settings } from '@/settings' import { formatAppDateTime, formatPhone } from '@/utils' import { ListPage } from '@/components' import { useAccessCapabilities } from '@/hooks' -import { - filterConfidentialRows, - sanitizeContacts, -} from '@/utils' +import { filterConfidentialRows, sanitizeContacts } from '@/utils' export const ContactList: React.FC = () => { const { canViewConfidential } = useAccessCapabilities() @@ -126,7 +123,13 @@ export const ContactList: React.FC = () => { renderCell: (params) => { const things = params.row.things ?? [] return ( -
+
{things.map((thing, idx) => ( {idx > 0 && ', '} @@ -138,6 +141,7 @@ export const ContactList: React.FC = () => { id: thing.id, }, }} + onClick={(e) => e.stopPropagation()} > {thing.name} @@ -202,9 +206,15 @@ export const ContactList: React.FC = () => { /> {selectedContactId && ( <> - {canViewConfidential && } - {canViewConfidential && } - {canViewConfidential && } + {canViewConfidential && ( + + )} + {canViewConfidential && ( + + )} + {canViewConfidential && ( + + )} )} @@ -349,7 +359,11 @@ const InfoCard = ({ }) => ( - + ) diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index b4730fea..d846452e 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -1,5 +1,5 @@ -import { useEffect, useMemo } from 'react' -import { useExport, useGo } from '@refinedev/core' +import { useEffect, useMemo, useState } from 'react' +import { useExport, useGo, useLink } from '@refinedev/core' import { ExportButton, useDataGrid } from '@refinedev/mui' import { GridColDef, GridFilterModel } from '@mui/x-data-grid' import { captureEvent } from '@/analytics/posthog' @@ -22,7 +22,7 @@ export const SpringList: React.FC = () => { field: 'name', headerName: 'Name', type: 'string', - minWidth: 180, + minWidth: 100, flex: 1, }, { @@ -63,9 +63,26 @@ export const WellList: React.FC = () => { captureEvent('feature_used', { feature: 'wells_list' }) }, []) + const [searchInput, setSearchInput] = useState('') + const [search, setSearch] = useState('') + + useEffect(() => { + const timer = setTimeout(() => { + setSearch(searchInput.trim()) + }, 300) + + return () => clearTimeout(timer) + }, [searchInput]) + const { dataGridProps } = useDataGrid({ resource: 'thing/water-well', dataProviderName: 'ocotillo', + meta: { + params: { + include_contacts: true, + ...(search ? { query: search } : {}), + }, + }, pagination: { pageSize: 50 }, }) @@ -87,17 +104,20 @@ export const WellList: React.FC = () => { meta: { params: { thing_type: ['water well', 'geothermal well'], + include_contacts: true, }, }, }) + const Link = useLink() + const columns = useMemo[]>( () => [ { field: 'name', headerName: 'Name', type: 'string', - minWidth: 160, + minWidth: 100, flex: 1, }, { @@ -125,7 +145,12 @@ export const WellList: React.FC = () => { flex: 1, sortable: false, valueGetter: (_: unknown, row: IWell) => - row.aquifers?.map((a) => a.aquifer_system).join(', ') ?? '', + row.aquifers + ?.map( + (a: { aquifer_system: string; aquifer_types: string[] }) => + a.aquifer_system + ) + .join(', ') ?? '', }, { field: 'release_status', @@ -155,6 +180,45 @@ export const WellList: React.FC = () => { width: 130, valueGetter: (v: string) => formatAppDate(v), }, + { + field: 'contacts', + headerName: 'Contacts', + minWidth: 180, + flex: 1, + sortable: false, + valueGetter: (_: unknown, row: IWell) => + row.contacts?.map((c) => c.name ?? '').join(', ') ?? '', + renderCell: (params) => { + const contacts = params.row.contacts ?? [] + return ( +
+ {contacts.map((contact, idx) => ( + + {idx > 0 && ', '} + e.stopPropagation()} + > + {contact.name} + + + ))} +
+ ) + }, + }, { field: 'well_completion_date', headerName: 'Completed', @@ -244,9 +308,15 @@ export const WellList: React.FC = () => { ' construction depending on the local geology and intended use.' } columns={columns} - dataGridProps={{ ...dataGridProps, onFilterModelChange: handleFilterModelChange }} + dataGridProps={{ + ...dataGridProps, + onFilterModelChange: handleFilterModelChange, + }} getRowId={(row) => row.id} headerButtons={customHeaderButtons} + searchMode="server" + searchValue={searchInput} + onSearchChange={setSearchInput} /> ) }