diff --git a/src/api/schema/typePolicies/typePolicies.base.ts b/src/api/schema/typePolicies/typePolicies.base.ts index 90b95800b..d1cdbbec1 100644 --- a/src/api/schema/typePolicies/typePolicies.base.ts +++ b/src/api/schema/typePolicies/typePolicies.base.ts @@ -85,10 +85,19 @@ export const typePolicies: TypePolicies = { engagements: {}, }, }, + FieldRegion: { + fields: { + projects: {}, // no page merging (infinite scroll) + }, + }, + FieldZone: { + fields: { + projects: {}, // no page merging (infinite scroll) + }, + }, Query: { fields: { projects: {}, - engagements: {}, progressReports: {}, partners: {}, users: {}, diff --git a/src/components/EngagementDataGrid/EngagementColumns.tsx b/src/components/EngagementDataGrid/EngagementColumns.tsx index b4bb7231b..31cf30723 100644 --- a/src/components/EngagementDataGrid/EngagementColumns.tsx +++ b/src/components/EngagementDataGrid/EngagementColumns.tsx @@ -155,8 +155,25 @@ export const EngagementColumns: Array> = [ headerName: 'Country', field: 'project.primaryLocation.name', ...textColumn(), + width: 250, valueGetter: (_, row) => row.project.primaryLocation.value?.name.value, }, + { + field: 'project.fieldRegion.name', + headerName: 'Field Region', + ...textColumn(), + width: 250, + valueGetter: (_, { project }) => project.fieldRegion.value?.name.value, + renderCell: ({ row: engagement }) => { + const { fieldRegion } = engagement.project; + + return fieldRegion.value ? ( + + {fieldRegion.value.name.value} + + ) : null; + }, + }, { headerName: 'ISO', description: 'Ethnologue Code', diff --git a/src/components/EngagementDataGrid/engagementDataGridRow.graphql b/src/components/EngagementDataGrid/engagementDataGridRow.graphql index 8f4255c00..9f2e94b10 100644 --- a/src/components/EngagementDataGrid/engagementDataGridRow.graphql +++ b/src/components/EngagementDataGrid/engagementDataGridRow.graphql @@ -9,7 +9,6 @@ fragment engagementDataGridRow on Engagement { endDate { value } - project { id type @@ -27,6 +26,14 @@ fragment engagementDataGridRow on Engagement { } } } + fieldRegion { + value { + id + name { + value + } + } + } mouStart { value } diff --git a/src/components/ProjectDataGrid/ProjectColumns.tsx b/src/components/ProjectDataGrid/ProjectColumns.tsx index 75d671653..c753e06b8 100644 --- a/src/components/ProjectDataGrid/ProjectColumns.tsx +++ b/src/components/ProjectDataGrid/ProjectColumns.tsx @@ -27,6 +27,7 @@ import { } from '../Grid'; import { ProjectNameColumn } from '../Grid/Columns/ProjectNameColumn'; import { SensitivityColumn } from '../Grid/Columns/SensitivityColumn'; +import { Link } from '../Routing'; import { ProjectDataGridRowFragment as Project } from './projectDataGridRow.graphql'; export const ProjectColumns: Array> = [ @@ -43,6 +44,21 @@ export const ProjectColumns: Array> = [ headerName: 'Country', width: 300, }, + { + field: 'fieldRegion.name', + headerName: 'Field Region', + ...textColumn(), + width: 250, + valueGetter: (_, { fieldRegion }) => fieldRegion.value?.name.value, + renderCell: ({ row: project }) => { + const { fieldRegion } = project; + return fieldRegion.value ? ( + + {fieldRegion.value.name.value} + + ) : null; + }, + }, { field: 'step', ...enumColumn(ProjectStepList, ProjectStepLabels, { diff --git a/src/components/ProjectDataGrid/projectDataGridRow.graphql b/src/components/ProjectDataGrid/projectDataGridRow.graphql index 8488cf5bb..f63ca60db 100644 --- a/src/components/ProjectDataGrid/projectDataGridRow.graphql +++ b/src/components/ProjectDataGrid/projectDataGridRow.graphql @@ -18,12 +18,21 @@ fragment projectDataGridRow on Project { } } } + fieldRegion { + value { + id + name { + value + } + } + } mouStart { value } mouEnd { value } + isMember pinned } diff --git a/src/scenes/FieldRegions/Detail/FieldRegionDetail.tsx b/src/scenes/FieldRegions/Detail/FieldRegionDetail.tsx index 873bf4955..a1caa2a94 100644 --- a/src/scenes/FieldRegions/Detail/FieldRegionDetail.tsx +++ b/src/scenes/FieldRegions/Detail/FieldRegionDetail.tsx @@ -1,154 +1,99 @@ import { useQuery } from '@apollo/client'; -import { Edit } from '@mui/icons-material'; -import { Box, Skeleton, Typography } from '@mui/material'; +import { TabContext, TabList, TabPanel } from '@mui/lab'; +import { Box, Skeleton, Stack, Typography } from '@mui/material'; import { Helmet } from 'react-helmet-async'; import { useParams } from 'react-router-dom'; -import { canEditAny } from '~/common'; -import { useDialog } from '~/components/Dialog'; -import { - DisplaySimpleProperty, - DisplaySimplePropertyProps, -} from '~/components/DisplaySimpleProperty'; -import { EditFieldRegion } from '~/components/FieldRegion'; -import { Link } from '~/components/Routing'; +import { Tab, TabsContainer } from '~/components/Tabs'; +import { EnumParam, makeQueryHandler, withDefault } from '~/hooks'; import { Error } from '../../../components/Error'; -import { Fab } from '../../../components/Fab'; import { Redacted } from '../../../components/Redacted'; import { FieldRegionDetailDocument } from './FieldRegionDetail.graphql'; +import { FieldRegionProfile } from './Tabs/Profile/FieldRegionProfile'; +import { FieldRegionProjects } from './Tabs/Projects/FieldRegionProjects'; + +const useFieldRegionDetailsFilters = makeQueryHandler({ + tab: withDefault(EnumParam(['profile', 'projects']), 'profile'), +}); export const FieldRegionDetail = () => { const { fieldRegionId = '' } = useParams(); - const [editRegionState, editRegion] = useDialog(); - - const { data, error } = useQuery(FieldRegionDetailDocument, { + const { data, error, loading } = useQuery(FieldRegionDetailDocument, { variables: { fieldRegionId }, }); + const [filters, setFilters] = useFieldRegionDetailsFilters(); + const fieldRegion = data?.fieldRegion; return ( - theme.breakpoints.values.xl, }} > - {{ NotFound: 'Could not find field region', Default: 'Error loading field region', }} + + {!error && ( - theme.breakpoints.values.md, - display: 'flex', - flexDirection: 'column', - gap: 3, - }} - > + <> - {!fieldRegion ? ( - + {loading ? ( + + ) : !fieldRegion ? ( + ) : ( fieldRegion.name.value ?? ( ) )} - {canEditAny(fieldRegion, true) && ( - - - - )} - - - - {fieldRegion ? 'Field Region' : } - - - {fieldRegion?.fieldZone.value?.name.value} - - } - loading={!fieldRegion} - /> - - {fieldRegion?.director.value?.fullName} - - } - loading={!fieldRegion} - /> - - )} - {fieldRegion && ( - + + + setFilters({ ...filters, tab })} + aria-label="field region navigation tabs" + variant="scrollable" + > + + + + + + + + + + + + )} - + ); }; - -const DisplayProperty = (props: DisplaySimplePropertyProps) => - !props.value && !props.loading ? null : ( - - - - - - - - - ) : null - } - LabelProps={{ - color: 'textSecondary', - variant: 'body2', - ...props.LabelProps, - }} - ValueProps={{ - color: 'textPrimary', - ...props.ValueProps, - }} - /> - ); diff --git a/src/scenes/FieldRegions/Detail/Tabs/Profile/FieldRegionProfile.graphql b/src/scenes/FieldRegions/Detail/Tabs/Profile/FieldRegionProfile.graphql new file mode 100644 index 000000000..d11506c8c --- /dev/null +++ b/src/scenes/FieldRegions/Detail/Tabs/Profile/FieldRegionProfile.graphql @@ -0,0 +1,4 @@ +fragment FieldRegionProfile on FieldRegion { + ...DisplayFieldRegion + ...FieldRegionForm +} diff --git a/src/scenes/FieldRegions/Detail/Tabs/Profile/FieldRegionProfile.tsx b/src/scenes/FieldRegions/Detail/Tabs/Profile/FieldRegionProfile.tsx new file mode 100644 index 000000000..78dbdf49f --- /dev/null +++ b/src/scenes/FieldRegions/Detail/Tabs/Profile/FieldRegionProfile.tsx @@ -0,0 +1,116 @@ +import { Edit } from '@mui/icons-material'; +import { + Box, + IconButton, + Paper, + Skeleton, + Stack, + Tooltip, + Typography, +} from '@mui/material'; +import { canEditAny } from '~/common'; +import { useDialog } from '~/components/Dialog'; +import { + DisplaySimpleProperty, + DisplaySimplePropertyProps, +} from '~/components/DisplaySimpleProperty'; +import { EditFieldRegion } from '~/components/FieldRegion/EditFieldRegion/EditFieldRegion'; +import { Link } from '~/components/Routing'; +import { FieldRegionProfileFragment } from './FieldRegionProfile.graphql'; + +interface FieldRegionProfileProps { + fieldRegion: FieldRegionProfileFragment | undefined; +} + +export const FieldRegionProfile = ({ + fieldRegion, +}: FieldRegionProfileProps) => { + const [editRegionState, editRegion] = useDialog(); + + const canEditAnyFields = canEditAny(fieldRegion); + + return ( + ({ + display: 'flex', + justifyContent: 'space-between', + width: theme.breakpoints.values.md, + minHeight: 200, + })} + > + + + {fieldRegion.fieldZone.value.name.value} + + ) : null + } + loading={!fieldRegion} + /> + + {fieldRegion.director.value.fullName} + + ) : null + } + loading={!fieldRegion} + /> + + + {canEditAnyFields ? ( + + + + + + ) : null} + + {fieldRegion && ( + + )} + + ); +}; + +const DisplayProperty = (props: DisplaySimplePropertyProps) => + !props.value && !props.loading ? null : ( + + + + + + + + + ) : null + } + LabelProps={{ + color: 'textSecondary', + variant: 'body2', + ...props.LabelProps, + }} + ValueProps={{ + color: 'textPrimary', + ...props.ValueProps, + }} + /> + ); diff --git a/src/scenes/FieldRegions/Detail/Tabs/Projects/FieldRegionProjects.graphql b/src/scenes/FieldRegions/Detail/Tabs/Projects/FieldRegionProjects.graphql new file mode 100644 index 000000000..cc3dc42e8 --- /dev/null +++ b/src/scenes/FieldRegions/Detail/Tabs/Projects/FieldRegionProjects.graphql @@ -0,0 +1,17 @@ +query FieldRegionProjects($fieldRegionId: ID!, $input: ProjectListInput) { + fieldRegion(id: $fieldRegionId) { + id + projects(input: $input) { + canRead + hasMore + total + items { + ...fieldRegionProjectDataGridRow + } + } + } +} + +fragment fieldRegionProjectDataGridRow on Project { + ...projectDataGridRow +} diff --git a/src/scenes/FieldRegions/Detail/Tabs/Projects/FieldRegionProjects.tsx b/src/scenes/FieldRegions/Detail/Tabs/Projects/FieldRegionProjects.tsx new file mode 100644 index 000000000..a1fd47d37 --- /dev/null +++ b/src/scenes/FieldRegions/Detail/Tabs/Projects/FieldRegionProjects.tsx @@ -0,0 +1,70 @@ +import { + DataGridPro as DataGrid, + DataGridProProps as DataGridProps, +} from '@mui/x-data-grid-pro'; +import { merge } from 'lodash'; +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { + DefaultDataGridStyles, + flexLayout, + noFooter, + noHeaderFilterButtons, + useDataGridSource, +} from '~/components/Grid'; +import { + ProjectColumns, + ProjectInitialState, + ProjectToolbar, +} from '~/components/ProjectDataGrid'; +import { TabPanelContent } from '~/components/Tabs'; +import { + FieldRegionProjectDataGridRowFragment as FieldRegionProject, + FieldRegionProjectsDocument, +} from './FieldRegionProjects.graphql'; + +export const FieldRegionProjects = () => { + const { fieldRegionId = '' } = useParams(); + + const [props] = useDataGridSource({ + query: FieldRegionProjectsDocument, + variables: { fieldRegionId }, + listAt: 'fieldRegion.projects', + initialInput: { + sort: 'name', + }, + }); + + const slots = useMemo( + () => + merge({}, DefaultDataGridStyles.slots, props.slots, { + toolbar: ProjectToolbar, + } satisfies DataGridProps['slots']), + [props.slots] + ); + const slotProps = useMemo( + () => merge({}, DefaultDataGridStyles.slotProps, props.slotProps), + [props.slotProps] + ); + + return ( + + + {...DefaultDataGridStyles} + {...props} + slots={slots} + slotProps={slotProps} + columns={FieldRegionProjectColumns} + initialState={ProjectInitialState} + headerFilters + hideFooter + sx={[flexLayout, noHeaderFilterButtons, noFooter]} + /> + + ); +}; + +// Remove the 'fieldRegion' column since this view is scoped to a specific field region +const FieldRegionProjectColumns = ProjectColumns.filter( + (col) => col.field !== 'fieldRegion.name' +); diff --git a/src/scenes/FieldZones/Detail/FieldZoneDetail.tsx b/src/scenes/FieldZones/Detail/FieldZoneDetail.tsx index e729a82e5..fff78b01b 100644 --- a/src/scenes/FieldZones/Detail/FieldZoneDetail.tsx +++ b/src/scenes/FieldZones/Detail/FieldZoneDetail.tsx @@ -1,146 +1,97 @@ import { useQuery } from '@apollo/client'; -import { Edit } from '@mui/icons-material'; -import { Box, Skeleton, Typography } from '@mui/material'; +import { TabContext, TabList, TabPanel } from '@mui/lab'; +import { Box, Skeleton, Stack, Typography } from '@mui/material'; import { Helmet } from 'react-helmet-async'; import { useParams } from 'react-router-dom'; -import { canEditAny } from '~/common'; -import { useDialog } from '~/components/Dialog'; -import { - DisplaySimpleProperty, - DisplaySimplePropertyProps, -} from '~/components/DisplaySimpleProperty'; -import { Link } from '~/components/Routing'; +import { Tab, TabsContainer } from '~/components/Tabs'; +import { EnumParam, makeQueryHandler, withDefault } from '~/hooks'; import { Error } from '../../../components/Error'; -import { Fab } from '../../../components/Fab'; -import { EditFieldZone } from '../../../components/FieldZone'; import { Redacted } from '../../../components/Redacted'; import { FieldZoneDetailDocument } from './FieldZoneDetail.graphql'; +import { FieldZoneProfile } from './Tabs/Profile/FieldZoneProfile'; +import { FieldZoneProjects } from './Tabs/Projects/FieldZoneProjects'; + +const useFieldZoneDetailsFilters = makeQueryHandler({ + tab: withDefault(EnumParam(['profile', 'projects']), 'profile'), +}); export const FieldZoneDetail = () => { const { fieldZoneId = '' } = useParams(); - const [editZoneState, editZone] = useDialog(); - const { data, error } = useQuery(FieldZoneDetailDocument, { variables: { fieldZoneId }, }); + const [filters, setFilters] = useFieldZoneDetailsFilters(); + const fieldZone = data?.fieldZone; return ( - theme.breakpoints.values.xl, }} > - {{ NotFound: 'Could not find field zone', Default: 'Error loading field zone', }} + + {!error && ( - theme.breakpoints.values.md, - display: 'flex', - flexDirection: 'column', - gap: 3, - }} - > + <> {!fieldZone ? ( - + ) : ( fieldZone.name.value ?? ( ) )} - {canEditAny(fieldZone, true) && ( - - - - )} - - - - {fieldZone ? 'Field Zone' : } - - - - {fieldZone.director.value.fullName} - - ) - } - loading={!fieldZone} - /> - + + + setFilters({ ...filters, tab })} + aria-label="field zone navigation tabs" + variant="scrollable" + > + + + + + + + + {fieldZone && } + + + + )} - {fieldZone && } - + ); }; - -const DisplayProperty = (props: DisplaySimplePropertyProps) => - !props.value && !props.loading ? null : ( - - - - - - - - - ) : null - } - LabelProps={{ - color: 'textSecondary', - variant: 'body2', - ...props.LabelProps, - }} - ValueProps={{ - color: 'textPrimary', - ...props.ValueProps, - }} - /> - ); diff --git a/src/scenes/FieldZones/Detail/Tabs/Profile/FieldZoneProfile.graphql b/src/scenes/FieldZones/Detail/Tabs/Profile/FieldZoneProfile.graphql new file mode 100644 index 000000000..58b3c8761 --- /dev/null +++ b/src/scenes/FieldZones/Detail/Tabs/Profile/FieldZoneProfile.graphql @@ -0,0 +1,4 @@ +fragment FieldZoneProfile on FieldZone { + ...DisplayFieldZone + ...FieldZoneForm +} diff --git a/src/scenes/FieldZones/Detail/Tabs/Profile/FieldZoneProfile.tsx b/src/scenes/FieldZones/Detail/Tabs/Profile/FieldZoneProfile.tsx new file mode 100644 index 000000000..b6b34529a --- /dev/null +++ b/src/scenes/FieldZones/Detail/Tabs/Profile/FieldZoneProfile.tsx @@ -0,0 +1,100 @@ +import { Edit } from '@mui/icons-material'; +import { + Box, + IconButton, + Paper, + Skeleton, + Stack, + Tooltip, + Typography, +} from '@mui/material'; +import { canEditAny } from '~/common'; +import { useDialog } from '~/components/Dialog'; +import { + DisplaySimpleProperty, + DisplaySimplePropertyProps, +} from '~/components/DisplaySimpleProperty'; +import { EditFieldZone } from '~/components/FieldZone'; +import { Link } from '~/components/Routing'; +import { FieldZoneProfileFragment } from './FieldZoneProfile.graphql'; + +interface FieldZoneProfileProps { + fieldZone: FieldZoneProfileFragment | undefined; +} + +export const FieldZoneProfile = ({ fieldZone }: FieldZoneProfileProps) => { + const [editZoneState, editZone] = useDialog(); + + const canEditAnyFields = canEditAny(fieldZone); + + return ( + ({ + display: 'flex', + justifyContent: 'space-between', + width: theme.breakpoints.values.md, + minHeight: 200, + })} + > + + + {fieldZone.director.value.fullName} + + ) : null + } + loading={!fieldZone} + /> + + + {canEditAnyFields ? ( + + + + + + ) : null} + + {fieldZone && } + + ); +}; + +const DisplayProperty = (props: DisplaySimplePropertyProps) => + !props.value && !props.loading ? null : ( + + + + + + + + + ) : null + } + LabelProps={{ + color: 'textSecondary', + variant: 'body2', + ...props.LabelProps, + }} + ValueProps={{ + color: 'textPrimary', + ...props.ValueProps, + }} + /> + ); diff --git a/src/scenes/FieldZones/Detail/Tabs/Projects/FieldZoneProjects.graphql b/src/scenes/FieldZones/Detail/Tabs/Projects/FieldZoneProjects.graphql new file mode 100644 index 000000000..45ac23d5e --- /dev/null +++ b/src/scenes/FieldZones/Detail/Tabs/Projects/FieldZoneProjects.graphql @@ -0,0 +1,17 @@ +query FieldZoneProjects($fieldZoneId: ID!, $input: ProjectListInput) { + fieldZone(id: $fieldZoneId) { + id + projects(input: $input) { + canRead + hasMore + total + items { + ...fieldZoneProjectDataGridRow + } + } + } +} + +fragment fieldZoneProjectDataGridRow on Project { + ...projectDataGridRow +} diff --git a/src/scenes/FieldZones/Detail/Tabs/Projects/FieldZoneProjects.tsx b/src/scenes/FieldZones/Detail/Tabs/Projects/FieldZoneProjects.tsx new file mode 100644 index 000000000..3ce3f4cb4 --- /dev/null +++ b/src/scenes/FieldZones/Detail/Tabs/Projects/FieldZoneProjects.tsx @@ -0,0 +1,65 @@ +import { + DataGridPro as DataGrid, + DataGridProProps as DataGridProps, +} from '@mui/x-data-grid-pro'; +import { merge } from 'lodash'; +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { + DefaultDataGridStyles, + flexLayout, + noFooter, + noHeaderFilterButtons, + useDataGridSource, +} from '~/components/Grid'; +import { + ProjectColumns, + ProjectInitialState, + ProjectToolbar, +} from '~/components/ProjectDataGrid'; +import { TabPanelContent } from '~/components/Tabs'; +import { + FieldZoneProjectDataGridRowFragment as FieldZoneProject, + FieldZoneProjectsDocument, +} from './FieldZoneProjects.graphql'; + +export const FieldZoneProjects = () => { + const { fieldZoneId = '' } = useParams(); + + const [props] = useDataGridSource({ + query: FieldZoneProjectsDocument, + variables: { fieldZoneId }, + listAt: 'fieldZone.projects', + initialInput: { + sort: 'name', + }, + }); + + const slots = useMemo( + () => + merge({}, DefaultDataGridStyles.slots, props.slots, { + toolbar: ProjectToolbar, + } satisfies DataGridProps['slots']), + [props.slots] + ); + const slotProps = useMemo( + () => merge({}, DefaultDataGridStyles.slotProps, props.slotProps), + [props.slotProps] + ); + + return ( + + + {...DefaultDataGridStyles} + {...props} + slots={slots} + slotProps={slotProps} + columns={ProjectColumns} + initialState={ProjectInitialState} + headerFilters + hideFooter + sx={[flexLayout, noHeaderFilterButtons, noFooter]} + /> + + ); +};