diff --git a/packages/tupaia-web/public/images/matrix-placeholder-dot-only.png b/packages/tupaia-web/public/images/matrix-placeholder-dot-only.png new file mode 100644 index 0000000000..983325bbc5 Binary files /dev/null and b/packages/tupaia-web/public/images/matrix-placeholder-dot-only.png differ diff --git a/packages/tupaia-web/public/images/matrix-placeholder-mix.png b/packages/tupaia-web/public/images/matrix-placeholder-mix.png new file mode 100644 index 0000000000..0e32c614ea Binary files /dev/null and b/packages/tupaia-web/public/images/matrix-placeholder-mix.png differ diff --git a/packages/tupaia-web/public/images/matrix-placeholder-text-only.png b/packages/tupaia-web/public/images/matrix-placeholder-text-only.png new file mode 100644 index 0000000000..08a6a319e5 Binary files /dev/null and b/packages/tupaia-web/public/images/matrix-placeholder-text-only.png differ diff --git a/packages/tupaia-web/src/api/queries/useReport.ts b/packages/tupaia-web/src/api/queries/useReport.ts index be3f8d2a6b..4488d226de 100644 --- a/packages/tupaia-web/src/api/queries/useReport.ts +++ b/packages/tupaia-web/src/api/queries/useReport.ts @@ -24,6 +24,7 @@ export const useReport = (reportCode: DashboardItemType['reportCode'], params: Q const timeZone = getBrowserTimeZone(); const formattedStartDate = formatDateForApi(startDate, null); const formattedEndDate = formatDateForApi(endDate, null); + const endPoint = legacy ? 'legacyDashboardReport' : 'report'; return useQuery( [ 'report', @@ -36,10 +37,9 @@ export const useReport = (reportCode: DashboardItemType['reportCode'], params: Q formattedEndDate, ], () => - get(`report/${reportCode}`, { + get(`${endPoint}/${reportCode}`, { params: { dashboardCode, - legacy, itemCode, projectCode, organisationUnitCode: entityCode, diff --git a/packages/tupaia-web/src/components/DateRangePicker.tsx b/packages/tupaia-web/src/components/DateRangePicker.tsx index b06102b87e..ed503f8724 100644 --- a/packages/tupaia-web/src/components/DateRangePicker.tsx +++ b/packages/tupaia-web/src/components/DateRangePicker.tsx @@ -23,6 +23,7 @@ const Wrapper = styled.div` padding: 0.2rem; text-transform: none; font-size: 0.875rem; + min-width: 0; color: ${({ theme }) => theme.palette.text.primary}; svg { height: 1.3rem; diff --git a/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx b/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx index c7d93f8da5..225dd6b32f 100644 --- a/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx +++ b/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx @@ -7,17 +7,18 @@ import React from 'react'; import styled from 'styled-components'; import { UseQueryResult } from 'react-query'; import { Alert as BaseAlert, TextButton } from '@tupaia/ui-components'; +import { ViewContent as ChartViewContent } from '@tupaia/ui-chart-components'; import { Typography, Link, CircularProgress } from '@material-ui/core'; -import { ReportDisplayProps } from '../../types'; import { Chart } from '../Chart'; import { ExpandItemButton } from './ExpandItemButton'; +import { Matrix } from '../Matrix'; +import { DashboardItemType, MatrixViewContent } from '../../types'; const ErrorLink = styled(Link)` color: inherit; text-decoration: underline; font-weight: ${({ theme }) => theme.typography.fontWeightBold}; `; - const RetryButton = styled(TextButton)` margin: 0; padding: 0; @@ -30,6 +31,7 @@ const RetryButton = styled(TextButton)` `; const Alert = styled(BaseAlert)` + overflow: hidden; // this is to stop any extra long text from overflowing the alert and causing a horizontal scroll on the dashboard .MuiAlert-message { max-width: 100%; } @@ -49,13 +51,13 @@ const LoadingContainer = styled.div` padding: 1rem; `; -// Eventually matrix etc will be added here const DisplayComponents = { chart: Chart, + matrix: Matrix, }; interface DashboardItemContentProps { - viewContent: ReportDisplayProps; + viewContent: DashboardItemType & (ChartViewContent | MatrixViewContent); isEnlarged?: boolean; isLoading: boolean; error: UseQueryResult['error'] | null; diff --git a/packages/tupaia-web/src/features/DashboardItem/EnlargedDashboardItem.tsx b/packages/tupaia-web/src/features/DashboardItem/EnlargedDashboardItem.tsx index 59508d7cec..3ea93a37e5 100644 --- a/packages/tupaia-web/src/features/DashboardItem/EnlargedDashboardItem.tsx +++ b/packages/tupaia-web/src/features/DashboardItem/EnlargedDashboardItem.tsx @@ -46,6 +46,14 @@ const Title = styled(Typography).attrs({ const TitleWrapper = styled(FlexColumn)` align-items: center; + margin-bottom: 1rem; +`; + +const Subheading = styled(Typography).attrs({ + variant: 'h3', +})` + font-size: 1rem; + margin-bottom: 1rem; `; /** @@ -128,6 +136,7 @@ export const EnlargedDashboardItem = () => { /> )} + {currentReport?.description && {currentReport?.description}} { + let topLevelRows = []; + // if a categoryId is not passed in, then we need to find the top level rows + if (!categoryId) { + // get the highest level rows, which are the ones that have a category but no categoryId + const highestLevel = rows.filter(row => row.category && !row.categoryId); + // if there are no highest level rows, then the top level rows are just all of the rows + topLevelRows = highestLevel.length ? highestLevel : rows; + } else { + // otherwise, the top level rows are the ones that have the categoryId that was passed in + topLevelRows = rows.filter(row => row.categoryId === categoryId); + } + + // loop through the topLevelRows, and parse them into the format that the Matrix component can use + return topLevelRows.map(row => { + const { dataElement = '', category, ...rest } = row; + // if the row has a category, then it has children, so we need to parse them using this same function + if (category) { + return { + title: category, + ...rest, + children: parseRows(rows, category), + }; + } + // otherwise, handle as a regular row + return { + title: dataElement, + ...rest, + }; + }); +}; + +// This is a recursive function that parses the columns of the matrix into a format that the Matrix component can use. +const parseColumns = (columns: MatrixDataColumn[]): MatrixColumnType[] => { + return columns.map(column => { + const { category, key, title, columns: children } = column; + // if a column has a category, then it has children, so we need to parse them using this same function + if (category) + return { + title: category, + key: category, + children: parseColumns(children!), + }; + // otherwise, handle as a regular column + return { + title, + key, + }; + }); +}; + +const getPlaceholderImage = ({ presentationOptions = {}, categoryPresentationOptions = {} }) => { + // if the matrix is not using any dots, show a text-only placeholder + if (!getIsUsingDots(presentationOptions) && !getIsUsingDots(categoryPresentationOptions)) + return '/images/matrix-placeholder-text-only.png'; + // if the matrix has applyLocation.columnIndexes, show a mix placeholder, because this means it is a mix of dots and text + if ((presentationOptions as ConditionalPresentationOptions)?.applyLocation?.columnIndexes) + return '/images/matrix-placeholder-mix.png'; + // otherwise, show a dot-only placeholder + return '/images/matrix-placeholder-dot-only.png'; +}; + +/** + * This is the component that is used to display a matrix. It handles the parsing of the data into the format that the Matrix component can use, as well as placeholder images. It shows a message when there are no rows available to display. + */ + +interface MatrixProps { + viewContent: MatrixViewContent; + isEnlarged?: boolean; +} +export const Matrix = ({ viewContent, isEnlarged = false }: MatrixProps) => { + const { columns, rows, ...config } = viewContent; + + const placeholderImage = getPlaceholderImage(config); + // in the dashboard, show a placeholder image + if (!isEnlarged) return Matrix Placeholder; + + const parsedRows = parseRows(rows); + const parsedColumns = parseColumns(columns); + + if (!parsedRows.length) return No data available; + + return ; +}; diff --git a/packages/tupaia-web/src/types/types.d.ts b/packages/tupaia-web/src/types/types.d.ts index 7e29031bc6..07409ed0b0 100644 --- a/packages/tupaia-web/src/types/types.d.ts +++ b/packages/tupaia-web/src/types/types.d.ts @@ -9,12 +9,14 @@ import { MapOverlay, MapOverlayGroupRelation, EntityType, + MatrixConfig, } from '@tupaia/types'; import { ActivePolygonProps, LeafletMapProps } from '@tupaia/ui-map-components'; -import { ViewContent } from '@tupaia/ui-chart-components'; +import { ViewContent as ChartViewContent } from '@tupaia/ui-chart-components'; import { Position } from 'geojson'; import { KeysToCamelCase } from './helpers'; import { GRANULARITY_CONFIG } from '@tupaia/utils'; +import { MatrixColumnType, MatrixRowType } from '@tupaia/ui-components'; export type SingleProject = KeysToCamelCase & { hasAccess: boolean; @@ -63,7 +65,6 @@ export type TupaiaUrlParams = { dashboardCode?: DashboardCode; }; -export type ReportDisplayProps = ViewContent & DashboardItemType; export type DashboardName = DashboardResponse['dashboardName']; export type SingleMapOverlayItem = KeysToCamelCase< @@ -101,3 +102,24 @@ export type EntityResponse = Entity & { photoUrl?: string; children?: Entity[]; }; + +// This is the row type in the response from the report endpoint when the report is a matrix. It will contain data for each column, keyed by the column key, as well as dataElement, categoryId and category +export type MatrixDataRow = Record & { + dataElement?: string; // this is the data to display in the row header cell + categoryId?: string; // this means the row is a child of a grouped row + category?: string; // this means the row is a grouped row +}; + +// This is the column type in the response from the report endpoint when the report is a matrix +export type MatrixDataColumn = { + title: string; + key: string; + category?: string; // this means the column is a grouped column + columns?: MatrixDataColumn[]; // these are the child columns of a grouped column +}; + +// The 'ViewContent' is the data that is passed to the matrix view component +export type MatrixViewContent = MatrixConfig & { + rows: MatrixDataRow[]; + columns: MatrixDataColumn[]; +}; diff --git a/packages/ui-components/src/components/Matrix/Matrix.tsx b/packages/ui-components/src/components/Matrix/Matrix.tsx index df11977f90..a6fe220533 100644 --- a/packages/ui-components/src/components/Matrix/Matrix.tsx +++ b/packages/ui-components/src/components/Matrix/Matrix.tsx @@ -20,6 +20,7 @@ const MatrixTable = styled.table` border-collapse: collapse; border: 1px solid ${({ theme }) => getFullHex(theme.palette.text.primary)}33; color: ${({ theme }) => theme.palette.text.primary}; + table-layout: fixed; // this is to allow us to set max-widths on the columns height: 1px; // this is to make the cell content (eg. buttons) take full height of the cell, and does not actually get applied td, th { diff --git a/packages/ui-components/src/components/Matrix/index.ts b/packages/ui-components/src/components/Matrix/index.ts index dd73d8877f..32e3eb86e7 100644 --- a/packages/ui-components/src/components/Matrix/index.ts +++ b/packages/ui-components/src/components/Matrix/index.ts @@ -4,3 +4,4 @@ */ export { Matrix } from './Matrix'; +export * from './utils';