From 10a77b8859f22dd6464b892b488ce4f817ad5502 Mon Sep 17 00:00:00 2001 From: roshni73 Date: Thu, 19 Dec 2024 11:06:54 +0545 Subject: [PATCH] Add api integration for keyfigures,charts --- .../OperationalLearningMap/i18n.json | 3 +- .../OperationalLearningMap/index.tsx | 92 ++++--- .../OperationalLearningMap/styles.module.css | 1 + app/src/views/OperationalLearning/i18n.json | 7 +- app/src/views/OperationalLearning/index.tsx | 231 ++++++++---------- .../OperationalLearning/styles.module.css | 38 +-- 6 files changed, 172 insertions(+), 200 deletions(-) diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json b/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json index 4f8b0c457..acb106275 100644 --- a/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json +++ b/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json @@ -2,6 +2,7 @@ "namespace": "operationalLearning", "strings": { "downloadMapTitle": "Operational learning map", - "learningLegendLabel": "Learnings" + "learningLegendLabel": "Learnings", + "learningCountLegendLabel":"Learning count" } } diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx b/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx index 345c5121d..bfc425f9b 100644 --- a/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx +++ b/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx @@ -9,6 +9,7 @@ import { TextOutput, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; +import { maxSafe } from '@ifrc-go/ui/utils'; import { _cs, isDefined, @@ -32,13 +33,16 @@ import { adminFillLayerOptions, getPointCircleHaloPaint, } from '#utils/map'; +import { GoApiResponse } from '#utils/restRequest'; import i18n from './i18n.json'; import styles from './styles.module.css'; +type learningStatsResponse = GoApiResponse<'/api/v2/ops-learning/stats/'>; const sourceOptions: mapboxgl.GeoJSONSourceRaw = { type: 'geojson', }; + interface CountryProperties { country_id: number; name: string; @@ -48,66 +52,39 @@ interface ClickedPoint { feature: GeoJSON.Feature; lngLat: mapboxgl.LngLatLike; } -interface Props { - className?: string; -} const LEARNINGS_LOW_COLOR = COLOR_LIGHT_BLUE; const LEARNINGS_HIGH_COLOR = COLOR_BLUE; +interface Props { + className?: string; + learning: learningStatsResponse | undefined; + } + function OperationalLearningMap(props: Props) { const strings = useTranslation(i18n); const { className, + learning, } = props; + const [ clickedPointProperties, setClickedPointProperties, ] = useState(); + const countryResponse = useCountry(); const countryCentroidGeoJson = useMemo( (): GeoJSON.FeatureCollection => { - const learning_by_country = [ - { - country_name: 'Afghanistan', - country_id: 14, - operation_count: 40, - }, - { - country_name: 'Albania', - country_id: 15, - operation_count: 10, - }, - { - country_name: 'Argentina', - country_id: 20, - operation_count: 29, - }, - { - country_name: 'Australia', - country_id: 22, - operation_count: 11, - }, - { - country_name: 'Belgium', - country_id: 30, - operation_count: 222, - }, - { - country_name: 'Canada', - country_id: 42, - operation_count: 1, - }, - ]; const features = countryResponse ?.map((country) => { - const learningList = learning_by_country.find( - (item) => item.country_id === country.id, + const learningList = learning?.learning_by_country?.find( + (item: { country_id: number; }) => item.country_id === country.id, ); if (isNotDefined(learningList)) { return undefined; } - const units = learningList.operation_count; + const units = learningList.count; return { type: 'Feature' as const, geometry: country.centroid as { @@ -127,18 +104,23 @@ function OperationalLearningMap(props: Props) { features, }; }, - [countryResponse], + [countryResponse, learning], ); - const maxScaleValue = 10; // FIX ME - const { - bluePointHaloCirclePaint, - } = useMemo( - () => ({ - bluePointHaloCirclePaint: getPointCircleHaloPaint(COLOR_BLUE, 'operation_count', maxScaleValue), - }), - [maxScaleValue], - ); + const bluePointHaloCirclePaint = useMemo(() => { + const learningCount = learning?.learning_by_country + .filter((country) => country.count > 0); + + const maxScaleValue = learningCount && learningCount.length > 0 + ? Math.max( + ...(learningCount + .map((activity: { count: number; }) => activity.count) + .filter(isDefined) ?? []), + ) + : 0; + + return getPointCircleHaloPaint(COLOR_BLUE, 'operation_count', maxScaleValue); + }, [learning]); const handleCountryClick = useCallback( (feature: mapboxgl.MapboxGeoJSONFeature, lngLat: mapboxgl.LngLatLike) => { @@ -164,6 +146,17 @@ function OperationalLearningMap(props: Props) { }, [setClickedPointProperties], ); + + const maxLearning = useMemo(() => { + const learningData = learning?.learning_by_country.filter( + ({ count }) => isDefined(count), + ); + + return maxSafe( + learningData?.map(({ count }) => count), + ); + }, [learning]); + return ( @@ -239,6 +232,7 @@ function OperationalLearningMap(props: Props) { > diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css b/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css index 9170cbd26..9194647d9 100644 --- a/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css +++ b/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css @@ -37,6 +37,7 @@ display: flex; flex-direction: column; gap: var(--go-ui-spacing-md); + .popup-appeal { gap: var(--go-ui-spacing-xs); diff --git a/app/src/views/OperationalLearning/i18n.json b/app/src/views/OperationalLearning/i18n.json index dc1279b74..83a3857d7 100644 --- a/app/src/views/OperationalLearning/i18n.json +++ b/app/src/views/OperationalLearning/i18n.json @@ -25,8 +25,9 @@ "sourcesUsed": "Sources Used", "learningExtract": "Learning Extracts", "sectorsCovered": "Sectors Covered", - "learningBySector": "learnings by sectors", - "learningByRegions": "learnings by regions", - "sourceOvertime": "Sources Overtime" + "learningBySector": "Learning by sectors", + "learningByRegions": "Learning by regions", + "sourceOvertime": "Sources overtime", + "sourceOvertimeResponseError" :"Chart not available" } } diff --git a/app/src/views/OperationalLearning/index.tsx b/app/src/views/OperationalLearning/index.tsx index f741143ca..c4666a773 100644 --- a/app/src/views/OperationalLearning/index.tsx +++ b/app/src/views/OperationalLearning/index.tsx @@ -15,6 +15,7 @@ import { Header, KeyFigure, List, + Message, Tab, TabList, TabPanel, @@ -24,7 +25,7 @@ import { } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; import { - getDatesSeparatedByMonths, + getDatesSeparatedByYear, getFormattedDateKey, hasSomeDefinedValue, numericIdSelector, @@ -35,6 +36,7 @@ import { } from '@ifrc-go/ui/utils'; import { isDefined, + isNotDefined, isTruthyString, sum, } from '@togglecorp/fujs'; @@ -81,8 +83,8 @@ const SUMMARY_STATUS_FAILED = 5 satisfies SummaryStatusEnum; type OpsLearningSummaryResponse = GoApiResponse<'/api/v2/ops-learning/summary/'>; type OpsLearningSectorSummary = OpsLearningSummaryResponse['sectors'][number]; type OpsLearningComponentSummary = OpsLearningSummaryResponse['components'][number]; - type OpsLearningQuery = GoApiUrlQuery<'/api/v2/ops-learning/'>; + type QueryType = Pick< OpsLearningQuery, | 'appeal_code__region' @@ -94,7 +96,6 @@ type QueryType = Pick< | 'per_component_validated__in' | 'search_extracts' >; - const regionKeySelector = (region: RegionOption) => region.key; const countryKeySelector = (country: Country) => country.id; const sectorKeySelector = (d: SecondarySector) => d.key; @@ -103,105 +104,32 @@ const perComponentKeySelector = (option: PerComponent) => option.id; const disasterTypeKeySelector = (type: DisasterType) => type.id; const disasterTypeLabelSelector = (type: DisasterType) => type.name ?? '?'; -const responseData = { - operations_included: 9, - learning_extracts: 6, - sectors_covered: 6, - sources_used: 8, - learning_by_region: [ - { - region_name: 'Americas', - region_id: 1, - count: 2, - }, - { - region_name: 'Asia Pacific', - region_id: 2, - count: 5, - }, - { - region_name: 'Europe', - region_id: 3, - count: 2, - }, - ], - learning_by_sector: [ - { - id: 17, - count: 1, - title: 'health', - }, - { - id: 18, - count: 1, - title: 'education', - }, - { - id: 19, - count: 3, - title: 'Livelihoods and basic needs', - }, - { - id: 20, - count: 4, - title: 'Migration', - }, - { - id: 21, - count: 1, - title: 'WASH', - }, - { - id: 22, - count: 1, - title: 'Shelter', - }, - ], - sources_overtime: { - DREF: [ - { year: 2023, count: 1 }, - { year: 2024, count: 3 }, - ], - 'Emergency Appeal': [ - { year: 2023, count: 1 }, - { year: 2024, count: 1 }, - ], - 'International Appeal': [ - { year: 2023, count: 1 }, - { year: 2024, count: 1 }, - ], - 'Forecast Based Action': [ - { year: 2022, count: 1 }, - ], - }, +type DATA_KEY = 'dref' | 'emergencyAppeal'; + +const dataKeys: DATA_KEY[] = [ + 'dref', + 'emergencyAppeal', +]; + +const dataKeyToClassNameMap = { + dref: styles.dref, + emergencyAppeal: styles.emergencyAppeal, }; +const now = new Date(); -const timeSeriesDataKeys = Object.entries( - responseData.sources_overtime, -).flatMap(([source, entries]) => entries.map((entry) => ({ - date: `${entry.year}-01-01`, - value: entry.count, - source, -}))); - -const oneYearAgo = new Date(); -oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); -oneYearAgo.setDate(1); -oneYearAgo.setMonth(oneYearAgo.getMonth() + 1); -oneYearAgo.setHours(0, 0, 0, 0); -const timeseriesChartClassNameSelector = () => styles.sourceChart; +// FIXME : ADD START DATE PROPERLY +const startDate = new Date(now.getFullYear() - 20, 0, 1); +const endDate = new Date(now.getFullYear(), 11, 31); +const dateList = getDatesSeparatedByYear(startDate, endDate); + +const classNameSelector = (dataKey: DATA_KEY) => dataKeyToClassNameMap[dataKey]; const xAxisFormatter = (date: Date) => date.toLocaleString( navigator.language, - { month: 'short' }, + { year: 'numeric' }, ); -const startDate = oneYearAgo; -const endDate = new Date(); -const dateList = getDatesSeparatedByMonths(startDate, endDate); - -const dateSelector = (d: { date: string }) => d.date; - -const sectorsKeySelector = (datum: { id: number }) => datum.id; +const sectorsKeySelector = (datum: + { count: number; title: string; sector_id: number; }) => datum.sector_id; const sectorsValueSelector = (datum: { count: number }) => datum.count; const sectorsLabelSelector = (datum: { title: string }) => datum.title; @@ -209,6 +137,32 @@ const regionsKeySelector = (datum: { region_id: number }) => datum.region_id; const regionValueSelector = (datum: { count: number }) => datum.count; const regionLabelSelector = (datum: { region_name: string }) => datum.region_name; +const transformSourcesOvertimeData = (data: { + date: string; + type_display: string; + count: number; +}[]) => { + const groupedData: Record = {}; + + data.forEach((entry: { + date: string | number | Date; + type_display: string; + count: number; + }) => { + const year = new Date(entry.date).getFullYear().toString(); + if (!groupedData[year]) { + groupedData[year] = { dref: 0, emergencyAppeal: 0 }; + } + if (entry.type_display === 'DREF') { + groupedData[year].dref = entry.count; + } else if (entry.type_display === 'Emergency Appeal') { + groupedData[year].emergencyAppeal += entry.count; + } + }); + + return groupedData; +}; + /** @knipignore */ // eslint-disable-next-line import/prefer-default-export export function Component() { @@ -394,14 +348,34 @@ export function Component() { setQuery(undefined); }, [resetFilter]); - const timeSeriesValueSelector = useCallback( - (_: string, date: Date) => { - const entry = timeSeriesDataKeys?.find( - (source) => getFormattedDateKey(source.date) === getFormattedDateKey(date), - ); - return entry ? entry.value : undefined; + const { + error: learningStatsResponseError, + response: learningStatsResponse, + pending: learningStatsResponsePending, + } = useRequest({ + url: '/api/v2/ops-learning/stats/', + query, + }); + + const [activePointKey, setActivePointKey] = useState( + () => getFormattedDateKey(dateList[dateList.length - 1]), + ); + + const sourceOvertimeData = useMemo( + () => { + if (isNotDefined(learningStatsResponse)) { + return undefined; + } + return transformSourcesOvertimeData(learningStatsResponse.sources_overtime); }, - [], + [learningStatsResponse], + ); + + const chartValueSelector = useCallback( + (key: DATA_KEY, date: Date) => ( + sourceOvertimeData?.[date.getFullYear()]?.[key] + ), + [sourceOvertimeData], ); return ( @@ -566,45 +540,42 @@ export function Component() { )} /> -
+
-
-
-
- - )} > - + {isDefined(learningStatsResponseError) && ( + + )} + {!learningStatsResponseError && ( + + )}
diff --git a/app/src/views/OperationalLearning/styles.module.css b/app/src/views/OperationalLearning/styles.module.css index 5cd7bdaf4..c054370cc 100644 --- a/app/src/views/OperationalLearning/styles.module.css +++ b/app/src/views/OperationalLearning/styles.module.css @@ -1,4 +1,6 @@ .operational-learning { + --color-dref: var(--go-ui-color-primary-blue); + --color-emergency-appeal: var(--go-ui-color-primary-blue); .beta-tag { font-size: var(--go-ui-font-size-sm); } @@ -89,7 +91,7 @@ border-radius: var(--go-ui-border-radius-lg); box-shadow: var(--go-ui-box-shadow-md); background-color: var(--go-ui-color-white); - padding: var(--go-ui-spacing-md); + padding: var(--go-ui-spacing-xl); .separator { flex-shrink: 0; @@ -111,29 +113,29 @@ display: grid; grid-gap: var(--go-ui-spacing-md); grid-template-columns: 5fr 2fr; - - .map-container { - grid-column: 1 / 2; - grid-row: 1 / 2; - } - + .charts { - display: grid; - grid-gap: var(--go-ui-spacing-md); - - .learning-chart { + .learning-chart { border-radius: var(--go-ui-border-radius-lg); box-shadow: var(--go-ui-box-shadow-md); - + .time-series-chart { - width: 100%; - height: 10rem; + flex-basis: 40rem; + flex-grow: 2; + --path-stroke-width: 1pt; + + .dref { + stroke: var(--color-dref); + stroke-width: var(--path-stroke-width); + fill: none; + color: var(--color-dref); + } - .source-chart { - color: var(--go-ui-color-primary-blue); - stroke: var(--go-ui-color-primary-red); - stroke-width: 1pt; + .emergency-appeal { + stroke: var(--color-emergency-appeal); + stroke-width: var(--path-stroke-width); fill: none; + color: var(--color-emergency-appeal); } } }