From 60fc942a95a56204cd5a6e2df8c57569903b7bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mir=C3=B3=20Sorja?= Date: Wed, 16 Oct 2024 12:35:29 +0200 Subject: [PATCH] 4000 - Dashboard cycle 2029: dynamic metadata (#4017) * 4000 - Dashboard metadata migration to node ext (#4015) * 4000 - Dashboard metadata migration to node ext * type camel case * update nodeExtType: dataquery -> dashboard * 4000 - Dashboard API (#4016) * 4000 - Initial commit for data query API * 4000 - Move api to Cycle level * remove unused type * 4000 - Dashboard store (#4018) * 4000 - Dashboard store * 4000 - repository: return in correct format * 4000 - Overview: use dashboard store * 4000 - return result * 4000 - Utils for object manipulation (#4020) * 4000 - introduce new object utils mergePartial, getDiffAsPartialObject.ts * 4000 - use lodash.merge * 4000 - getDiffAsPartialObject -> getDiff * 4000 - Migrate dashboard region partial data (pending) (#4021) * 4000 - restore original metadata * 4000 - dashboard: migrate partial metadata for regions * use correct function call * 4000 - dashboard API: merge region data (#4022) * 4000 - Remove unused files (#4027) * 4000 - dashboard: refetch even when existing items (region) * remove unused files * region/country dashboard in store * 4000 - code quality * Fix typo in import --- package.json | 2 + .../pages/CountryHome/Overview/Overview.tsx | 8 +- .../pages/CountryHome/Overview/hooks/index.ts | 63 ---------------- .../store/metadata/actions/getDashboard.ts | 21 ++++++ .../extraReducers/getDashboardReducer.ts | 16 ++++ .../store/metadata/hooks/useDashboardItems.ts | 14 ++++ .../store/metadata/hooks/useGetDashboard.ts | 19 +++++ src/client/store/metadata/index.ts | 3 + src/client/store/metadata/selectors/index.ts | 13 ++++ src/client/store/metadata/slice.ts | 4 + src/client/store/metadata/state.ts | 16 ++++ src/meta/api/endpoint/ApiEndPoint.ts | 4 + src/meta/dashboard/index.ts | 2 +- src/meta/nodeExt/nodeExt.ts | 1 + .../cycleData/dashboard/getDashboardItems.ts | 20 +++++ src/server/api/cycleData/index.ts | 4 + .../cycleData/dashboard/getManyItems.ts | 24 ++++++ .../controller/cycleData/dashboard/index.ts | 5 ++ .../nodeExt/getManyDashboardItems.ts | 29 ++++++++ .../assessmentCycle/nodeExt/index.ts | 2 + ...09113849-step-dashboard-metadata-insert.ts | 73 +++++++++++++++++++ .../metadata/dashboard}/forestArea/index.ts | 9 +-- .../forestAreaPercentOfLandArea/index.ts | 6 +- .../forestAreaWithinProtectedAreas/index.ts | 5 +- .../forestGrowingStockAndCarbon/index.ts | 3 +- .../dashboard}/forestOwnership/index.ts | 8 +- .../naturallyRegeneratingForestArea/index.ts | 10 +-- .../index.ts | 3 +- .../primaryForestPercentOfForestArea/index.ts | 8 +- .../steps/metadata/dashboard}/utils/index.ts | 12 ++- .../metadata/dashboard}/utils/rowsMetadata.ts | 0 .../steps/metadata/dashboard}/utils/unit.ts | 0 src/utils/objects/getDiff.ts | 62 ++++++++++++++++ src/utils/objects/index.ts | 7 +- yarn.lock | 7 ++ 35 files changed, 380 insertions(+), 103 deletions(-) delete mode 100644 src/client/pages/CountryHome/Overview/hooks/index.ts create mode 100644 src/client/store/metadata/actions/getDashboard.ts create mode 100644 src/client/store/metadata/extraReducers/getDashboardReducer.ts create mode 100644 src/client/store/metadata/hooks/useDashboardItems.ts create mode 100644 src/client/store/metadata/hooks/useGetDashboard.ts create mode 100644 src/server/api/cycleData/dashboard/getDashboardItems.ts create mode 100644 src/server/controller/cycleData/dashboard/getManyItems.ts create mode 100644 src/server/controller/cycleData/dashboard/index.ts create mode 100644 src/server/repository/assessmentCycle/nodeExt/getManyDashboardItems.ts create mode 100644 src/test/migrations/steps/20241009113849-step-dashboard-metadata-insert.ts rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/forestArea/index.ts (75%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/forestAreaPercentOfLandArea/index.ts (90%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/forestAreaWithinProtectedAreas/index.ts (90%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/forestGrowingStockAndCarbon/index.ts (96%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/forestOwnership/index.ts (83%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/naturallyRegeneratingForestArea/index.ts (79%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/primaryDesignatedManagementObjective/index.ts (91%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/primaryForestPercentOfForestArea/index.ts (90%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/utils/index.ts (91%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/utils/rowsMetadata.ts (100%) rename src/{client/pages/CountryHome/Overview/meta => test/migrations/steps/metadata/dashboard}/utils/unit.ts (100%) create mode 100644 src/utils/objects/getDiff.ts diff --git a/package.json b/package.json index 3f2346ee66..32364b4c1c 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@types/lodash.isequal": "^4.5.5", "@types/lodash.isfunction": "^3.0.6", "@types/lodash.isnil": "^4.0.6", + "@types/lodash.merge": "^4.6.9", "@types/lodash.pick": "^4.4.7", "@types/lodash.range": "^3.2.6", "@types/lodash.reverse": "^4.0.6", @@ -213,6 +214,7 @@ "lodash.isequal": "^4.5.0", "lodash.isfunction": "^3.0.9", "lodash.isnil": "^4.0.0", + "lodash.merge": "^4.6.2", "lodash.pick": "^4.4.0", "lodash.range": "^3.2.0", "lodash.reverse": "^4.0.1", diff --git a/src/client/pages/CountryHome/Overview/Overview.tsx b/src/client/pages/CountryHome/Overview/Overview.tsx index 3093d2a9b2..355f954b8d 100644 --- a/src/client/pages/CountryHome/Overview/Overview.tsx +++ b/src/client/pages/CountryHome/Overview/Overview.tsx @@ -1,14 +1,14 @@ import React from 'react' -import { useSections } from 'client/store/metadata' +import { useDashboardItems, useGetDashboard, useSections } from 'client/store/metadata' import Dashboard from 'client/components/Dashboard' -import { useDashboardItems } from 'client/pages/CountryHome/Overview/hooks' const Overview: React.FC = () => { + const sections = useSections() const items = useDashboardItems() + useGetDashboard() - const sections = useSections() - if (!sections) return null + if (!sections || !items) return null return } diff --git a/src/client/pages/CountryHome/Overview/hooks/index.ts b/src/client/pages/CountryHome/Overview/hooks/index.ts deleted file mode 100644 index 5601e4aa73..0000000000 --- a/src/client/pages/CountryHome/Overview/hooks/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useMemo } from 'react' - -import { Areas } from 'meta/area' -import { AssessmentName, AssessmentNames, Cycle, CycleName } from 'meta/assessment' -import { DashboardItem } from 'meta/dashboard' - -import { useCycle } from 'client/store/assessment' -import { useCountryIso } from 'client/hooks' -import { useAssessmentRouteParams } from 'client/hooks/useRouteParams' - -import { forestArea } from '../meta/forestArea' -import { forestAreaPercentOfLandArea } from '../meta/forestAreaPercentOfLandArea' -import { forestAreaWithinProtectedAreas } from '../meta/forestAreaWithinProtectedAreas' -import { forestGrowingStockAndCarbonDashboard } from '../meta/forestGrowingStockAndCarbon' -import { forestOwnership } from '../meta/forestOwnership' -import { naturallyRegeneratingForestArea } from '../meta/naturallyRegeneratingForestArea' -import { primaryDesignatedManagementObjectiveDashboard } from '../meta/primaryDesignatedManagementObjective' -import { primaryForestPercentOfForestArea } from '../meta/primaryForestPercentOfForestArea' - -type DashboardItemFactory = (cycle: Cycle, region: boolean) => DashboardItem - -const defaultDashboardItemFactories: Array = [ - forestArea, - forestGrowingStockAndCarbonDashboard, - forestAreaPercentOfLandArea, - primaryForestPercentOfForestArea, - forestAreaWithinProtectedAreas, - forestOwnership, - primaryDesignatedManagementObjectiveDashboard, - naturallyRegeneratingForestArea, -] - -const dashboardItemFactoriesMap: Record>> = { - [AssessmentNames.fra]: { - '2020': defaultDashboardItemFactories, - '2025': defaultDashboardItemFactories, - }, -} - -const getDashboardItemFactories = ( - assessmentName: AssessmentName, - cycleName: CycleName -): Array => { - return dashboardItemFactoriesMap[assessmentName]?.[cycleName] || [] -} - -const getDashboardItems = (assessmentName: AssessmentName, cycle: Cycle, region: boolean): Array => { - const factories = getDashboardItemFactories(assessmentName, cycle.name) - return factories.map((factory) => factory(cycle, region)) -} - -export type Dashboard = ReadonlyArray - -export const useDashboardItems = (): Dashboard => { - const { assessmentName } = useAssessmentRouteParams() - const cycle = useCycle() - const countryIso = useCountryIso() - const isRegion = !Areas.isISOCountry(countryIso) - - return useMemo(() => { - return getDashboardItems(assessmentName, cycle, isRegion) - }, [assessmentName, cycle, isRegion]) -} diff --git a/src/client/store/metadata/actions/getDashboard.ts b/src/client/store/metadata/actions/getDashboard.ts new file mode 100644 index 0000000000..893f746730 --- /dev/null +++ b/src/client/store/metadata/actions/getDashboard.ts @@ -0,0 +1,21 @@ +import { createAsyncThunk } from '@reduxjs/toolkit' +import axios from 'axios' + +import { ApiEndPoint } from 'meta/api/endpoint' +import { AreaCode } from 'meta/area' +import { AssessmentName, CycleName } from 'meta/assessment' +import { DashboardItem } from 'meta/dashboard' + +type Returned = Array + +type Props = { + assessmentName: AssessmentName + cycleName: CycleName + countryIso: AreaCode +} + +export const getDashboard = createAsyncThunk('metadata/dashboard/get', async (props) => { + const params = { ...props } + const { data } = await axios.get(ApiEndPoint.CycleData.Dashboard.one(), { params }) + return data +}) diff --git a/src/client/store/metadata/extraReducers/getDashboardReducer.ts b/src/client/store/metadata/extraReducers/getDashboardReducer.ts new file mode 100644 index 0000000000..052868bdcb --- /dev/null +++ b/src/client/store/metadata/extraReducers/getDashboardReducer.ts @@ -0,0 +1,16 @@ +import { ActionReducerMapBuilder } from '@reduxjs/toolkit' +import { Objects } from 'utils/objects' + +import { Areas } from 'meta/area' + +import { getDashboard } from 'client/store/metadata/actions/getDashboard' +import { DashboardAreaType, MetadataState } from 'client/store/metadata/state' + +export const getDashboardReducer = (builder: ActionReducerMapBuilder): void => { + builder.addCase(getDashboard.fulfilled, (state, action) => { + const { assessmentName, cycleName, countryIso } = action.meta.arg + const key = Areas.isISOCountry(countryIso) ? DashboardAreaType.Country : DashboardAreaType.Region + + Objects.setInPath({ obj: state.dashboard, path: [assessmentName, cycleName, key], value: action.payload }) + }) +} diff --git a/src/client/store/metadata/hooks/useDashboardItems.ts b/src/client/store/metadata/hooks/useDashboardItems.ts new file mode 100644 index 0000000000..a09d7d1466 --- /dev/null +++ b/src/client/store/metadata/hooks/useDashboardItems.ts @@ -0,0 +1,14 @@ +import { Areas } from 'meta/area' +import { DashboardItem } from 'meta/dashboard' + +import { MetadataSelectors } from 'client/store/metadata/selectors' +import { useAppSelector } from 'client/store/store' +import { useCountryRouteParams } from 'client/hooks/useRouteParams' + +import { DashboardAreaType } from '../state' + +export const useDashboardItems = (): Array => { + const { assessmentName, cycleName, countryIso } = useCountryRouteParams() + const key = Areas.isISOCountry(countryIso) ? DashboardAreaType.Country : DashboardAreaType.Region + return useAppSelector((state) => MetadataSelectors.getDashboard(state, assessmentName, cycleName, key)) +} diff --git a/src/client/store/metadata/hooks/useGetDashboard.ts b/src/client/store/metadata/hooks/useGetDashboard.ts new file mode 100644 index 0000000000..beb2cab4e3 --- /dev/null +++ b/src/client/store/metadata/hooks/useGetDashboard.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react' + +import { useAppDispatch } from 'client/store' +import { useCountryRouteParams } from 'client/hooks/useRouteParams' + +import { MetadataActions } from '../slice' +import { useDashboardItems } from './useDashboardItems' + +export const useGetDashboard = () => { + const dispatch = useAppDispatch() + const { assessmentName, cycleName, countryIso } = useCountryRouteParams() + const dashboardItems = useDashboardItems() + + useEffect(() => { + if (!dashboardItems) { + dispatch(MetadataActions.getDashboard({ assessmentName, cycleName, countryIso })) + } + }, [assessmentName, cycleName, countryIso, dispatch, dashboardItems]) +} diff --git a/src/client/store/metadata/index.ts b/src/client/store/metadata/index.ts index 66ba3584fe..680855cef5 100644 --- a/src/client/store/metadata/index.ts +++ b/src/client/store/metadata/index.ts @@ -1,6 +1,9 @@ +export { useDashboardItems } from './hooks/useDashboardItems' +export { useGetDashboard } from './hooks/useGetDashboard' export { useGetTableSections } from './hooks/useGetTableSections' export { usePreviousSection, useSection, useSections } from './hooks/useSections' export { useTableSections, useTableSectionsCycle } from './hooks/useTableSections' export { MetadataSelectors } from './selectors' export { MetadataActions } from './slice' export type { MetadataState } from './state' +export { DashboardAreaType } from './state' diff --git a/src/client/store/metadata/selectors/index.ts b/src/client/store/metadata/selectors/index.ts index 4aaaed8510..aecf2d0e48 100644 --- a/src/client/store/metadata/selectors/index.ts +++ b/src/client/store/metadata/selectors/index.ts @@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit' import { AssessmentName, CycleName } from 'meta/assessment' +import { DashboardAreaType } from 'client/store/metadata/state' import { RootState } from 'client/store/RootState' const getSections = createSelector( @@ -12,6 +13,18 @@ const getSections = createSelector( ], (metadataState, assessmentName, cycleName) => metadataState.sections?.[assessmentName]?.[cycleName] ) + +const getDashboard = createSelector( + [ + (state: RootState) => state.metadata, + (_state: RootState, assessmentName: AssessmentName) => assessmentName, + (_state: RootState, _assessmentName: AssessmentName, cycleName: CycleName) => cycleName, + (_state: RootState, _assessmentName: AssessmentName, _cycleName: CycleName, key: DashboardAreaType) => key, + ], + (metadataState, assessmentName, cycleName, key) => metadataState.dashboard?.[assessmentName]?.[cycleName]?.[key] +) + export const MetadataSelectors = { getSections, + getDashboard, } diff --git a/src/client/store/metadata/slice.ts b/src/client/store/metadata/slice.ts index 2e5478972b..3fa74493a2 100644 --- a/src/client/store/metadata/slice.ts +++ b/src/client/store/metadata/slice.ts @@ -1,7 +1,9 @@ import { createSlice, Reducer } from '@reduxjs/toolkit' +import { getDashboard } from 'client/store/metadata/actions/getDashboard' import { getSections } from 'client/store/metadata/actions/getSections' import { getTableSections } from 'client/store/metadata/actions/getTableSections' +import { getDashboardReducer } from 'client/store/metadata/extraReducers/getDashboardReducer' import { getSectionsReducer } from 'client/store/metadata/extraReducers/getSectionsReducer' import { setTableSectionsReducer } from 'client/store/metadata/extraReducers/setTableSectionsReducer' import { initialState, MetadataState } from 'client/store/metadata/state' @@ -13,6 +15,7 @@ export const metadataSlice = createSlice({ extraReducers: (builder) => { getSectionsReducer(builder) setTableSectionsReducer(builder) + getDashboardReducer(builder) }, }) @@ -20,6 +23,7 @@ export const MetadataActions = { ...metadataSlice.actions, getSections, getTableSections, + getDashboard, } export default metadataSlice.reducer as Reducer diff --git a/src/client/store/metadata/state.ts b/src/client/store/metadata/state.ts index 323ecbd591..409615c97a 100644 --- a/src/client/store/metadata/state.ts +++ b/src/client/store/metadata/state.ts @@ -1,12 +1,28 @@ import { AssessmentName, CycleName, Section, TableSection } from 'meta/assessment' import { SectionName } from 'meta/assessment/section' +import { DashboardItem } from 'meta/dashboard' + +export enum DashboardAreaType { + Region = 'region', + Country = 'country', +} + +type DashboardState = Record< + AssessmentName, + Record< + CycleName, + { [DashboardAreaType.Region]?: Array; [DashboardAreaType.Country]?: Array } + > +> export interface MetadataState { sections: Record>> tableSections: Record>>> + dashboard: DashboardState } export const initialState: MetadataState = { sections: {}, tableSections: {}, + dashboard: {}, } diff --git a/src/meta/api/endpoint/ApiEndPoint.ts b/src/meta/api/endpoint/ApiEndPoint.ts index fc049ae05c..ef32c15529 100644 --- a/src/meta/api/endpoint/ApiEndPoint.ts +++ b/src/meta/api/endpoint/ApiEndPoint.ts @@ -37,6 +37,10 @@ export const ApiEndPoint = { history: (target = ':target') => apiPath('cycle-data', 'history', target), historyCount: (target = ':target') => apiPath('cycle-data', 'history', target, 'count'), + Dashboard: { + one: () => apiPath('cycle-data', 'dashboard'), + }, + Descriptions: { many: () => apiPath('cycle-data', 'descriptions'), diff --git a/src/meta/dashboard/index.ts b/src/meta/dashboard/index.ts index 2059879643..a232bcfdf6 100644 --- a/src/meta/dashboard/index.ts +++ b/src/meta/dashboard/index.ts @@ -1,2 +1,2 @@ -export type { DashboardItem, DashboardTable } from './dashboard' +export type { DashboardBarChart, DashboardItem, DashboardPieChart, DashboardTable } from './dashboard' export { DashboardItemType } from './dashboard' diff --git a/src/meta/nodeExt/nodeExt.ts b/src/meta/nodeExt/nodeExt.ts index b74d1b413d..a7f9432991 100644 --- a/src/meta/nodeExt/nodeExt.ts +++ b/src/meta/nodeExt/nodeExt.ts @@ -11,6 +11,7 @@ export enum NodeExtCellType { export enum NodeExtType { contact = 'contact', node = 'node', + dashboard = 'dashboard', } export type NodeExt = { diff --git a/src/server/api/cycleData/dashboard/getDashboardItems.ts b/src/server/api/cycleData/dashboard/getDashboardItems.ts new file mode 100644 index 0000000000..486689fdfb --- /dev/null +++ b/src/server/api/cycleData/dashboard/getDashboardItems.ts @@ -0,0 +1,20 @@ +import { Response } from 'express' + +import { CycleRequest } from 'meta/api/request' + +import { AssessmentController } from 'server/controller/assessment' +import { DashboardController } from 'server/controller/cycleData/dashboard' +import Requests from 'server/utils/requests' + +export const getDashboardItems = async (req: CycleRequest, res: Response) => { + try { + const { assessmentName, cycleName, countryIso } = req.query + + const { assessment, cycle } = await AssessmentController.getOneWithCycle({ assessmentName, cycleName }) + + const result = await DashboardController.getManyItems({ assessment, cycle, countryIso }) + Requests.send(res, result) + } catch (e) { + Requests.sendErr(res, e) + } +} diff --git a/src/server/api/cycleData/index.ts b/src/server/api/cycleData/index.ts index e9a041d809..604b70ddc4 100644 --- a/src/server/api/cycleData/index.ts +++ b/src/server/api/cycleData/index.ts @@ -4,6 +4,7 @@ import * as queue from 'express-queue' import { ApiEndPoint } from 'meta/api/endpoint' +import { getDashboardItems } from 'server/api/cycleData/dashboard/getDashboardItems' import { getHistory } from 'server/api/cycleData/history/getHistory' import { getHistoryCount } from 'server/api/cycleData/history/getHistoryCount' import { AuthMiddleware } from 'server/middleware/auth' @@ -193,5 +194,8 @@ export const CycleDataApi = { express.get(ApiEndPoint.CycleData.Links.count(), AuthMiddleware.requireAdmin, getLinksCount) express.post(ApiEndPoint.CycleData.Links.verify(), AuthMiddleware.requireAdmin, verifyLinks) express.get(ApiEndPoint.CycleData.Links.verifyStatus(), AuthMiddleware.requireAdmin, isVerificationInProgress) + + // dashboard + express.get(ApiEndPoint.CycleData.Dashboard.one(), getDashboardItems) }, } diff --git a/src/server/controller/cycleData/dashboard/getManyItems.ts b/src/server/controller/cycleData/dashboard/getManyItems.ts new file mode 100644 index 0000000000..9c518f81b8 --- /dev/null +++ b/src/server/controller/cycleData/dashboard/getManyItems.ts @@ -0,0 +1,24 @@ +import { Objects } from 'utils/objects' + +import { AreaCode, Areas } from 'meta/area' +import { Assessment, Cycle } from 'meta/assessment' +import { DashboardItem, DashboardItemType } from 'meta/dashboard' + +import { NodeExtRepository } from 'server/repository/assessmentCycle/nodeExt' + +type Props = { + assessment: Assessment + cycle: Cycle + countryIso: AreaCode +} + +export const getManyItems = async (props: Props): Promise>> => { + const { assessment, cycle, countryIso } = props + const isISOCountry = Areas.isISOCountry(countryIso) + const countryDashboardItems = await NodeExtRepository.getManyDashboardItems({ assessment, cycle }) + if (isISOCountry) return countryDashboardItems + + const regionDashboardItems = await NodeExtRepository.getManyDashboardItems({ assessment, cycle, region: true }) + + return Objects.merge(countryDashboardItems, regionDashboardItems) +} diff --git a/src/server/controller/cycleData/dashboard/index.ts b/src/server/controller/cycleData/dashboard/index.ts new file mode 100644 index 0000000000..c2ed76bb1e --- /dev/null +++ b/src/server/controller/cycleData/dashboard/index.ts @@ -0,0 +1,5 @@ +import { getManyItems } from './getManyItems' + +export const DashboardController = { + getManyItems, +} diff --git a/src/server/repository/assessmentCycle/nodeExt/getManyDashboardItems.ts b/src/server/repository/assessmentCycle/nodeExt/getManyDashboardItems.ts new file mode 100644 index 0000000000..3932e60e1d --- /dev/null +++ b/src/server/repository/assessmentCycle/nodeExt/getManyDashboardItems.ts @@ -0,0 +1,29 @@ +import { Assessment, Cycle } from 'meta/assessment' +import { DashboardItem, DashboardItemType } from 'meta/dashboard' +import { NodeExtType } from 'meta/nodeExt' + +import { BaseProtocol, DB, Schemas } from 'server/db' + +type Props = { assessment: Assessment; cycle: Cycle; region?: boolean } + +export const getManyDashboardItems = async ( + props: Props, + client: BaseProtocol = DB +): Promise>> => { + const { assessment, cycle, region } = props + const schemaCycle = Schemas.getNameCycle(assessment, cycle) + return client.one>>( + ` + select value + from ${schemaCycle}.node_ext + where type = $1 + ${ + region + ? `and (props->>'region')::boolean = true` + : `and (props->>'region' is null or (props->>'region')::boolean = false)` + } + `, + [NodeExtType.dashboard], + (result) => result.value + ) +} diff --git a/src/server/repository/assessmentCycle/nodeExt/index.ts b/src/server/repository/assessmentCycle/nodeExt/index.ts index e7c6e9c3ad..8cf55d9a84 100644 --- a/src/server/repository/assessmentCycle/nodeExt/index.ts +++ b/src/server/repository/assessmentCycle/nodeExt/index.ts @@ -1,9 +1,11 @@ import { getManyContacts } from './getManyContacts' +import { getManyDashboardItems } from './getManyDashboardItems' import { removeContact } from './removeContact' import { upsert } from './upsert' export const NodeExtRepository = { getManyContacts, + getManyDashboardItems, removeContact, upsert, } diff --git a/src/test/migrations/steps/20241009113849-step-dashboard-metadata-insert.ts b/src/test/migrations/steps/20241009113849-step-dashboard-metadata-insert.ts new file mode 100644 index 0000000000..5732a005b8 --- /dev/null +++ b/src/test/migrations/steps/20241009113849-step-dashboard-metadata-insert.ts @@ -0,0 +1,73 @@ +import * as pgPromise from 'pg-promise' +import { Objects } from 'utils/objects' +import { Promises } from 'utils/promises' + +import { AssessmentNames, Cycle } from 'meta/assessment' +import { DashboardItem } from 'meta/dashboard' +import { NodeExtType } from 'meta/nodeExt' + +import { AssessmentController } from 'server/controller/assessment' +import { BaseProtocol, Schemas } from 'server/db' +import { Logger } from 'server/utils/logger' + +import { forestArea } from './metadata/dashboard/forestArea' +import { forestAreaPercentOfLandArea } from './metadata/dashboard/forestAreaPercentOfLandArea' +import { forestAreaWithinProtectedAreas } from './metadata/dashboard/forestAreaWithinProtectedAreas' +import { forestGrowingStockAndCarbonDashboard } from './metadata/dashboard/forestGrowingStockAndCarbon' +import { forestOwnership } from './metadata/dashboard/forestOwnership' +import { naturallyRegeneratingForestArea } from './metadata/dashboard/naturallyRegeneratingForestArea' +import { primaryDesignatedManagementObjectiveDashboard } from './metadata/dashboard/primaryDesignatedManagementObjective' +import { primaryForestPercentOfForestArea } from './metadata/dashboard/primaryForestPercentOfForestArea' + +type DashboardItemFactory = (cycle: Cycle, region: boolean) => DashboardItem + +const dashboardItemFactories: Array = [ + forestArea, + forestGrowingStockAndCarbonDashboard, + forestAreaPercentOfLandArea, + primaryForestPercentOfForestArea, + forestAreaWithinProtectedAreas, + forestOwnership, + primaryDesignatedManagementObjectiveDashboard, + naturallyRegeneratingForestArea, +] + +const keysToIgnore = ['uuid', 'id', 'rowId', 'tableId'] + +const _getDiffs = ( + countryItems: Array, + regionItems: Array +): Array | undefined> => { + return countryItems.map((countryItem, index) => + Objects.getDiff(countryItem, regionItems[index], keysToIgnore) + ) +} + +export default async (client: BaseProtocol) => { + const pgp = pgPromise() + const assessment = await AssessmentController.getOne({ assessmentName: AssessmentNames.fra }, client) + await Promises.each(assessment.cycles, async (cycle) => { + const schemaCycle = Schemas.getNameCycle(assessment, cycle) + const dashboardItemsCountry = dashboardItemFactories.map((factory) => factory(cycle, false)) + const dashboardItemsRegion = dashboardItemFactories.map((factory) => factory(cycle, true)) + const diffs = _getDiffs(dashboardItemsCountry, dashboardItemsRegion) + const nodeExtType = NodeExtType.dashboard + + const queryExists = `select 1 from ${schemaCycle}.node_ext where type = $1` + const exists = (await client.manyOrNone(queryExists, [nodeExtType])).length > 0 + if (!exists) { + const columns = ['type', 'value', 'props'] + const options = { table: { table: 'node_ext', schema: schemaCycle } } + const cs = new pgp.helpers.ColumnSet(columns, options) + const values = [ + { type: nodeExtType, value: JSON.stringify(dashboardItemsCountry), props: {} }, + { type: `${nodeExtType}`, value: JSON.stringify(diffs), props: { region: true } }, + ] + const query = pgp.helpers.insert(values, cs) + await client.none(query) + Logger.info(`Inserted dashboard items for cycle ${cycle.name}`) + } else { + Logger.info(`Dashboard items for cycle ${cycle.name} already exist`) + } + }) +} diff --git a/src/client/pages/CountryHome/Overview/meta/forestArea/index.ts b/src/test/migrations/steps/metadata/dashboard/forestArea/index.ts similarity index 75% rename from src/client/pages/CountryHome/Overview/meta/forestArea/index.ts rename to src/test/migrations/steps/metadata/dashboard/forestArea/index.ts index b20b7e0b6f..8741d43f22 100644 --- a/src/client/pages/CountryHome/Overview/meta/forestArea/index.ts +++ b/src/test/migrations/steps/metadata/dashboard/forestArea/index.ts @@ -1,11 +1,9 @@ import { Cycle } from 'meta/assessment' import { ChartColor } from 'meta/chart' -import { DashboardItemType } from 'meta/dashboard' -import { DashboardBarChart } from 'meta/dashboard/dashboard' +import { DashboardBarChart, DashboardItemType } from 'meta/dashboard' -import { getTable } from 'client/pages/CountryHome/Overview/meta/utils' -import { RowsMetadata } from 'client/pages/CountryHome/Overview/meta/utils/rowsMetadata' -import { unit } from 'client/pages/CountryHome/Overview/meta/utils/unit' +import { getTable, RowsMetadata } from '../utils' +import { unit } from '../utils/unit' const commonColumns = ['1990', '2000', '2010', '2020'] @@ -36,7 +34,6 @@ export const forestArea = (cycle: Cycle, region: boolean): DashboardBarChart => table: getTable({ cycle, cols: cols[cycle.name], tableId, rowMetadata: rowMetadata(region), tableName }), chart: { columns: cols[cycle.name], - label: ({ variableName, percent }: any) => `${variableName} ${(percent * 100).toFixed(0)}%`, cells: [ { variableName: 'forestArea', diff --git a/src/client/pages/CountryHome/Overview/meta/forestAreaPercentOfLandArea/index.ts b/src/test/migrations/steps/metadata/dashboard/forestAreaPercentOfLandArea/index.ts similarity index 90% rename from src/client/pages/CountryHome/Overview/meta/forestAreaPercentOfLandArea/index.ts rename to src/test/migrations/steps/metadata/dashboard/forestAreaPercentOfLandArea/index.ts index f685b6f69a..f940b5182c 100644 --- a/src/client/pages/CountryHome/Overview/meta/forestAreaPercentOfLandArea/index.ts +++ b/src/test/migrations/steps/metadata/dashboard/forestAreaPercentOfLandArea/index.ts @@ -3,9 +3,9 @@ import { ChartColor } from 'meta/chart' import { DashboardItemType } from 'meta/dashboard' import { DashboardPieChart } from 'meta/dashboard/dashboard' -import { getTable } from 'client/pages/CountryHome/Overview/meta/utils' -import { RowsMetadata } from 'client/pages/CountryHome/Overview/meta/utils/rowsMetadata' -import { unit } from 'client/pages/CountryHome/Overview/meta/utils/unit' +import type { RowsMetadata } from '../utils' +import { getTable } from '../utils' +import { unit } from '../utils/unit' const cols: Record> = { '2020': ['2020'], diff --git a/src/client/pages/CountryHome/Overview/meta/forestAreaWithinProtectedAreas/index.ts b/src/test/migrations/steps/metadata/dashboard/forestAreaWithinProtectedAreas/index.ts similarity index 90% rename from src/client/pages/CountryHome/Overview/meta/forestAreaWithinProtectedAreas/index.ts rename to src/test/migrations/steps/metadata/dashboard/forestAreaWithinProtectedAreas/index.ts index 5851d054ae..f465d44b05 100644 --- a/src/client/pages/CountryHome/Overview/meta/forestAreaWithinProtectedAreas/index.ts +++ b/src/test/migrations/steps/metadata/dashboard/forestAreaWithinProtectedAreas/index.ts @@ -3,9 +3,8 @@ import { ChartColor } from 'meta/chart' import { DashboardItemType } from 'meta/dashboard' import { DashboardPieChart } from 'meta/dashboard/dashboard' -import { getTable } from 'client/pages/CountryHome/Overview/meta/utils' -import { RowsMetadata } from 'client/pages/CountryHome/Overview/meta/utils/rowsMetadata' -import { unit } from 'client/pages/CountryHome/Overview/meta/utils/unit' +import { getTable, RowsMetadata } from '../utils' +import { unit } from '../utils/unit' const cols: Record> = { '2020': ['2020'], diff --git a/src/client/pages/CountryHome/Overview/meta/forestGrowingStockAndCarbon/index.ts b/src/test/migrations/steps/metadata/dashboard/forestGrowingStockAndCarbon/index.ts similarity index 96% rename from src/client/pages/CountryHome/Overview/meta/forestGrowingStockAndCarbon/index.ts rename to src/test/migrations/steps/metadata/dashboard/forestGrowingStockAndCarbon/index.ts index 3dddd8aa33..49f1d3d414 100644 --- a/src/client/pages/CountryHome/Overview/meta/forestGrowingStockAndCarbon/index.ts +++ b/src/test/migrations/steps/metadata/dashboard/forestGrowingStockAndCarbon/index.ts @@ -1,8 +1,7 @@ import { Cycle, TableNames } from 'meta/assessment' import { DashboardItemType, DashboardTable } from 'meta/dashboard' -import { getTable } from 'client/pages/CountryHome/Overview/meta/utils' -import { RowsMetadata } from 'client/pages/CountryHome/Overview/meta/utils/rowsMetadata' +import { getTable, RowsMetadata } from '../utils' const cols: Record> = { '2020': ['1990', '2000', '2010', '2020'], diff --git a/src/client/pages/CountryHome/Overview/meta/forestOwnership/index.ts b/src/test/migrations/steps/metadata/dashboard/forestOwnership/index.ts similarity index 83% rename from src/client/pages/CountryHome/Overview/meta/forestOwnership/index.ts rename to src/test/migrations/steps/metadata/dashboard/forestOwnership/index.ts index d05a65bf7f..020e54c410 100644 --- a/src/client/pages/CountryHome/Overview/meta/forestOwnership/index.ts +++ b/src/test/migrations/steps/metadata/dashboard/forestOwnership/index.ts @@ -1,11 +1,9 @@ import { Cycle, CycleName } from 'meta/assessment' import { ChartColor } from 'meta/chart' -import { DashboardItemType } from 'meta/dashboard' -import { DashboardPieChart } from 'meta/dashboard/dashboard' +import { DashboardItemType, DashboardPieChart } from 'meta/dashboard' -import { getTable } from 'client/pages/CountryHome/Overview/meta/utils' -import { RowsMetadata } from 'client/pages/CountryHome/Overview/meta/utils/rowsMetadata' -import { unit } from 'client/pages/CountryHome/Overview/meta/utils/unit' +import { getTable, RowsMetadata } from '../utils' +import { unit } from '../utils/unit' const tableId = 6 const tableName = 'forestOwnership' diff --git a/src/client/pages/CountryHome/Overview/meta/naturallyRegeneratingForestArea/index.ts b/src/test/migrations/steps/metadata/dashboard/naturallyRegeneratingForestArea/index.ts similarity index 79% rename from src/client/pages/CountryHome/Overview/meta/naturallyRegeneratingForestArea/index.ts rename to src/test/migrations/steps/metadata/dashboard/naturallyRegeneratingForestArea/index.ts index 4730bd17cd..daab48f171 100644 --- a/src/client/pages/CountryHome/Overview/meta/naturallyRegeneratingForestArea/index.ts +++ b/src/test/migrations/steps/metadata/dashboard/naturallyRegeneratingForestArea/index.ts @@ -1,11 +1,10 @@ import { Cycle, TableNames } from 'meta/assessment' import { ChartColor } from 'meta/chart' -import { DashboardItemType } from 'meta/dashboard' -import { DashboardBarChart } from 'meta/dashboard/dashboard' +import { DashboardBarChart, DashboardItemType } from 'meta/dashboard' -import { getTable } from 'client/pages/CountryHome/Overview/meta/utils' -import { RowsMetadata } from 'client/pages/CountryHome/Overview/meta/utils/rowsMetadata' -import { unit } from 'client/pages/CountryHome/Overview/meta/utils/unit' +import type { RowsMetadata } from '../utils' +import { getTable } from '../utils' +import { unit } from '../utils/unit' const commonColumns = ['1990', '2000', '2010', '2020'] @@ -52,7 +51,6 @@ export const naturallyRegeneratingForestArea = (cycle: Cycle, region: boolean): table: getTable({ cycle, cols: cols[cycle.name], tableId, rowMetadata: rowMetadata(region), tableName }), chart: { columns: cols[cycle.name], - label: ({ variableName, percent }: any) => `${variableName} ${(percent * 100).toFixed(0)}%`, cells, xAxis: { label: { key: 'common.year' } }, yAxis: { label: { key: unit(region) } }, diff --git a/src/client/pages/CountryHome/Overview/meta/primaryDesignatedManagementObjective/index.ts b/src/test/migrations/steps/metadata/dashboard/primaryDesignatedManagementObjective/index.ts similarity index 91% rename from src/client/pages/CountryHome/Overview/meta/primaryDesignatedManagementObjective/index.ts rename to src/test/migrations/steps/metadata/dashboard/primaryDesignatedManagementObjective/index.ts index 6df37f6396..955144d7d6 100644 --- a/src/client/pages/CountryHome/Overview/meta/primaryDesignatedManagementObjective/index.ts +++ b/src/test/migrations/steps/metadata/dashboard/primaryDesignatedManagementObjective/index.ts @@ -1,8 +1,7 @@ import { Cycle } from 'meta/assessment' import { DashboardItemType, DashboardTable } from 'meta/dashboard' -import { getTable } from 'client/pages/CountryHome/Overview/meta/utils' -import { RowsMetadata } from 'client/pages/CountryHome/Overview/meta/utils/rowsMetadata' +import { getTable, RowsMetadata } from '../utils' const cols: Record> = { '2020': ['1990', '2000', '2010', '2020'], diff --git a/src/client/pages/CountryHome/Overview/meta/primaryForestPercentOfForestArea/index.ts b/src/test/migrations/steps/metadata/dashboard/primaryForestPercentOfForestArea/index.ts similarity index 90% rename from src/client/pages/CountryHome/Overview/meta/primaryForestPercentOfForestArea/index.ts rename to src/test/migrations/steps/metadata/dashboard/primaryForestPercentOfForestArea/index.ts index 60e63f43d2..e573d6da3f 100644 --- a/src/client/pages/CountryHome/Overview/meta/primaryForestPercentOfForestArea/index.ts +++ b/src/test/migrations/steps/metadata/dashboard/primaryForestPercentOfForestArea/index.ts @@ -1,11 +1,9 @@ import { Cycle, TableNames } from 'meta/assessment' import { ChartColor } from 'meta/chart' -import { DashboardItemType } from 'meta/dashboard' -import { DashboardPieChart } from 'meta/dashboard/dashboard' +import { DashboardItemType, DashboardPieChart } from 'meta/dashboard' -import { getTable } from 'client/pages/CountryHome/Overview/meta/utils' -import { RowsMetadata } from 'client/pages/CountryHome/Overview/meta/utils/rowsMetadata' -import { unit } from 'client/pages/CountryHome/Overview/meta/utils/unit' +import { getTable, RowsMetadata } from '../utils' +import { unit } from '../utils/unit' const cols: Record> = { '2020': ['2020'], diff --git a/src/client/pages/CountryHome/Overview/meta/utils/index.ts b/src/test/migrations/steps/metadata/dashboard/utils/index.ts similarity index 91% rename from src/client/pages/CountryHome/Overview/meta/utils/index.ts rename to src/test/migrations/steps/metadata/dashboard/utils/index.ts index a079f9fcc6..08ae35e09b 100644 --- a/src/client/pages/CountryHome/Overview/meta/utils/index.ts +++ b/src/test/migrations/steps/metadata/dashboard/utils/index.ts @@ -1,8 +1,16 @@ import { UUIDs } from 'utils/uuids' -import { Col, ColStyle, ColType, Cycle, CycleUuid, Row, RowType, Table, VariableCache } from 'meta/assessment' +import { Col, ColStyle, ColType, Cycle, CycleUuid, Label, Row, RowType, Table, VariableCache } from 'meta/assessment' -import { RowMetadata, RowsMetadata } from 'client/pages/CountryHome/Overview/meta/utils/rowsMetadata' +export type RowMetadata = { + id: number + label: Label + variableName: string + calculateFn: string + calculationDependencies: Array +} + +export type RowsMetadata = Array const getStyle = (cycle: Cycle): Record => { return { diff --git a/src/client/pages/CountryHome/Overview/meta/utils/rowsMetadata.ts b/src/test/migrations/steps/metadata/dashboard/utils/rowsMetadata.ts similarity index 100% rename from src/client/pages/CountryHome/Overview/meta/utils/rowsMetadata.ts rename to src/test/migrations/steps/metadata/dashboard/utils/rowsMetadata.ts diff --git a/src/client/pages/CountryHome/Overview/meta/utils/unit.ts b/src/test/migrations/steps/metadata/dashboard/utils/unit.ts similarity index 100% rename from src/client/pages/CountryHome/Overview/meta/utils/unit.ts rename to src/test/migrations/steps/metadata/dashboard/utils/unit.ts diff --git a/src/utils/objects/getDiff.ts b/src/utils/objects/getDiff.ts new file mode 100644 index 0000000000..df8a896fb9 --- /dev/null +++ b/src/utils/objects/getDiff.ts @@ -0,0 +1,62 @@ +import { isEmpty } from './isEmpty' +import { setInPath } from './setInPath' + +const _traverseAndCompare = >( + a: unknown, + b: unknown, + path: Array = [], + keysToIgnore: Array = [], + diff: Partial = {} +): Partial => { + const notObject = typeof a !== 'object' || typeof b !== 'object' + const isNull = a === null || b === null + const isNotObjectOrNull = notObject || isNull + + if (isNotObjectOrNull) { + const lastPathElement = path.at(-1) + const isDifferentAndNotIgnored = a !== b && !keysToIgnore.includes(lastPathElement) + + if (isDifferentAndNotIgnored) { + setInPath({ obj: diff, path, value: b }) + } + + return diff + } + + const keys = Object.keys(a as object) + + keys.forEach((key) => { + if (keysToIgnore.includes(key)) return + + const _a = (a as Record)[key] + const _b = (b as Record)[key] + const _path = [...path, key] + _traverseAndCompare(_a, _b, _path, keysToIgnore, diff) + }) + + return diff +} + +/** + * Compares two objects and returns a partial object representing the differences. + * + * @template T - The type of the objects, which should extend Record + * @param {T} baseObject - The original object to compare from + * @param {T} compareObject - The object to compare against + * @param {Array | undefined} keysToIgnore - An optional array of keys to ignore during comparison + * @returns {Partial | undefined} The partial object representing the differences, or undefined if there are no differences + * + * @example + * const base = { a: 1, b: { c: 2 } }; + * const compare = { a: 1, b: { c: 3, d: 4 } }; + * const result = getDiff(base, compare); + * // result: { b: { c: 3, d: 4 } } + */ +export const getDiff = >( + baseObject: T, + compareObject: T, + keysToIgnore?: Array +): Partial | undefined => { + const diff = _traverseAndCompare(baseObject, compareObject, [], keysToIgnore) + return isEmpty(diff) ? undefined : (diff as Partial) +} diff --git a/src/utils/objects/index.ts b/src/utils/objects/index.ts index 700b8b3187..8fd719a439 100644 --- a/src/utils/objects/index.ts +++ b/src/utils/objects/index.ts @@ -7,12 +7,15 @@ import * as isFunction from 'lodash.isfunction' // @ts-ignore import * as isNil from 'lodash.isnil' // @ts-ignore +import * as merge from 'lodash.merge' +// @ts-ignore import * as pick from 'lodash.pick' // @ts-ignore import * as unset from 'lodash.unset' -import { getInPath } from 'utils/objects/getInPath' import { camelize } from './camelize' +import { getDiff } from './getDiff' +import { getInPath } from './getInPath' import { isEmpty } from './isEmpty' import { propertyOf } from './propertyOf' import { setInPath } from './setInPath' @@ -20,11 +23,13 @@ import { setInPath } from './setInPath' export const Objects = { camelize, cloneDeep, + getDiff, getInPath, isEmpty, isEqual, isFunction, isNil, + merge, pick, propertyOf, setInPath, diff --git a/yarn.lock b/yarn.lock index 4791fc9908..feba34d21d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2514,6 +2514,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.merge@^4.6.9": + version "4.6.9" + resolved "https://registry.yarnpkg.com/@types/lodash.merge/-/lodash.merge-4.6.9.tgz#93e94796997ed9a3ebe9ccf071ccaec4c6bc8fb8" + integrity sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ== + dependencies: + "@types/lodash" "*" + "@types/lodash.pick@^4.4.7": version "4.4.9" resolved "https://registry.yarnpkg.com/@types/lodash.pick/-/lodash.pick-4.4.9.tgz#06f7d88faa81a6c5665584778aea7b1374a1dc5b"