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"