diff --git a/app/src/App/routes/index.tsx b/app/src/App/routes/index.tsx index b25ad826a3..5ca52f6fe2 100644 --- a/app/src/App/routes/index.tsx +++ b/app/src/App/routes/index.tsx @@ -1080,9 +1080,12 @@ const fieldReportFormEdit = customWrapRoute({ }, }); +type DefaultFieldReportChild = 'overview'; + const fieldReportDetails = customWrapRoute({ parent: rootLayout, path: 'field-reports/:fieldReportId', + forwardPath: 'overview' satisfies DefaultFieldReportChild, component: { render: () => import('#views/FieldReportDetails'), props: {}, @@ -1094,6 +1097,47 @@ const fieldReportDetails = customWrapRoute({ }, }); +const fieldReportEmergencyOverview = customWrapRoute({ + parent: fieldReportDetails, + path: 'overview' satisfies DefaultFieldReportChild, + component: { + render: () => import('#views/FieldReportEmergencyOverview'), + props: {}, + }, + context: { + title: 'Field Report Emergency Overview', + visibility: 'anything', + }, +}); + +const fieldReportBackground = customWrapRoute({ + parent: fieldReportDetails, + path: 'background', + component: { + render: () => import('#views/FieldReportBackground'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Field Report Background', + visibility: 'anything', + }, +}); + +const fieldReportActionsSummary = customWrapRoute({ + parent: fieldReportDetails, + path: 'summary', + component: { + render: () => import('#views/FieldReportActionsSummary'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Field Report Actions Summary', + visibility: 'anything', + }, +}); + type DefaultPerProcessChild = 'new'; const perProcessLayout = customWrapRoute({ parent: rootLayout, @@ -1363,6 +1407,9 @@ const wrappedRoutes = { // Redirects preparednessOperationalLearning, obsoleteFieldReportDetails, + fieldReportEmergencyOverview, + fieldReportBackground, + fieldReportActionsSummary, }; export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes)); diff --git a/app/src/views/FieldReportActionsSummary/i18n.json b/app/src/views/FieldReportActionsSummary/i18n.json new file mode 100644 index 0000000000..9d89b5221a --- /dev/null +++ b/app/src/views/FieldReportActionsSummary/i18n.json @@ -0,0 +1,10 @@ +{ + "namespace": "fieldReportActionsSummary", + "strings": { + "actionsTakenHeader": "Actions Taken", + "actionsTakenByOthersHeading": "Actions Taken by Others", + "actionsTakenHeading": "Actions Taken by {organization}", + "notesLabel": "Notes", + "descriptionLabel": "Description" + } +} diff --git a/app/src/views/FieldReportActionsSummary/index.tsx b/app/src/views/FieldReportActionsSummary/index.tsx new file mode 100644 index 0000000000..4de6410711 --- /dev/null +++ b/app/src/views/FieldReportActionsSummary/index.tsx @@ -0,0 +1,201 @@ +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { + Chip, + Container, + HtmlOutput, + ListView, + TextOutput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { resolveToComponent } from '@ifrc-go/ui/utils'; +import { + isFalsyString, + isNotDefined, + isTruthyString, + listToGroupList, + listToMap, + mapToList, +} from '@togglecorp/fujs'; + +import TabPage from '#components/TabPage'; +import useGlobalEnums from '#hooks/domain/useGlobalEnums'; +import { + type CategoryType, + DISASTER_TYPE_EPIDEMIC, + FIELD_REPORT_STATUS_EARLY_WARNING, + type ReportType, +} from '#utils/constants'; +import { useRequest } from '#utils/restRequest'; + +import i18n from './i18n.json'; + +/** @knipignore */ +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + + const { fieldReportId } = useParams<{ fieldReportId: string }>(); + + const { + api_action_org, + } = useGlobalEnums(); + + const organizationMap = listToMap( + api_action_org, + (option) => option.key, + (option) => option.value, + ); + + const { + pending: fetchingFieldReport, + response: fieldReportResponse, + } = useRequest({ + skip: isNotDefined(fieldReportId), + url: '/api/v2/field-report/{id}/', + pathVariables: { + id: Number(fieldReportId), + }, + }); + + const actionsTaken = fieldReportResponse?.actions_taken; + const notesHealth = fieldReportResponse?.notes_health; + const notesNs = fieldReportResponse?.notes_ns; + const notesSocioeco = fieldReportResponse?.notes_socioeco; + const actionsOthers = fieldReportResponse?.actions_others; + + const reportType: ReportType = useMemo(() => { + if (fieldReportResponse?.status === FIELD_REPORT_STATUS_EARLY_WARNING) { + return 'EW'; + } + + if (fieldReportResponse?.is_covid_report) { + return 'COVID'; + } + + if (fieldReportResponse?.dtype === DISASTER_TYPE_EPIDEMIC) { + return 'EPI'; + } + + return 'EVT'; + }, [fieldReportResponse]); + + return ( + + + + {actionsTaken?.map((actionTaken) => { + if ( + actionTaken.actions_details.length <= 0 + && isFalsyString(actionTaken.summary) + ) { + return null; + } + + const actionsGroupedByCategory = listToGroupList( + actionTaken.actions_details, + (item) => item.category ?? '', + (item) => item, + ); + const categoryItems = mapToList( + actionsGroupedByCategory, + (item, key) => ({ + category: key as CategoryType, + actions: item, + }), + ); + + return ( + + + {categoryItems?.map((value) => ( + + + {value.actions.map((action) => ( + + + + ))} + {reportType === 'COVID' && value.category === 'Health' && ( + + )} + {reportType === 'COVID' && value.category === 'NS Institutional Strengthening' && ( + + )} + {reportType === 'COVID' && value.category === 'Socioeconomic Interventions' && ( + + )} + + + ))} + + + + ); + })} + {isTruthyString(actionsOthers) && ( + + + + )} + + + + ); +} + +Component.displayName = 'ActionsSummary'; diff --git a/app/src/views/FieldReportBackground/PastEventsChart/i18n.json b/app/src/views/FieldReportBackground/PastEventsChart/i18n.json new file mode 100644 index 0000000000..3d83fac7ab --- /dev/null +++ b/app/src/views/FieldReportBackground/PastEventsChart/i18n.json @@ -0,0 +1,11 @@ +{ + "namespace": "fieldReportBackground", + "strings": { + "pastEventsChartEvents": "Previous Similar Emergencies", + "pastEventsTargetedPopulation": "Targeted population", + "pastEventsAmountRequested": "Amount requested", + "pastEventsAmountFunded": "Amount funded", + "pastEventsLastFiveYearsLabel": "Last 5 years", + "pastEventsLastTenYearsLabel": "Last 10 years" + } +} diff --git a/app/src/views/FieldReportBackground/PastEventsChart/index.tsx b/app/src/views/FieldReportBackground/PastEventsChart/index.tsx new file mode 100644 index 0000000000..b280b8c157 --- /dev/null +++ b/app/src/views/FieldReportBackground/PastEventsChart/index.tsx @@ -0,0 +1,236 @@ +import { + useMemo, + useState, +} from 'react'; +import { + ChartAxes, + ChartContainer, + ChartPoint, + Container, + DateOutput, + SelectInput, + TextOutput, + Tooltip, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + getPercentage, + stringLabelSelector, +} from '@ifrc-go/ui/utils'; +import { + _cs, + encodeDate, + isDefined, + isNotDefined, + listToMap, +} from '@togglecorp/fujs'; + +import useTemporalChartData from '#hooks/useTemporalChartData'; +import { DEFAULT_Y_AXIS_WIDTH_WITH_LABEL } from '#utils/constants'; +import { useRequest } from '#utils/restRequest'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type TimePeriodKey = 'last-five-years' | 'last-ten-years' | 'last-twenty-years'; +type EventMetricKey = 'targeted_population' | 'amount_funded' | 'amount_requested'; + +function timePeriodKeySelector({ key }: { key: TimePeriodKey }) { + return key; +} + +function eventMetricKeySelector({ key }: { key: EventMetricKey }) { + return key; +} + +interface Props { + className?: string; + countryId: string | undefined; +} + +function PastEventsChart(props: Props) { + const { + className, + countryId, + } = props; + + const strings = useTranslation(i18n); + const [selectedTimePeriodKey, setSelectedTimePeriodKey] = useState('last-five-years'); + const [selectedEventMetricKey, setSelectedEventMetricKey] = useState('targeted_population'); + + const timePeriodOptions = useMemo<{ + key: TimePeriodKey; + label: string; + startDate: Date; + endDate: Date; + }[]>( + () => { + const now = new Date(); + + return [ + { + key: 'last-five-years', + label: strings.pastEventsLastFiveYearsLabel, + startDate: new Date(now.getFullYear() - 5, 0, 1), + endDate: new Date(now.getFullYear(), 11, 31), + }, + { + key: 'last-ten-years', + label: strings.pastEventsLastTenYearsLabel, + startDate: new Date(now.getFullYear() - 10, 0, 1), + endDate: new Date(now.getFullYear(), 11, 31), + }, + ]; + }, + [strings], + ); + + const timePeriodMap = useMemo( + () => listToMap(timePeriodOptions, ({ key }) => key), + [timePeriodOptions], + ); + + const eventMetricOptions = useMemo<{ + key: EventMetricKey, + label: string; + }[]>( + () => [ + { + key: 'targeted_population', + label: strings.pastEventsTargetedPopulation, + }, + { + key: 'amount_funded', + label: strings.pastEventsAmountFunded, + }, + { + key: 'amount_requested', + label: strings.pastEventsAmountRequested, + }, + ], + [strings], + ); + + const eventMetricMap = useMemo( + () => listToMap(eventMetricOptions, ({ key }) => key), + [eventMetricOptions], + ); + + const selectedTimePeriod = isDefined(selectedTimePeriodKey) + ? timePeriodMap[selectedTimePeriodKey] + : undefined; + + const { + pending: historicalDisastersPending, + response: historicalDisastersResponse, + error: historicalDisastersError, + } = useRequest({ + skip: isNotDefined(countryId) || isNotDefined(selectedTimePeriod), + url: '/api/v2/country/{id}/historical-disaster/', + pathVariables: { id: countryId }, + query: isDefined(selectedTimePeriod) ? { + start_date_from: encodeDate(selectedTimePeriod.startDate), + start_date_to: encodeDate(selectedTimePeriod.endDate), + } : undefined, + }); + + const chartData = useTemporalChartData( + historicalDisastersResponse, + { + keySelector: (datum, i) => `${datum.date}-${i}`, + xValueSelector: (datum) => datum.date, + yValueSelector: (datum) => datum[selectedEventMetricKey], + yAxisWidth: DEFAULT_Y_AXIS_WIDTH_WITH_LABEL, + temporalResolution: 'year', + yValueStartsFromZero: true, + }, + ); + + const selectedEventMetric = isDefined(selectedEventMetricKey) + ? eventMetricMap[selectedEventMetricKey] + : undefined; + + return ( + + + + + )} + > + + + {chartData.chartPoints.map( + (chartPoint) => ( + + + + + + + + )} + /> + + ), + )} + + + ); +} + +export default PastEventsChart; diff --git a/app/src/views/FieldReportBackground/PastEventsChart/styles.module.css b/app/src/views/FieldReportBackground/PastEventsChart/styles.module.css new file mode 100644 index 0000000000..e21c63df04 --- /dev/null +++ b/app/src/views/FieldReportBackground/PastEventsChart/styles.module.css @@ -0,0 +1,5 @@ +.past-events-chart { + .data-point { + color: var(--go-ui-color-primary-red); + } +} diff --git a/app/src/views/FieldReportBackground/RiskBarChart/CombinedChart/i18n.json b/app/src/views/FieldReportBackground/RiskBarChart/CombinedChart/i18n.json new file mode 100644 index 0000000000..5ba03997a3 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/CombinedChart/i18n.json @@ -0,0 +1,11 @@ +{ + "namespace": "fieldReportBackground", + "strings": { + "riskBarChartVeryLowLabel": "Very low", + "riskBarChartLowLabel": "Low", + "riskBarChartMediumLabel": "Medium", + "riskBarChartHighLabel": "High", + "riskVeryHighLabel": "Very high", + "riskScoreLabel": "Risk score" + } +} diff --git a/app/src/views/FieldReportBackground/RiskBarChart/CombinedChart/index.tsx b/app/src/views/FieldReportBackground/RiskBarChart/CombinedChart/index.tsx new file mode 100644 index 0000000000..0c0eed88b6 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/CombinedChart/index.tsx @@ -0,0 +1,335 @@ +import { + Fragment, + useCallback, + useMemo, +} from 'react'; +import { + ChartAxes, + ChartContainer, + NumberOutput, + TextOutput, + Tooltip, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { maxSafe } from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + listToMap, + mapToMap, +} from '@togglecorp/fujs'; + +import { type components } from '#generated/riskTypes'; +import useTemporalChartData from '#hooks/useTemporalChartData'; +import { + CATEGORY_RISK_HIGH, + CATEGORY_RISK_LOW, + CATEGORY_RISK_MEDIUM, + CATEGORY_RISK_VERY_HIGH, + CATEGORY_RISK_VERY_LOW, +} from '#utils/constants'; +import { + getDataWithTruthyHazardType, + getFiRiskDataItem, + getWfRiskDataItem, + hazardTypeToColorMap, + monthNumberToNameMap, + type RiskMetricOption, + riskScoreToCategory, +} from '#utils/domain/risk'; +import { type RiskApiResponse } from '#utils/restRequest'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type CountryRiskResponse = RiskApiResponse<'/api/v1/country-seasonal/'>; +type RiskData = CountryRiskResponse[number]; +type HazardType = components<'read'>['schemas']['CommonHazardTypeEnumKey']; + +const selectedMonths = { + 0: true, + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true, + 11: true, +}; + +const BAR_GAP = 2; + +interface Props { + riskData: RiskData | undefined; + selectedRiskMetricDetail: RiskMetricOption; + selectedHazardType: HazardType | undefined; + hazardListForDisplay: { + hazard_type: HazardType; + hazard_type_display: string; + }[]; +} + +function CombinedChart(props: Props) { + const { + riskData, + selectedHazardType, + selectedRiskMetricDetail, + hazardListForDisplay, + } = props; + const strings = useTranslation(i18n); + + const riskCategoryToLabelMap: Record = useMemo( + () => ({ + [CATEGORY_RISK_VERY_LOW]: strings.riskBarChartVeryLowLabel, + [CATEGORY_RISK_LOW]: strings.riskBarChartLowLabel, + [CATEGORY_RISK_MEDIUM]: strings.riskBarChartMediumLabel, + [CATEGORY_RISK_HIGH]: strings.riskBarChartHighLabel, + [CATEGORY_RISK_VERY_HIGH]: strings.riskVeryHighLabel, + }), + [ + strings.riskBarChartVeryLowLabel, + strings.riskBarChartLowLabel, + strings.riskBarChartMediumLabel, + strings.riskBarChartHighLabel, + strings.riskVeryHighLabel, + ], + ); + + const fiRiskDataItem = useMemo( + () => getFiRiskDataItem(riskData?.ipc_displacement_data), + [riskData], + ); + const wfRiskDataItem = useMemo( + () => getWfRiskDataItem(riskData?.gwis), + [riskData], + ); + + const selectedRiskData = useMemo( + () => { + if (selectedRiskMetricDetail.key === 'displacement') { + return listToMap( + riskData?.idmc + ?.map(getDataWithTruthyHazardType) + ?.filter(isDefined) ?? [], + (data) => data.hazard_type, + ); + } + + if (selectedRiskMetricDetail.key === 'riskScore') { + return { + ...listToMap( + riskData?.inform_seasonal + ?.map(getDataWithTruthyHazardType) + ?.filter(isDefined) + ?.map((riskItem) => ({ + ...riskItem, + ...mapToMap( + monthNumberToNameMap, + (_, monthName) => monthName, + (monthName) => riskScoreToCategory( + riskItem?.[monthName], + riskItem?.hazard_type, + ), + ), + })) ?? [], + (data) => data.hazard_type, + ), + WF: { + ...wfRiskDataItem, + ...mapToMap( + monthNumberToNameMap, + (_, monthName) => monthName, + (monthName) => riskScoreToCategory( + wfRiskDataItem?.[monthName], + 'WF', + ), + ), + }, + }; + } + + const rasterDisplacementData = listToMap( + riskData?.raster_displacement_data + ?.map(getDataWithTruthyHazardType) + ?.filter(isDefined) ?? [], + (datum) => datum.hazard_type, + ); + + if (isNotDefined(fiRiskDataItem)) { + return rasterDisplacementData; + } + + return { + ...rasterDisplacementData, + FI: fiRiskDataItem, + }; + }, + [riskData, selectedRiskMetricDetail, fiRiskDataItem, wfRiskDataItem], + ); + + const filteredRiskData = useMemo( + () => { + if (isNotDefined(selectedHazardType)) { + return selectedRiskData; + } + + const riskDataItem = selectedRiskData[selectedHazardType]; + + return { + [selectedHazardType]: riskDataItem, + }; + }, + [selectedRiskData, selectedHazardType], + ); + + const hazardData = useMemo( + () => { + const monthKeys = Object.keys(selectedMonths) as unknown as ( + keyof typeof selectedMonths + )[]; + + const hazardKeysFromSelectedRisk = Object.keys(filteredRiskData ?? {}) as HazardType[]; + const currentYear = new Date().getFullYear(); + + return ( + monthKeys.map( + (monthKey) => { + const month = monthNumberToNameMap[monthKey]!; + const value = listToMap( + hazardKeysFromSelectedRisk, + (hazardKey) => hazardKey, + (hazardKey) => (filteredRiskData?.[hazardKey]?.[month] ?? 0), + ); + + return { + value, + month, + date: new Date(currentYear, monthKey, 1), + }; + }, + ) + ); + }, + [filteredRiskData], + ); + + const yAxisTickLabelSelector = useCallback( + (value: number) => { + if (selectedRiskMetricDetail.key === 'riskScore') { + return riskCategoryToLabelMap[value]; + } + + return ( + + ); + }, + [riskCategoryToLabelMap, selectedRiskMetricDetail.key], + ); + + const chartData = useTemporalChartData( + hazardData, + { + keySelector: (datum) => datum.date.getTime(), + xValueSelector: (datum) => datum.date, + yValueSelector: (datum) => maxSafe(Object.values(datum.value)), + yAxisTickLabelSelector, + yearlyChart: true, + yValueStartsFromZero: true, + yAxisWidth: selectedRiskMetricDetail.key === 'riskScore' ? 80 : undefined, + yScale: selectedRiskMetricDetail.key === 'riskScore' ? 'linear' : 'cbrt', + yDomain: selectedRiskMetricDetail.key === 'riskScore' ? { min: 0, max: 5 } : undefined, + numYAxisTicks: 6, + }, + ); + + function getChartHeight(y: number) { + return isDefined(y) + ? Math.max( + chartData.dataAreaSize.height - y + chartData.dataAreaOffset.top, + 0, + ) : 0; + } + + const xAxisDiff = chartData.dataAreaSize.width / chartData.numXAxisTicks; + const barGap = Math.min(BAR_GAP, xAxisDiff / 30); + + return ( + + + {chartData.chartPoints.map( + (datum) => ( + + {hazardListForDisplay.map( + ({ hazard_type: hazard, hazard_type_display }, hazardIndex) => { + const value = datum.originalData.value[hazard]; + const y = chartData.yScaleFn(value); + const height = getChartHeight(y); + + const offsetX = barGap; + const numItems = hazardListForDisplay.length; + + const width = Math.max( + + (xAxisDiff / numItems) - offsetX * 2, + 0, + ); + // eslint-disable-next-line max-len + const x = (datum.x - xAxisDiff / 2) + offsetX + (width + barGap) * hazardIndex; + + return ( + + + {datum.originalData.date.toLocaleDateString('default', { month: 'long' })} + {selectedRiskMetricDetail.key === 'riskScore' ? ( + + ) : ( + + )} + + )} + /> + + ); + }, + )} + + ), + )} + + ); +} + +export default CombinedChart; diff --git a/app/src/views/FieldReportBackground/RiskBarChart/CombinedChart/styles.module.css b/app/src/views/FieldReportBackground/RiskBarChart/CombinedChart/styles.module.css new file mode 100644 index 0000000000..87bbedb629 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/CombinedChart/styles.module.css @@ -0,0 +1,4 @@ +.combined-chart { + height: 20rem; +} + diff --git a/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/FiChartPoint/i18n.json b/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/FiChartPoint/i18n.json new file mode 100644 index 0000000000..4ea1a0ea23 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/FiChartPoint/i18n.json @@ -0,0 +1,8 @@ +{ + "namespace": "fieldReportBackground", + "strings": { + "foodInsecurityChartAverage": "Average for", + "foodInsecurityAnalysisDate": "Analysis date", + "foodInsecurityPeopleExposed": "People Exposed" + } +} diff --git a/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/FiChartPoint/index.tsx b/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/FiChartPoint/index.tsx new file mode 100644 index 0000000000..aeb1e32e0c --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/FiChartPoint/index.tsx @@ -0,0 +1,99 @@ +import { useMemo } from 'react'; +import { + ChartPoint, + TextOutput, + Tooltip, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { isDefined } from '@togglecorp/fujs'; + +import i18n from './i18n.json'; + +const currentYear = new Date().getFullYear(); + +interface Props { + className?: string; + dataPoint: { + originalData: { + year?: number; + month: number; + analysis_date?: string; + total_displacement: number; + }, + key: number | string; + x: number; + y: number; + }; +} + +function FiChartPoint(props: Props) { + const { + dataPoint: { + x, + y, + originalData, + }, + className, + } = props; + + // FIXME: strings should be used only by the main component + const strings = useTranslation(i18n); + + const title = useMemo( + () => { + const { + year, + month, + } = originalData; + + if (isDefined(year)) { + return new Date(year, month - 1, 1).toLocaleString( + navigator.language, + { + year: 'numeric', + month: 'long', + }, + ); + } + + const formattedMonth = new Date(currentYear, month - 1, 1).toLocaleString( + navigator.language, + { month: 'long' }, + ); + + return `${strings.foodInsecurityChartAverage} ${formattedMonth}`; + }, + [originalData, strings.foodInsecurityChartAverage], + ); + + return ( + + + {isDefined(originalData.analysis_date) && ( + + )} + + + )} + /> + + ); +} + +export default FiChartPoint; diff --git a/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/index.tsx b/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/index.tsx new file mode 100644 index 0000000000..b6dceceee5 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/index.tsx @@ -0,0 +1,224 @@ +import { useMemo } from 'react'; +import { + ChartAxes, + ChartContainer, +} from '@ifrc-go/ui'; +import { + avgSafe, + getDiscretePathDataList, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + listToGroupList, + mapToList, +} from '@togglecorp/fujs'; + +import useTemporalChartData from '#hooks/useTemporalChartData'; +import { getPrioritizedIpcData } from '#utils/domain/risk'; +import { type RiskApiResponse } from '#utils/restRequest'; + +import FiChartPoint from './FiChartPoint'; + +import styles from './styles.module.css'; + +type CountryRiskResponse = RiskApiResponse<'/api/v1/country-seasonal/'>; +type RiskData = CountryRiskResponse[number]; + +const colors = [ + 'var(--go-ui-color-gray-30)', + 'var(--go-ui-color-gray-40)', + 'var(--go-ui-color-gray-50)', + 'var(--go-ui-color-gray-60)', + 'var(--go-ui-color-gray-70)', + 'var(--go-ui-color-gray-80)', + 'var(--go-ui-color-gray-90)', +]; + +interface Props { + ipcData: RiskData['ipc_displacement_data'] | undefined; + showHistoricalData?: boolean; + showProjection?: boolean; +} + +function FoodInsecurityChart(props: Props) { + const { + ipcData, + showHistoricalData, + showProjection, + } = props; + + const uniqueData = useMemo( + () => getPrioritizedIpcData(ipcData ?? []), + [ipcData], + ); + + const chartData = useTemporalChartData( + uniqueData, + { + keySelector: (datum) => datum.id, + xValueSelector: (datum) => new Date(datum.year, datum.month - 1, 1), + yValueSelector: (datum) => datum.total_displacement, + yValueStartsFromZero: true, + yearlyChart: true, + }, + ); + + const latestProjectionYear = useMemo( + () => { + const projectionData = uniqueData.filter( + (fiData) => fiData.estimation_type !== 'current', + ).map( + (fiData) => fiData.year, + ); + + return Math.max(...projectionData); + }, + [uniqueData], + ); + + const historicalPointsDataList = useMemo( + () => { + const yearGroupedDataPoints = listToGroupList( + chartData.chartPoints.filter( + (pathPoints) => pathPoints.originalData.year !== latestProjectionYear, + ), + (dataPoint) => dataPoint.originalData.year, + ); + + return mapToList( + yearGroupedDataPoints, + (list, key) => ({ + key, + list, + }), + ); + }, + [latestProjectionYear, chartData.chartPoints], + ); + + const averagePointsData = useMemo( + () => { + const monthGroupedDataPoints = listToGroupList( + chartData.chartPoints, + (dataPoint) => dataPoint.originalData.month, + ); + + return mapToList( + monthGroupedDataPoints, + (list, month) => { + const averageDisplacement = avgSafe( + list.map( + (fiData) => fiData.originalData.total_displacement, + ), + ); + + if (isNotDefined(averageDisplacement)) { + return undefined; + } + + return { + key: month, + x: list[0]!.x, + y: chartData.yScaleFn(averageDisplacement), + originalData: { + total_displacement: averageDisplacement, + month: Number(month), + }, + }; + }, + ).filter(isDefined); + }, + [chartData], + ); + + const predictionPointsData = useMemo( + () => ( + chartData.chartPoints.filter( + (pathPoints) => pathPoints.originalData.year === latestProjectionYear, + ) + ), + [chartData.chartPoints, latestProjectionYear], + ); + + return ( + + + {showHistoricalData && historicalPointsDataList.map( + (historicalPointsData, i) => ( + + {getDiscretePathDataList(historicalPointsData.list).map( + (discretePath) => ( + + ), + )} + {historicalPointsData.list.map( + (pointData) => ( + + ), + )} + + ), + )} + {showProjection && ( + + {getDiscretePathDataList(predictionPointsData).map( + (discretePath) => ( + + ), + )} + {predictionPointsData.map( + (pointData) => ( + + ), + )} + + )} + + {getDiscretePathDataList(averagePointsData).map( + (discretePath) => ( + + ), + )} + {averagePointsData.map( + (pointData) => ( + + ), + )} + + + ); +} + +export default FoodInsecurityChart; diff --git a/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/styles.module.css b/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/styles.module.css new file mode 100644 index 0000000000..723687bee2 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/FoodInsecurityChart/styles.module.css @@ -0,0 +1,29 @@ +.food-insecurity-chart { + height: 20rem; + + .point { + color: currentColor; + } + + .path { + fill: none; + stroke-width: 2; + stroke: currentColor; + } + + .historical-data { + opacity: 0.6; + + &:hover { + opacity: 1; + } + } + + .prediction { + color: var(--go-ui-color-primary-red); + } + + .average { + color: var(--go-ui-color-hazard-fi); + } +} diff --git a/app/src/views/FieldReportBackground/RiskBarChart/WildfireChart/i18n.json b/app/src/views/FieldReportBackground/RiskBarChart/WildfireChart/i18n.json new file mode 100644 index 0000000000..e0648c4dd6 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/WildfireChart/i18n.json @@ -0,0 +1,10 @@ +{ + "namespace": "fieldReportBackground", + "strings": { + "average": "Average (2003 - {currentYear})", + "year": "Year {currentYear}", + "minMax": "Min-max (2003 - {currentYear})", + "minMaxValue": "{min} - {max}", + "monthlySeverityRating": "Monthly Severity Rating" + } +} diff --git a/app/src/views/FieldReportBackground/RiskBarChart/WildfireChart/index.tsx b/app/src/views/FieldReportBackground/RiskBarChart/WildfireChart/index.tsx new file mode 100644 index 0000000000..b90a834968 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/WildfireChart/index.tsx @@ -0,0 +1,252 @@ +import { + useCallback, + useMemo, + useState, +} from 'react'; +import { + ChartAxes, + ChartContainer, + ChartPoint, + TextOutput, + Tooltip, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + avgSafe, + formatNumber, + getDiscretePathDataList, + getPathData, + resolveToString, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + listToGroupList, + mapToList, +} from '@togglecorp/fujs'; + +import { type paths } from '#generated/riskTypes'; +import useTemporalChartData from '#hooks/useTemporalChartData'; +import { + COLOR_PRIMARY_BLUE, + DEFAULT_Y_AXIS_WIDTH_WITH_LABEL, +} from '#utils/constants'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type GetCountryRisk = paths['/api/v1/country-seasonal/']['get']; +type CountryRiskResponse = GetCountryRisk['responses']['200']['content']['application/json']; +type RiskData = CountryRiskResponse[number]; + +interface ChartPoint { + x: number; + y: number; + label: string | undefined; +} + +interface Props { + gwisData: RiskData['gwis'] | undefined; +} + +const currentYear = new Date().getFullYear(); + +function WildfireChart(props: Props) { + const { gwisData } = props; + + const strings = useTranslation(i18n); + + const aggregatedList = useMemo( + () => { + const monthGroupedData = listToGroupList( + gwisData?.filter((dataItem) => dataItem.dsr_type === 'monthly') ?? [], + (gwisItem) => gwisItem.month, + ); + + return mapToList( + monthGroupedData, + (monthlyData, monthKey) => { + const average = avgSafe(monthlyData.map((dataItem) => dataItem.dsr)) ?? 0; + const min = avgSafe(monthlyData.map((dataItem) => dataItem.dsr_min)) ?? 0; + const max = avgSafe(monthlyData.map((dataItem) => dataItem.dsr_max)) ?? 0; + + const current = monthlyData.find( + (dataItem) => dataItem.year === currentYear, + )?.dsr; + + const month = Number(monthKey) - 1; + + return { + date: new Date(currentYear, month, 1), + month, + min, + max, + average, + current, + maxValue: Math.max(min, max, average, current ?? 0), + }; + }, + ); + }, + [gwisData], + ); + + const chartData = useTemporalChartData( + aggregatedList, + { + keySelector: (datum) => datum.month, + xValueSelector: (datum) => datum.date, + yValueSelector: (datum) => datum.maxValue, + yearlyChart: true, + yAxisWidth: DEFAULT_Y_AXIS_WIDTH_WITH_LABEL, + yValueStartsFromZero: true, + }, + ); + + const minPoints = chartData.chartPoints.map( + (dataPoint) => ({ + ...dataPoint, + y: chartData.yScaleFn(dataPoint.originalData.min), + }), + ); + + const maxPoints = chartData.chartPoints.map( + (dataPoint) => ({ + ...dataPoint, + y: chartData.yScaleFn(dataPoint.originalData.max), + }), + ); + + const minMaxPoints = [...minPoints, ...[...maxPoints].reverse()]; + + const currentYearPoints = chartData.chartPoints.map( + (dataPoint) => { + if (isNotDefined(dataPoint.originalData.current)) { + return undefined; + } + + return { + ...dataPoint, + y: chartData.yScaleFn(dataPoint.originalData.current), + }; + }, + ).filter(isDefined); + + const averagePoints = chartData.chartPoints.map( + (dataPoint) => ({ + ...dataPoint, + y: chartData.yScaleFn(dataPoint.originalData.average), + }), + ); + + const tooltipSelector = useCallback( + (_: number | string, i: number) => { + const date = new Date(currentYear, i, 1); + const monthData = aggregatedList[i]!; + + return ( + + + + + + )} + /> + ); + }, + [strings, aggregatedList], + ); + + const [hoveredAxisIndex, setHoveredAxisIndex] = useState(); + + const handleHover = useCallback( + (_: number | string | undefined, index: number | undefined) => { + setHoveredAxisIndex(index); + }, + [], + ); + + return ( + + + + {getDiscretePathDataList(currentYearPoints).map( + (points) => ( + + ), + )} + {currentYearPoints.map( + (pointData, i) => ( + + ), + )} + + + + {averagePoints.map( + (pointData, index) => ( + + ), + )} + + + + ); +} + +export default WildfireChart; diff --git a/app/src/views/FieldReportBackground/RiskBarChart/WildfireChart/styles.module.css b/app/src/views/FieldReportBackground/RiskBarChart/WildfireChart/styles.module.css new file mode 100644 index 0000000000..dc24901152 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/WildfireChart/styles.module.css @@ -0,0 +1,25 @@ +.wildfire-chart { + height: 20rem; + + .min-max-path { + fill: var(--go-ui-color-gray-20); + } + + .path { + fill: none; + stroke: currentColor; + stroke-width: 2; + } + + .point { + color: currentColor; + } + + .average { + color: var(--go-ui-color-primary-blue); + } + + .current-year { + color: var(--go-ui-color-primary-red); + } +} diff --git a/app/src/views/FieldReportBackground/RiskBarChart/i18n.json b/app/src/views/FieldReportBackground/RiskBarChart/i18n.json new file mode 100644 index 0000000000..4a81856292 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/i18n.json @@ -0,0 +1,15 @@ +{ + "namespace": "fieldReportBackground", + "strings": { + "riskBarChartTitle": "Average Risks by Month", + "riskBarChartFilterHazardPlaceholder": "All Hazard types", + "riskBarChartRiskScoreLabel": "Risk Score", + "riskBarChartExposureLabel": "People Exposed", + "riskBarChartDisplacementLabel": "People at Risk of Displacement", + "currentYearTooltipLabel": "Year {currentYear}", + "averageTooltipLabel": "Average (2003-{currentYear})", + "minMaxLabel": "Min-Max (2003-{currentYear})", + "showHistoricalDataLabel": "Show historical data", + "showProjectionsLabel": "Show projections" + } +} diff --git a/app/src/views/FieldReportBackground/RiskBarChart/index.tsx b/app/src/views/FieldReportBackground/RiskBarChart/index.tsx new file mode 100644 index 0000000000..d6d6ca26c4 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/index.tsx @@ -0,0 +1,381 @@ +import { + useCallback, + useMemo, +} from 'react'; +import { + Checkbox, + Container, + LegendItem, + SelectInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + resolveToString, + stringLabelSelector, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isFalsyString, + isNotDefined, + listToGroupList, + listToMap, + unique, +} from '@togglecorp/fujs'; + +import useInputState from '#hooks/useInputState'; +import { + COLOR_LIGHT_GREY, + COLOR_PRIMARY_BLUE, + COLOR_PRIMARY_RED, +} from '#utils/constants'; +import type { + HazardType, + RiskMetric, + RiskMetricOption, +} from '#utils/domain/risk'; +import { + applicableHazardsByRiskMetric, + getDataWithTruthyHazardType, + getFiRiskDataItem, + hasSomeDefinedValue, + hazardTypeKeySelector, + hazardTypeLabelSelector, + hazardTypeToColorMap, + riskMetricKeySelector, +} from '#utils/domain/risk'; +import { type RiskApiResponse } from '#utils/restRequest'; + +import CombinedChart from './CombinedChart'; +import FoodInsecurityChart from './FoodInsecurityChart'; +import WildfireChart from './WildfireChart'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type CountryRiskResponse = RiskApiResponse<'/api/v1/country-seasonal/'>; +type RiskData = CountryRiskResponse[number]; + +const currentYear = new Date().getFullYear(); + +interface Props { + pending: boolean; + seasonalRiskData: RiskData | undefined; +} + +function RiskBarChart(props: Props) { + const { + seasonalRiskData, + pending, + } = props; + const strings = useTranslation(i18n); + + const [selectedRiskMetric, setSelectedRiskMetric] = useInputState('exposure'); + const [ + selectedHazardType, + setSelectedHazardType, + ] = useInputState(undefined); + const [showFiHistoricalData, setShowFiHistoricalData] = useInputState(false); + const [showFiProjection, setShowFiProjection] = useInputState(false); + + const handleRiskMetricChange = useCallback( + (riskMetric: RiskMetric) => { + setSelectedRiskMetric(riskMetric); + setSelectedHazardType(undefined); + }, + [setSelectedHazardType, setSelectedRiskMetric], + ); + + const riskMetricOptions: RiskMetricOption[] = useMemo( + () => ([ + { + key: 'exposure', + label: strings.riskBarChartExposureLabel, + applicableHazards: applicableHazardsByRiskMetric.exposure, + }, + { + key: 'displacement', + label: strings.riskBarChartDisplacementLabel, + applicableHazards: applicableHazardsByRiskMetric.displacement, + }, + { + key: 'riskScore', + label: strings.riskBarChartRiskScoreLabel, + applicableHazards: applicableHazardsByRiskMetric.riskScore, + }, + ]), + [ + strings.riskBarChartExposureLabel, + strings.riskBarChartDisplacementLabel, + strings.riskBarChartRiskScoreLabel, + ], + ); + + const selectedRiskMetricDetail = useMemo( + () => riskMetricOptions.find( + (option) => option.key === selectedRiskMetric, + ) ?? riskMetricOptions[0]!, + [selectedRiskMetric, riskMetricOptions], + ); + + const data = useMemo( + () => { + if (isNotDefined(seasonalRiskData)) { + return undefined; + } + + const { + idmc, + ipc_displacement_data, + raster_displacement_data, + gwis_seasonal, + inform_seasonal, + } = seasonalRiskData; + + const displacement = idmc?.map( + (dataItem) => { + if (!hasSomeDefinedValue(dataItem)) { + return undefined; + } + + return getDataWithTruthyHazardType(dataItem); + }, + ).filter(isDefined) ?? []; + + const groupedIpc = Object.values( + listToGroupList( + ipc_displacement_data ?? [], + (ipcDataItem) => ipcDataItem.country, + ), + ); + + const exposure = [ + ...raster_displacement_data?.map( + (dataItem) => { + if (!hasSomeDefinedValue(dataItem)) { + return undefined; + } + + return getDataWithTruthyHazardType(dataItem); + }, + ) ?? [], + ...groupedIpc.map(getFiRiskDataItem), + ].filter(isDefined); + + const riskScore = unique( + [ + ...inform_seasonal?.map( + (dataItem) => { + if (!hasSomeDefinedValue(dataItem)) { + return undefined; + } + + return getDataWithTruthyHazardType(dataItem); + }, + ) ?? [], + ...gwis_seasonal?.map( + (dataItem) => { + if (!hasSomeDefinedValue(dataItem)) { + return undefined; + } + + return getDataWithTruthyHazardType(dataItem); + }, + ) ?? [], + ].filter(isDefined), + (item) => `${item.country_details.iso3}-${item.hazard_type}`, + ); + + return { + displacement, + exposure, + riskScore, + }; + }, + [seasonalRiskData], + ); + + const availableHazards: { [key in HazardType]?: string } | undefined = useMemo( + () => { + if (isNotDefined(data)) { + return undefined; + } + + if (selectedRiskMetric === 'exposure') { + return { + ...listToMap( + data.exposure, + (item) => item.hazard_type, + (item) => item.hazard_type_display, + ), + }; + } + + if (selectedRiskMetric === 'displacement') { + return { + ...listToMap( + data.displacement, + (item) => item.hazard_type, + (item) => item.hazard_type_display, + ), + }; + } + + if (selectedRiskMetric === 'riskScore') { + return { + ...listToMap( + data.riskScore, + (item) => item.hazard_type, + (item) => item.hazard_type_display, + ), + }; + } + + return undefined; + }, + [data, selectedRiskMetric], + ); + + const hazardTypeOptions = useMemo( + () => ( + selectedRiskMetricDetail.applicableHazards.map( + (hazardType) => { + const hazard_type_display = availableHazards?.[hazardType]; + if (isFalsyString(hazard_type_display)) { + return undefined; + } + + return { + hazard_type: hazardType, + hazard_type_display, + }; + }, + ).filter(isDefined) + ), + [availableHazards, selectedRiskMetricDetail], + ); + + const hazardListForDisplay = useMemo( + () => { + if (isNotDefined(selectedHazardType)) { + return hazardTypeOptions; + } + + return hazardTypeOptions.filter( + (hazardType) => hazardType.hazard_type === selectedHazardType, + ); + }, + [selectedHazardType, hazardTypeOptions], + ); + + return ( + + + + {selectedHazardType === 'FI' && ( +
+ + +
+ )} + + )} + pending={pending} + > + {selectedHazardType === 'FI' && ( + + )} + {selectedHazardType === 'WF' && ( + + )} + {selectedHazardType !== 'FI' && selectedHazardType !== 'WF' && ( + + )} +
+ {hazardListForDisplay.map( + (hazard) => ( + + ), + )} + {selectedHazardType === 'WF' && ( + <> + + + + + )} +
+
+ ); +} + +export default RiskBarChart; diff --git a/app/src/views/FieldReportBackground/RiskBarChart/styles.module.css b/app/src/views/FieldReportBackground/RiskBarChart/styles.module.css new file mode 100644 index 0000000000..efc6d0c629 --- /dev/null +++ b/app/src/views/FieldReportBackground/RiskBarChart/styles.module.css @@ -0,0 +1,20 @@ +.risk-bar-chart { + .fi-filters { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--go-ui-spacing-md); + grid-column: span 2; + + @media screen and (max-width: 40rem) { + grid-column: unset; + } + } + + .legend { + display: flex; + gap: var(--go-ui-spacing-md); + background-color: var(--go-ui-color-background); + padding: var(--go-ui-spacing-sm) var(--go-ui-spacing-md); + } +} diff --git a/app/src/views/FieldReportBackground/SeasonalCalender/i18n.json b/app/src/views/FieldReportBackground/SeasonalCalender/i18n.json new file mode 100644 index 0000000000..1cd017a53f --- /dev/null +++ b/app/src/views/FieldReportBackground/SeasonalCalender/i18n.json @@ -0,0 +1,11 @@ +{ + "namespace": "fieldReportBackground", + "strings": { + "seasonalCalendarHeading": "Seasonal Calendar", + "seasonalCalendarTooltipEventLabel": "Event", + "seasonalCalendarTooltipEventTypeLabel": "Event type", + "seasonalCalendarTooltipMonthsLabel": "Months", + "seasonalCalendarTooltipSourceLabel": "Source", + "seasonalCalenderDataNotAvailable": "Data is not available!" + } +} diff --git a/app/src/views/FieldReportBackground/SeasonalCalender/index.tsx b/app/src/views/FieldReportBackground/SeasonalCalender/index.tsx new file mode 100644 index 0000000000..e54e0760d5 --- /dev/null +++ b/app/src/views/FieldReportBackground/SeasonalCalender/index.tsx @@ -0,0 +1,249 @@ +import { useMemo } from 'react'; +import { + Container, + Message, + TextOutput, + Tooltip, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { splitList } from '@ifrc-go/ui/utils'; +import { + compareNumber, + isDefined, + isNotDefined, + listToGroupList, + listToMap, + mapToList, +} from '@togglecorp/fujs'; + +import { + type GoApiResponse, + useRequest, +} from '#utils/restRequest'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +// NOTE: these labels should not be translated +const colorMap: Record = { + 'Planting and growing': '#d8e800', + Harvest: '#cbbb73', + 'Seasonal hazard': '#f69650', + 'Lean season': '#c88d5b', + Livestock: '#fedf65', + Outbreak: '#fd3900', +}; + +interface SeasonalCalendarEventProps { + data: GoApiResponse<'/api/v2/country/{id}/databank/'>['acaps'][number] & { + monthIndices: { key: number; start: number; end: number; }[]; + } +} + +function SeasonalCalendarEvent(props: SeasonalCalendarEventProps) { + const { data } = props; + const strings = useTranslation(i18n); + + if (isNotDefined(data)) { + return null; + } + + const { + event_type, + } = data; + + if (isNotDefined(event_type)) { + return null; + } + + return data.monthIndices.map(({ key, start, end }) => ( +
+ {data.label} + + + + + + + )} + /> +
+ )); +} + +interface Props { + countryId: string | undefined; +} + +function SeasonalCalender(props: Props) { + const { countryId } = props; + + const strings = useTranslation(i18n); + + const { + pending: databankResponsePending, + response: databankResponse, + } = useRequest({ + url: '/api/v2/country/{id}/databank/', + skip: isNotDefined(countryId), + pathVariables: isDefined(countryId) ? { + id: Number(countryId), + } : undefined, + }); + // NOTE: these are keys in the data + const monthsWithOrder = [ + { month: 'January', order: 1 }, + { month: 'February', order: 2 }, + { month: 'March', order: 3 }, + { month: 'April', order: 4 }, + { month: 'May', order: 5 }, + { month: 'June', order: 6 }, + { month: 'July', order: 7 }, + { month: 'August', order: 8 }, + { month: 'September', order: 9 }, + { month: 'October', order: 10 }, + { month: 'November', order: 11 }, + { month: 'December', order: 12 }, + ]; + + const monthToOrderMap = listToMap( + monthsWithOrder, + ({ month }) => month, + ({ order }) => order, + ); + + const seasonalCalendarData = useMemo( + () => ( + databankResponse?.acaps?.map( + ({ month, event_type, ...otherProps }) => { + if (isNotDefined(month) || isNotDefined(event_type)) { + return undefined; + } + + // FIXME: this sort will mutate the data + const orderedMonth = month.sort( + (a, b) => compareNumber(monthToOrderMap[a], monthToOrderMap[b]), + ); + + const monthIndices = orderedMonth.map( + (monthName) => monthToOrderMap[monthName]!, + ); + + const discreteMonthIndices = splitList( + monthIndices, + (item, index): item is number => ( + index === 0 + ? false + : (item - monthIndices[index - 1]!) > 1 + ), + true, + ); + + return { + ...otherProps, + event_type, + month: orderedMonth, + monthIndices: discreteMonthIndices.map( + (continuousList, index) => ({ + key: index, + start: continuousList[0]!, + end: continuousList[continuousList.length - 1]! + 1, + }), + ).sort((a, b) => compareNumber(a.start, b.start)), + startMonth: monthToOrderMap[orderedMonth[0]!], + }; + }, + ).filter(isDefined).sort( + (a, b) => compareNumber(a.startMonth, b.startMonth), + ) + ), + [databankResponse, monthToOrderMap], + ); + + const eventTypeGroupedData = mapToList( + listToGroupList( + seasonalCalendarData, + ({ event_type }) => event_type[0]!, + ), + (list, key) => ({ + event_type: key, + events: list, + }), + ); + + return ( + + {isDefined(databankResponse) && ( +
+
+ {monthsWithOrder.map( + ({ month, order }) => ( +
+ {month.substring(0, 3)} +
+ ), + )} +
+ {(isNotDefined(eventTypeGroupedData) + || eventTypeGroupedData.length === 0 + ) && ( + + )} + {eventTypeGroupedData?.map( + ({ event_type, events }) => ( +
+ {events.map((event) => ( + + ))} +
+ ), + )} +
+ )} +
+ ); +} + +export default SeasonalCalender; diff --git a/app/src/views/FieldReportBackground/SeasonalCalender/styles.module.css b/app/src/views/FieldReportBackground/SeasonalCalender/styles.module.css new file mode 100644 index 0000000000..295c2316f4 --- /dev/null +++ b/app/src/views/FieldReportBackground/SeasonalCalender/styles.module.css @@ -0,0 +1,33 @@ +.seasonal-calendar-content { + background-image: + linear-gradient(to right, var(--go-ui-color-separator) var(--go-ui-width-separator-thin), transparent var(--go-ui-width-separator-thin)), + linear-gradient(to bottom, var(--go-ui-color-separator) var(--go-ui-width-separator-thin), transparent var(--go-ui-width-separator-thin)); + background-size: calc(100% / 12); + + .event-list { + display: grid; + border-bottom: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator); + padding: var(--go-ui-spacing-md) 0; + grid-template-columns: repeat(12, 1fr); + gap: var(--go-ui-spacing-3xs); + + .month-name { + text-align: center; + font-size: var(--go-ui-font-size-sm); + + @media screen and (max-width: 30rem) { + transform: rotate(-30deg) translateX(-10%) translateY(20%); + padding: 0; + font-size: var(--go-ui-font-size-2xs); + } + } + + .event { + padding: var(--go-ui-spacing-2xs) var(--go-ui-spacing-xs); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: var(--go-ui-font-size-xs); + } + } +} diff --git a/app/src/views/FieldReportBackground/i18n.json b/app/src/views/FieldReportBackground/i18n.json new file mode 100644 index 0000000000..3c905950da --- /dev/null +++ b/app/src/views/FieldReportBackground/i18n.json @@ -0,0 +1,6 @@ +{ + "namespace": "fieldReportBackground", + "strings": { + "selectCountryTitle": "Select a Country" + } +} diff --git a/app/src/views/FieldReportBackground/index.tsx b/app/src/views/FieldReportBackground/index.tsx new file mode 100644 index 0000000000..e632c7eeff --- /dev/null +++ b/app/src/views/FieldReportBackground/index.tsx @@ -0,0 +1,118 @@ +import { + useCallback, + useEffect, + useState, +} from 'react'; +import { useParams } from 'react-router-dom'; +import { + ListView, + SelectInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { isNotDefined } from '@togglecorp/fujs'; + +import TabPage from '#components/TabPage'; +import { + type GoApiResponse, + useRequest, + useRiskRequest, +} from '#utils/restRequest'; + +import PastEventsChart from './PastEventsChart'; +import RiskBarChart from './RiskBarChart'; +import SeasonalCalender from './SeasonalCalender'; + +import i18n from './i18n.json'; + +type FieldReportResponse = GoApiResponse<'/api/v2/field-report/'>; +type FieldReportListItem = NonNullable[number]; + +type CountryListItem = NonNullable[number]; + +const countryKeySelector = (option: CountryListItem) => String(option.id); + +const countryLabelSelector = (option: CountryListItem) => option.name ?? ''; + +/** @knipignore */ +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + + const { fieldReportId } = useParams<{ fieldReportId: string }>(); + const [countryId, setCountryId] = useState(); + + const { + pending: fetchingFieldReport, + response: fieldReportResponse, + } = useRequest({ + skip: isNotDefined(fieldReportId), + url: '/api/v2/field-report/{id}/', + pathVariables: { + id: Number(fieldReportId), + }, + }); + + const selectedCountry = fieldReportResponse?.countries_details?.find( + (country) => String(country.id) === countryId, + ); + + const { + pending: pendingCountryRiskResponse, + response: countryRiskResponse, + } = useRiskRequest({ + apiType: 'risk', + url: '/api/v1/country-seasonal/', + query: { + // FIXME: why do we need to use lowercase? + iso3: selectedCountry?.iso3?.toLowerCase(), + }, + skip: !selectedCountry?.iso3, + }); + + // TODO: Verify can we do it without useEffect? + useEffect(() => { + if (!countryId && fieldReportResponse?.countries_details?.length === 1) { + setCountryId(String(fieldReportResponse.countries_details[0]?.id)); + } + }, [fieldReportResponse, countryId]); + + // NOTE: we always get 1 child in the response + const riskResponse = countryRiskResponse?.[0]; + const showCountrySelect = (fieldReportResponse?.countries_details?.length ?? 0) > 1; + + const handleCountryChange = useCallback((value: string | undefined) => { + setCountryId(value); + }, []); + + return ( + + {showCountrySelect && ( + + + + )} + + + + + ); +} + +Component.displayName = 'Background'; diff --git a/app/src/views/FieldReportDetails/CovidNumericDetails/i18n.json b/app/src/views/FieldReportDetails/CovidNumericDetails/i18n.json deleted file mode 100644 index 94ea4258ec..0000000000 --- a/app/src/views/FieldReportDetails/CovidNumericDetails/i18n.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "namespace": "fieldReportDetails", - "strings": { - "covidCumulativeCasesLabel": "Cumulative Cases", - "covidCumulativeDeadLabel": "Cumulative Dead", - "covidAssistedRcLabel": "Assisted (RC)", - "covidAssistedGovernmentLabel": "Assisted (Government)", - "covidNumberOfCasesLabel": "Number of new cases since last Field Report", - "covidNumberOfNewDeathsLabel": "Number of new deaths since last Field Report", - "covidLocalStaff": "Local Staff", - "covidSourceLabel": "Source", - "covidVolunteersLabel": "Volunteers" - } -} diff --git a/app/src/views/FieldReportDetails/CovidNumericDetails/index.tsx b/app/src/views/FieldReportDetails/CovidNumericDetails/index.tsx deleted file mode 100644 index 93286e9144..0000000000 --- a/app/src/views/FieldReportDetails/CovidNumericDetails/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - KeyFigure, - TextOutput, -} from '@ifrc-go/ui'; -import { useTranslation } from '@ifrc-go/ui/hooks'; - -import { type GoApiResponse } from '#utils/restRequest'; - -import i18n from './i18n.json'; - -type FieldReportResponse = GoApiResponse<'/api/v2/field-report/{id}/'>; - -interface Props { - value: FieldReportResponse | undefined; -} - -function CovidNumericDetails(props: Props) { - const { value } = props; - const strings = useTranslation(i18n); - - // FIXME: Show dynamic labels for disaster type 1 - - return ( - <> - - - - - - - {/* FIXME: This is not there in old details */} - - - - - ); -} - -export default CovidNumericDetails; diff --git a/app/src/views/FieldReportDetails/EarlyWarningNumericDetails/i18n.json b/app/src/views/FieldReportDetails/EarlyWarningNumericDetails/i18n.json deleted file mode 100644 index 7ef405ec69..0000000000 --- a/app/src/views/FieldReportDetails/EarlyWarningNumericDetails/i18n.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "namespace": "fieldReportDetails", - "strings": { - "earlyPotentiallyAffectedRCLabel": "Potentially Affected (RC)", - "earlyPotentiallyAffectedGovernmentLabel": "Potentially Affected (Government)", - "earlyPotentiallyAffectedOtherLabel": "Potentially Affected (Other)", - "earlyPeopleAtHighestRiskRcLabel": "People at Highest Risk (RC)", - "earlyPeopleAtHighestRiskGovernmentLabel": "People at Highest Risk (Government)", - "earlyPeopleAtHighestRiskOtherLabel": "People at Highest Risk (Other)", - "earlyAffectedPopCentresRCLabel": "Affected Pop Centres (RC)", - "earlyAffectedGovernmentLabel": "Affected (Government)", - "earlyAffectedPopCentersOtherLabel": "Affected Pop Centres (Other)", - "earlyAssistedRCLabel": "Assisted (RC)", - "earlyAssistedRCGovernmentLabel": "Assisted (Government)" - } -} \ No newline at end of file diff --git a/app/src/views/FieldReportDetails/EarlyWarningNumericDetails/index.tsx b/app/src/views/FieldReportDetails/EarlyWarningNumericDetails/index.tsx deleted file mode 100644 index eb681b5eaf..0000000000 --- a/app/src/views/FieldReportDetails/EarlyWarningNumericDetails/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { KeyFigure } from '@ifrc-go/ui'; -import { useTranslation } from '@ifrc-go/ui/hooks'; - -import { type GoApiResponse } from '#utils/restRequest'; - -import i18n from './i18n.json'; - -type FieldReportResponse = GoApiResponse<'/api/v2/field-report/{id}/'>; - -interface Props { - value: FieldReportResponse | undefined; -} - -function EarlyWarningNumericDetails(props: Props) { - const { value } = props; - const strings = useTranslation(i18n); - - return ( - <> - - - - - - - - - - - - - ); -} - -export default EarlyWarningNumericDetails; diff --git a/app/src/views/FieldReportDetails/EpidemicNumericDetails/i18n.json b/app/src/views/FieldReportDetails/EpidemicNumericDetails/i18n.json deleted file mode 100644 index d8943e274d..0000000000 --- a/app/src/views/FieldReportDetails/EpidemicNumericDetails/i18n.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "namespace": "fieldReportDetails", - "strings": { - "epidemicCumulativeCasesLabel": "Cumulative Cases", - "epidemicSuspectedCasesLabel": "Suspected Cases", - "epidemicProbableCasesLabel": "Probable Cases", - "epidemicConfirmedCasesLabel": "Confirmed Cases", - "epidemicDeadLabel": "Dead", - "epidemicAssistedRCLabel": "Assisted (RC)", - "epidemicAssistedGovernmentLabel": "Assisted (Government)", - "epidemicLocalStaffLabel": "Local Staff", - "epidemicVolunteersLabel": "Volunteers", - "epidemicDelegatesLabel": "Delegates" - } -} diff --git a/app/src/views/FieldReportDetails/EpidemicNumericDetails/index.tsx b/app/src/views/FieldReportDetails/EpidemicNumericDetails/index.tsx deleted file mode 100644 index d78067faf1..0000000000 --- a/app/src/views/FieldReportDetails/EpidemicNumericDetails/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { KeyFigure } from '@ifrc-go/ui'; -import { useTranslation } from '@ifrc-go/ui/hooks'; - -import { type GoApiResponse } from '#utils/restRequest'; - -import i18n from './i18n.json'; - -type FieldReportResponse = GoApiResponse<'/api/v2/field-report/{id}/'>; - -interface Props { - value: FieldReportResponse | undefined; -} - -function EpidemicNumericDetails(props: Props) { - const { value } = props; - const strings = useTranslation(i18n); - - /* epi_deaths_since_last_fr */ - - // FIXME: Show conditional labels - - return ( - <> - - - - - - - - - - - - ); -} - -export default EpidemicNumericDetails; diff --git a/app/src/views/FieldReportDetails/EventNumericDetails/i18n.json b/app/src/views/FieldReportDetails/EventNumericDetails/i18n.json deleted file mode 100644 index b4fca0813c..0000000000 --- a/app/src/views/FieldReportDetails/EventNumericDetails/i18n.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "namespace": "fieldReportDetails", - "strings": { - "eventInjuredRCLabel": "Injured (RC)", - "eventInjuredGovernmentLabel": "Injured (Government)", - "eventInjuredOtherLabel": "Injured (Other)", - "eventMissingRCLabel": "Missing (RC)", - "eventMissingGovernmentLabel": "Missing (Government)", - "eventMissingOtherLabel": "Missing (Other)", - "eventDeadRCLabel": "Dead (RC)", - "eventDeadGovernmentLabel": "Dead (Government)", - "eventDeadOtherLabel": "Dead (Other)", - "eventDisplacedRCLabel": "Displaced (RC)", - "eventDisplacedGovernmentLabel": "Displaced (Government)", - "eventDisplacedOtherLabel": "Displaced (Other)", - "eventAffectedRCLabel": "Affected (RC)", - "eventAffectedGovernmentLabel": "Affected (Government)", - "eventAffectedOtherLabel": "Affected (Other)", - "eventAssistedRCLabel": "Assisted (RC)", - "eventAssistedGovernmentLabel": "Assisted (Government)", - "eventLocalStaffLabel": "Local Staff", - "eventVolunteersLabel": "Volunteers", - "eventERULabel": "Emergency Response Units", - "eventDelegatedLabel": "Delegates" - } -} diff --git a/app/src/views/FieldReportDetails/EventNumericDetails/index.tsx b/app/src/views/FieldReportDetails/EventNumericDetails/index.tsx deleted file mode 100644 index 0eadd22793..0000000000 --- a/app/src/views/FieldReportDetails/EventNumericDetails/index.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { KeyFigure } from '@ifrc-go/ui'; -import { useTranslation } from '@ifrc-go/ui/hooks'; - -import { type GoApiResponse } from '#utils/restRequest'; - -import i18n from './i18n.json'; - -type FieldReportResponse = GoApiResponse<'/api/v2/field-report/{id}/'>; - -interface Props { - value: FieldReportResponse | undefined; -} - -function EventNumericDetails(props: Props) { - const { value } = props; - const strings = useTranslation(i18n); - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - ); -} - -export default EventNumericDetails; diff --git a/app/src/views/FieldReportDetails/i18n.json b/app/src/views/FieldReportDetails/i18n.json index e97107a06e..6284caaf4d 100644 --- a/app/src/views/FieldReportDetails/i18n.json +++ b/app/src/views/FieldReportDetails/i18n.json @@ -5,38 +5,10 @@ "fieldReportDefaultHeading": "Field Report", "editReportButtonLabel": "Edit Report", "lastUpdatedByLabel": "Last updated by {user} on {date} ({region} - {district})", - "numericDetailsTitle": "Numeric Details (People)", - "riskAnalysisTitle": "Risk Analysis", - "descriptionTitle": "Description", - "covidFieldReportLabel": "COVID-19 Field Report", - "visibilityLabel": "Visibility", - "startDateLabel": "Start Date", - "forecastedDateLabel": "Forecasted Date of Impact", - "reportDateLabel": "Report Date", - "requestForAssistanceHeading": "Request For Assistance", - "governmentAssistanceLabel": "Government Requests International Assistance", - "nsAssistanceLabel": "NS Requests International Assistance", - "informationBulletinPublishedLabel": "Information Bulletin Published", - "actionsTakenByOthersHeading": "Actions taken by others", - "actionsTakenHeading": "Actions taken by {organization}", - "externalPartnersLabel": "External partners", - "supportedActivitiesLabel": "Supported activities", - "plannedResponsesLabel": "Planned International Response", - "contactTitle": "Contacts", - "sourcesTitle": "Sources", - "sourcesForDataMarkedLabel": "Sources for data marked as Other", - "epiSource": "Source", - "notesLabel": "Notes", - "dateOfData": "Date of Data", - "fieldReportDREFTitle": "DREF", - "fieldReportEmergencyAppealTitle": "Emergency Appeal", - "fieldReportRDRTTitle": "RDRT/RITS", - "fieldReportRapidResponseTitle": "Rapid Response Personnel", - "fieldReportEmergencyResponseTitle": "Emergency Response Units", - "fieldReportForecastBasedTitle": "Forecast Based Action", - "fieldReportDetailsNotes": "Notes", - "fieldReportDetailsSummary": "Summary", "home": "Home", - "emergencies": "Emergencies" + "emergencies": "Emergencies", + "emergencyOverview": "Emergency Overview", + "emergencyActionsSummary": "Actions Summary", + "emergencyBackground": "Background" } } diff --git a/app/src/views/FieldReportDetails/index.tsx b/app/src/views/FieldReportDetails/index.tsx index 3e17b32b80..34ad0e19d8 100644 --- a/app/src/views/FieldReportDetails/index.tsx +++ b/app/src/views/FieldReportDetails/index.tsx @@ -1,54 +1,30 @@ +import { Fragment } from 'react'; import { - Fragment, - useMemo, -} from 'react'; -import { useParams } from 'react-router-dom'; -import { CheckboxCircleLineIcon } from '@ifrc-go/icons'; + Outlet, + useParams, +} from 'react-router-dom'; import { Breadcrumbs, - Container, DateOutput, Description, - HtmlOutput, - InlineLayout, - Label, ListView, - Message, - TextOutput, + NavigationTabList, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; -import { - joinList, - resolveToComponent, -} from '@ifrc-go/ui/utils'; +import { resolveToComponent } from '@ifrc-go/ui/utils'; import { isDefined, - isFalsyString, isNotDefined, - isTruthyString, - listToGroupList, listToMap, - mapToList, } from '@togglecorp/fujs'; -import DetailsFailedToLoadMessage from '#components/domain/DetailsFailedToLoadMessage'; import Link from '#components/Link'; +import NavigationTab from '#components/NavigationTab'; import Page from '#components/Page'; import useGlobalEnums from '#hooks/domain/useGlobalEnums'; -import { - type CategoryType, - DISASTER_TYPE_EPIDEMIC, - FIELD_REPORT_STATUS_EARLY_WARNING, - type ReportType, -} from '#utils/constants'; import { getUserName } from '#utils/domain/user'; import { useRequest } from '#utils/restRequest'; -import CovidNumericDetails from './CovidNumericDetails'; -import EarlyWarningNumericDetails from './EarlyWarningNumericDetails'; -import EpidemicNumericDetails from './EpidemicNumericDetails'; -import EventNumericDetails from './EventNumericDetails'; - import i18n from './i18n.json'; /** @knipignore */ @@ -59,36 +35,14 @@ export function Component() { const { api_region_name, - api_action_org, - api_field_report_bulletin, - api_request_choices, } = useGlobalEnums(); - const requestChoicesMap = listToMap( - api_request_choices, - // NOTE: we are converting the type of option.key to number - (option) => Number(option.key), - (option) => option.value, - ); - const regionNameMap = listToMap( api_region_name, (option) => option.key, (option) => option.value, ); - const bulletinMap = listToMap( - api_field_report_bulletin, - (option) => option.key, - (option) => option.value, - ); - - const organizationMap = listToMap( - api_action_org, - (option) => option.key, - (option) => option.value, - ); - const { pending: fetchingFieldReport, response: fieldReportResponse, @@ -104,117 +58,17 @@ export function Component() { // ALWAYS const disasterType = fieldReportResponse?.dtype_details?.name; - const actionsTaken = fieldReportResponse?.actions_taken; const countries = fieldReportResponse?.countries_details; const districts = fieldReportResponse?.districts_details; const eventDetails = fieldReportResponse?.event_details; const summary = fieldReportResponse?.summary; - const contacts = fieldReportResponse?.contacts; - const requestAssistance = fieldReportResponse?.request_assistance; - const nsRequestAssistance = fieldReportResponse?.ns_request_assistance; - const visibility = fieldReportResponse?.visibility_display; - const startDate = fieldReportResponse?.start_date; - const otherSources = fieldReportResponse?.other_sources; - const description = fieldReportResponse?.description; - const actionsOthers = fieldReportResponse?.actions_others; - const sources = fieldReportResponse?.sources; - - // NOTE: Only in EPIDEMIC or EVENT or EARLY WARNING - - const bulletin = fieldReportResponse?.bulletin; - - // NOTE: Only in EPIDEMIC or COVID - - const epiNotesSinceLastFr = fieldReportResponse?.epi_notes_since_last_fr; - const sitFieldsDate = fieldReportResponse?.sit_fields_date; - - // NOTE: Only in COVID - const externalPartners = fieldReportResponse?.external_partners_details; - const supportedActivities = fieldReportResponse?.supported_activities_details; - const notesHealth = fieldReportResponse?.notes_health; - const notesNs = fieldReportResponse?.notes_ns; - const notesSocioeco = fieldReportResponse?.notes_socioeco; - const dref = fieldReportResponse?.dref; - const appeal = fieldReportResponse?.appeal; - const fact = fieldReportResponse?.fact; - const emergencyResponseUnit = fieldReportResponse?.emergency_response_unit; - - // NOTE: Only in EW - - const forecastBasedAction = fieldReportResponse?.forecast_based_action; - // const forecastBasedResponse: number | undefined = fieldReportResponse - // ?.forecast_based_response; // NOTE: Not coming from form const regions = fieldReportResponse?.regions_details; - const reportDate = fieldReportResponse?.report_date; const user = getUserName(fieldReportResponse?.user_details); const lastTouchedAt = fieldReportResponse?.updated_at ?? fieldReportResponse?.created_at; - const rdrt = fieldReportResponse?.rdrt; - const epiSources = fieldReportResponse?.epi_figures_source_display; - - const reportType: ReportType = useMemo(() => { - if (fieldReportResponse?.status === FIELD_REPORT_STATUS_EARLY_WARNING) { - return 'EW'; - } - - if (fieldReportResponse?.is_covid_report) { - return 'COVID'; - } - - if (fieldReportResponse?.dtype === DISASTER_TYPE_EPIDEMIC) { - return 'EPI'; - } - - return 'EVT'; - }, [fieldReportResponse]); - - const plannedResponses = [ - { - key: 'dref', - title: strings.fieldReportDREFTitle, - value: reportType !== 'COVID' ? dref : undefined, - }, - { - key: 'appeal', - title: strings.fieldReportEmergencyAppealTitle, - value: reportType !== 'COVID' ? appeal : undefined, - }, - { - key: 'rdrt', - title: strings.fieldReportRDRTTitle, - // FIXME: We do not know when to hide this field - value: rdrt, - }, - { - key: 'fact', - title: strings.fieldReportRapidResponseTitle, - value: reportType !== 'COVID' ? fact : undefined, - }, - { - key: 'ifrc-staff', - title: strings.fieldReportEmergencyResponseTitle, - value: reportType !== 'COVID' ? emergencyResponseUnit : undefined, - }, - /* - { - key: 'forecast-based-response', - title: 'Forecast Based Response', - // FIXME: We do not know when to hide this field - value: forecastBasedResponse, - }, - */ - { - key: 'forecast-based-action', - title: strings.fieldReportForecastBasedTitle, - value: reportType === 'EW' ? forecastBasedAction : undefined, - }, - ].filter((plannedResponse) => isDefined(plannedResponse.value) && plannedResponse.value !== 0); - - // FIXME: Translation Warning Banner should be shown - // FIXME: Breadcrumbs const shouldHideDetails = fetchingFieldReport || isDefined(fieldReportResponseError); @@ -268,7 +122,7 @@ export function Component() { / - {countries?.map((country, i) => ( + {countries?.map((country, index) => ( {country.name} - {i !== countries.length - 1 ? ', ' : null} + {index !== countries.length - 1 ? ', ' : null} ))} @@ -319,381 +173,27 @@ export function Component() { )} contentOriginalLanguage={fieldReportResponse?.translation_module_original_language} > - {fetchingFieldReport && ( - - )} - {isDefined(fieldReportResponseError) && ( - - )} - {!shouldHideDetails && ( - <> - - - {reportType === 'EW' ? ( - - ) : ( - - )} - - {reportType === 'COVID' && ( - - )} - - - {(reportType === 'EPI' || reportType === 'COVID') && isTruthyString(sitFieldsDate) && ( - - )} - {(reportType === 'EPI' || reportType === 'COVID') && isTruthyString(epiSources) && ( - - )} - {/* FIXME: We need to add more content here */} -
- - )} - > - - {reportType === 'COVID' && ( - - )} - {reportType === 'EPI' && ( - - )} - {reportType === 'EVT' && ( - - )} - {reportType === 'EW' && ( - - )} - - - {/* NOTE: there was no condition in old details */} - {(reportType === 'EPI' || reportType === 'COVID') && isTruthyString(epiNotesSinceLastFr) && ( - - - - )} - {isTruthyString(otherSources) && ( - - - - )} - - {isTruthyString(description) && ( - - - - )} - - - - - - - - {isDefined(bulletin) && reportType !== 'COVID' && ( - - {bulletinMap?.[bulletin] ?? '--'} - - )} - {actionsTaken?.map((actionTaken) => { - if ( - actionTaken.actions_details.length <= 0 - && isFalsyString(actionTaken.summary) - ) { - return null; - } - - const actionsGroupedByCategory = listToGroupList( - actionTaken.actions_details, - (item) => item.category ?? '', - (item) => item, - ); - const categoryItems = mapToList( - actionsGroupedByCategory, - (item, key) => ({ - category: key as CategoryType, - actions: item, - }), - ); - - return ( - - - {categoryItems?.map((value) => ( - - - {value.actions.map((action) => ( - } - spacing="sm" - > - {action.name} - - ))} - {reportType === 'COVID' && value.category === 'Health' && ( - - )} - {reportType === 'COVID' && value.category === 'NS Institutional Strengthening' && ( - - )} - {reportType === 'COVID' && value.category === 'Socioeconomic Interventions' && ( - - )} - - - ))} - - - - ); - })} - {isTruthyString(actionsOthers) && ( - - - - )} - {/* NOTE: There was not condition on old details */} - {isDefined(externalPartners) && externalPartners.length > 0 && reportType === 'COVID' && ( - -
    - {externalPartners.map((partner) => ( -
  • - {partner?.name || '--'} -
  • - ))} -
-
- )} - {/* NOTE: There was not condition on old details */} - {isDefined(supportedActivities) && supportedActivities.length > 0 && reportType === 'COVID' && ( - -
    - {supportedActivities.map((supportedActivity) => ( -
  • - {supportedActivity?.name || '--'} -
  • - ))} -
-
- )} - {plannedResponses.length > 0 && ( - - {plannedResponses.map((plannedResponse) => ( - - ))} - - )} - {isDefined(sources) && sources.length > 0 && ( - - {sources.map((source) => ( - - ))} - - )} - {isDefined(contacts) && contacts.length > 0 && ( - - - {contacts?.map((contact) => ( - - -
- {joinList([ - isTruthyString(contact.name) - ? contact.name - : undefined, - isTruthyString(contact.title) - ? contact.title - : undefined, - isTruthyString(contact.email) ? ( - - {contact.email} - - ) : undefined, - isTruthyString(contact.phone) ? ( - - {contact.phone} - - ) : undefined, - ].filter(isDefined), ', ')} -
-
- ))} -
-
- )} - - )} + + + {strings.emergencyOverview} + + + {strings.emergencyActionsSummary} + + + {strings.emergencyBackground} + + + ); } diff --git a/app/src/views/FieldReportEmergencyOverview/i18n.json b/app/src/views/FieldReportEmergencyOverview/i18n.json new file mode 100644 index 0000000000..74675ca63d --- /dev/null +++ b/app/src/views/FieldReportEmergencyOverview/i18n.json @@ -0,0 +1,23 @@ +{ + "namespace": "fieldReportEmergencyOverview", + "strings": { + "keyFiguresHeading": "Key Figures", + "injuredLabel": "Injured", + "deadLabel": "Dead", + "missingLabel": "Missing", + "affectedLabel": "Affected", + "displacedLabel": "Displaced", + "emergencyOverviewHeading": "Emergency Overview", + "countryLabel": "Country", + "startDateLabel": "Start Date", + "governmentRequestLabel": "Government Requests International Assistance", + "disasterTypeLabel": "Disaster Type", + "drefLabel": "DREF", + "nsRequestLabel": "NS Requests International Assistance", + "visibilityLabel": "Visibility", + "emergencyAppealLabel": "Emergency Appeal", + "contactHeading": "Contact", + "contactIFRCLabel": "IFRC", + "contactNsLabel": "National Societies" + } +} diff --git a/app/src/views/FieldReportEmergencyOverview/index.tsx b/app/src/views/FieldReportEmergencyOverview/index.tsx new file mode 100644 index 0000000000..0c0d6ef66e --- /dev/null +++ b/app/src/views/FieldReportEmergencyOverview/index.tsx @@ -0,0 +1,278 @@ +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { + Container, + Description, + KeyFigure, + ListView, + TextOutput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + isDefined, + isNotDefined, + isTruthyString, + listToGroupList, +} from '@togglecorp/fujs'; + +import Link from '#components/Link'; +import TabPage from '#components/TabPage'; +import { useRequest } from '#utils/restRequest'; + +import i18n from './i18n.json'; + +/** @knipignore */ +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + const { fieldReportId } = useParams<{ fieldReportId: string }>(); + + const { + pending: fetchingFieldReport, + response: fieldReportResponse, + } = useRequest({ + skip: isNotDefined(fieldReportId), + url: '/api/v2/field-report/{id}/', + pathVariables: { + id: Number(fieldReportId), + }, + }); + + const countries = fieldReportResponse?.countries_details; + const disasterType = fieldReportResponse?.dtype_details?.name; + const startDate = fieldReportResponse?.start_date; + const visibility = fieldReportResponse?.visibility_display; + const appeal = fieldReportResponse?.appeal; + const drefRequested = fieldReportResponse?.dref_display; + const nsRequested = fieldReportResponse?.ns_request_assistance; + + // TODO: Please verify requested assistance is gov req assistance? + const govRequested = fieldReportResponse?.request_assistance; + const fieldReportContact = fieldReportResponse?.contacts; + + const groupedContacts = useMemo( + () => { + type Contact = Omit[number], 'event'>; + let contactsToProcess: Contact[] | undefined = fieldReportContact; + if (!fieldReportContact || fieldReportContact.length <= 0) { + contactsToProcess = fieldReportContact; + } + const grouped = listToGroupList( + contactsToProcess?.map( + (contact) => { + if (isNotDefined(contact)) { + return undefined; + } + + const { ctype } = contact; + if (isNotDefined(ctype)) { + return undefined; + } + + return { + ...contact, + ctype, + }; + }, + ).filter(isDefined) ?? [], + (contact) => ( + contact.email.endsWith('ifrc.org') + ? strings.contactIFRCLabel + : strings.contactNsLabel + ), + ); + return grouped; + }, + [ + strings, + fieldReportContact, + ], + ); + + return ( + + {isDefined(fieldReportResponse) && ( + + + + + + + + + + + + )} + {isDefined(fieldReportResponse) && ( + + + country.name).join(', ')} + strongValue + /> + + + + + + + + + + )} + {isDefined(groupedContacts) && Object.keys(groupedContacts).length > 0 + && ( + + + {/* FIXME: lets not use Object.entries here */} + {Object.entries(groupedContacts).map(([contactGroup, contacts]) => ( + + + {contacts.map((contact) => ( + + + + {contact.title} + + + + {contact.ctype} + + {isTruthyString(contact.email) && ( + + {contact.email} + + )} + {isTruthyString(contact.phone) && ( + + {contact.phone} + + )} + + + + ))} + + + ))} + + + )} + + ); +} + +Component.displayName = 'EmergencyOverview';