diff --git a/client/src/containers/analysis-visualization/analysis-table/component.tsx b/client/src/containers/analysis-visualization/analysis-table/component.tsx deleted file mode 100644 index 6d49eb57c..000000000 --- a/client/src/containers/analysis-visualization/analysis-table/component.tsx +++ /dev/null @@ -1,571 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { DownloadIcon, InformationCircleIcon } from '@heroicons/react/outline'; -import { uniq, omit } from 'lodash-es'; -import toast from 'react-hot-toast'; -import { ArrowLeftIcon } from '@heroicons/react/solid'; - -import ComparisonCell from './comparison-cell/component'; -import ChartCell from './chart-cell'; -import AnalysisTableFooter from './footer'; - -import { useAppSelector, useSyncIndicators, useSyncTableDetailView } from 'store/hooks'; -import { filtersForTabularAPI } from 'store/features/analysis/selector'; -import { scenarios } from 'store/features/analysis/scenarios'; -import { useIndicators } from 'hooks/indicators'; -import { - useImpactData, - useDownloadImpactData, - useDownloadImpactActualVsScenarioData, - useDownloadImpactScenarioVsScenarioData, -} from 'hooks/impact'; -import { useImpactComparison, useImpactScenarioComparison } from 'hooks/impact/comparison'; -import AnalysisDynamicMetadata from 'containers/analysis-visualization/analysis-dynamic-metadata'; -import { Button } from 'components/button'; -import Table from 'components/table/component'; -import { formatNumber } from 'utils/number-format'; -import { DEFAULT_PAGE_SIZES } from 'components/table/pagination/constants'; -import { handleResponseError } from 'services/api'; - -import type { - ExpandedState, - PaginationState, - RowSelectionState, - SortingState, - TableState, - Table as TableType, -} from '@tanstack/react-table'; -import type { TableProps } from 'components/table/component'; -import type { ColumnDefinition } from 'components/table/column'; -import type { ChartData } from './chart-cell/types'; -import type { ComparisonMode, ImpactRowType, ImpactTableValueItem } from './types'; - -const isParentRow = ( - row: ImpactRowType, -): row is ImpactRowType => { - return 'metadata' in row; -}; - -const AnalysisTable = () => { - const [paginationState, setPaginationState] = useState({ - pageIndex: 1, - pageSize: DEFAULT_PAGE_SIZES[0], - }); - const [sortingState, setSortingState] = useState([]); - const [expandedState, setExpandedState] = useState(null); - const [rowSelectionState, setRowSelectionState] = useState({}); - const tableState: Partial = useMemo(() => { - return { - pagination: paginationState, - sorting: sortingState, - expanded: expandedState, - rowSelection: rowSelectionState, - }; - }, [expandedState, paginationState, rowSelectionState, sortingState]); - - const [syncedIndicators, setSyncedIndicators] = useSyncIndicators(); - const [syncedDetailView, setSyncedDetailView] = useSyncTableDetailView(); - - const selectedIndicators = syncedIndicators; - - const { scenarioToCompare, isComparisonEnabled, currentScenario } = useAppSelector(scenarios); - const { data: indicators } = useIndicators(undefined, { select: (data) => data?.data }); - const downloadImpactData = useDownloadImpactData({ - onSuccess: () => { - toast.success('Data was downloaded successfully'); - }, - onError: handleResponseError, - }); - - const downloadActualVsScenarioData = useDownloadImpactActualVsScenarioData({ - onSuccess: () => { - toast.success('Data was downloaded successfully'); - }, - onError: handleResponseError, - }); - - const downloadScenarioVsScenarioData = useDownloadImpactScenarioVsScenarioData({ - onSuccess: () => { - toast.success('Data was downloaded successfully'); - }, - onError: handleResponseError, - }); - - const { indicatorId, ...restFilters } = useAppSelector(filtersForTabularAPI); - - const useIsComparison = useCallback( - (table: ImpactRowType[]): table is ImpactRowType[] => { - return isComparisonEnabled && !!scenarioToCompare; - }, - [isComparisonEnabled, scenarioToCompare], - ); - - const useIsScenarioComparison = useCallback( - (table: ImpactRowType[]): table is ImpactRowType<'scenario'>[] => { - return isComparisonEnabled && !!currentScenario; - }, - [isComparisonEnabled, currentScenario], - ); - - const isEnable = - !!indicators?.length && - !!restFilters.startYear && - !!restFilters.endYear && - restFilters.endYear !== restFilters.startYear; - - const indicatorIds = useMemo(() => { - if (Array.isArray(selectedIndicators)) { - return selectedIndicators.map((id) => id); - } - - if (selectedIndicators && !Array.isArray(selectedIndicators)) { - return [selectedIndicators]; - } - - return indicators.map((indicator) => indicator.id); - }, [indicators, selectedIndicators]); - - const sortingParams = useMemo(() => { - if (!!sortingState.length) { - return { - sortingYear: Number(sortingState?.[0].id), - sortingOrder: sortingState[0].desc ? 'DESC' : 'ASC', - }; - } - return {}; - }, [sortingState]); - - const params = useMemo( - () => ({ - indicatorIds, - startYear: restFilters.startYear, - endYear: restFilters.endYear, - groupBy: restFilters.groupBy, - ...restFilters, - ...sortingParams, - scenarioId: currentScenario, - 'page[number]': paginationState.pageIndex, - 'page[size]': paginationState.pageSize, - }), - [ - currentScenario, - indicatorIds, - paginationState.pageIndex, - paginationState.pageSize, - restFilters, - sortingParams, - ], - ); - - const plainImpactData = useImpactData(params, { - enabled: !isComparisonEnabled && isEnable, - }); - - const impactActualComparisonData = useImpactComparison( - { ...omit(params, 'scenarioId'), comparedScenarioId: scenarioToCompare }, - { - enabled: isComparisonEnabled && !currentScenario && isEnable, - }, - ); - const impactScenarioComparisonData = useImpactScenarioComparison( - { - ...omit(params, 'scenarioId'), - baseScenarioId: currentScenario, - comparedScenarioId: scenarioToCompare, - }, - { - enabled: isComparisonEnabled && !!currentScenario && isEnable, - }, - ); - - const impactComparisonData = !!currentScenario - ? impactScenarioComparisonData - : impactActualComparisonData; - - const { - data: impactData, - isLoading, - isFetching, - } = useMemo(() => { - if (isComparisonEnabled && !!scenarioToCompare) return impactComparisonData; - return plainImpactData; - }, [impactComparisonData, plainImpactData, isComparisonEnabled, scenarioToCompare]); - - const { - data: { impactTable = [] }, - metadata, - } = useMemo(() => { - if (impactData) return impactData; - return { data: { impactTable: [] }, metadata: {} }; - }, [impactData]); - - const firstProjectedYear = useMemo(() => { - if (!impactTable) return null; - return impactTable[0]?.rows[0]?.values.find((value) => value.isProjected)?.year; - }, [impactTable]); - - const handleDownloadData = useCallback(async () => { - let csv = null; - // actual vs scenario - if (!currentScenario && scenarioToCompare) { - csv = await downloadActualVsScenarioData.mutateAsync({ - ...omit(params, 'page[number]', 'page[size]'), - comparedScenarioId: scenarioToCompare, - }); - } - // scenario vs scenario - else if (currentScenario && scenarioToCompare) { - csv = await downloadScenarioVsScenarioData.mutateAsync({ - ...omit(params, 'page[number]', 'page[size]', 'scenarioId'), - baseScenarioId: currentScenario, - comparedScenarioId: scenarioToCompare, - }); - } - // no scenario or comparison - else { - csv = await downloadImpactData.mutateAsync(omit(params, 'page[number]', 'page[size]')); - } - - if (csv) { - const url = window.URL.createObjectURL(new Blob([csv])); - const link = document.createElement('a'); - link.setAttribute('href', url); - link.setAttribute('download', `impact_data_${Date.now()}.csv`); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } - // do not pass pagination params to download data endpoint - }, [ - currentScenario, - downloadActualVsScenarioData, - downloadImpactData, - downloadScenarioVsScenarioData, - params, - scenarioToCompare, - ]); - - const handleExpandedChange = useCallback( - (table: TableType>) => { - if (!!expandedState) { - const expandedIds = Object.keys(expandedState); - const rowsToSelect = {}; - table - .getRowModel() - .rows.filter((row) => expandedIds.includes(row.id)) - .forEach((row) => { - rowsToSelect[row.id] = true; - row.originalSubRows.forEach( - (_subRow, index) => (rowsToSelect[`${row.id}.${index}`] = true), - ); - }); - setRowSelectionState(rowsToSelect); - } - }, - [expandedState], - ); - - // Years from impact table - const years = useMemo(() => { - // TODO: do we have to check all rows or is the first one guaranteed to have all years? - // const years = impactTable[0]?.yearSum?.map((sum) => sum.year); - const years = impactTable?.flatMap(({ yearSum }) => yearSum.map((sum) => sum.year)); - - // TODO: if the above is true, we don't need this - return uniq(years); - }, [impactTable]); - - const initialTableData: ImpactRowType[] = useMemo( - () => - impactTable.map(({ indicatorShortName, yearSum, rows, ...impact }) => ({ - ...impact, - children: rows, - name: indicatorShortName, - ...(yearSum && { - values: yearSum.map((sum) => ({ - ...sum, - isProjected: rows[0]?.values.find((v) => v.year === sum.year)?.isProjected, - })), - }), - })), - [impactTable], - ); - - const [tableData, setTableData] = useState[]>([]); - - useEffect(() => { - // ? a single indicator is expanded - if (syncedDetailView) { - setTableData(initialTableData.find((row) => row.indicatorId === syncedDetailView)?.children); - } - - // ? several indicators are selected - if (selectedIndicators && !syncedDetailView) { - setTableData(initialTableData.filter((row) => selectedIndicators.includes(row.indicatorId))); - } - - // ? all indicators are selected - if (!selectedIndicators?.length && !syncedDetailView) { - setTableData(initialTableData); - } - - setExpandedState(null); - setRowSelectionState({}); - }, [selectedIndicators, initialTableData, syncedDetailView]); - - const handleExitExpanded = useCallback(() => { - setExpandedState({}); - setSyncedDetailView(null); - - if (syncedIndicators?.length === 1) { - setSyncedIndicators(null); - } - }, [setSyncedDetailView, syncedIndicators, setSyncedIndicators]); - - const handleExpandRow = useCallback( - (indicatorId: string) => { - setExpandedState({}); - setSyncedDetailView(indicatorId); - }, - [setSyncedDetailView], - ); - - const isComparison = useIsComparison(tableData); - const isScenarioComparison = useIsScenarioComparison(tableData); - - const valueIsScenarioComparison = useCallback( - (value: ImpactTableValueItem): value is ImpactTableValueItem<'scenario'> => { - return isScenarioComparison && isComparison; - }, - [isComparison, isScenarioComparison], - ); - - const expanded = useMemo(() => { - return indicators.find((i) => i.id === syncedDetailView); - }, [syncedDetailView, indicators]); - - const comparisonColumn = useCallback( - (year: number): ColumnDefinition> => { - const valueIsComparison = ( - value: ImpactTableValueItem, - ): value is ImpactTableValueItem => { - return !isScenarioComparison && isComparison; - }; - - return { - header: () => {year}, - id: `${year}`, - size: 170, - align: 'center', - enableSorting: true, - cell: ({ row: { original: data, id }, table }) => { - //* The metadata is only present at the parent row, so we need to get it from there - const { rowsById } = table.getExpandedRowModel(); - const parentRowData = rowsById[id.split('.')[0]].original as unknown as ImpactRowType< - Mode, - true - >; - - const unit: string = parentRowData.metadata?.unit || expanded?.metadata?.units; - - const value = data.values?.find((value) => value.year === year); - const isComparison = valueIsComparison(value); - const isScenarioComparison = valueIsScenarioComparison(value); - - if (!isComparison && !isScenarioComparison) { - if (unit) { - return `${formatNumber(value.value)} ${unit}`; - } - return formatNumber(value?.value); - } - - if (isScenarioComparison) { - const { baseScenarioValue, comparedScenarioValue, ...rest } = value; - return ( - - ); - } - - return ( - - ); - }, - }; - }, - [expanded, isComparison, isScenarioComparison, valueIsScenarioComparison], - ); - - const baseColumns = useMemo( - (): ColumnDefinition>[] => [ - { - id: 'name', - header: () => ( -
- {!!expanded?.name ? ( - - ) : ( - - Selected Indicators - - )} -
- ), - align: 'left', - isSticky: 'left', - size: 260, - cell: ({ row: { original, depth } }) => { - const name = - isParentRow(original) && - depth === 0 && - indicators.find((i) => i.id === original.indicatorId)?.metadata?.short_name; - - return ( -
- {!expanded?.name && ( - - )} -
- {expanded?.name ? ( - original.name - ) : ( -
- {name || original.name} - {isParentRow(original) && depth === 0 && <> ({original.metadata.unit})} -
- )} - - {!expanded?.name && isParentRow(original) && ( - - )} -
-
- ); - }, - }, - { - id: 'datesRangeChart', - header: () => ( - - {years?.length ? `${years[0]}-${years[years.length - 1]}` : '-'} - - ), - className: 'px-2 mx-auto', - align: 'center', - size: 170, - cell: ({ - row: { - original: { values }, - }, - }) => { - const chartData = values as ChartData[]; - return ( -
- -
- ); - }, - }, - ...years.map((year) => comparisonColumn(year as number)), - ], - [years, expanded?.name, handleExitExpanded, indicators, handleExpandRow, comparisonColumn], - ); - - const tableProps = useMemo( - (): TableProps> & { - firstProjectedYear: number; - } => ({ - showPagination: Boolean(selectedIndicators), - paginationProps: { - totalItems: metadata.totalItems, - totalPages: metadata.totalPages, - currentPage: metadata.page, - pageSize: metadata.size, - }, - getSubRows: (row) => row.children, - state: tableState, - onSortingChange: setSortingState, - onPaginationChange: setPaginationState, - onExpandedChange: setExpandedState, - isLoading: isFetching, - enableExpanding: Boolean(syncedDetailView), - data: (tableData as ImpactRowType[]) || [], - columns: baseColumns as ColumnDefinition>[], - handleExpandedChange, - firstProjectedYear, - }), - [ - metadata, - tableState, - isFetching, - tableData, - baseColumns, - handleExpandedChange, - firstProjectedYear, - selectedIndicators, - syncedDetailView, - ], - ); - - return ( -
-
-
- -
- -
-
-
-
- - - - - ); -}; - -export default AnalysisTable; diff --git a/client/src/containers/analysis-visualization/analysis-table/footer/index.tsx b/client/src/containers/analysis-visualization/analysis-table/footer/index.tsx index 7c458f6d6..0809c6a1b 100644 --- a/client/src/containers/analysis-visualization/analysis-table/footer/index.tsx +++ b/client/src/containers/analysis-visualization/analysis-table/footer/index.tsx @@ -15,7 +15,7 @@ const AnalysisTableFooter: FC = () => { }); return ( -
+
    {Boolean(scenarioName) && (
  • @@ -24,7 +24,7 @@ const AnalysisTableFooter: FC = () => { 140 - + +70 @@ -39,24 +39,6 @@ const AnalysisTableFooter: FC = () => { 70
- -
- - - - The years at the right of the doted line are projected -
); }; diff --git a/client/src/containers/analysis-visualization/analysis-table/index.tsx b/client/src/containers/analysis-visualization/analysis-table/index.tsx index b404d7fd4..9068b5e44 100644 --- a/client/src/containers/analysis-visualization/analysis-table/index.tsx +++ b/client/src/containers/analysis-visualization/analysis-table/index.tsx @@ -1 +1,574 @@ -export { default } from './component'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { DownloadIcon, InformationCircleIcon } from '@heroicons/react/outline'; +import { uniq, omit } from 'lodash-es'; +import toast from 'react-hot-toast'; +import { ArrowLeftIcon } from '@heroicons/react/solid'; +import { useSearchParams } from 'next/navigation'; + +import ComparisonCell from './comparison-cell/component'; +import ChartCell from './chart-cell'; +import AnalysisTableFooter from './footer'; + +import { useAppSelector, useSyncIndicators, useSyncTableDetailView } from 'store/hooks'; +import { filtersForTabularAPI } from 'store/features/analysis/selector'; +import { scenarios } from 'store/features/analysis/scenarios'; +import { useIndicators } from 'hooks/indicators'; +import { + useImpactData, + useDownloadImpactData, + useDownloadImpactActualVsScenarioData, + useDownloadImpactScenarioVsScenarioData, +} from 'hooks/impact'; +import { useImpactComparison, useImpactScenarioComparison } from 'hooks/impact/comparison'; +import AnalysisDynamicMetadata from 'containers/analysis-visualization/analysis-dynamic-metadata'; +import { Button } from 'components/button'; +import Table from 'components/table/component'; +import { formatNumber } from 'utils/number-format'; +import { DEFAULT_PAGE_SIZES } from 'components/table/pagination/constants'; +import { handleResponseError } from 'services/api'; + +import type { + ExpandedState, + PaginationState, + RowSelectionState, + SortingState, + TableState, + Table as TableType, +} from '@tanstack/react-table'; +import type { TableProps } from 'components/table/component'; +import type { ColumnDefinition } from 'components/table/column'; +import type { ChartData } from './chart-cell/types'; +import type { ComparisonMode, ImpactRowType, ImpactTableValueItem } from './types'; + +const isParentRow = ( + row: ImpactRowType, +): row is ImpactRowType => { + return 'metadata' in row; +}; + +const AnalysisTable = () => { + const searchParams = useSearchParams(); + const isComparisonEnabled = Boolean(searchParams.get('compareScenarioId')); + + const [paginationState, setPaginationState] = useState({ + pageIndex: 1, + pageSize: DEFAULT_PAGE_SIZES[0], + }); + const [sortingState, setSortingState] = useState([]); + const [expandedState, setExpandedState] = useState(null); + const [rowSelectionState, setRowSelectionState] = useState({}); + const tableState: Partial = useMemo(() => { + return { + pagination: paginationState, + sorting: sortingState, + expanded: expandedState, + rowSelection: rowSelectionState, + }; + }, [expandedState, paginationState, rowSelectionState, sortingState]); + + const [syncedIndicators, setSyncedIndicators] = useSyncIndicators(); + const [syncedDetailView, setSyncedDetailView] = useSyncTableDetailView(); + + const selectedIndicators = syncedIndicators; + + const { scenarioToCompare, currentScenario } = useAppSelector(scenarios); + const { data: indicators } = useIndicators(undefined, { select: (data) => data?.data }); + const downloadImpactData = useDownloadImpactData({ + onSuccess: () => { + toast.success('Data was downloaded successfully'); + }, + onError: handleResponseError, + }); + + const downloadActualVsScenarioData = useDownloadImpactActualVsScenarioData({ + onSuccess: () => { + toast.success('Data was downloaded successfully'); + }, + onError: handleResponseError, + }); + + const downloadScenarioVsScenarioData = useDownloadImpactScenarioVsScenarioData({ + onSuccess: () => { + toast.success('Data was downloaded successfully'); + }, + onError: handleResponseError, + }); + + const { indicatorId, ...restFilters } = useAppSelector(filtersForTabularAPI); + + const useIsComparison = useCallback( + (table: ImpactRowType[]): table is ImpactRowType[] => { + return isComparisonEnabled && !!scenarioToCompare; + }, + [isComparisonEnabled, scenarioToCompare], + ); + + const useIsScenarioComparison = useCallback( + (table: ImpactRowType[]): table is ImpactRowType<'scenario'>[] => { + return isComparisonEnabled && !!currentScenario; + }, + [isComparisonEnabled, currentScenario], + ); + + const isEnable = + !!indicators?.length && + !!restFilters.startYear && + !!restFilters.endYear && + restFilters.endYear !== restFilters.startYear; + + const indicatorIds = useMemo(() => { + if (Array.isArray(selectedIndicators)) { + return selectedIndicators.map((id) => id); + } + + if (selectedIndicators && !Array.isArray(selectedIndicators)) { + return [selectedIndicators]; + } + + return indicators.map((indicator) => indicator.id); + }, [indicators, selectedIndicators]); + + const sortingParams = useMemo(() => { + if (!!sortingState.length) { + return { + sortingYear: Number(sortingState?.[0].id), + sortingOrder: sortingState[0].desc ? 'DESC' : 'ASC', + }; + } + return {}; + }, [sortingState]); + + const params = useMemo( + () => ({ + indicatorIds, + startYear: restFilters.startYear, + endYear: restFilters.endYear, + groupBy: restFilters.groupBy, + ...restFilters, + ...sortingParams, + scenarioId: currentScenario, + 'page[number]': paginationState.pageIndex, + 'page[size]': paginationState.pageSize, + }), + [ + currentScenario, + indicatorIds, + paginationState.pageIndex, + paginationState.pageSize, + restFilters, + sortingParams, + ], + ); + + const plainImpactData = useImpactData(params, { + enabled: !isComparisonEnabled && isEnable, + }); + + const impactActualComparisonData = useImpactComparison( + { ...omit(params, 'scenarioId'), comparedScenarioId: scenarioToCompare }, + { + enabled: isComparisonEnabled && !currentScenario && isEnable, + }, + ); + const impactScenarioComparisonData = useImpactScenarioComparison( + { + ...omit(params, 'scenarioId'), + baseScenarioId: currentScenario, + comparedScenarioId: scenarioToCompare, + }, + { + enabled: isComparisonEnabled && !!currentScenario && isEnable, + }, + ); + + const impactComparisonData = !!currentScenario + ? impactScenarioComparisonData + : impactActualComparisonData; + + const { + data: impactData, + isLoading, + isFetching, + } = useMemo(() => { + if (isComparisonEnabled && !!scenarioToCompare) return impactComparisonData; + return plainImpactData; + }, [impactComparisonData, plainImpactData, isComparisonEnabled, scenarioToCompare]); + + const { + data: { impactTable = [] }, + metadata, + } = useMemo(() => { + if (impactData) return impactData; + return { data: { impactTable: [] }, metadata: {} }; + }, [impactData]); + + const firstProjectedYear = useMemo(() => { + if (!impactTable) return null; + return impactTable[0]?.rows[0]?.values.find((value) => value.isProjected)?.year; + }, [impactTable]); + + const handleDownloadData = useCallback(async () => { + let csv = null; + // actual vs scenario + if (!currentScenario && scenarioToCompare) { + csv = await downloadActualVsScenarioData.mutateAsync({ + ...omit(params, 'page[number]', 'page[size]'), + comparedScenarioId: scenarioToCompare, + }); + } + // scenario vs scenario + else if (currentScenario && scenarioToCompare) { + csv = await downloadScenarioVsScenarioData.mutateAsync({ + ...omit(params, 'page[number]', 'page[size]', 'scenarioId'), + baseScenarioId: currentScenario, + comparedScenarioId: scenarioToCompare, + }); + } + // no scenario or comparison + else { + csv = await downloadImpactData.mutateAsync(omit(params, 'page[number]', 'page[size]')); + } + + if (csv) { + const url = window.URL.createObjectURL(new Blob([csv])); + const link = document.createElement('a'); + link.setAttribute('href', url); + link.setAttribute('download', `impact_data_${Date.now()}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + // do not pass pagination params to download data endpoint + }, [ + currentScenario, + downloadActualVsScenarioData, + downloadImpactData, + downloadScenarioVsScenarioData, + params, + scenarioToCompare, + ]); + + const handleExpandedChange = useCallback( + (table: TableType>) => { + if (!!expandedState) { + const expandedIds = Object.keys(expandedState); + const rowsToSelect = {}; + table + .getRowModel() + .rows.filter((row) => expandedIds.includes(row.id)) + .forEach((row) => { + rowsToSelect[row.id] = true; + row.originalSubRows.forEach( + (_subRow, index) => (rowsToSelect[`${row.id}.${index}`] = true), + ); + }); + setRowSelectionState(rowsToSelect); + } + }, + [expandedState], + ); + + // Years from impact table + const years = useMemo(() => { + // TODO: do we have to check all rows or is the first one guaranteed to have all years? + // const years = impactTable[0]?.yearSum?.map((sum) => sum.year); + const years = impactTable?.flatMap(({ yearSum }) => yearSum.map((sum) => sum.year)); + + // TODO: if the above is true, we don't need this + return uniq(years); + }, [impactTable]); + + const initialTableData: ImpactRowType[] = useMemo( + () => + impactTable.map(({ indicatorShortName, yearSum, rows, ...impact }) => ({ + ...impact, + children: rows, + name: indicatorShortName, + ...(yearSum && { + values: yearSum.map((sum) => ({ + ...sum, + isProjected: rows[0]?.values.find((v) => v.year === sum.year)?.isProjected, + })), + }), + })), + [impactTable], + ); + + const [tableData, setTableData] = useState[]>([]); + + useEffect(() => { + // ? a single indicator is expanded + if (syncedDetailView) { + setTableData(initialTableData.find((row) => row.indicatorId === syncedDetailView)?.children); + } + + // ? several indicators are selected + if (selectedIndicators && !syncedDetailView) { + setTableData(initialTableData.filter((row) => selectedIndicators.includes(row.indicatorId))); + } + + // ? all indicators are selected + if (!selectedIndicators?.length && !syncedDetailView) { + setTableData(initialTableData); + } + + setExpandedState(null); + setRowSelectionState({}); + }, [selectedIndicators, initialTableData, syncedDetailView]); + + const handleExitExpanded = useCallback(() => { + setExpandedState({}); + setSyncedDetailView(null); + + if (syncedIndicators?.length === 1) { + setSyncedIndicators(null); + } + }, [setSyncedDetailView, syncedIndicators, setSyncedIndicators]); + + const handleExpandRow = useCallback( + (indicatorId: string) => { + setExpandedState({}); + setSyncedDetailView(indicatorId); + }, + [setSyncedDetailView], + ); + + const isComparison = useIsComparison(tableData); + const isScenarioComparison = useIsScenarioComparison(tableData); + + const valueIsScenarioComparison = useCallback( + (value: ImpactTableValueItem): value is ImpactTableValueItem<'scenario'> => { + return isScenarioComparison && isComparison; + }, + [isComparison, isScenarioComparison], + ); + + const expanded = useMemo(() => { + return indicators.find((i) => i.id === syncedDetailView); + }, [syncedDetailView, indicators]); + + const comparisonColumn = useCallback( + (year: number): ColumnDefinition> => { + const valueIsComparison = ( + value: ImpactTableValueItem, + ): value is ImpactTableValueItem => { + return !isScenarioComparison && isComparison; + }; + + return { + header: () => {year}, + id: `${year}`, + size: 170, + align: 'center', + enableSorting: true, + cell: ({ row: { original: data, id }, table }) => { + //* The metadata is only present at the parent row, so we need to get it from there + const { rowsById } = table.getExpandedRowModel(); + const parentRowData = rowsById[id.split('.')[0]].original as unknown as ImpactRowType< + Mode, + true + >; + + const unit: string = parentRowData.metadata?.unit || expanded?.metadata?.units; + + const value = data.values?.find((value) => value.year === year); + const isComparison = valueIsComparison(value); + const isScenarioComparison = valueIsScenarioComparison(value); + + if (!isComparison && !isScenarioComparison) { + if (unit) { + return `${formatNumber(value.value)} ${unit}`; + } + return formatNumber(value?.value); + } + + if (isScenarioComparison) { + const { baseScenarioValue, comparedScenarioValue, ...rest } = value; + return ( + + ); + } + + return ( + + ); + }, + }; + }, + [expanded, isComparison, isScenarioComparison, valueIsScenarioComparison], + ); + + const baseColumns = useMemo( + (): ColumnDefinition>[] => [ + { + id: 'name', + header: () => ( +
+ {!!expanded?.name ? ( + + ) : ( + + Selected Indicators + + )} +
+ ), + align: 'left', + isSticky: 'left', + size: 260, + cell: ({ row: { original, depth } }) => { + const name = + isParentRow(original) && + depth === 0 && + indicators.find((i) => i.id === original.indicatorId)?.metadata?.short_name; + + return ( +
+ {!expanded?.name && ( + + )} +
+ {expanded?.name ? ( + original.name + ) : ( +
+ {name || original.name} + {isParentRow(original) && depth === 0 && <> ({original.metadata.unit})} +
+ )} + + {!expanded?.name && isParentRow(original) && ( + + )} +
+
+ ); + }, + }, + { + id: 'datesRangeChart', + header: () => ( + + {years?.length ? `${years[0]}-${years[years.length - 1]}` : '-'} + + ), + className: 'px-2 mx-auto', + align: 'center', + size: 170, + cell: ({ + row: { + original: { values }, + }, + }) => { + const chartData = values as ChartData[]; + return ( +
+ +
+ ); + }, + }, + ...years.map((year) => comparisonColumn(year as number)), + ], + [years, expanded?.name, handleExitExpanded, indicators, handleExpandRow, comparisonColumn], + ); + + const tableProps = useMemo( + (): TableProps> & { + firstProjectedYear: number; + } => ({ + showPagination: Boolean(syncedDetailView), + paginationProps: { + totalItems: metadata.totalItems, + totalPages: metadata.totalPages, + currentPage: metadata.page, + pageSize: metadata.size, + }, + getSubRows: (row) => row.children, + state: tableState, + onSortingChange: setSortingState, + onPaginationChange: setPaginationState, + onExpandedChange: setExpandedState, + isLoading: isFetching, + enableExpanding: Boolean(syncedDetailView), + data: (tableData as ImpactRowType[]) || [], + columns: baseColumns as ColumnDefinition>[], + handleExpandedChange, + firstProjectedYear, + }), + [ + metadata, + tableState, + isFetching, + tableData, + baseColumns, + handleExpandedChange, + firstProjectedYear, + syncedDetailView, + ], + ); + + return ( +
+
+
+ +
+ +
+
+
+
+
+ + + + ); +}; + +export default AnalysisTable;