From 6914ddc650ac8a3851712f5e56e9b70becda99db Mon Sep 17 00:00:00 2001 From: Stefanos Hadjipetrou Date: Tue, 7 Mar 2023 16:52:52 +0100 Subject: [PATCH] feat: allocations table --- .../components/Table/Expandable/Toolbar.tsx | 2 + src/app/components/Table/Simple/data.ts | 2 + src/app/components/Table/Simple/index.tsx | 92 +++++++++++-------- .../ToolBoxPanel/components/filters/data.ts | 5 + .../components/iconbuttons/index.tsx | 1 + .../components/subtoolboxpanel/index.tsx | 3 +- src/app/hooks/useGetAllVizData.tsx | 5 + .../modules/country-detail-module/index.tsx | 8 ++ src/app/modules/viz-module/index.tsx | 7 ++ .../sub-modules/allocations/table/index.tsx | 77 ++++++++++++++++ .../api/action-reducers/viz/allocations.ts | 4 + src/app/state/api/interfaces/index.ts | 1 + src/app/state/store/index.ts | 2 + src/app/utils/exportCSV.ts | 30 ++++++ 14 files changed, 199 insertions(+), 40 deletions(-) create mode 100644 src/app/modules/viz-module/sub-modules/allocations/table/index.tsx diff --git a/src/app/components/Table/Expandable/Toolbar.tsx b/src/app/components/Table/Expandable/Toolbar.tsx index 02878cd35..a1319ee5c 100644 --- a/src/app/components/Table/Expandable/Toolbar.tsx +++ b/src/app/components/Table/Expandable/Toolbar.tsx @@ -251,8 +251,10 @@ export function TableToolbar(props: TableToolbarProps) { {props.columns.map((c, index) => ( void; onSortByChange: (value: string) => void; diff --git a/src/app/components/Table/Simple/index.tsx b/src/app/components/Table/Simple/index.tsx index f99388463..3466c8314 100644 --- a/src/app/components/Table/Simple/index.tsx +++ b/src/app/components/Table/Simple/index.tsx @@ -14,6 +14,7 @@ import ArrowDownward from "@material-ui/icons/ArrowDownward"; import { TriangleXSIcon } from "app/assets/icons/TriangleXS"; import TableContainer from "@material-ui/core/TableContainer"; import { tablecell } from "app/components/Table/Simple/styles"; +import { formatFinancialValue } from "app/utils/formatFinancialValue"; import { TableToolbar } from "app/components/Table/Expandable/Toolbar"; import { TableToolbarCols } from "app/components/Table/Expandable/data"; import { @@ -36,9 +37,11 @@ function Row(props: { columns: SimpleTableColumn[]; paddingLeft?: number; visibleColumnsIndexes: number[]; + formatNumbers?: boolean; + forceExpand?: boolean; }) { const classes = useRowStyles(); - const [open, setOpen] = React.useState(false); + const [open, setOpen] = React.useState(Boolean(props.forceExpand)); const firstColumnWidth = props.columns.length > 3 ? "30%" : ""; const firstColumnPadding = props.paddingLeft ? props.paddingLeft : 40; @@ -80,58 +83,65 @@ function Row(props: { {filter( props.columns, (_c, index) => props.visibleColumnsIndexes.indexOf(index) > -1 - ).map((column: SimpleTableColumn, index: number) => ( - -
{ + const value = get(props.row, column.key, ""); + let formattedValue = + props.formatNumbers && !Number.isNaN(value) + ? formatFinancialValue(value, true) + : value; + return ( +
+
* { - @supports (-webkit-touch-callout: none) and - (not (translate: none)) { - &:not(:last-child) { - margin-right: 12px; + > * { + @supports (-webkit-touch-callout: none) and + (not (translate: none)) { + &:not(:last-child) { + margin-right: 12px; + } } } - } - > svg { - transition: transform 0.1s ease-in-out; - transform: rotate(${open ? "0deg" : "-180deg"}); - } - `} - > - {index === 0 && props.row.children && } - {get(props.row, column.key, "")} + > svg { + transition: transform 0.1s ease-in-out; + transform: rotate(${open ? "0deg" : "-180deg"}); + } + `} + > + {index === 0 && props.row.children && } + {formattedValue} +
-
-
- ))} + + ); + })} ))} @@ -270,6 +282,8 @@ export function SimpleTable(props: SimpleTableProps) { key={row.name} row={row} columns={props.columns} + forceExpand={props.forceExpand} + formatNumbers={props.formatNumbers} visibleColumnsIndexes={visibleColumnsIndexes} /> ))} diff --git a/src/app/components/ToolBoxPanel/components/filters/data.ts b/src/app/components/ToolBoxPanel/components/filters/data.ts index 80816f4a7..7e7968cd5 100644 --- a/src/app/components/ToolBoxPanel/components/filters/data.ts +++ b/src/app/components/ToolBoxPanel/components/filters/data.ts @@ -258,6 +258,11 @@ export const pathnameToFilterGroups = { (fg: FilterGroupProps) => fg.name === "Locations" || fg.name === "Components" ), + "/viz/allocations/table": filter( + filtergroups, + (fg: FilterGroupProps) => + fg.name === "Locations" || fg.name === "Components" + ), "/viz/eligibility": filter( filtergroups, (fg: FilterGroupProps) => diff --git a/src/app/components/ToolBoxPanel/components/iconbuttons/index.tsx b/src/app/components/ToolBoxPanel/components/iconbuttons/index.tsx index 8199f4525..9bd30f7fd 100644 --- a/src/app/components/ToolBoxPanel/components/iconbuttons/index.tsx +++ b/src/app/components/ToolBoxPanel/components/iconbuttons/index.tsx @@ -45,6 +45,7 @@ const locationsToNotShowImageExport = [ "/grant//commitment/table", "/viz/pledges-contributions/map", "/viz/pledges-contributions/table", + "/viz/allocations/table", "/viz/budgets/map", "/viz/allocations/map", "/grants", diff --git a/src/app/components/ToolBoxPanel/components/subtoolboxpanel/index.tsx b/src/app/components/ToolBoxPanel/components/subtoolboxpanel/index.tsx index eb48b4920..cfdd7d1b9 100644 --- a/src/app/components/ToolBoxPanel/components/subtoolboxpanel/index.tsx +++ b/src/app/components/ToolBoxPanel/components/subtoolboxpanel/index.tsx @@ -220,7 +220,8 @@ export function SubToolBoxPanel(props: SubToolBoxPanelProps) { /> )} {(params.vizType === "allocations" || - params.vizType === "allocation") && } + params.vizType === "allocation") && + params.subType !== "table" && } {params.vizType === "eligibility" && !isLocationDetail && ( )} diff --git a/src/app/hooks/useGetAllVizData.tsx b/src/app/hooks/useGetAllVizData.tsx index a733823fb..f2258371a 100644 --- a/src/app/hooks/useGetAllVizData.tsx +++ b/src/app/hooks/useGetAllVizData.tsx @@ -13,6 +13,7 @@ import { DotChartModel } from "app/components/Charts/Eligibility/DotChart/data"; import { EligibilityScatterplotDataModel } from "app/components/Charts/Eligibility/Scatterplot/data"; import { DisbursementsTreemapDataItem } from "app/components/Charts/Investments/Disbursements/data"; import { BudgetsTreemapDataItem } from "app/components/Charts/Budgets/Treemap/data"; +import { SimpleTableRow } from "app/components/Table/Simple/data"; export function useGetAllVizData() { const allocations = useStoreState((state) => ({ @@ -26,6 +27,9 @@ export function useGetAllVizData() { features: get(state.AllocationsGeomap.data, "data", []), } as FeatureCollection) ); + const allocationsTable = useStoreState( + (state) => get(state.AllocationsTable.data, "data", []) as SimpleTableRow[] + ); const allocationsMCGeomap = useStoreState( (state) => get( @@ -284,6 +288,7 @@ export function useGetAllVizData() { countries: allocationsGeomap, multicountries: allocationsMCGeomap, }, + "/viz/allocations/table": allocationsTable, // Budgets "/viz/budgets/flow": budgetsFlow, "/viz/budgets/time-cycle": budgetsTimeCycle, diff --git a/src/app/modules/country-detail-module/index.tsx b/src/app/modules/country-detail-module/index.tsx index 1f10ab06d..3185e525a 100644 --- a/src/app/modules/country-detail-module/index.tsx +++ b/src/app/modules/country-detail-module/index.tsx @@ -27,6 +27,7 @@ import { LocationGrants } from "app/modules/country-detail-module/sub-modules/gr import { LocationResults } from "app/modules/country-detail-module/sub-modules/results"; import { AllocationsGeoMap } from "app/modules/viz-module/sub-modules/allocations/geomap"; import { InvestmentsGeoMap } from "app/modules/viz-module/sub-modules/investments/geomap"; +import { AllocationsTableModule } from "app/modules/viz-module/sub-modules/allocations/table"; import { LocationDetailOverviewModule } from "app/modules/country-detail-module/sub-modules/overview"; import { LocationDetailDocumentsModule } from "app/modules/country-detail-module/sub-modules/documents"; import { LocationDetailEligibilityWrapper } from "app/modules/viz-module/sub-modules/eligibility/data-wrappers/location"; @@ -384,6 +385,13 @@ export default function CountryDetail() { + + + + + + void; +} + +export function AllocationsTableModule(props: AllocationsTableProps) { + useTitle(`The Data Explorer -${props.code ? " Location" : ""} Allocations`); + + const [search, setSearch] = React.useState(""); + const [sortBy, setSortBy] = React.useState(""); + + const fetchData = useStoreActions((store) => store.AllocationsTable.fetch); + const loading = useStoreState((state) => state.AllocationsTable.loading); + const appliedFilters = useStoreState((state) => state.AppliedFiltersState); + const data = useStoreState( + (state) => get(state.AllocationsTable.data, "data", []) as SimpleTableRow[] + ); + + function reloadData() { + const filterString = getAPIFormattedFilters( + props.code + ? { + ...appliedFilters, + locations: [...appliedFilters.locations, props.code], + } + : appliedFilters, + { search, sortBy } + ); + fetchData({ filterString }); + } + + React.useEffect(() => reloadData(), [props.code, sortBy, appliedFilters]); + + const [,] = useDebounce(() => reloadData(), 500, [search]); + + if (loading) { + return ; + } + + const columns = + data.length > 0 + ? filter(Object.keys(data[0]), (key) => key !== "children").map( + (key) => ({ + name: key === "name" ? "Component/Location" : `${key} (USD)`, + key, + }) + ) + : []; + + return ( + <> + + + ); +} diff --git a/src/app/state/api/action-reducers/viz/allocations.ts b/src/app/state/api/action-reducers/viz/allocations.ts index b6eb00382..1fa589a85 100644 --- a/src/app/state/api/action-reducers/viz/allocations.ts +++ b/src/app/state/api/action-reducers/viz/allocations.ts @@ -22,3 +22,7 @@ export const AllocationsGeomap: ApiCallModel = { export const AllocationsMCGeomap: ApiCallModel = { ...APIModel(`${process.env.REACT_APP_API}/allocations/geomap/multicountries`), }; + +export const AllocationsTable: ApiCallModel = { + ...APIModel(`${process.env.REACT_APP_API}/allocations/table`), +}; diff --git a/src/app/state/api/interfaces/index.ts b/src/app/state/api/interfaces/index.ts index ee6e29ef2..f8184dd88 100644 --- a/src/app/state/api/interfaces/index.ts +++ b/src/app/state/api/interfaces/index.ts @@ -145,6 +145,7 @@ export interface StoreModel { AllocationsDrilldown: ApiCallModel; AllocationsGeomap: ApiCallModel; AllocationsMCGeomap: ApiCallModel; + AllocationsTable: ApiCallModel; Eligibility: ApiCallModel; EligibilityYears: ApiCallModel; BudgetsFlow: ApiCallModel; diff --git a/src/app/state/store/index.ts b/src/app/state/store/index.ts index 8e28a58b7..6557e355a 100644 --- a/src/app/state/store/index.ts +++ b/src/app/state/store/index.ts @@ -24,6 +24,7 @@ import Allocations, { AllocationsGeomap, AllocationsMCGeomap, AllocationsPeriods, + AllocationsTable, } from "app/state/api/action-reducers/viz/allocations"; import BudgetsFlow, { BudgetsFlowDrilldownLevel1, @@ -178,6 +179,7 @@ const storeContent: StoreModel = { AllocationsDrilldown: persist(AllocationsDrilldown), AllocationsGeomap: persist(AllocationsGeomap), AllocationsMCGeomap: persist(AllocationsMCGeomap), + AllocationsTable: persist(AllocationsTable), Eligibility: persist(Eligibility), EligibilityYears: persist(EligibilityYears), BudgetsGeomap: persist(BudgetsGeomap), diff --git a/src/app/utils/exportCSV.ts b/src/app/utils/exportCSV.ts index a3026ca3b..f981f7b10 100644 --- a/src/app/utils/exportCSV.ts +++ b/src/app/utils/exportCSV.ts @@ -615,6 +615,36 @@ export function exportCSV( { label: "Budget (USD)", key: "budget" }, ], }; + case "/viz/allocations/table": + data.forEach((item: any) => { + item.children.forEach((subItem: any) => { + const { name, ...otherProps } = subItem; + csvData.push({ + component: item.name, + location: name, + ...otherProps, + }); + }); + }); + let extraHeaders: { label: string; key: string }[] = []; + if (csvData.length > 0) { + extraHeaders = filter( + Object.keys(csvData[0]), + (key) => key !== "component" && key !== "location" + ).map((key) => ({ + label: `${key[0].toUpperCase()}${key.slice(1)}`, + key, + })); + } + return { + data: csvData, + filename: "allocations.csv", + headers: [ + { label: "Component", key: "component" }, + { label: "Location", key: "location" }, + ...extraHeaders, + ], + }; case "/viz/eligibility": if (options.isDetail) { filter(