diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index 921bd3567..4f9c918bc 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -1942,6 +1942,7 @@ const preparednessGlobalCatalogue = customWrapRoute({ }, }); +// FIXME: update name to `preparednessOperationalLearning` const preparednessGlobalOperational = customWrapRoute({ parent: preparednessLayout, path: 'operational-learning', diff --git a/src/components/DateOutput/index.tsx b/src/components/DateOutput/index.tsx index 0bf32789a..a8d01e597 100644 --- a/src/components/DateOutput/index.tsx +++ b/src/components/DateOutput/index.tsx @@ -1,12 +1,12 @@ import { useMemo } from 'react'; import { _cs } from '@togglecorp/fujs'; -import { formatDate } from '#utils/common'; +import { DateLike, formatDate } from '#utils/common'; import styles from './styles.module.css'; export interface Props { className?: string; - value?: string | number | null; + value: DateLike | undefined | null; format?: string; invalidText?: React.ReactNode; } @@ -14,7 +14,7 @@ export interface Props { function DateOutput(props: Props) { const { value, - format = 'yyyy-MM-dd', + format, className, invalidText, } = props; diff --git a/src/components/MapPopup/index.tsx b/src/components/MapPopup/index.tsx index b0c7f4655..409e95af1 100644 --- a/src/components/MapPopup/index.tsx +++ b/src/components/MapPopup/index.tsx @@ -47,7 +47,7 @@ function MapPopup(props: Props) { // eslint-disable-next-line react/jsx-props-no-spreading {...containerProps} className={styles.container} - ellipsizeHeading + withoutWrapInHeading childrenContainerClassName={_cs(styles.content, childrenContainerClassName)} withHeaderBorder withInternalPadding diff --git a/src/components/Table/ColumnShortcuts/TimelineHeader/index.tsx b/src/components/Table/ColumnShortcuts/TimelineHeader/index.tsx new file mode 100644 index 000000000..a5f4f5dbd --- /dev/null +++ b/src/components/Table/ColumnShortcuts/TimelineHeader/index.tsx @@ -0,0 +1,43 @@ +import DateOutput from '#components/DateOutput'; +import { _cs } from '@togglecorp/fujs'; + +import HeaderCell, { HeaderCellProps } from '../../HeaderCell'; + +import styles from './styles.module.css'; + +export interface Props extends HeaderCellProps { + className?: string; + dateRange: { + start: Date, + end: Date, + } | undefined; +} + +function TimelineHeader(props: Props) { + const { + className, + dateRange, + ...otherProps + } = props; + + return ( + + + + + )} + /> + ); +} + +export default TimelineHeader; diff --git a/src/components/Table/ColumnShortcuts/TimelineHeader/styles.module.css b/src/components/Table/ColumnShortcuts/TimelineHeader/styles.module.css new file mode 100644 index 000000000..a043f8cef --- /dev/null +++ b/src/components/Table/ColumnShortcuts/TimelineHeader/styles.module.css @@ -0,0 +1,7 @@ +.timeline-header { + .title { + display: flex; + flex-grow: 1; + justify-content: space-between; + } +} diff --git a/src/components/Table/ColumnShortcuts/TimelineItem/index.tsx b/src/components/Table/ColumnShortcuts/TimelineItem/index.tsx new file mode 100644 index 000000000..249b8ddc2 --- /dev/null +++ b/src/components/Table/ColumnShortcuts/TimelineItem/index.tsx @@ -0,0 +1,88 @@ +import { _cs, isNotDefined } from '@togglecorp/fujs'; + +import Tooltip from '#components/Tooltip'; +import TextOutput from '#components/TextOutput'; + +import { type DateLike, isValidDate } from '#utils/common'; + +import styles from './styles.module.css'; + +export interface Props { + className?: string; + startDate: DateLike | null | undefined; + endDate: DateLike | null | undefined; + dateRange: { + start: Date, + end: Date, + } | undefined; +} + +function TimelineItem(props: Props) { + const { + className, + startDate, + endDate, + dateRange, + } = props; + + if (isNotDefined(dateRange)) { + return null; + } + + if (!isValidDate(startDate)) { + return null; + } + + if (!isValidDate(endDate)) { + return null; + } + + const domainWidth = dateRange.end.getTime() - dateRange.start.getTime(); + + const start = 1 - (dateRange.end.getTime() - new Date(startDate).getTime()) / domainWidth; + const end = (dateRange.end.getTime() - new Date(endDate).getTime()) / domainWidth; + + const today = 1 - (dateRange.end.getTime() - new Date().getTime()) / domainWidth; + + return ( + <> +
+
+
+
+
+
+ + + + + )} + /> + + ); +} + +export default TimelineItem; diff --git a/src/components/Table/ColumnShortcuts/TimelineItem/styles.module.css b/src/components/Table/ColumnShortcuts/TimelineItem/styles.module.css new file mode 100644 index 000000000..1cd9cca09 --- /dev/null +++ b/src/components/Table/ColumnShortcuts/TimelineItem/styles.module.css @@ -0,0 +1,36 @@ +.timeline-item { + position: absolute; + top: 0; + left: var(--go-ui-spacing-sm); + width: calc(100% - 2 * var(--go-ui-spacing-sm)); + height: 100%; + + .timeline-progress { + position: absolute; + top: 50%; + transform: translateY(-50%); + border-radius: 0.25em; + background-color: var(--go-ui-color-primary-red); + height: 0.5rem; + } + + .today-marker { + position: absolute; + border-left: var(--go-ui-width-separator-sm) dashed var(--go-ui-color-primary-blue); + height: 100%; + } + + .start-date-marker { + position: absolute; + left: 0; + border-left: var(--go-ui-width-separator-sm) dashed var(--go-ui-color-separator); + height: 100%; + } + + .end-date-marker { + position: absolute; + right: 0; + border-left: var(--go-ui-width-separator-sm) dashed var(--go-ui-color-separator); + height: 100%; + } +} diff --git a/src/components/Table/ColumnShortcuts/index.ts b/src/components/Table/ColumnShortcuts/index.ts index 7428949e5..e67501cc7 100644 --- a/src/components/Table/ColumnShortcuts/index.ts +++ b/src/components/Table/ColumnShortcuts/index.ts @@ -7,21 +7,13 @@ import { randomString, } from '@togglecorp/fujs'; -import DateOutput from '#components/DateOutput'; -import { type Props as DateOutputProps } from '#components/DateOutput'; -import DateRangeOutput from '#components/DateRangeOutput'; -import { type Props as DateRangeOutputProps } from '#components/DateRangeOutput'; -import NumberOutput from '#components/NumberOutput'; -import { type Props as NumberOutputProps } from '#components/NumberOutput'; -import BooleanOutput from '#components/BooleanOutput'; -import { type Props as BooleanOutputProps } from '#components/BooleanOutput'; -import ProgressBar from '#components/ProgressBar'; -import { type Props as ProgressBarProps } from '#components/ProgressBar'; -import ReducedListDisplay, { - Props as ReducedListDisplayProps, -} from '#components/ReducedListDisplay'; -import { type Props as LinkProps } from '#components/Link'; -import Link from '#components/Link'; +import DateOutput, { type Props as DateOutputProps } from '#components/DateOutput'; +import DateRangeOutput, { type Props as DateRangeOutputProps } from '#components/DateRangeOutput'; +import NumberOutput, { type Props as NumberOutputProps } from '#components/NumberOutput'; +import BooleanOutput, { type Props as BooleanOutputProps } from '#components/BooleanOutput'; +import ProgressBar, { type Props as ProgressBarProps } from '#components/ProgressBar'; +import ReducedListDisplay, { Props as ReducedListDisplayProps } from '#components/ReducedListDisplay'; +import Link, { type Props as LinkProps } from '#components/Link'; import { numericIdSelector } from '#utils/selectors'; import { type GoApiResponse } from '#utils/restRequest'; @@ -38,6 +30,8 @@ import { type ExpandButtonProps } from './ExpandButton'; import ExpansionIndicator from './ExpansionIndicator'; import { type Props as ExpansionIndicatorProps } from './ExpansionIndicator'; import CountryLink from './CountryLink'; +import TimelineItem, { type Props as TimelineItemProps } from './TimelineItem'; +import TimelineHeader, { type Props as TimelineHeaderProps } from './TimelineHeader'; import type { Props as CountryLinkProps } from './CountryLink'; import RegionLink from './RegionLink'; @@ -383,10 +377,10 @@ export function createExpandColumn( return item; } -export function createExpansionIndicatorColumn( +export function createExpansionIndicatorColumn( isExpanded?: boolean, ) { - const item: Column = { + const item: Column = { id: randomString(), title: '', headerCellRenderer: HeaderCell, @@ -417,14 +411,14 @@ export function createExpansionIndicatorColumn( return item; } -export function createElementColumn( +export function createElementColumn( id: string, title: string, renderer: React.ComponentType, - rendererParams: (key: KEY, datum: DATA) => ELEMENT_PROPS, - options?: Options, + rendererParams: (key: KEY, datum: DATUM) => ELEMENT_PROPS, + options?: Options, ) { - const item: Column = { + const item: Column = { id, title, headerCellRenderer: HeaderCell, @@ -446,12 +440,50 @@ export function createElementColumn( return item; } -export function createActionColumn( +export function createTimelineColumn( id: string, - rendererParams: (datum: D) => TableActionsProps, - options?: Options, + dateRange: { + start: Date, + end: Date, + } | undefined, + rendererParams: (datum: DATUM) => Omit, + options?: Options, +) { + const item: Column = { + id, + title: '', + headerCellRenderer: TimelineHeader, + headerCellRendererParams: { + dateRange, + sortable: options?.sortable, + }, + cellRenderer: TimelineItem, + cellRendererParams: (_, datum) => ({ + dateRange, + ...rendererParams(datum), + }), + headerContainerClassName: options?.headerContainerClassName, + cellRendererClassName: options?.cellRendererClassName, + columnClassName: options?.columnClassName, + headerCellRendererClassName: options?.headerCellRendererClassName, + cellContainerClassName: _cs( + options?.cellContainerClassName, + styles.timelineCellContainer, + ), + columnWidth: options?.columnWidth, + columnStretch: options?.columnStretch, + columnStyle: options?.columnStyle, + }; + + return item; +} + +export function createActionColumn( + id: string, + rendererParams: (datum: DATUM) => TableActionsProps, + options?: Options, ) { - const item: Column = { + const item: Column = { id, title: '', headerCellRenderer: HeaderCell, diff --git a/src/components/Table/ColumnShortcuts/styles.module.css b/src/components/Table/ColumnShortcuts/styles.module.css index 25bce0f70..edb830586 100644 --- a/src/components/Table/ColumnShortcuts/styles.module.css +++ b/src/components/Table/ColumnShortcuts/styles.module.css @@ -5,3 +5,7 @@ .expansion-indicator-cell-container { position: relative; } + +.timeline-cell-container { + position: relative; +} diff --git a/src/components/Table/HeaderCell/index.tsx b/src/components/Table/HeaderCell/index.tsx index 6f86c9f14..a4c9d13da 100644 --- a/src/components/Table/HeaderCell/index.tsx +++ b/src/components/Table/HeaderCell/index.tsx @@ -102,6 +102,7 @@ function HeaderCell(props: HeaderCellProps) {
{infoTitle && infoDescription && ( diff --git a/src/components/Table/HeaderCell/styles.module.css b/src/components/Table/HeaderCell/styles.module.css index 127135ece..5bfa21a69 100644 --- a/src/components/Table/HeaderCell/styles.module.css +++ b/src/components/Table/HeaderCell/styles.module.css @@ -10,6 +10,11 @@ } .icon { + flex-shrink: 0; font-size: var(--go-ui-height-icon-multiplier); } + + .info-popup-icon { + flex-shrink: 0; + } } diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index adfb1aaea..b33da8217 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -6,7 +6,7 @@ export interface BaseHeader { name: string; index: number; - title?: string; + title?: React.ReactNode; } export interface BaseCell { diff --git a/src/components/TextArea/index.tsx b/src/components/TextArea/index.tsx index 01b3a1f67..a56125163 100644 --- a/src/components/TextArea/index.tsx +++ b/src/components/TextArea/index.tsx @@ -5,7 +5,7 @@ import InputContainer, { Props as InputContainerProps } from '../InputContainer' import RawTextArea, { Props as RawTextAreaProps } from '../RawTextArea'; const BULLET = '•'; -const KEY_ENTER = 'ENTER'; +const KEY_ENTER = 'Enter'; type InheritedProps = (Omit & Omit, 'type'>); export interface Props extends InheritedProps { diff --git a/src/components/TextOutput/index.tsx b/src/components/TextOutput/index.tsx index 6b5afd920..3b721003c 100644 --- a/src/components/TextOutput/index.tsx +++ b/src/components/TextOutput/index.tsx @@ -65,7 +65,7 @@ function TextOutput(props: Props) { } = props; const { value: propValue } = props; - let valueComponent: React.ReactNode = propValue || invalidText; + let valueComponent: React.ReactNode = invalidText; if (otherProps.valueType === 'number') { valueComponent = ( @@ -91,6 +91,8 @@ function TextOutput(props: Props) { invalidText={invalidText} /> ); + } else if (!(propValue instanceof Date)) { + valueComponent = propValue || invalidText; } return ( diff --git a/src/components/domain/RiskImminentEvents/Gdacs/EventDetails/i18n.json b/src/components/domain/RiskImminentEvents/Gdacs/EventDetails/i18n.json index 0c7a92362..0dc18aa59 100644 --- a/src/components/domain/RiskImminentEvents/Gdacs/EventDetails/i18n.json +++ b/src/components/domain/RiskImminentEvents/Gdacs/EventDetails/i18n.json @@ -2,10 +2,7 @@ "namespace": "common", "strings": { "eventStartOnLabel": "Started on", - "usefulLinksHeading": "Useful links:", "eventMoreDetailsLink": "More Details", - "eventGeometryLink": "Geometry", - "eventReportLink": "Report", "eventSourceLabel": "Source", "eventDeathLabel": "Estimated deaths", "eventDisplacedLabel": "Estimated number of people displaced", diff --git a/src/components/domain/RiskImminentEvents/Gdacs/EventDetails/index.tsx b/src/components/domain/RiskImminentEvents/Gdacs/EventDetails/index.tsx index dc1c2c2ff..39912c53d 100644 --- a/src/components/domain/RiskImminentEvents/Gdacs/EventDetails/index.tsx +++ b/src/components/domain/RiskImminentEvents/Gdacs/EventDetails/index.tsx @@ -103,42 +103,6 @@ function EventDetails(props: Props) { )} > {pending && } - {isDefined(eventDetails?.url) && ( - - {isDefined(eventDetails?.url.details) && ( - - {strings.eventMoreDetailsLink} - - )} - {isDefined(eventDetails?.url.geometry) && ( - - {strings.eventGeometryLink} - - )} - {isDefined(eventDetails?.url.report) && ( - - {strings.eventReportLink} - - )} - - )}
{isDefined(eventDetails?.source) && ( )}
+ {isDefined(eventDetails) + && isDefined(eventDetails.url) + && isDefined(eventDetails.url.report) + && ( + + {strings.eventMoreDetailsLink} + + )} ); } diff --git a/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/i18n.json b/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/i18n.json index 08af8f59c..934b05122 100644 --- a/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/i18n.json +++ b/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/i18n.json @@ -24,6 +24,7 @@ "wfpExposed90": "Exposed (90km/h)", "wfpExposed120": "Exposed (120km/h)", "wfpFloodArea": "Flood Area", - "wfpFloodCropland": "Flood Cropland" + "wfpFloodCropland": "Flood Cropland", + "wfpChartLabel": "Windspeed over time" } } diff --git a/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/index.tsx b/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/index.tsx index 6f614f7f2..e7418a31d 100644 --- a/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/index.tsx +++ b/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/index.tsx @@ -10,9 +10,10 @@ import Link from '#components/Link'; import BlockLoading from '#components/BlockLoading'; import Container from '#components/Container'; import TextOutput from '#components/TextOutput'; +import Tooltip from '#components/Tooltip'; +import useTranslation from '#hooks/useTranslation'; import { getPercentage, maxSafe, roundSafe } from '#utils/common'; import { type RiskApiResponse } from '#utils/restRequest'; -import useTranslation from '#hooks/useTranslation'; import { isValidFeatureCollection, isValidPointFeature } from '#utils/domain/risk'; import { resolveToString } from '#utils/translation'; @@ -167,23 +168,33 @@ function EventDetails(props: Props) { {stormPoints && stormPoints.length > 0 && isDefined(maxWindSpeed) && ( /* TODO: use proper svg charts */
- {stormPoints.map( - (point) => ( -
- ), - )} +
+ {stormPoints.map( + (point) => ( +
+ +
+
+ ), + )} +
+
+ {strings.wfpChartLabel} +
)} {isDefined(eventDetails) diff --git a/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/styles.module.css b/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/styles.module.css index 07a8064c8..20c79cf7d 100644 --- a/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/styles.module.css +++ b/src/components/domain/RiskImminentEvents/WfpAdam/EventDetails/styles.module.css @@ -6,15 +6,42 @@ .wind-speed-chart { display: flex; - align-items: flex-end; - justify-content: space-around; - height: 10rem; + flex-direction: column; + gap: var(--go-ui-spacing-xs); - .bar { - border-top-left-radius: 2pt; - border-top-right-radius: 2pt; - background-color: var(--go-ui-color-primary-red); - width: 4pt; + .bar-list-container { + display: flex; + align-items: stretch; + height: 10rem; + + .bar-container { + display: flex; + align-items: flex-end; + flex-basis: 0; + flex-grow: 1; + justify-content: center; + transition: var(--go-ui-duration-transition-medium) background-color ease-in-out; + border-top-left-radius: 2pt; + border-top-right-radius: 2pt; + background-color: transparent; + + .bar { + background-color: var(--go-ui-color-primary-red); + width: 4pt; + height: 100%; + } + + &:hover { + background-color: var(--go-ui-color-background-hover); + } + } + } + + .chart-label { + text-align: center; + color: var(--go-ui-color-text-light); + font-size: var(--go-ui-font-size-sm); + font-weight: var(--go-ui-font-weight-medium); } } diff --git a/src/components/domain/RiskImminentEvents/index.tsx b/src/components/domain/RiskImminentEvents/index.tsx index 422d2e2f4..74ef3c893 100644 --- a/src/components/domain/RiskImminentEvents/index.tsx +++ b/src/components/domain/RiskImminentEvents/index.tsx @@ -27,13 +27,14 @@ import MeteoSwiss from './MeteoSwiss'; import i18n from './i18n.json'; import styles from './styles.module.css'; -type ActiveView = 'pdc' | 'wfpAdam' | 'gdacs' | 'meteoSwiss'; +export type ImminentEventSource = 'pdc' | 'wfpAdam' | 'gdacs' | 'meteoSwiss'; type HazardType = components<'read'>['schemas']['HazardTypeEnum']; type BaseProps = { className?: string; title: React.ReactNode; bbox: LngLatBoundsLike | undefined; + defaultSource?: ImminentEventSource; } type Props = BaseProps & ({ @@ -47,8 +48,12 @@ type Props = BaseProps & ({ }) function RiskImminentEvents(props: Props) { - const [activeView, setActiveView] = useState('pdc'); - const { className, ...otherProps } = props; + const { + className, + defaultSource = 'pdc', + ...otherProps + } = props; + const [activeView, setActiveView] = useState(defaultSource); const strings = useTranslation(i18n); @@ -75,7 +80,7 @@ function RiskImminentEvents(props: Props) { [activeView], ); - const handleRadioClick = useCallback((key: ActiveView) => { + const handleRadioClick = useCallback((key: ImminentEventSource) => { setActiveView(key); }, []); diff --git a/src/components/domain/RiskSeasonalMap/index.tsx b/src/components/domain/RiskSeasonalMap/index.tsx index b41156746..55f61df25 100644 --- a/src/components/domain/RiskSeasonalMap/index.tsx +++ b/src/components/domain/RiskSeasonalMap/index.tsx @@ -64,6 +64,8 @@ import { COLOR_DARK_GREY, } from '#utils/constants'; import BaseMap from '#components/domain/BaseMap'; +import Message from '#components/Message'; +import Tooltip from '#components/Tooltip'; import Filters, { type FilterValue } from './Filters'; import i18n from './i18n.json'; @@ -78,9 +80,91 @@ const defaultFilterValue: FilterValue = { includeCopingCapacity: false, }; -type BaseProps = { - className?: string; - bbox: LngLatBoundsLike | undefined; +interface TooltipContentProps { + selectedRiskMetric: RiskMetric, + valueListByHazard: { + value: number; + riskCategory: number; + hazard_type: HazardType; + hazard_type_display: string; + }[]; +} + +function TooltipContent(props: TooltipContentProps) { + const { + selectedRiskMetric, + valueListByHazard, + } = props; + + const strings = useTranslation(i18n); + const riskCategoryToLabelMap: Record = useMemo( + () => ({ + [CATEGORY_RISK_VERY_LOW]: strings.riskCategoryVeryLow, + [CATEGORY_RISK_LOW]: strings.riskCategoryLow, + [CATEGORY_RISK_MEDIUM]: strings.riskCategoryMedium, + [CATEGORY_RISK_HIGH]: strings.riskCategoryHigh, + [CATEGORY_RISK_VERY_HIGH]: strings.riskCategoryVeryHigh, + }), + [ + strings.riskCategoryVeryLow, + strings.riskCategoryLow, + strings.riskCategoryMedium, + strings.riskCategoryHigh, + strings.riskCategoryVeryHigh, + ], + ); + + const riskMetricLabelMap: Record = { + riskScore: strings.riskScoreOptionLabel, + displacement: strings.peopleAtRiskOptionLabel, + exposure: strings.peopleExposedOptionLabel, + }; + + return valueListByHazard.map( + ({ + hazard_type_display, + hazard_type, + riskCategory, + value, + }) => ( + +
+
+ )} + > + + {selectedRiskMetric !== 'riskScore' && ( + + )} +
+ ), + ); +} + +const RISK_LOW_COLOR = '#c7d3e0'; +const RISK_HIGH_COLOR = '#f5333f'; + +interface ClickedPoint { + feature: GeoJSON.Feature; + lngLat: mapboxgl.LngLatLike; } interface GeoJsonProps { @@ -96,6 +180,11 @@ interface GeoJsonProps { region_id: number; } +type BaseProps = { + className?: string; + bbox: LngLatBoundsLike | undefined; +} + type Props = BaseProps & ({ variant: 'global'; regionId?: never; @@ -104,14 +193,6 @@ type Props = BaseProps & ({ regionId: number; }); -interface ClickedPoint { - feature: GeoJSON.Feature; - lngLat: mapboxgl.LngLatLike; -} - -const RISK_LOW_COLOR = '#c7d3e0'; -const RISK_HIGH_COLOR = '#f5333f'; - function RiskSeasonalMap(props: Props) { const { className, @@ -164,8 +245,10 @@ function RiskSeasonalMap(props: Props) { apiType: 'risk', url: '/api/v1/risk-score/', query: variant === 'region' - ? { region: regionId } - : undefined, + ? { + region: regionId, + limit: 9999, + } : { limit: 9999 }, }); // NOTE: We get single element as array in response @@ -600,7 +683,7 @@ function RiskSeasonalMap(props: Props) { ]; if (isDefined(riskCategory) && isDefined(populationFactor)) { - riskCategory *= populationFactor; + riskCategory = Math.ceil(riskCategory * populationFactor); } } @@ -610,7 +693,7 @@ function RiskSeasonalMap(props: Props) { ]; if (isDefined(riskCategory) && isDefined(lccFactor)) { - riskCategory *= lccFactor; + riskCategory = Math.ceil(riskCategory * lccFactor); } } @@ -634,6 +717,9 @@ function RiskSeasonalMap(props: Props) { const maxRiskCategory = maxSafe( valueListByHazard.map(({ riskCategory }) => riskCategory), ); + const riskCategorySum = sumSafe( + valueListByHazard.map(({ riskCategory }) => riskCategory), + ); if ( isNotDefined(maxValue) @@ -658,6 +744,7 @@ function RiskSeasonalMap(props: Props) { totalValue, maxValue, riskCategory: maxRiskCategory, + riskCategorySum, valueListByHazard: normalizedValueListByHazard, country_details: firstItem.country_details, }; @@ -675,7 +762,7 @@ function RiskSeasonalMap(props: Props) { normalizedValue: item.totalValue / maxValue, maxValue, }), - ).sort((a, b) => compareNumber(a.totalValue, b.totalValue, -1)); + ).sort((a, b) => compareNumber(a.riskCategorySum, b.riskCategorySum, -1)); } if (filters.riskMetric === 'displacement') { @@ -697,39 +784,6 @@ function RiskSeasonalMap(props: Props) { ); return transformedData; - - /* - return transformedData?.map( - (item) => { - let newNormalizedValue = item.normalizedValue; - let newTotalValue = item.totalValue; - - if (filters.normalizeByPopulation - && isDefined(maxPopulation) - && maxPopulation > 0 - ) { - const population = mappings?.population[item.iso3] ?? 0; - newNormalizedValue *= (population / maxPopulation); - newTotalValue *= (population / maxPopulation); - } - - if (filters.includeCopingCapacity - && isDefined(maxLackOfCopingCapacity) - && maxLackOfCopingCapacity > 0 - ) { - const lcc = mappings?.lcc[item.iso3] ?? 0; - newNormalizedValue *= (lcc / maxLackOfCopingCapacity); - newTotalValue *= (lcc / maxLackOfCopingCapacity); - } - - return { - ...item, - totalValue: newTotalValue, - normalizedValue: newNormalizedValue, - }; - }, - ).sort((a, b) => compareNumber(a.totalValue, b.totalValue, -1)); - */ } return undefined; @@ -762,7 +816,7 @@ function RiskSeasonalMap(props: Props) { item.country_details.iso3.toUpperCase(), [ 'interpolate', - ['exponential', 1], + ['linear'], ['number', item.riskCategory], CATEGORY_RISK_VERY_LOW, COLOR_LIGHT_BLUE, @@ -785,23 +839,6 @@ function RiskSeasonalMap(props: Props) { [filteredData], ); - const riskCategoryToLabelMap: Record = useMemo( - () => ({ - [CATEGORY_RISK_VERY_LOW]: strings.riskCategoryVeryLow, - [CATEGORY_RISK_LOW]: strings.riskCategoryLow, - [CATEGORY_RISK_MEDIUM]: strings.riskCategoryMedium, - [CATEGORY_RISK_HIGH]: strings.riskCategoryHigh, - [CATEGORY_RISK_VERY_HIGH]: strings.riskCategoryVeryHigh, - }), - [ - strings.riskCategoryVeryLow, - strings.riskCategoryLow, - strings.riskCategoryMedium, - strings.riskCategoryHigh, - strings.riskCategoryVeryHigh, - ], - ); - const handleCountryClick = useCallback( (feature: mapboxgl.MapboxGeoJSONFeature, lngLat: mapboxgl.LngLat) => { setClickedPointProperties({ @@ -822,8 +859,8 @@ function RiskSeasonalMap(props: Props) { const riskPopupValue = useMemo(() => ( filteredData?.find( - (filter) => filter.iso3 - === clickedPointProperties?.feature.properties.iso3.toLowerCase(), + (filter) => filter.iso3 === clickedPointProperties + ?.feature.properties.iso3.toLowerCase(), ) ), [filteredData, clickedPointProperties]); @@ -932,20 +969,10 @@ function RiskSeasonalMap(props: Props) { contentViewType="vertical" childrenContainerClassName={styles.popupContent} > - {riskPopupValue.valueListByHazard.map((hazard) => ( - - - - ))} + )} @@ -959,6 +986,12 @@ function RiskSeasonalMap(props: Props) { contentViewType="vertical" > {dataPending && } + {!dataPending && (isNotDefined(filteredData) || filteredData?.length === 0) && ( + + )} {/* FIXME: use List */} {!dataPending && filteredData?.map( (dataItem) => { @@ -984,13 +1017,11 @@ function RiskSeasonalMap(props: Props) {
{dataItem.valueListByHazard.map( ({ - normalizedValue, - value, hazard_type, - hazard_type_display, + riskCategory, }) => { // eslint-disable-next-line max-len - const percentage = 100 * normalizedValue * dataItem.normalizedValue; + const percentage = (100 * riskCategory) / (5 * filters.hazardTypes.length); if (percentage < 1) { return null; @@ -999,7 +1030,6 @@ function RiskSeasonalMap(props: Props) { return (
+ + )} + />
); }, diff --git a/src/components/domain/RiskSeasonalMap/styles.module.css b/src/components/domain/RiskSeasonalMap/styles.module.css index 8f38203c4..99a601a47 100644 --- a/src/components/domain/RiskSeasonalMap/styles.module.css +++ b/src/components/domain/RiskSeasonalMap/styles.module.css @@ -105,19 +105,18 @@ } } } - .popup-content { - display: flex; - flex-direction: column; - gap: var(--go-ui-spacing-md); - - .popup-appeal { - gap: var(--go-ui-spacing-xs); +} - .popup-appeal-detail { - display: flex; - flex-direction: column; - font-size: var(--go-ui-font-size-sm); - } - } +.tooltip-hazard-indicator { + display: flex; + align-items: center; + width: 1rem; + height: 1.2rem; + + .color { + flex-shrink: 0; + border-radius: 0.3rem; + width: 0.6rem; + height: 0.6rem; } } diff --git a/src/components/printable/TextOutput/index.tsx b/src/components/printable/TextOutput/index.tsx index 7a620ebb7..c2a674bc5 100644 --- a/src/components/printable/TextOutput/index.tsx +++ b/src/components/printable/TextOutput/index.tsx @@ -3,6 +3,7 @@ import { _cs } from '@togglecorp/fujs'; import NumberOutput, { Props as NumberOutputProps } from '#components/NumberOutput'; import BooleanOutput, { Props as BooleanOutputProps } from '#components/BooleanOutput'; import DateOutput, { Props as DateOutputProps } from '#components/DateOutput'; +import { DEFAULT_PRINT_DATE_FORMAT } from '#utils/constants'; import styles from './styles.module.css'; @@ -59,7 +60,7 @@ function TextOutput(props: Props) { } = props; const { value: propValue } = props; - let valueComponent: React.ReactNode = propValue || invalidText; + let valueComponent: React.ReactNode = invalidText; if (otherProps.valueType === 'number') { valueComponent = ( @@ -75,6 +76,7 @@ function TextOutput(props: Props) { // eslint-disable-next-line react/jsx-props-no-spreading {...otherProps} invalidText={invalidText} + format={DEFAULT_PRINT_DATE_FORMAT} /> ); } else if (otherProps.valueType === 'boolean') { @@ -85,6 +87,8 @@ function TextOutput(props: Props) { invalidText={invalidText} /> ); + } else if (!(propValue instanceof Date)) { + valueComponent = propValue || invalidText; } return ( diff --git a/src/index.css b/src/index.css index 7c0a92230..6711b39d1 100644 --- a/src/index.css +++ b/src/index.css @@ -4,7 +4,8 @@ --go-ui-font-family-mono: SFMono-Regular, Menlo, Monaco, Consolas, monospace; --base-font-size: 0.875rem; - --font-multiplier: 1.5; + --go-ui-font-size-export: 0.6875rem; + --go-ui-export-page-margin: 10mm 10mm 16mm 10mm; --go-ui-font-size-2xs: calc(var(--base-font-size) * 0.625); --go-ui-font-size-xs: calc(var(--base-font-size) * 0.75); @@ -261,7 +262,7 @@ ul, ol, p { @media print { @page { size: portrait A4; - margin: 10mm; + margin: var(--go-ui-export-page-margin); } body { diff --git a/src/utils/common.ts b/src/utils/common.ts index 0abf2278d..77d99fea9 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -373,16 +373,10 @@ export function formatNumber( return newValue; } +export type DateLike = string | number | Date; + export function formatDate( - value: null | undefined, - format?: string, -): undefined; -export function formatDate( - value: Date | string | number, - format?: string, -): string | undefined; -export function formatDate( - value: Date | string | number | null | undefined, + value: DateLike | null | undefined, format = DEFAULT_DATE_FORMAT, ) { if (isNotDefined(value)) { @@ -668,3 +662,19 @@ export function addNumMonthsToDate( return dateSafe; } + +export function isValidDate( + value: T | null | undefined, +): value is T { + if (isNotDefined(value)) { + return false; + } + + const date = new Date(value); + + if (Number.isNaN(date.getTime())) { + return false; + } + + return true; +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 7933c5d4a..ae0eb298f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,6 +1,7 @@ import { type components } from '#generated/types'; -export const DEFAULT_DATE_FORMAT = 'dd-MM-yyyy'; +export const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd'; +export const DEFAULT_PRINT_DATE_FORMAT = 'dd-MM-yyyy'; // Alert @@ -92,6 +93,8 @@ export const DREF_STATUS_IN_PROGRESS = 0 satisfies DrefStatus; type TypeOfDrefEnum = components<'read'>['schemas']['TypeOfDrefEnum']; export const DREF_TYPE_IMMINENT = 0 satisfies TypeOfDrefEnum; export const DREF_TYPE_ASSESSMENT = 1 satisfies TypeOfDrefEnum; +export const DREF_TYPE_RESPONSE = 2 satisfies TypeOfDrefEnum; +export const DREF_TYPE_LOAN = 3 satisfies TypeOfDrefEnum; // Subscriptions type SubscriptionRecordTypeEnum = components<'read'>['schemas']['RtypeEnum']; diff --git a/src/utils/domain/dref.ts b/src/utils/domain/dref.ts new file mode 100644 index 000000000..e71a9c6f4 --- /dev/null +++ b/src/utils/domain/dref.ts @@ -0,0 +1,59 @@ +import { components } from '#generated/types'; + +type PlannedIntervention = components<'read'>['schemas']['PlannedIntervention']; +type PlannedInterventionTitle = NonNullable; +type IdentifiedNeeds = components<'read'>['schemas']['IdentifiedNeed']; +type IdentifiedNeedsTitle = NonNullable; +type NsActions = components<'read'>['schemas']['NationalSocietyAction']; + +export const plannedInterventionOrder: Record = { + shelter_housing_and_settlements: 1, + livelihoods_and_basic_needs: 2, + multi_purpose_cash: 3, + health: 4, + water_sanitation_and_hygiene: 5, + protection_gender_and_inclusion: 6, + education: 7, + migration_and_displacement: 8, + risk_reduction_climate_adaptation_and_recovery: 9, + community_engagement_and_accountability: 10, + environmental_sustainability: 11, + coordination_and_partnerships: 12, + secretariat_services: 13, + national_society_strengthening: 14, +}; + +export const identifiedNeedsAndGapsOrder: Record = { + shelter_housing_and_settlements: 1, + livelihoods_and_basic_needs: 2, + multi_purpose_cash_grants: 3, + health: 4, + water_sanitation_and_hygiene: 5, + protection_gender_and_inclusion: 6, + education: 7, + migration_and_displacement: 8, + risk_reduction_climate_adaptation_and_recovery: 9, + community_engagement_and_accountability: 10, + environment_sustainability: 11, +}; + +export const nsActionsOrder: Record = { + shelter_housing_and_settlements: 1, + livelihoods_and_basic_needs: 2, + multi_purpose_cash: 3, + health: 4, + water_sanitation_and_hygiene: 5, + protection_gender_and_inclusion: 6, + education: 7, + migration_and_displacement: 8, + risk_reduction_climate_adaptation_and_recovery: 9, + community_engagement_and_accountability: 10, + environment_sustainability: 11, + coordination: 12, + national_society_readiness: 13, + assessment: 14, + resource_mobilization: 15, + activation_of_contingency_plans: 16, + national_society_eoc: 17, + other: 18, +}; diff --git a/src/utils/domain/risk.test.ts b/src/utils/domain/risk.test.ts new file mode 100644 index 000000000..92397fad2 --- /dev/null +++ b/src/utils/domain/risk.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest'; + +import { CATEGORY_RISK_LOW, CATEGORY_RISK_VERY_LOW } from '#utils/constants'; + +import { riskScoreToCategory } from './risk.ts'; + +test('Risk score to category', () => { + expect( + riskScoreToCategory( + 0, + 'FL', + ), + ).toEqual(CATEGORY_RISK_VERY_LOW); + expect( + riskScoreToCategory( + 3, + 'FL', + ), + ).toEqual(CATEGORY_RISK_LOW); +}); diff --git a/src/utils/domain/risk.ts b/src/utils/domain/risk.ts index 2fef90818..d90f0b6a0 100644 --- a/src/utils/domain/risk.ts +++ b/src/utils/domain/risk.ts @@ -77,7 +77,7 @@ export interface RiskDataItem { annual_average?: number | null, } -export const monthNumberToNameMap: Record = { +const monthToKeyMap: Record = { 0: 'january', 1: 'february', 2: 'march', @@ -90,6 +90,10 @@ export const monthNumberToNameMap: Record = { 9: 'october', 10: 'november', 11: 'december', +}; + +export const monthNumberToNameMap: Record = { + ...monthToKeyMap, // FIXME: we should not have these different // class of data into same list 12: 'annual_average', @@ -100,8 +104,42 @@ export function getValueForSelectedMonths( riskDataItem: RiskDataItem | undefined, aggregationMode: 'sum' | 'max' = 'sum', ) { - if (isNotDefined(selectedMonths)) { - return riskDataItem?.annual_average ?? undefined; + let annualValue; + + if (aggregationMode === 'sum') { + annualValue = sumSafe([ + riskDataItem?.january, + riskDataItem?.february, + riskDataItem?.march, + riskDataItem?.april, + riskDataItem?.may, + riskDataItem?.june, + riskDataItem?.july, + riskDataItem?.august, + riskDataItem?.september, + riskDataItem?.october, + riskDataItem?.november, + riskDataItem?.december, + ]); + } else if (aggregationMode === 'max') { + annualValue = maxSafe([ + riskDataItem?.january, + riskDataItem?.february, + riskDataItem?.march, + riskDataItem?.april, + riskDataItem?.may, + riskDataItem?.june, + riskDataItem?.july, + riskDataItem?.august, + riskDataItem?.september, + riskDataItem?.october, + riskDataItem?.november, + riskDataItem?.december, + ]); + } + + if (isNotDefined(selectedMonths) || selectedMonths[12] === true) { + return riskDataItem?.annual_average ?? annualValue ?? undefined; } const monthKeys = Object.keys( @@ -269,7 +307,7 @@ export function riskScoreToCategory( score: number | undefined | null, hazardType: HazardType, ) { - if (isNotDefined(score) || score <= 0) { + if (isNotDefined(score) || score < 0) { return undefined; } diff --git a/src/views/AccountMyFormsDref/DrefTableActions/drefAllocationExport.ts b/src/views/AccountMyFormsDref/DrefTableActions/drefAllocationExport.ts index f55357ed7..6095c915b 100644 --- a/src/views/AccountMyFormsDref/DrefTableActions/drefAllocationExport.ts +++ b/src/views/AccountMyFormsDref/DrefTableActions/drefAllocationExport.ts @@ -60,13 +60,25 @@ export async function exportDrefAllocation(exportData: ExportData) { buffer, extension: 'png', }); - const worksheet = workbook.addWorksheet(`${name} DREF Allocation`, { + const worksheet = workbook.addWorksheet('DREF Allocation', { + properties: { + defaultRowHeight: 20, + }, pageSetup: { paperSize: 9, showGridLines: false, fitToPage: true, + margins: { + left: 0.25, + right: 0.25, + top: 0.25, + bottom: 0.25, + header: 1, + footer: 1, + }, }, }); + const borderStyles: Partial = { top: { style: 'thin' }, left: { style: 'thin' }, @@ -662,6 +674,10 @@ export async function exportDrefAllocation(exportData: ExportData) { worksheet.eachRow({ includeEmpty: false }, (row) => { row.eachCell({ includeEmpty: false }, (cell) => { cell.border = borderStyles; // eslint-disable-line no-param-reassign + if (!cell.font?.size) { + // eslint-disable-next-line no-param-reassign + cell.font = Object.assign(cell.font || {}, { size: 12 }); + } }); }); diff --git a/src/views/AccountMyFormsDref/DrefTableActions/index.tsx b/src/views/AccountMyFormsDref/DrefTableActions/index.tsx index 2723b2a16..6e742d4d4 100644 --- a/src/views/AccountMyFormsDref/DrefTableActions/index.tsx +++ b/src/views/AccountMyFormsDref/DrefTableActions/index.tsx @@ -24,7 +24,7 @@ import useBooleanState from '#hooks/useBooleanState'; import useTranslation from '#hooks/useTranslation'; import useAlert from '#hooks/useAlert'; import { useLazyRequest } from '#utils/restRequest'; -import { DREF_STATUS_IN_PROGRESS } from '#utils/constants'; +import { DREF_STATUS_IN_PROGRESS, DREF_TYPE_IMMINENT, DREF_TYPE_LOAN } from '#utils/constants'; import { exportDrefAllocation } from './drefAllocationExport'; import i18n from './i18n.json'; @@ -79,13 +79,15 @@ function DrefTableActions(props: Props) { ), onSuccess: (response) => { const exportData = { - allocationFor: response?.type_of_dref_display === 'Loan' ? 'Emergency Appeal' : 'DREF Operation', + // FIXME: use translations + allocationFor: response?.type_of_dref === DREF_TYPE_LOAN ? 'Emergency Appeal' : 'DREF Operation', appealManager: response?.ifrc_appeal_manager_name, projectManager: response?.ifrc_project_manager_name, affectedCountry: response?.country_details?.name, name: response?.title, disasterType: response?.disaster_type_details?.name, - responseType: response?.type_of_dref_display === 'Imminent' ? 'Imminent Crisis' : response?.type_of_onset_display, + // FIXME: use translations + responseType: response?.type_of_dref === DREF_TYPE_IMMINENT ? 'Imminent Crisis' : response?.type_of_onset_display, noOfPeopleTargeted: response?.num_assisted, nsRequestDate: response?.ns_request_date, disasterStartDate: response?.event_date, @@ -93,7 +95,8 @@ function DrefTableActions(props: Props) { allocationRequested: response?.amount_requested, previousAllocation: undefined, totalDREFAllocation: response?.amount_requested, - toBeAllocatedFrom: response?.type_of_dref_display === 'Imminent' ? 'Anticipatory Pillar' : 'Response Pillar', + // FIXME: use translations + toBeAllocatedFrom: response?.type_of_dref === DREF_TYPE_IMMINENT ? 'Anticipatory Pillar' : 'Response Pillar', focalPointName: response?.regional_focal_point_name, }; exportDrefAllocation(exportData); @@ -112,7 +115,7 @@ function DrefTableActions(props: Props) { ), onSuccess: (response) => { const exportData = { - allocationFor: 'DREF Operation', + allocationFor: response?.type_of_dref_display === 'Loan' ? 'Emergency Appeal' : 'DREF Operation', appealManager: response?.ifrc_appeal_manager_name, projectManager: response?.ifrc_project_manager_name, affectedCountry: response?.country_details?.name, @@ -246,6 +249,7 @@ function DrefTableActions(props: Props) { navigate( 'drefOperationalUpdateForm', { params: { opsUpdateId: response.id } }, + { state: { isNewOpsUpdate: true } }, ); }, onFailure: ({ diff --git a/src/views/CountryRiskWatch/PossibleEarlyActionTable/index.tsx b/src/views/CountryRiskWatch/PossibleEarlyActionTable/index.tsx index b4079b8e4..644faa503 100644 --- a/src/views/CountryRiskWatch/PossibleEarlyActionTable/index.tsx +++ b/src/views/CountryRiskWatch/PossibleEarlyActionTable/index.tsx @@ -143,6 +143,10 @@ function PossibleEarlyActionTable(props: Props) { }, }); + if (!filtered && possibleEarlyActionResponse?.count === 0) { + return null; + } + return (
diff --git a/src/views/CountryRiskWatch/index.tsx b/src/views/CountryRiskWatch/index.tsx index 6e2b7651e..1dacc7c2f 100644 --- a/src/views/CountryRiskWatch/index.tsx +++ b/src/views/CountryRiskWatch/index.tsx @@ -1,12 +1,13 @@ import { useMemo } from 'react'; import { useParams, useOutletContext } from 'react-router-dom'; -import { isDefined } from '@togglecorp/fujs'; +import { isDefined, isNotDefined, mapToList } from '@togglecorp/fujs'; import getBbox from '@turf/bbox'; import Container from '#components/Container'; import Link from '#components/Link'; -import RiskImminentEvents from '#components/domain/RiskImminentEvents'; +import RiskImminentEvents, { type ImminentEventSource } from '#components/domain/RiskImminentEvents'; import HistoricalDataChart from '#components/domain/HistoricalDataChart'; +import BlockLoading from '#components/BlockLoading'; import useTranslation from '#hooks/useTranslation'; import useInputState from '#hooks/useInputState'; import type { CountryOutletContext } from '#utils/outletContext'; @@ -43,6 +44,69 @@ export function Component() { }, }); + const { + pending: pendingImminentEventCounts, + response: imminentEventCountsResponse, + } = useRiskRequest({ + apiType: 'risk', + url: '/api/v1/country-imminent-counts/', + query: { + iso3: countryResponse?.iso3?.toLowerCase(), + }, + }); + + const hasImminentEvents = useMemo( + () => { + if (isNotDefined(imminentEventCountsResponse)) { + return false; + } + + const eventCounts = mapToList( + imminentEventCountsResponse, + (value) => value, + ).filter(isDefined).filter( + (value) => value > 0, + ); + + return eventCounts.length > 0; + }, + [imminentEventCountsResponse], + ); + + const defaultImminentEventSource = useMemo( + () => { + if (isNotDefined(imminentEventCountsResponse)) { + return undefined; + } + + const { + pdc, + adam, + gdacs, + meteoswiss, + } = imminentEventCountsResponse; + + if (isDefined(pdc) && pdc > 0) { + return 'pdc'; + } + + if (isDefined(adam) && adam > 0) { + return 'wfpAdam'; + } + + if (isDefined(gdacs) && gdacs > 0) { + return 'gdacs'; + } + + if (isDefined(meteoswiss) && meteoswiss > 0) { + return 'meteoSwiss'; + } + + return undefined; + }, + [imminentEventCountsResponse], + ); + // NOTE: we always get 1 child in the response const riskResponse = countryRiskResponse?.[0]; const bbox = useMemo( @@ -52,12 +116,16 @@ export function Component() { return (
- {countryResponse && isDefined(countryResponse.iso3) && ( + {pendingImminentEventCounts && ( + + )} + {hasImminentEvents && isDefined(countryResponse) && isDefined(countryResponse.iso3) && ( )} { + if (isNotDefined(drefResponse) || isNotDefined(drefResponse.planned_interventions)) { + return undefined; + } + + const { planned_interventions } = drefResponse; + + return planned_interventions.map( + (intervention) => { + if (isNotDefined(intervention.title)) { + return undefined; + } + return { ...intervention, title: intervention.title }; + }, + ).filter(isDefined).sort( + (a, b) => plannedInterventionOrder[a.title] - plannedInterventionOrder[b.title], + ); + }, + [drefResponse], + ); + + const needsIdentified = useMemo( + () => { + if (isNotDefined(drefResponse) || isNotDefined(drefResponse.needs_identified)) { + return undefined; + } + + const { needs_identified } = drefResponse; + + return needs_identified.map( + (need) => { + if (isNotDefined(need.title)) { + return undefined; + } + + return { + ...need, + title: need.title, + }; + }, + ).filter(isDefined).sort((a, b) => ( + identifiedNeedsAndGapsOrder[a.title] - identifiedNeedsAndGapsOrder[b.title] + )); + }, + [drefResponse], + ); + + const nsActions = useMemo( + () => { + if (isNotDefined(drefResponse) || isNotDefined(drefResponse.needs_identified)) { + return undefined; + } + + const { national_society_actions } = drefResponse; + + return national_society_actions?.map((nsAction) => { + if (isNotDefined(nsAction.title)) { + return undefined; + } + return { ...nsAction, title: nsAction.title }; + }).filter(isDefined).sort((a, b) => ( + nsActionsOrder[a.title] - nsActionsOrder[b.title] + )); + }, + [drefResponse], + ); + const eventDescriptionDefined = isTruthyString(drefResponse?.event_description?.trim()); const eventScopeDefined = drefResponse?.type_of_dref !== DREF_TYPE_ASSESSMENT && isTruthyString(drefResponse?.event_scope?.trim()); @@ -112,12 +186,12 @@ export function Component() { const lessonsLearnedDefined = isTruthyString(drefResponse?.lessons_learned?.trim()); const showPreviousOperations = drefResponse?.type_of_dref !== DREF_TYPE_ASSESSMENT && ( isDefined(drefResponse?.did_it_affect_same_area) - || isDefined(drefResponse?.did_it_affect_same_population) - || isDefined(drefResponse?.did_ns_respond) - || isDefined(drefResponse?.did_ns_request_fund) - || isTruthyString(drefResponse?.ns_request_text?.trim()) - || isTruthyString(drefResponse?.dref_recurrent_text?.trim()) - || lessonsLearnedDefined + || isDefined(drefResponse?.did_it_affect_same_population) + || isDefined(drefResponse?.did_ns_respond) + || isDefined(drefResponse?.did_ns_request_fund) + || isTruthyString(drefResponse?.ns_request_text?.trim()) + || isTruthyString(drefResponse?.dref_recurrent_text?.trim()) + || lessonsLearnedDefined ); const ifrcActionsDefined = isTruthyString(drefResponse?.ifrc?.trim()); @@ -126,7 +200,8 @@ export function Component() { const showNsAction = isDefined(drefResponse) && isDefined(drefResponse.national_society_actions) - && drefResponse.national_society_actions.length > 0; + && drefResponse.national_society_actions.length > 0 + && isDefined(nsActions); const icrcActionsDefined = isTruthyString(drefResponse?.icrc?.trim()); @@ -147,10 +222,16 @@ export function Component() { && isTruthyString(drefResponse?.identified_gaps?.trim()); const needsIdentifiedDefined = isDefined(drefResponse) && isDefined(drefResponse.needs_identified) - && drefResponse.needs_identified.length > 0; + && drefResponse.needs_identified.length > 0 + && isDefined(needsIdentified); + + const assessmentReportDefined = isDefined(drefResponse) + && isDefined(drefResponse.assessment_report_details) + && isDefined(drefResponse.assessment_report_details.file); + const showNeedsIdentifiedSection = isDefined(drefResponse) && drefResponse.type_of_dref !== DREF_TYPE_ASSESSMENT - && (identifiedGapsDefined || needsIdentifiedDefined); + && (identifiedGapsDefined || needsIdentifiedDefined || assessmentReportDefined); const operationObjectiveDefined = isTruthyString(drefResponse?.operation_objective?.trim()); const responseStrategyDefined = isTruthyString(drefResponse?.response_strategy?.trim()); @@ -158,17 +239,28 @@ export function Component() { const peopleAssistedDefined = isTruthyString(drefResponse?.people_assisted?.trim()); const selectionCriteriaDefined = isTruthyString(drefResponse?.selection_criteria?.trim()); - const showTargetingStrategySection = peopleAssistedDefined || selectionCriteriaDefined; + const targetingStrategySupportingDocumentDefined = isDefined( + drefResponse?.targeting_strategy_support_file_details, + ); + const showTargetingStrategySection = peopleAssistedDefined + || selectionCriteriaDefined + || targetingStrategySupportingDocumentDefined; const riskSecurityDefined = isDefined(drefResponse) && isDefined(drefResponse.risk_security) && drefResponse.risk_security.length > 0; const riskSecurityConcernDefined = isTruthyString(drefResponse?.risk_security_concern?.trim()); - const showRiskAndSecuritySection = riskSecurityDefined || riskSecurityConcernDefined; + const hasChildrenSafeguardingDefined = isDefined( + drefResponse?.has_child_safeguarding_risk_analysis_assessment, + ); + const showRiskAndSecuritySection = riskSecurityDefined + || riskSecurityConcernDefined + || hasChildrenSafeguardingDefined; const plannedInterventionDefined = isDefined(drefResponse) && isDefined(drefResponse.planned_interventions) - && drefResponse.planned_interventions.length > 0; + && drefResponse.planned_interventions.length > 0 + && isDefined(plannedInterventions); const humanResourceDefined = isTruthyString(drefResponse?.human_resource?.trim()); const surgePersonnelDeployedDefined = isTruthyString( @@ -185,6 +277,10 @@ export function Component() { || pmerDefined || communicationDefined; + const sourceInformationDefined = isDefined(drefResponse) + && isDefined(drefResponse.source_information) + && drefResponse.source_information.length > 0; + const showBudgetOverview = isTruthyString(drefResponse?.budget_file_details?.file); const nsContactText = [ @@ -227,7 +323,6 @@ export function Component() { || projectManagerContactDefined || focalPointContactDefined || mediaContactDefined; - return (
@@ -287,8 +382,8 @@ export function Component() { value={drefResponse?.disaster_category_display} valueClassName={_cs( isDefined(drefResponse) - && isDefined(drefResponse.disaster_category) - && colorMap[drefResponse.disaster_category], + && isDefined(drefResponse.disaster_category) + && colorMap[drefResponse.disaster_category], )} strongValue /> @@ -332,7 +427,6 @@ export function Component() { className={styles.metaItem} label={strings.operationStartDateLabel} value={drefResponse?.date_of_approval} - valueType="date" strongValue /> + {drefResponse?.disaster_category_analysis_details?.file && ( + + + {strings.crisisCategorySupportingDocumentLabel} + + + )} {showEventDescriptionSection && ( <>
@@ -392,6 +497,16 @@ export function Component() { /> )} + {isDefined(drefResponse?.end_date) && ( + + + + )} {eventDescriptionDefined && ( )} + {drefResponse?.supporting_document_details?.file && ( + + + {strings.drefApplicationSupportingDocumentation} + + + )} + {sourceInformationDefined && ( + +
+ {strings.sourceInformationSourceNameTitle} +
+
+ {strings.sourceInformationSourceLinkTitle} +
+ {drefResponse?.source_information?.map( + (source, index) => ( + + +
+ {`${index + 1}. ${source.source_name}`} +
+
+ + + {source?.source_link} + + +
+ ), + )} + +
+ )} )} {showPreviousOperations && ( @@ -490,23 +651,39 @@ export function Component() { )} {showNsAction && ( - - {drefResponse?.national_society_actions?.map( - (nsAction) => ( - + + {strings.currentNationalSocietyActionsHeading} + + {drefResponse?.ns_respond_date && ( + + - ), + )} - + + {nsActions?.map( + (nsAction) => ( + + ), + )} + + )} {showMovementPartnersActionsSection && ( {strings.needsIdentifiedSectionHeading} - {needsIdentifiedDefined && drefResponse?.needs_identified?.map( + {needsIdentifiedDefined && needsIdentified?.map( (identifiedNeed) => ( @@ -614,6 +791,17 @@ export function Component() { )} + {assessmentReportDefined && ( + + + {strings.drefAssessmentReportLink} + + + )} )} {showOperationStrategySection && ( @@ -664,6 +852,17 @@ export function Component() { )} + {targetingStrategySupportingDocumentDefined && ( + + + {strings.targetingStrategySupportingDocument} + + + )} )} )} + {hasChildrenSafeguardingDefined && ( + + + + )} )} {plannedInterventionDefined && ( @@ -784,67 +994,72 @@ export function Component() { {strings.plannedInterventionSectionHeading} - {drefResponse?.planned_interventions?.map( - (plannedIntervention) => ( - - - - {plannedIntervention.title_display} - - - - - - -
- {strings.indicatorTitleLabel} -
-
- {strings.indicatorTargetLabel} -
- {plannedIntervention.indicators?.map( - (indicator) => ( - - ), - )} -
- - - {plannedIntervention.description} - - -
- ), - )} + {plannedInterventions?.map((plannedIntervention) => ( + + + + {plannedIntervention.title_display} + + + + + + + +
+ {strings.indicatorTitleLabel} +
+
+ {strings.indicatorTargetLabel} +
+ {plannedIntervention.indicators?.map( + (indicator) => ( + + ), + )} +
+ + + {plannedIntervention.description} + + +
+ ))} )} {showAboutSupportServicesSection && ( diff --git a/src/views/DrefApplicationExport/styles.module.css b/src/views/DrefApplicationExport/styles.module.css index ef612438b..b5d8d81ad 100644 --- a/src/views/DrefApplicationExport/styles.module.css +++ b/src/views/DrefApplicationExport/styles.module.css @@ -2,12 +2,12 @@ --pdf-element-bg: var(--go-ui-color-background); font-family: 'Open Sans', sans-serif; - font-size: var(--go-ui-font-size-sm); + font-size: var(--go-ui-font-size-export); @media screen { margin: var(--go-ui-spacing-xl) auto; background-color: var(--go-ui-color-foreground); - padding: 10mm; + padding: var(--go-ui-export-page-margin); width: 210mm; min-height: 297mm; } @@ -90,6 +90,7 @@ .lessons-learned { grid-column: span 2; background-color: var(--pdf-element-bg); + padding: var(--go-ui-spacing-sm); gap: var(--go-ui-spacing-sm); } @@ -152,18 +153,28 @@ } } - .risk-list { + .risk-list, + .source-information-list { display: grid; grid-template-columns: 1fr 1fr; grid-gap: var(--go-ui-width-separator-md); .risk, + .name, + .link, .mitigation { background-color: var(--pdf-element-bg); padding: var(--go-ui-spacing-xs); } + .name-list { + display: flex; + gap: var(--go-ui-spacing-sm); + } + .risk-title, + .name-title, + .link-title, .mitigation-title { background-color: var(--pdf-element-bg); padding: var(--go-ui-spacing-xs); @@ -218,4 +229,3 @@ } } } - diff --git a/src/views/DrefApplicationForm/EventDetail/SourceInformationInput/i18n.json b/src/views/DrefApplicationForm/EventDetail/SourceInformationInput/i18n.json new file mode 100644 index 000000000..5094a51e5 --- /dev/null +++ b/src/views/DrefApplicationForm/EventDetail/SourceInformationInput/i18n.json @@ -0,0 +1,8 @@ +{ + "namespace": "drefApplicationForm", + "strings": { + "sourceInformationNameLabel": "Sources of information (optional) name", + "sourceInformationLinkLabel": "Sources of information (optional) link", + "sourceInformationDeleteButton": "Delete Source Information" + } +} diff --git a/src/views/DrefApplicationForm/EventDetail/SourceInformationInput/index.tsx b/src/views/DrefApplicationForm/EventDetail/SourceInformationInput/index.tsx new file mode 100644 index 000000000..1243a4f22 --- /dev/null +++ b/src/views/DrefApplicationForm/EventDetail/SourceInformationInput/index.tsx @@ -0,0 +1,89 @@ +import { + type ArrayError, + useFormObject, + getErrorObject, + type SetValueArg, +} from '@togglecorp/toggle-form'; +import { DeleteBinTwoLineIcon } from '@ifrc-go/icons'; +import { randomString } from '@togglecorp/fujs'; + +import Button from '#components/Button'; +import NonFieldError from '#components/NonFieldError'; +import TextInput from '#components/TextInput'; +import useTranslation from '#hooks/useTranslation'; + +import { type PartialDref } from '../../schema'; +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type SourceInformationFormFields = NonNullable[number]; + +interface Props { + value: SourceInformationFormFields; + error: ArrayError | undefined; + onChange: (value: SetValueArg, index: number) => void; + onRemove: (index: number) => void; + index: number; + disabled?: boolean; +} + +function SourceInformationInput(props: Props) { + const { + error: errorFromProps, + onChange, + value, + index, + onRemove, + disabled, + } = props; + + const strings = useTranslation(i18n); + + const onFieldChange = useFormObject( + index, + onChange, + () => ({ + client_id: randomString(), + }), + ); + + const error = (value && value.client_id && errorFromProps) + ? getErrorObject(errorFromProps?.[value.client_id]) + : undefined; + + return ( +
+ + + + +
+ ); +} + +export default SourceInformationInput; diff --git a/src/views/DrefApplicationForm/EventDetail/SourceInformationInput/styles.module.css b/src/views/DrefApplicationForm/EventDetail/SourceInformationInput/styles.module.css new file mode 100644 index 000000000..4a88db49a --- /dev/null +++ b/src/views/DrefApplicationForm/EventDetail/SourceInformationInput/styles.module.css @@ -0,0 +1,14 @@ +.source-information-input { + display: flex; + align-items: flex-start; + gap: var(--go-ui-spacing-md); + + .input { + flex-basis: 0; + flex-grow: 1; + } + + .remove-button { + flex-shrink: 0; + } +} diff --git a/src/views/DrefApplicationForm/EventDetail/i18n.json b/src/views/DrefApplicationForm/EventDetail/i18n.json index d781e7935..2868bfe0f 100644 --- a/src/views/DrefApplicationForm/EventDetail/i18n.json +++ b/src/views/DrefApplicationForm/EventDetail/i18n.json @@ -1,11 +1,23 @@ { "namespace": "drefApplicationForm", "strings": { - "drefFormAffectedthePopulationTitle": "Did it affect the same population groups?", + "drefFormAffectedThePopulationTitle": "Did it affect the same population groups?", "drefFormAffectSameArea": "Has a similar event affected the same area(s) in the last 3 years?", "drefFormApproximateDateOfImpact": "Approximate date of impact", "drefFormDescriptionEvent": "Description of the Event", "drefFormEventDate": "Date of the Event", + "numericDetailsSectionTitle": "Numeric Details", + "drefFormPeopleAffectedDescriptionSlowSudden": "People Affected include all those whose lives and livelihoods have been impacted as a direct result of the shock or stress.", + "drefFormClickEmergencyResponseFramework": "Click to view Emergency Response Framework", + "drefFormEstimatedPeopleInNeed": "Estimated people in need (Optional)", + "drefFormPeopleTargetedDescription": "Include all those whose the National Society is aiming or planning to assist", + "drefFormPeopleInNeed": "People in need (Optional)", + "drefFormPeopleTargeted": "Number of people targeted", + "drefFormPeopleAffected": "Total affected population", + "drefFormRiskPeopleLabel": "Total population at risk", + "drefFormPeopleInNeedDescriptionImminent": "Include all those whose physical security, basic rights, dignity, living conditions or livelihoods are threatened or have been disrupted, and whose current level of access to basic services, goods and social protection will be inadequate to re-establish normal living conditions without additional assistance", + "drefFormPeopleInNeedDescriptionSlowSudden": "People in Need (PIN) are those members whose physical security, basic rights, dignity, living conditions or livelihoods are threatened or have been disrupted, and whose current level of access to basic services, goods and social protection is inadequate to re-establish normal living conditions without additional assistance.", + "drefFormPeopleAffectedDescriptionImminent": "Includes all those whose lives and livelihoods are at risk as a direct result of the shock or stress.", "drefFormImminentDisaster": "Provide any updates in the situation since the field report and explain what is expected to happen.", "drefFormLessonsLearnedDescription": "Specify how the lessons learnt from these previous operations are being used to mitigate similar challenges in the current operation", "drefFormLessonsLearnedTitle": "Lessons learned", @@ -26,7 +38,11 @@ "drefFormUploadSupportingDocumentButtonLabel": "Upload document", "drefFormUploadSupportingDocumentDescription": "Here the National Society can upload documentation that substantiates their decision to launch this operation now. For example: seasonal outlooks, forecast information, reports from credible sources...etc.", "drefFormWhatWhereWhen": "What happened, where and when?", - "drefOperationalLearningPlatformLabel": "To access lessons from previous operations, please visit the DREF Operational Learning Platform", - "drefFormSelectImages": "Select images" + "drefOperationalLearningPlatformLabel": "To access the Operational Learning Dashboard and check learnings for the country, {clickHereLink}", + "clickHereLinkLabel": "Click Here", + "drefFormSelectImages": "Select images", + "drefFormSourceInformationAddButton": "Add New Source Information", + "drefFormSourceInformationTitle": "Source Information", + "drefFormSourceInformationDescription": "Add the links and the name of the sources, the name will be shown in the export, as an hyperlink." } } diff --git a/src/views/DrefApplicationForm/EventDetail/index.tsx b/src/views/DrefApplicationForm/EventDetail/index.tsx index d01bc3491..d541a18dd 100644 --- a/src/views/DrefApplicationForm/EventDetail/index.tsx +++ b/src/views/DrefApplicationForm/EventDetail/index.tsx @@ -1,20 +1,27 @@ +import { useCallback } from 'react'; import { type Error, type EntriesAsList, getErrorObject, + useFormArray, } from '@togglecorp/toggle-form'; +import { WikiHelpSectionLineIcon } from '@ifrc-go/icons'; +import { randomString } from '@togglecorp/fujs'; -import { resolveUrl } from '#utils/resolveUrl'; import Container from '#components/Container'; import InputSection from '#components/InputSection'; import TextInput from '#components/TextInput'; import BooleanInput from '#components/BooleanInput'; import TextArea from '#components/TextArea'; import DateInput from '#components/DateInput'; -import useTranslation from '#hooks/useTranslation'; +import NonFieldError from '#components/NonFieldError'; +import Button from '#components/Button'; import GoSingleFileInput from '#components/domain/GoSingleFileInput'; -import Link from '#components/Link'; +import Link, { useLink } from '#components/Link'; import MultiImageWithCaptionInput from '#components/domain/MultiImageWithCaptionInput'; +import NumberInput from '#components/NumberInput'; +import useTranslation from '#hooks/useTranslation'; +import { resolveToComponent } from '#utils/translation'; import { TYPE_IMMINENT, @@ -24,10 +31,13 @@ import { } from '../common'; import { type PartialDref } from '../schema'; +import SourceInformationInput from './SourceInformationInput'; import i18n from './i18n.json'; import styles from './styles.module.css'; type Value = PartialDref; +type SourceInformationFormFields = NonNullable[number]; + interface Props { value: Value; setFieldValue: (...entries: EntriesAsList) => void; @@ -40,6 +50,11 @@ interface Props { function EventDetail(props: Props) { const strings = useTranslation(i18n); + const totalPopulationRiskImminentLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; + const totalPeopleAffectedSlowSuddenLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; + const peopleTargetedLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; + const peopleInNeedLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; + const { error: formError, setFieldValue, @@ -51,7 +66,69 @@ function EventDetail(props: Props) { const error = getErrorObject(formError); - const operationalLearningPlatformUrl = resolveUrl(window.location.origin, 'preparedness#operational-learning'); + const { + setValue: onSourceInformationChange, + removeValue: onSourceInformationRemove, + } = useFormArray<'source_information', SourceInformationFormFields>( + 'source_information', + setFieldValue, + ); + + const handleSourceInformationAdd = useCallback(() => { + const newSourceInformationItem: SourceInformationFormFields = { + client_id: randomString(), + }; + + setFieldValue( + (oldValue: SourceInformationFormFields[] | undefined) => ( + [...(oldValue ?? []), newSourceInformationItem] + ), + 'source_information' as const, + ); + }, [setFieldValue]); + + const operationalLearningUrl = useLink({ + to: 'preparednessGlobalOperational', + external: false, + }); + + const handleDidItAffectSafeAreaChange = useCallback( + (newValue: boolean | undefined) => { + setFieldValue(newValue, 'did_it_affect_same_area'); + setFieldValue(undefined, 'did_it_affect_same_population'); + setFieldValue(undefined, 'did_ns_respond'); + setFieldValue(undefined, 'did_ns_request_fund'); + setFieldValue(undefined, 'ns_request_text'); + }, + [setFieldValue], + ); + + const handleDidItAffectSamePopulationChange = useCallback( + (newValue: boolean | undefined) => { + setFieldValue(newValue, 'did_it_affect_same_population'); + setFieldValue(undefined, 'did_ns_respond'); + setFieldValue(undefined, 'did_ns_request_fund'); + setFieldValue(undefined, 'ns_request_text'); + }, + [setFieldValue], + ); + + const handleDidNsRespondChange = useCallback( + (newValue: boolean | undefined) => { + setFieldValue(newValue, 'did_ns_respond'); + setFieldValue(undefined, 'did_ns_request_fund'); + setFieldValue(undefined, 'ns_request_text'); + }, + [setFieldValue], + ); + + const handleDidNsRequestFundChange = useCallback( + (newValue: boolean | undefined) => { + setFieldValue(newValue, 'did_ns_request_fund'); + setFieldValue(undefined, 'ns_request_text'); + }, + [setFieldValue], + ); return (
@@ -60,12 +137,21 @@ function EventDetail(props: Props) { heading={strings.drefFormPreviousOperations} className={styles.previousOperations} headerDescription={( - - {strings.drefOperationalLearningPlatformLabel} - + resolveToComponent( + strings.drefOperationalLearningPlatformLabel, + { + clickHereLink: ( + + {strings.clickHereLinkLabel} + + ), + }, + ) )} > - - - - - - - - - + {value.did_it_affect_same_area && ( + + + + )} + {value.did_it_affect_same_population && ( + + + + )} + {value.did_ns_respond && ( + + + + )} {value.did_ns_request_fund && ( )} + + + {/* FIXME: use string template */} + {strings.drefFormRiskPeopleLabel} + + + + + ) : ( + <> + {/* FIXME: use string template */} + {strings.drefFormPeopleAffected} + + + + + )} + value={value?.num_affected} + onChange={setFieldValue} + error={error?.num_affected} + hint={( + value?.type_of_dref === TYPE_IMMINENT + ? strings.drefFormPeopleAffectedDescriptionImminent + : strings.drefFormPeopleAffectedDescriptionSlowSudden + )} + disabled={disabled} + /> + {value?.type_of_dref !== TYPE_LOAN && ( + + {/* FIXME: use string template */} + { + value?.type_of_dref === TYPE_IMMINENT + ? strings.drefFormEstimatedPeopleInNeed + : strings.drefFormPeopleInNeed + } + + + + + )} + name="people_in_need" + value={value?.people_in_need} + onChange={setFieldValue} + error={error?.people_in_need} + hint={( + value?.type_of_dref === TYPE_IMMINENT + ? strings.drefFormPeopleInNeedDescriptionImminent + : strings.drefFormPeopleInNeedDescriptionSlowSudden + )} + disabled={disabled} + /> + )} + + {/* FIXME: use string template */} + {strings.drefFormPeopleTargeted} + + + + + )} + name="num_assisted" + value={value?.num_assisted} + onChange={setFieldValue} + error={error?.num_assisted} + hint={strings.drefFormPeopleTargetedDescription} + disabled={disabled} + /> + {/* FIXME: use grid to fix the empty div issue */} + {/* NOTE: Empty div to preserve the layout */} +
+ {value.type_of_dref !== TYPE_LOAN && ( )} - {value.type_of_dref !== TYPE_LOAN && ( - - - - )} {value.type_of_dref !== TYPE_ASSESSMENT && value.type_of_dref !== TYPE_LOAN && ( )} + {value.type_of_dref !== TYPE_LOAN && ( + <> + + + {value.source_information?.map((source, index) => ( + + ))} +
+ +
+
+ + + + + )}
); diff --git a/src/views/DrefApplicationForm/Operation/i18n.json b/src/views/DrefApplicationForm/Operation/i18n.json index 57cdb0ce4..a57d49568 100644 --- a/src/views/DrefApplicationForm/Operation/i18n.json +++ b/src/views/DrefApplicationForm/Operation/i18n.json @@ -18,6 +18,7 @@ "drefFormRiskSecurityPotentialRisk": "Please indicate about potential operational risk for this operations and mitigation actions", "drefFormRiskSecurityPotentialRiskDescription": "Please consider any possible challenges to conduct the assessment, any possible limitations in getting the information required and overall risk to the implementation", "drefFormRiskSecuritySafetyConcern": "Please indicate any security and safety concerns for this operation", + "drefFormRiskSecurityHasChildRiskCompleted": "Has the child safeguarding risk analysis assessment been completed?", "drefFormSelectionCriteria": "Explain the selection criteria for the targeted population", "drefFormSelectionCriteriaDescription": "Explain the rational and logic behind which groups are being targeted and why and address vulnerable groups", "drefFormSupportServices": "About Support Services", @@ -49,6 +50,8 @@ "drefFormLogisticCapacityOfNsDescription": "Will it be for replenishment or for distribution? If for distribution, how long is the tendering expected to take? For Cash and Voucher Assistance, what is the status of the Financial Service Provider?", "drefFormMen": "Men", "drefFormTotalTargeted": "Total targeted population is different from that in Operation Overview", - "drefFormTotalTargetedPopulation": "Total targeted population is not equal to sum of other population fields" + "drefFormTotalTargetedPopulation": "Total targeted population is not equal to sum of other population fields", + "drefFormUploadTargetingSupportingDocument": "Upload any additional support document (Optional)", + "drefFormUploadTargetingDocumentButtonLabel": "Upload document" } } diff --git a/src/views/DrefApplicationForm/Operation/index.tsx b/src/views/DrefApplicationForm/Operation/index.tsx index 3685615bf..be7c76a28 100644 --- a/src/views/DrefApplicationForm/Operation/index.tsx +++ b/src/views/DrefApplicationForm/Operation/index.tsx @@ -263,6 +263,22 @@ function Operation(props: Props) { disabled={disabled} />
+ + + {strings.drefFormUploadTargetingDocumentButtonLabel} + + + + + a.organization === 'PNS')?.summary; const ifrc = value?.ifrc ?? fieldReportResponse.actions_taken?.find((a) => a.organization === 'FDRN')?.summary; - const icrc = value?.icrc - ?? fieldReportResponse.actions_taken?.find((a) => a.organization === 'NTLS')?.summary; let { national_society_contact_name, @@ -210,7 +212,7 @@ function CopyFieldReportSection(props: Props) { setFieldValue(num_affected, 'num_affected'); setFieldValue(partner_national_society, 'partner_national_society'); setFieldValue(ifrc, 'ifrc'); - setFieldValue(icrc, 'icrc'); + setFieldValue(government_assistance, 'government_requested_assistance'); // set field_report_option and districts diff --git a/src/views/DrefApplicationForm/Overview/i18n.json b/src/views/DrefApplicationForm/Overview/i18n.json index acf05bfd4..be2a76710 100644 --- a/src/views/DrefApplicationForm/Overview/i18n.json +++ b/src/views/DrefApplicationForm/Overview/i18n.json @@ -22,17 +22,6 @@ "drefFormAddRegion": "Region/Province", "drefFormTitle": "DREF Title", "drefFormGenerateTitle": "Generate title", - "numericDetailsSectionTitle": "Numeric Details", - "drefFormRiskPeopleLabel": "Total population at risk", - "drefFormPeopleAffected": "Total affected population", - "drefFormPeopleAffectedDescriptionImminent": "Includes all those whose lives and livelihoods are at risk as a direct result of the shock or stress.", - "drefFormPeopleAffectedDescriptionSlowSudden": "People Affected include all those whose lives and livelihoods have been impacted as a direct result of the shock or stress.", - "drefFormEstimatedPeopleInNeed": "Estimated people in need (Optional)", - "drefFormPeopleInNeed": "People in need (Optional)", - "drefFormPeopleInNeedDescriptionImminent": "Include all those whose physical security, basic rights, dignity, living conditions or livelihoods are threatened or have been disrupted, and whose current level of access to basic services, goods and social protection will be inadequate to re-establish normal living conditions without additional assistance", - "drefFormPeopleInNeedDescriptionSlowSudden": "People in Need (PIN) are those members whose physical security, basic rights, dignity, living conditions or livelihoods are threatened or have been disrupted, and whose current level of access to basic services, goods and social protection is inadequate to re-establish normal living conditions without additional assistance.", - "drefFormPeopleTargeted": "Number of people targeted", - "drefFormPeopleTargetedDescription": "Include all those whose the National Society is aiming or planning to assist", "drefFormRequestAmount": "Requested Amount in CHF", "drefFormEmergencyAppealPlanned": "Emergency appeal planned", "drefFormUploadMap": "Upload map", @@ -41,7 +30,9 @@ "drefFormUploadCoverImage": "Cover image", "drefFormUploadCoverImageDescription": "Upload a image for the cover page of the publicly published DREF application.", "drefFormDrefTypeTitle": "DREF Type", - "drefFormClickEmergencyResponseFramework": "Click to view Emergency Response Framework", - "userListEmptyMessage": "The DREF Application is not shared with anyone." + "drefFormClickEmergencyResponseFrameworkLabel": "Click to view Emergency Response Framework", + "userListEmptyMessage": "The DREF Application is not shared with anyone.", + "drefFormUploadCrisisDocument": "If available please upload Crisis categorization Analysis", + "drefFormUploadDocumentButtonLabel": "Upload document" } } diff --git a/src/views/DrefApplicationForm/Overview/index.tsx b/src/views/DrefApplicationForm/Overview/index.tsx index 196633c68..2b1a96bb5 100644 --- a/src/views/DrefApplicationForm/Overview/index.tsx +++ b/src/views/DrefApplicationForm/Overview/index.tsx @@ -40,6 +40,7 @@ import DistrictSearchMultiSelectInput, { type DistrictItem, } from '#components/domain/DistrictSearchMultiSelectInput'; import UserItem from '#components/domain/DrefShareModal/UserItem'; +import GoSingleFileInput from '#components/domain/GoSingleFileInput'; import useDisasterType from '#hooks/domain/useDisasterType'; import { @@ -55,10 +56,6 @@ import styles from './styles.module.css'; import i18n from './i18n.json'; const disasterCategoryLink = 'https://www.ifrc.org/sites/default/files/2021-07/IFRC%20Emergency%20Response%20Framework%20-%202017.pdf'; -const totalPopulationRiskImminentLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; -const totalPeopleAffectedSlowSuddenLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; -const peopleTargetedLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; -const peopleInNeedLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; type GlobalEnumsResponse = GoApiResponse<'/api/v2/global-enums/'>; type DrefTypeOption = NonNullable[number]; @@ -234,80 +231,100 @@ function Overview(props: Props) { disabled={disabled} /> - - + - - {(value?.disaster_type === DISASTER_FIRE - || value?.disaster_type === DISASTER_FLASH_FLOOD - || value?.disaster_type === DISASTER_FLOOD) - ? ( - - ) : ( -
- )} - - {value?.type_of_dref === TYPE_IMMINENT + numPreferredColumns={2} + > + + + {( + value?.disaster_type === DISASTER_FIRE + || value?.disaster_type === DISASTER_FLASH_FLOOD + || value?.disaster_type === DISASTER_FLOOD) ? ( + + ) : ( +
+ )} - ? strings.drefFormImminentDisasterCategoryLabel - : strings.drefFormDisasterCategoryLabel} - - - - - )} - options={drefDisasterCategoryOptions} - keySelector={disasterCategoryKeySelector} - labelSelector={stringValueSelector} - value={value?.disaster_category} - onChange={setFieldValue} - error={error?.disaster_category} - disabled={disabled} - /> - + + {/* FIXME: use string template */} + {value?.type_of_dref === TYPE_IMMINENT + + ? strings.drefFormImminentDisasterCategoryLabel + : strings.drefFormDisasterCategoryLabel} + + + + + )} + options={drefDisasterCategoryOptions} + keySelector={disasterCategoryKeySelector} + labelSelector={stringValueSelector} + value={value?.disaster_category} + onChange={setFieldValue} + error={error?.disaster_category} + disabled={disabled} + /> + + + + {strings.drefFormUploadDocumentButtonLabel} + + +
- - - {strings.drefFormRiskPeopleLabel} - - - - - ) : ( - <> - {strings.drefFormPeopleAffected} - - - - - )} - value={value?.num_affected} - onChange={setFieldValue} - error={error?.num_affected} - hint={( - value?.type_of_dref === TYPE_IMMINENT - ? strings.drefFormPeopleAffectedDescriptionImminent - : strings.drefFormPeopleAffectedDescriptionSlowSudden - )} - disabled={disabled} - /> - {value?.type_of_dref !== TYPE_LOAN && ( - - { - value?.type_of_dref === TYPE_IMMINENT - ? strings.drefFormEstimatedPeopleInNeed - : strings.drefFormPeopleInNeed - } - - - - - )} - name="people_in_need" - value={value?.people_in_need} - onChange={setFieldValue} - error={error?.people_in_need} - hint={( - value?.type_of_dref === TYPE_IMMINENT - ? strings.drefFormPeopleInNeedDescriptionImminent - : strings.drefFormPeopleInNeedDescriptionSlowSudden - )} - disabled={disabled} - /> - )} - - {strings.drefFormPeopleTargeted} - - - - - )} - name="num_assisted" - value={value?.num_assisted} - onChange={setFieldValue} - error={error?.num_assisted} - hint={strings.drefFormPeopleTargetedDescription} - disabled={disabled} - /> - {/* NOTE: Empty div to preserve the layout */} -
- ['schemas']['TypeOfDrefEnum']; -type TypeOfOnsetEnum = components<'read'>['schemas']['TypeOfOnsetEnum']; +type TypeOfOnsetEnum = components<'read'>['schemas']['TypeValidatedEnum']; // export const ONSET_SLOW = 1 satisfies TypeOfOnsetEnum; export const ONSET_SUDDEN = 2 satisfies TypeOfOnsetEnum; diff --git a/src/views/DrefApplicationForm/i18n.json b/src/views/DrefApplicationForm/i18n.json index 5dd841265..8e0748b48 100644 --- a/src/views/DrefApplicationForm/i18n.json +++ b/src/views/DrefApplicationForm/i18n.json @@ -13,7 +13,7 @@ "formTabEventDetailLabel": "Event Detail", "formTabActionsLabel": "Actions/Needs", "formTabOperationLabel": "Operation", - "formTabSubmissionLabel": "Submission/Contact", + "formTabSubmissionLabel": "Operational timeframes and contacts", "formLoadErrorTitle": "Failed to load DREF Application!", "formBackButtonLabel": "Back", "formImportFromDocument": "Import from Document", diff --git a/src/views/DrefApplicationForm/index.tsx b/src/views/DrefApplicationForm/index.tsx index eaa4c148c..1664ecd1d 100644 --- a/src/views/DrefApplicationForm/index.tsx +++ b/src/views/DrefApplicationForm/index.tsx @@ -209,6 +209,23 @@ export function Component() { } }); } + if ( + response.disaster_category_analysis_details + && response.disaster_category_analysis_details.file + ) { + newMap[ + response.disaster_category_analysis_details.id + ] = response.disaster_category_analysis_details.file; + } + if ( + response.targeting_strategy_support_file_details + && response.targeting_strategy_support_file_details.file + ) { + newMap[ + response.targeting_strategy_support_file_details.id + ] = response.targeting_strategy_support_file_details.file; + } + return newMap; }); }, @@ -236,9 +253,10 @@ export function Component() { event_map_file, cover_image_file, images_file, + source_information, + disaster_category_analysis_file, ...otherValues } = removeNull(response); - setValue({ ...otherValues, planned_interventions: planned_interventions?.map( @@ -247,6 +265,7 @@ export function Component() { indicators: intervention.indicators?.map(injectClientId), }), ), + source_information: source_information?.map(injectClientId), needs_identified: needs_identified?.map(injectClientId), national_society_actions: national_society_actions?.map(injectClientId), risk_security: risk_security?.map(injectClientId), @@ -257,6 +276,9 @@ export function Component() { ? injectClientId(cover_image_file) : undefined, images_file: images_file?.map(injectClientId), + disaster_category_analysis_file: isDefined(disaster_category_analysis_file) + ? injectClientId(disaster_category_analysis_file) + : undefined, }); setDistrictOptions(response.district_details); diff --git a/src/views/DrefApplicationForm/schema.ts b/src/views/DrefApplicationForm/schema.ts index ae871679c..a5c18397c 100644 --- a/src/views/DrefApplicationForm/schema.ts +++ b/src/views/DrefApplicationForm/schema.ts @@ -9,6 +9,7 @@ import { nullValue, lessThanOrEqualToCondition, emailCondition, + urlCondition, } from '@togglecorp/toggle-form'; import { isDefined } from '@togglecorp/fujs'; import { type DeepReplace } from '#utils/common'; @@ -56,11 +57,13 @@ type InterventionResponse = NonNullable[number]; type RiskSecurityResponse = NonNullable[number]; type ImagesFileResponse = NonNullable[number]; +type SourceInformationResponse = NonNullable[number]; type NeedIdentifiedFormFields = NeedIdentifiedResponse & { client_id: string }; type NsActionFormFields = NsActionResponse & { client_id: string; } type InterventionFormFields = InterventionResponse & { client_id: string }; type IndicatorFormFields = IndicatorResponse & { client_id: string }; +type SourceInformationFormFields = SourceInformationResponse & { client_id: string }; type RiskSecurityFormFields = RiskSecurityResponse & { client_id: string; }; type ImagesFileFormFields = ImagesFileResponse & { client_id: string }; @@ -80,31 +83,35 @@ type DrefFormFields = ( DeepReplace< DeepReplace< DeepReplace< - DrefRequestBody, - NeedIdentifiedResponse, - NeedIdentifiedFormFields + DeepReplace< + DrefRequestBody, + NeedIdentifiedResponse, + NeedIdentifiedFormFields + >, + NsActionResponse, + NsActionFormFields >, - NsActionResponse, - NsActionFormFields + InterventionResponse, + InterventionFormFields >, - InterventionResponse, - InterventionFormFields + IndicatorResponse, + IndicatorFormFields >, IndicatorResponse, IndicatorFormFields >, - IndicatorResponse, - IndicatorFormFields + RiskSecurityResponse, + RiskSecurityFormFields >, - RiskSecurityResponse, - RiskSecurityFormFields + ImagesFileResponse, + ImagesFileFormFields >, - ImagesFileResponse, - ImagesFileFormFields + EventMapFileResponse, + EventMapFileFormField >, - EventMapFileResponse, - EventMapFileFormField - > + SourceInformationResponse, + SourceInformationFormFields +> ); export type PartialDref = PartialForm< @@ -121,6 +128,7 @@ type ImageFileFields = ReturnType[number], PartialDref>['fields']>; type NeedsIdentifiedFields = ReturnType[number], PartialDref>['fields']>; type RiskSecurityFields = ReturnType[number], PartialDref>['fields']>; +type SourceInformationFields = ReturnType[number], PartialDref>['fields']>; type PlannedInterventionFields = ReturnType[number], PartialDref>['fields']>; type IndicatorFields = ReturnType[number]['indicators']>[number], PartialDref>['fields']>; @@ -133,19 +141,19 @@ const schema: DrefFormSchema = { disaster_type: {}, type_of_onset: { required: true }, disaster_category: {}, + disaster_category_analysis: {}, country: {}, district: { defaultValue: [] }, title: { required: true, requiredValidation: requiredStringCondition, }, - num_affected: { validations: [positiveIntegerCondition] }, - num_assisted: { validations: [positiveIntegerCondition] }, amount_requested: { validations: [positiveNumberCondition] }, field_report: {}, // This value is set from CopyFieldReportSection // EVENT DETAILS - + num_affected: { validations: [positiveIntegerCondition] }, + num_assisted: { validations: [positiveIntegerCondition] }, // none // ACTIONS @@ -153,6 +161,7 @@ const schema: DrefFormSchema = { // none // OPERATION + targeting_strategy_support_file: {}, // none @@ -181,8 +190,8 @@ const schema: DrefFormSchema = { regional_focal_point_phone_number: {}, }; + // Note: Section below include conditional form element only // OVERVIEW - formFields = addCondition( formFields, formValue, @@ -205,7 +214,6 @@ const schema: DrefFormSchema = { ); const overviewDrefTypeRelatedFields = [ - 'people_in_need', 'emergency_appeal_planned', 'event_map_file', 'cover_image_file', @@ -221,7 +229,6 @@ const schema: DrefFormSchema = { overviewDrefTypeRelatedFields, (val): OverviewDrefTypeRelatedFields => { const conditionalFields: OverviewDrefTypeRelatedFields = { - people_in_need: { forceValue: nullValue }, emergency_appeal_planned: { forceValue: nullValue }, event_map_file: { forceValue: nullValue }, // NOTE: check if this works cover_image_file: { forceValue: nullValue }, @@ -231,7 +238,6 @@ const schema: DrefFormSchema = { } return { ...conditionalFields, - people_in_need: { validations: [positiveIntegerCondition] }, emergency_appeal_planned: {}, event_map_file: { fields: (): EventMapFileFields => ({ @@ -254,6 +260,7 @@ const schema: DrefFormSchema = { // EVENT DETAILS const eventDetailDrefTypeRelatedFields = [ + 'people_in_need', 'did_it_affect_same_area', 'did_it_affect_same_population', 'did_ns_respond', @@ -266,11 +273,13 @@ const schema: DrefFormSchema = { 'event_date', 'event_description', 'images_file', + 'source_information', ] as const; type EventDetailDrefTypeRelatedFields = Pick< DrefFormSchemaFields, typeof eventDetailDrefTypeRelatedFields[number] >; + formFields = addCondition( formFields, formValue, @@ -278,12 +287,14 @@ const schema: DrefFormSchema = { eventDetailDrefTypeRelatedFields, (val): EventDetailDrefTypeRelatedFields => { let conditionalFields: EventDetailDrefTypeRelatedFields = { + people_in_need: { forceValue: nullValue }, did_it_affect_same_area: { forceValue: nullValue }, did_it_affect_same_population: { forceValue: nullValue }, did_ns_respond: { forceValue: nullValue }, did_ns_request_fund: { forceValue: nullValue }, lessons_learned: { forceValue: nullValue }, event_scope: { forceValue: nullValue }, + source_information: { forceValue: [] }, event_text: { forceValue: nullValue }, anticipatory_actions: { forceValue: nullValue }, supporting_document: { forceValue: nullValue }, @@ -304,6 +315,7 @@ const schema: DrefFormSchema = { did_ns_request_fund: {}, lessons_learned: {}, event_scope: {}, + people_in_need: { validations: [positiveIntegerCondition] }, }; } @@ -325,6 +337,22 @@ const schema: DrefFormSchema = { conditionalFields = { ...conditionalFields, event_description: {}, + source_information: { + keySelector: (source) => source.client_id, + member: () => ({ + fields: (): SourceInformationFields => ({ + client_id: {}, + source_name: { + required: true, + requiredValidation: requiredStringCondition, + }, + source_link: { + required: true, + validations: [urlCondition], + }, + }), + }), + }, images_file: { keySelector: (image_file) => image_file.client_id, member: () => ({ @@ -555,6 +583,7 @@ const schema: DrefFormSchema = { 'displaced_people', 'risk_security', 'risk_security_concern', + 'has_child_safeguarding_risk_analysis_assessment', 'budget_file', 'planned_interventions', 'human_resource', @@ -594,6 +623,7 @@ const schema: DrefFormSchema = { planned_interventions: { forceValue: [] }, human_resource: { forceValue: nullValue }, is_surge_personnel_deployed: { forceValue: nullValue }, + has_child_safeguarding_risk_analysis_assessment: { forceValue: nullValue }, }; if (val?.type_of_dref === TYPE_LOAN) { return conditionalFields; @@ -645,6 +675,7 @@ const schema: DrefFormSchema = { }), }, risk_security_concern: {}, + has_child_safeguarding_risk_analysis_assessment: {}, budget_file: {}, planned_interventions: { keySelector: (n) => n.client_id, diff --git a/src/views/DrefFinalReportExport/i18n.json b/src/views/DrefFinalReportExport/i18n.json index aaad44d31..8439d1892 100644 --- a/src/views/DrefFinalReportExport/i18n.json +++ b/src/views/DrefFinalReportExport/i18n.json @@ -21,7 +21,7 @@ "operationStartDateLabel": "Operation Start Date", "operationTimeframeLabel": "Total Operating Timeframe", "monthsSuffix": " months", - "operationEndDateLabel": "New Operational End Date", + "operationEndDateLabel": "Operational End Date", "additionalAllocationRequestedLabel": "Additional Allocation Requested", "targetedAreasLabel": "Targeted Areas", "eventDescriptionSectionHeading": "Description of the Event", @@ -46,7 +46,7 @@ "overallObjectiveHeading": "Overall objective of the operation", "operationStragegyHeading": "Operation strategy rationale", "targetingStrategySectionHeading": "Targeting Strategy", - "peopleAssistedHeading": "Who will be targeted through this operation?", + "peopleAssistedHeading": "Who was targeted by this operation?", "selectionCriteriaHeading": "Explain the selection criteria for the targeted population", "targetPopulationSectionHeading": "Total Targeted Population", "womenLabel": "Women", @@ -62,6 +62,7 @@ "riskLabel": "Risk", "mitigationLabel": "Mitigation action", "safetyConcernHeading": "Please indicate any security and safety concerns for this operation", + "hasChildRiskCompleted": "Has the child safeguarding risk analysis assessment been completed?", "interventionSectionHeading": "Implementation", "targetedPersonsLabel": "Targeted Persons", "assistedPersonsLabel": "Assisted Persons", diff --git a/src/views/DrefFinalReportExport/index.tsx b/src/views/DrefFinalReportExport/index.tsx index 793993a86..94b01e3f0 100644 --- a/src/views/DrefFinalReportExport/index.tsx +++ b/src/views/DrefFinalReportExport/index.tsx @@ -1,9 +1,10 @@ -import { Fragment, useState } from 'react'; +import { Fragment, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { _cs, isDefined, isFalsyString, + isNotDefined, isTruthyString, } from '@togglecorp/fujs'; @@ -25,6 +26,10 @@ import { DREF_TYPE_IMMINENT, DisasterCategory, } from '#utils/constants'; +import { + identifiedNeedsAndGapsOrder, + plannedInterventionOrder, +} from '#utils/domain/dref'; import ifrcLogo from '#assets/icons/ifrc-square.png'; @@ -96,6 +101,42 @@ export function Component() { }, }); + const filteredPlannedIntervention = useMemo( + () => drefResponse?.planned_interventions?.map((intervention) => { + if (isNotDefined(intervention.title)) { + return undefined; + } + return { ...intervention, title: intervention.title }; + }).filter(isDefined), + [drefResponse?.planned_interventions], + ); + + const filteredIdentifiedNeedsAndGaps = useMemo( + () => drefResponse?.needs_identified?.map((need) => { + if (isNotDefined(need.title)) { + return undefined; + } + return { ...need, title: need.title }; + }).filter(isDefined), + [drefResponse?.needs_identified], + ); + + const sortedPlannedInterventions = useMemo( + () => filteredPlannedIntervention?.sort( + // eslint-disable-next-line max-len + (a, b) => plannedInterventionOrder[a.title] - plannedInterventionOrder[b.title], + ), + [filteredPlannedIntervention], + ); + + const sortedIdentifiedNeedsAndGaps = useMemo( + () => filteredIdentifiedNeedsAndGaps?.sort( + // eslint-disable-next-line max-len + (a, b) => identifiedNeedsAndGapsOrder[a.title] - identifiedNeedsAndGapsOrder[b.title], + ), + [filteredIdentifiedNeedsAndGaps], + ); + const showMainDonorsSection = isTruthyString(drefResponse?.main_donors?.trim()); const eventDescriptionDefined = isTruthyString(drefResponse?.event_description?.trim()); const eventScopeDefined = drefResponse?.type_of_dref !== DREF_TYPE_ASSESSMENT @@ -149,7 +190,12 @@ export function Component() { && isDefined(drefResponse.risk_security) && drefResponse.risk_security.length > 0; const riskSecurityConcernDefined = isTruthyString(drefResponse?.risk_security_concern?.trim()); - const showRiskAndSecuritySection = riskSecurityDefined || riskSecurityConcernDefined; + const hasChildrenSafeguardingDefined = isDefined( + drefResponse?.has_child_safeguarding_risk_analysis_assessment, + ); + const showRiskAndSecuritySection = riskSecurityDefined + || riskSecurityConcernDefined + || hasChildrenSafeguardingDefined; const plannedInterventionDefined = isDefined(drefResponse) && isDefined(drefResponse.planned_interventions) @@ -483,7 +529,7 @@ export function Component() { {strings.needsIdentifiedSectionHeading} - {needsIdentifiedDefined && drefResponse?.needs_identified?.map( + {needsIdentifiedDefined && sortedIdentifiedNeedsAndGaps?.map( (identifiedNeed) => ( @@ -663,6 +709,17 @@ export function Component() { )} + {hasChildrenSafeguardingDefined && ( + + + + )} )} {plannedInterventionDefined && ( @@ -670,7 +727,7 @@ export function Component() { {strings.interventionSectionHeading} - {drefResponse?.planned_interventions?.map( + {sortedPlannedInterventions?.map( (plannedIntervention) => ( diff --git a/src/views/DrefFinalReportExport/styles.module.css b/src/views/DrefFinalReportExport/styles.module.css index 4bdae5e54..6e805e8d0 100644 --- a/src/views/DrefFinalReportExport/styles.module.css +++ b/src/views/DrefFinalReportExport/styles.module.css @@ -2,12 +2,12 @@ --pdf-element-bg: var(--go-ui-color-background); font-family: 'Open Sans', sans-serif; - font-size: var(--go-ui-font-size-sm); + font-size: var(--go-ui-font-size-export); @media screen { margin: var(--go-ui-spacing-xl) auto; background-color: var(--go-ui-color-foreground); - padding: 10mm; + padding: var(--go-ui-export-page-margin); width: 210mm; min-height: 297mm; } diff --git a/src/views/DrefFinalReportForm/EventDetail/i18n.json b/src/views/DrefFinalReportForm/EventDetail/i18n.json index 949d80842..7892a8fcf 100644 --- a/src/views/DrefFinalReportForm/EventDetail/i18n.json +++ b/src/views/DrefFinalReportForm/EventDetail/i18n.json @@ -11,6 +11,19 @@ "drefFormUploadPhotos": "Upload photos (e.g. impact of the events, NS in the field, assessments)", "drefFormUploadPhotosLimitation": "Add maximum 2 photos", "drefFormWhatWhereWhen": "What happened, where and when?", - "drefFinalReportFormSelectImages": "Select images" + "drefFinalReportFormSelectImages": "Select images", + "numericDetails": "Numeric Details", + "drefFormRiskPeopleLabel": "Total population at risk", + "drefFormClickEmergencyResponseFramework": "Click to view Emergency Response Framework", + "drefFormPeopleAffected": "Total affected population", + "drefFormPeopleAffectedDescriptionImminent": "Includes all those whose lives and livelihoods are at risk as a direct result of the shock or stress.", + "drefFormPeopleAffectedDescriptionSlowSudden": "People Affected include all those whose lives and livelihoods have been impacted as a direct result of the shock or stress.", + "drefFormEstimatedPeopleInNeed": "Estimated people in need (Optional)", + "drefFormPeopleInNeed": "People in need (Optional)", + "drefFormPeopleInNeedDescriptionImminent": "Include all those whose physical security, basic rights, dignity, living conditions or livelihoods are threatened or have been disrupted, and whose current level of access to basic services, goods and social protection will be inadequate to re-establish normal living conditions without additional assistance", + "finalReportPeopleTargeted": "Number of people targeted", + "drefFormPeopleTargetedDescription": "Include all those whose the National Society is aiming or planning to assist", + "drefFormPeopleTargeted": "Number of people targeted", + "drefFormPeopleInNeedDescriptionSlowSudden": "People in Need (PIN) are those members whose physical security, basic rights, dignity, living conditions or livelihoods are threatened or have been disrupted, and whose current level of access to basic services, goods and social protection is inadequate to re-establish normal living conditions without additional assistance." } } diff --git a/src/views/DrefFinalReportForm/EventDetail/index.tsx b/src/views/DrefFinalReportForm/EventDetail/index.tsx index a7e8a6da3..128202521 100644 --- a/src/views/DrefFinalReportForm/EventDetail/index.tsx +++ b/src/views/DrefFinalReportForm/EventDetail/index.tsx @@ -3,11 +3,14 @@ import { type EntriesAsList, getErrorObject, } from '@togglecorp/toggle-form'; +import { WikiHelpSectionLineIcon } from '@ifrc-go/icons'; import Container from '#components/Container'; import InputSection from '#components/InputSection'; import TextArea from '#components/TextArea'; import DateInput from '#components/DateInput'; +import NumberInput from '#components/NumberInput'; +import Link from '#components/Link'; import useTranslation from '#hooks/useTranslation'; import MultiImageWithCaptionInput from '#components/domain/MultiImageWithCaptionInput'; @@ -31,6 +34,11 @@ interface Props { disabled?: boolean; } +const totalPopulationRiskImminentLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; +const totalPeopleAffectedSlowSuddenLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; +const peopleTargetedLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; +const peopleInNeedLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; + function EventDetail(props: Props) { const strings = useTranslation(i18n); @@ -79,6 +87,117 @@ function EventDetail(props: Props) { /> )} + + + + {strings.drefFormRiskPeopleLabel} + + + + + ) : ( + <> + {strings.drefFormPeopleAffected} + + + + + )} + value={value?.number_of_people_affected} + onChange={setFieldValue} + error={error?.number_of_people_affected} + hint={( + value?.type_of_dref === TYPE_IMMINENT + ? strings.drefFormPeopleAffectedDescriptionImminent + : strings.drefFormPeopleAffectedDescriptionSlowSudden + )} + disabled={disabled} + /> + + { + value?.type_of_dref === TYPE_IMMINENT + ? strings.drefFormEstimatedPeopleInNeed + : strings.drefFormPeopleInNeed + } + + + + + )} + name="people_in_need" + value={value?.people_in_need} + onChange={setFieldValue} + error={error?.people_in_need} + hint={( + value?.type_of_dref === TYPE_IMMINENT + ? strings.drefFormPeopleInNeedDescriptionImminent + : strings.drefFormPeopleInNeedDescriptionSlowSudden + )} + disabled={disabled} + /> + + {strings.finalReportPeopleTargeted} + + + + + )} + name="number_of_people_targeted" + value={value.number_of_people_targeted} + onChange={setFieldValue} + error={error?.number_of_people_targeted} + hint={strings.drefFormPeopleTargetedDescription} + disabled={disabled} + /> + + {strings.drefFormPeopleTargeted} + + + + + )} + name="num_assisted" + value={value?.num_assisted} + onChange={setFieldValue} + error={error?.num_assisted} + hint={strings.drefFormPeopleTargetedDescription} + disabled={disabled} + /> + {/* NOTE: Empty div to preserve the layout */} +
+ + + + ; type DrefTypeOption = NonNullable[number]; @@ -258,7 +254,7 @@ function Overview(props: Props) { ? strings.drefFormImminentDisasterCategoryLabel : strings.drefFormDisasterCategoryLabel}
- - - {strings.drefFormRiskPeopleLabel} - - - - - ) : ( - <> - {strings.drefFormPeopleAffected} - - - - - )} - value={value?.number_of_people_affected} - onChange={setFieldValue} - error={error?.number_of_people_affected} - hint={( - value?.type_of_dref === TYPE_IMMINENT - ? strings.drefFormPeopleAffectedDescriptionImminent - : strings.drefFormPeopleAffectedDescriptionSlowSudden - )} - disabled={disabled} - /> - - { - value?.type_of_dref === TYPE_IMMINENT - ? strings.drefFormEstimatedPeopleInNeed - : strings.drefFormPeopleInNeed - } - - - - - )} - name="people_in_need" - value={value?.people_in_need} - onChange={setFieldValue} - error={error?.people_in_need} - hint={( - value?.type_of_dref === TYPE_IMMINENT - ? strings.drefFormPeopleInNeedDescriptionImminent - : strings.drefFormPeopleInNeedDescriptionSlowSudden - )} - disabled={disabled} - /> - - {strings.finalReportPeopleTargeted} - - - - - )} - name="number_of_people_targeted" - value={value.number_of_people_targeted} - onChange={setFieldValue} - error={error?.number_of_people_targeted} - hint={strings.drefFormPeopleTargetedDescription} - disabled={disabled} - /> - - {strings.drefFormPeopleTargeted} - - - - - )} - name="num_assisted" - value={value?.num_assisted} - onChange={setFieldValue} - error={error?.num_assisted} - hint={strings.drefFormPeopleTargetedDescription} - disabled={disabled} - /> - {/* NOTE: Empty div to preserve the layout */} -
- diff --git a/src/views/DrefFinalReportForm/i18n.json b/src/views/DrefFinalReportForm/i18n.json index 6e3f9e234..592c74607 100644 --- a/src/views/DrefFinalReportForm/i18n.json +++ b/src/views/DrefFinalReportForm/i18n.json @@ -14,7 +14,7 @@ "formTabEventDetailLabel": "Event Detail", "formTabActionsLabel": "Actions/Needs", "formTabOperationLabel": "Operation", - "formTabSubmissionLabel": "Submission/Contact", + "formTabSubmissionLabel": "Operational timeframes and contacts", "formNotAvailableInSelectedLanguageMessage": "DREF Final Report is not available for edit in the selected language!", "formLoadingMessage": "Loading DREF Final Report...", "formLoadErrorTitle": "Failed to load DREF Final Report", diff --git a/src/views/DrefFinalReportForm/schema.ts b/src/views/DrefFinalReportForm/schema.ts index d8b27ce87..af312ea03 100644 --- a/src/views/DrefFinalReportForm/schema.ts +++ b/src/views/DrefFinalReportForm/schema.ts @@ -138,8 +138,6 @@ const schema: FinalReportFormSchema = { required: true, requiredValidation: requiredStringCondition, }, - num_assisted: { validations: [positiveIntegerCondition] }, - people_in_need: { validations: [positiveIntegerCondition] }, event_map_file: { fields: (): EventMapFileFields => ({ client_id: {}, @@ -154,12 +152,6 @@ const schema: FinalReportFormSchema = { caption: {}, }), }, - number_of_people_affected: { - validations: [positiveIntegerCondition], - }, - number_of_people_targeted: { - validations: [positiveIntegerCondition], - }, total_dref_allocation: {}, main_donors: { validations: [max500CharCondition], @@ -169,7 +161,10 @@ const schema: FinalReportFormSchema = { financial_report_description: {}, // EVENT DETAILS - + number_of_people_affected: { + validations: [positiveIntegerCondition], + }, + people_in_need: { validations: [positiveIntegerCondition] }, event_description: {}, images_file: { keySelector: (image_file) => image_file.client_id, @@ -182,7 +177,10 @@ const schema: FinalReportFormSchema = { }), validations: [lessThanEqualToTwoImagesCondition], }, - + number_of_people_targeted: { + validations: [positiveIntegerCondition], + }, + num_assisted: { validations: [positiveIntegerCondition] }, // ACTIONS ifrc: {}, @@ -237,6 +235,7 @@ const schema: FinalReportFormSchema = { }), }, risk_security_concern: {}, + has_child_safeguarding_risk_analysis_assessment: {}, planned_interventions: { keySelector: (n) => n.client_id, member: () => ({ diff --git a/src/views/DrefOperationalUpdateExport/i18n.json b/src/views/DrefOperationalUpdateExport/i18n.json index 0443ac69e..acc8a458d 100644 --- a/src/views/DrefOperationalUpdateExport/i18n.json +++ b/src/views/DrefOperationalUpdateExport/i18n.json @@ -14,6 +14,8 @@ "glideNumberLabel": "Glide Number", "peopleAffectedLabel": "People Affected", "peopleTargetedLabel": "People Targeted", + "reportingTimeframeStartDateLabel": "Reporting Timeframe Start Date", + "reportingTimeframeEndDateLabel": "Reporting Timeframe End Date", "peopleSuffix": " people", "operationStartDateLabel": "Operation Start Date", "operationTimeframeLabel": "Total Operating Timeframe", @@ -39,6 +41,8 @@ "specifiedTriggerMetLabel": "Please explain how the operation is transitioning from Anticipatory to Response", "currentNationalSocietyActionsHeading": "Current National Society Actions", + "nationalSocietyActionsHeading": "National Society anticipatory actions started", + "drefFormNsResponseStarted": "Start date of National Society actions", "movementPartnersActionsHeading": "IFRC Network Actions Related To The Current Event", "drefExportReference": "Click here for the reference", "secretariatLabel": "Secretariat", @@ -69,6 +73,7 @@ "riskAndSecuritySectionHeading": "Risk and Security Considerations", "riskSecurityHeading": "Please indicate about potential operation risk for this operations and mitigation actions", "safetyConcernHeading": "Please indicate any security and safety concerns for this operation", + "hasChildRiskCompleted": "Has the child safeguarding risk analysis assessment been completed?", "plannedInterventionSectionHeading": "Planned Intervention", "targetedPersonsLabel": "Targeted Persons", "budgetLabel": "Budget", diff --git a/src/views/DrefOperationalUpdateExport/index.tsx b/src/views/DrefOperationalUpdateExport/index.tsx index 3a336568e..812d1a497 100644 --- a/src/views/DrefOperationalUpdateExport/index.tsx +++ b/src/views/DrefOperationalUpdateExport/index.tsx @@ -1,9 +1,10 @@ -import { Fragment, useState } from 'react'; +import { Fragment, useState, useMemo } from 'react'; import { useParams, ScrollRestoration } from 'react-router-dom'; import { _cs, isDefined, isFalsyString, + isNotDefined, isTruthyString, } from '@togglecorp/fujs'; @@ -12,6 +13,7 @@ import TextOutput, { type Props as TextOutputProps } from '#components/printable import Image from '#components/printable/Image'; import Heading from '#components/printable/Heading'; import DescriptionText from '#components/printable/DescriptionText'; +import DateOutput from '#components/DateOutput'; import Link from '#components/Link'; import NumberOutput from '#components/NumberOutput'; @@ -25,6 +27,11 @@ import { DREF_TYPE_IMMINENT, DisasterCategory, } from '#utils/constants'; +import { + identifiedNeedsAndGapsOrder, + nsActionsOrder, + plannedInterventionOrder, +} from '#utils/domain/dref'; import ifrcLogo from '#assets/icons/ifrc-square.png'; @@ -96,6 +103,60 @@ export function Component() { }, }); + const filteredPlannedIntervention = useMemo( + () => drefResponse?.planned_interventions?.map((intervention) => { + if (isNotDefined(intervention.title)) { + return undefined; + } + return { ...intervention, title: intervention.title }; + }).filter(isDefined), + [drefResponse?.planned_interventions], + ); + + const filteredIdentifiedNeedsAndGaps = useMemo( + () => drefResponse?.needs_identified?.map((need) => { + if (isNotDefined(need.title)) { + return undefined; + } + return { ...need, title: need.title }; + }).filter(isDefined), + [drefResponse?.needs_identified], + ); + + const filteredNsActions = useMemo( + () => drefResponse?.national_society_actions?.map((nsAction) => { + if (isNotDefined(nsAction.title)) { + return undefined; + } + return { ...nsAction, title: nsAction.title }; + }).filter(isDefined), + [drefResponse?.national_society_actions], + ); + + const sortedPlannedInterventions = useMemo( + () => filteredPlannedIntervention?.sort( + // eslint-disable-next-line max-len + (a, b) => plannedInterventionOrder[a.title] - plannedInterventionOrder[b.title], + ), + [filteredPlannedIntervention], + ); + + const sortedIdentifiedNeedsAndGaps = useMemo( + () => filteredIdentifiedNeedsAndGaps?.sort( + // eslint-disable-next-line max-len + (a, b) => identifiedNeedsAndGapsOrder[a.title] - identifiedNeedsAndGapsOrder[b.title], + ), + [filteredIdentifiedNeedsAndGaps], + ); + + const sortedNsActions = useMemo( + () => filteredNsActions?.sort((a, b) => ( + // eslint-disable-next-line max-len + nsActionsOrder[a.title] - nsActionsOrder[b.title] + )), + [filteredNsActions], + ); + const eventDescriptionDefined = isTruthyString(drefResponse?.event_description?.trim()); const eventScopeDefined = drefResponse?.type_of_dref !== DREF_TYPE_ASSESSMENT && isTruthyString(drefResponse?.event_scope?.trim()); @@ -171,7 +232,12 @@ export function Component() { && isDefined(drefResponse.risk_security) && drefResponse.risk_security.length > 0; const riskSecurityConcernDefined = isTruthyString(drefResponse?.risk_security_concern?.trim()); - const showRiskAndSecuritySection = riskSecurityDefined || riskSecurityConcernDefined; + const hasChildrenSafeguardingDefined = isDefined( + drefResponse?.has_child_safeguarding_risk_analysis_assessment, + ); + const showRiskAndSecuritySection = riskSecurityDefined + || riskSecurityConcernDefined + || hasChildrenSafeguardingDefined; const plannedInterventionDefined = isDefined(drefResponse) && isDefined(drefResponse.planned_interventions) @@ -283,8 +349,8 @@ export function Component() { value={drefResponse?.disaster_category_display} valueClassName={_cs( isDefined(drefResponse) - && isDefined(drefResponse.disaster_category) - && colorMap[drefResponse.disaster_category], + && isDefined(drefResponse.disaster_category) + && colorMap[drefResponse.disaster_category], )} strongValue /> @@ -344,6 +410,20 @@ export function Component() { suffix={strings.monthsSuffix} strongValue /> + + {strings.currentNationalSocietyActionsHeading} + + {drefResponse?.ns_respond_date && ( + + + + )} + {nsActionImagesDefined && ( {drefResponse?.photos_file?.map( @@ -505,18 +600,22 @@ export function Component() { )} {nsActionsDefined && ( - - {drefResponse?.national_society_actions?.map( - (nsAction) => ( - - ), - )} + + + {sortedNsActions?.map( + (nsAction) => ( + + ), + )} + )} @@ -603,7 +702,7 @@ export function Component() { {strings.needsIdentifiedSectionHeading} - {needsIdentifiedDefined && drefResponse?.needs_identified?.map( + {needsIdentifiedDefined && sortedIdentifiedNeedsAndGaps?.map( (identifiedNeed) => ( @@ -790,6 +889,17 @@ export function Component() { )} + {hasChildrenSafeguardingDefined && ( + + + + )} )} {plannedInterventionDefined && ( @@ -797,7 +907,7 @@ export function Component() { {strings.plannedInterventionSectionHeading} - {drefResponse?.planned_interventions?.map( + {sortedPlannedInterventions?.map( (plannedIntervention) => ( diff --git a/src/views/DrefOperationalUpdateExport/styles.module.css b/src/views/DrefOperationalUpdateExport/styles.module.css index 9fddea699..1082718fe 100644 --- a/src/views/DrefOperationalUpdateExport/styles.module.css +++ b/src/views/DrefOperationalUpdateExport/styles.module.css @@ -2,12 +2,12 @@ --pdf-element-bg: var(--go-ui-color-background); font-family: 'Open Sans', sans-serif; - font-size: var(--go-ui-font-size-sm); + font-size: var(--go-ui-font-size-export); @media screen { margin: var(--go-ui-spacing-xl) auto; background-color: var(--go-ui-color-foreground); - padding: 10mm; + padding: var(--go-ui-export-page-margin); width: 210mm; min-height: 297mm; } @@ -68,6 +68,8 @@ } .additional-allocation, + .reporting-timeframe-start-date, + .reporting-timeframe-end-date, .targeted-areas { display: flex; flex-direction: column; diff --git a/src/views/DrefOperationalUpdateForm/EventDetail/i18n.json b/src/views/DrefOperationalUpdateForm/EventDetail/i18n.json index 0b75404d0..3ea017aff 100644 --- a/src/views/DrefOperationalUpdateForm/EventDetail/i18n.json +++ b/src/views/DrefOperationalUpdateForm/EventDetail/i18n.json @@ -24,6 +24,18 @@ "drefFormUploadPhotos": "Upload photos (e.g. impact of the events, NS in the field, assessments)", "drefFormUploadPhotosLimitation": "Add maximum 2 photos", "drefFormWhatWhereWhen": "What happened, where and when?", - "drefOperationalUpdateFormSelectImages": "Select images" + "drefOperationalUpdateFormSelectImages": "Select images", + "numericDetails": "Numeric Details", + "drefFormRiskPeopleLabel": "Total population at risk", + "drefFormClickEmergencyResponseFramework": "Click to view Emergency Response Framework", + "drefFormPeopleAffected": "Total affected population", + "drefFormPeopleAffectedDescriptionImminent": "Includes all those whose lives and livelihoods are at risk as a direct result of the shock or stress.", + "drefFormPeopleAffectedDescriptionSlowSudden": "People Affected include all those whose lives and livelihoods have been impacted as a direct result of the shock or stress.", + "drefFormEstimatedPeopleInNeed": "Estimated people in need (Optional)", + "drefFormPeopleInNeed": "People in need (Optional)", + "drefFormPeopleInNeedDescriptionImminent": "Include all those whose physical security, basic rights, dignity, living conditions or livelihoods are threatened or have been disrupted, and whose current level of access to basic services, goods and social protection will be inadequate to re-establish normal living conditions without additional assistance", + "drefFormPeopleInNeedDescriptionSlowSudden": "People in Need (PIN) are those members whose physical security, basic rights, dignity, living conditions or livelihoods are threatened or have been disrupted, and whose current level of access to basic services, goods and social protection is inadequate to re-establish normal living conditions without additional assistance.", + "drefFormPeopleTargeted": "Number of people targeted", + "drefFormPeopleTargetedDescription": "Include all those whose the National Society is aiming or planning to assist" } -} +} \ No newline at end of file diff --git a/src/views/DrefOperationalUpdateForm/EventDetail/index.tsx b/src/views/DrefOperationalUpdateForm/EventDetail/index.tsx index f64e067ce..057810976 100644 --- a/src/views/DrefOperationalUpdateForm/EventDetail/index.tsx +++ b/src/views/DrefOperationalUpdateForm/EventDetail/index.tsx @@ -3,12 +3,15 @@ import { type EntriesAsList, getErrorObject, } from '@togglecorp/toggle-form'; +import { WikiHelpSectionLineIcon } from '@ifrc-go/icons'; import Container from '#components/Container'; import InputSection from '#components/InputSection'; import BooleanInput from '#components/BooleanInput'; import TextArea from '#components/TextArea'; import DateInput from '#components/DateInput'; +import NumberInput from '#components/NumberInput'; +import Link from '#components/Link'; import useTranslation from '#hooks/useTranslation'; import MultiImageWithCaptionInput from '#components/domain/MultiImageWithCaptionInput'; @@ -33,6 +36,11 @@ interface Props { disabled?: boolean; } +const totalPopulationRiskImminentLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; +const totalPeopleAffectedSlowSuddenLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; +const peopleTargetedLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; +const peopleInNeedLink = 'https://ifrcorg.sharepoint.com/sites/IFRCSharing/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF%2FHum%20Pop%20Definitions%20for%20DREF%20Form%5F21072022%2Epdf&parent=%2Fsites%2FIFRCSharing%2FShared%20Documents%2FDREF&p=true&ga=1'; + function EventDetail(props: Props) { const strings = useTranslation(i18n); @@ -193,6 +201,98 @@ function EventDetail(props: Props) { /> )} + + + {strings.drefFormRiskPeopleLabel} + + + + + ) : ( + <> + {strings.drefFormPeopleAffected} + + + + + )} + value={value?.number_of_people_affected} + onChange={setFieldValue} + error={error?.number_of_people_affected} + hint={( + value?.type_of_dref === TYPE_IMMINENT + ? strings.drefFormPeopleAffectedDescriptionImminent + : strings.drefFormPeopleAffectedDescriptionSlowSudden + )} + disabled={disabled} + /> + {value?.type_of_dref !== TYPE_LOAN && ( + + { + value?.type_of_dref === TYPE_IMMINENT + ? strings.drefFormEstimatedPeopleInNeed + : strings.drefFormPeopleInNeed + } + + + + + )} + name="people_in_need" + value={value?.people_in_need} + onChange={setFieldValue} + error={error?.people_in_need} + hint={( + value?.type_of_dref === TYPE_IMMINENT + ? strings.drefFormPeopleInNeedDescriptionImminent + : strings.drefFormPeopleInNeedDescriptionSlowSudden + )} + disabled={disabled} + /> + )} + + {strings.drefFormPeopleTargeted} + + + + + )} + name="number_of_people_targeted" + value={value?.number_of_people_targeted} + onChange={setFieldValue} + error={error?.number_of_people_targeted} + hint={strings.drefFormPeopleTargetedDescription} + disabled={disabled} + /> + {/* NOTE: Empty div to preserve the layout */} +
+ {value.type_of_dref !== TYPE_LOAN && ( + + + ; type DrefTypeOption = NonNullable[number]; @@ -101,6 +104,7 @@ function Overview(props: Props) { setDistrictOptions, drefUsers, } = props; + const { state } = useLocation(); const strings = useTranslation(i18n); const { @@ -113,6 +117,13 @@ function Overview(props: Props) { const disasterTypes = useDisasterType(); + const [ + showChangeDrefTypeModal, + { + setFalse: setShowChangeDrefTypeModalFalse, + }, + ] = useBooleanState(true); + const handleTypeOfOnsetChange = useCallback(( typeOfOnset: OnsetTypeOption['key'] | undefined, name: 'type_of_onset', @@ -153,6 +164,11 @@ function Overview(props: Props) { [setFieldValue], ); + const handleChangeToResponse = useCallback(() => { + setFieldValue(TYPE_RESPONSE, 'type_of_dref'); + setShowChangeDrefTypeModalFalse(); + }, [setFieldValue, setShowChangeDrefTypeModalFalse]); + const handleGenerateTitleButtonClick = useCallback( () => { const countryName = countryOptions?.find( @@ -184,6 +200,36 @@ function Overview(props: Props) { return (
+ {state?.isNewOpsUpdate + && showChangeDrefTypeModal + && (value?.type_of_dref === TYPE_IMMINENT + || value?.type_of_dref === TYPE_ASSESSMENT) && ( + + + + + )} + className={styles.flashUpdateShareModal} + > + {strings.isDrefChangingToResponse} + + )}
- - - {strings.drefFormRiskPeopleLabel} - - - - - ) : ( - <> - {strings.drefFormPeopleAffected} - - - - - )} - value={value?.number_of_people_affected} - onChange={setFieldValue} - error={error?.number_of_people_affected} - hint={( - value?.type_of_dref === TYPE_IMMINENT - ? strings.drefFormPeopleAffectedDescriptionImminent - : strings.drefFormPeopleAffectedDescriptionSlowSudden - )} - disabled={disabled} - /> - {value?.type_of_dref !== TYPE_LOAN && ( - - { - value?.type_of_dref === TYPE_IMMINENT - ? strings.drefFormEstimatedPeopleInNeed - : strings.drefFormPeopleInNeed - } - - - - - )} - name="people_in_need" - value={value?.people_in_need} - onChange={setFieldValue} - error={error?.people_in_need} - hint={( - value?.type_of_dref === TYPE_IMMINENT - ? strings.drefFormPeopleInNeedDescriptionImminent - : strings.drefFormPeopleInNeedDescriptionSlowSudden - )} - disabled={disabled} - /> - )} - - {strings.drefFormPeopleTargeted} - - - - - )} - name="number_of_people_targeted" - value={value?.number_of_people_targeted} - onChange={setFieldValue} - error={error?.number_of_people_targeted} - hint={strings.drefFormPeopleTargetedDescription} - disabled={disabled} - /> - {/* NOTE: Empty div to preserve the layout */} -
- - { const conditionalFields: OverviewDrefTypeRelatedFields = { - people_in_need: { forceValue: nullValue }, emergency_appeal_planned: { forceValue: nullValue }, event_map_file: { forceValue: nullValue }, cover_image_file: { forceValue: nullValue }, @@ -237,7 +235,6 @@ const schema: OpsUpdateFormSchema = { } return { ...conditionalFields, - people_in_need: { validations: [positiveIntegerCondition] }, emergency_appeal_planned: {}, event_map_file: { fields: (): EventMapFileFields => ({ @@ -266,6 +263,7 @@ const schema: OpsUpdateFormSchema = { 'event_date', 'event_description', 'images_file', + 'people_in_need', 'summary_of_change', 'changing_timeframe_operation', @@ -293,6 +291,7 @@ const schema: OpsUpdateFormSchema = { event_date: { forceValue: nullValue }, event_description: { forceValue: nullValue }, images_file: { forceValue: [] }, + people_in_need: { forceValue: nullValue }, summary_of_change: { forceValue: nullValue }, changing_timeframe_operation: { forceValue: nullValue }, @@ -311,6 +310,7 @@ const schema: OpsUpdateFormSchema = { conditionalFields = { ...conditionalFields, event_scope: {}, + people_in_need: { validations: [positiveIntegerCondition] }, }; } @@ -553,6 +553,7 @@ const schema: OpsUpdateFormSchema = { 'planned_interventions', 'human_resource', 'is_surge_personnel_deployed', + 'has_child_safeguarding_risk_analysis_assessment', ] as const; type OperationDrefTypeRelatedFields = Pick< OpsUpdateFormSchemaFields, @@ -588,6 +589,7 @@ const schema: OpsUpdateFormSchema = { planned_interventions: { forceValue: [] }, human_resource: { forceValue: nullValue }, is_surge_personnel_deployed: { forceValue: nullValue }, + has_child_safeguarding_risk_analysis_assessment: { forceValue: nullValue }, }; if (val?.type_of_dref === TYPE_LOAN) { return conditionalFields; @@ -636,6 +638,7 @@ const schema: OpsUpdateFormSchema = { }), }, risk_security_concern: {}, + has_child_safeguarding_risk_analysis_assessment: {}, budget_file: {}, planned_interventions: { keySelector: (n) => n.client_id, diff --git a/src/views/EmergencySurge/RapidResponsePerosnnelTable/i18n.json b/src/views/EmergencySurge/RapidResponsePerosnnelTable/i18n.json index a0ae2d10a..75c194eff 100644 --- a/src/views/EmergencySurge/RapidResponsePerosnnelTable/i18n.json +++ b/src/views/EmergencySurge/RapidResponsePerosnnelTable/i18n.json @@ -3,8 +3,6 @@ "strings": { "rapidResponseTitle": "Rapid Response Personnel", "rapidResponse": "Rapid Response", - "personnelTableStartDate": "Start Date", - "personnelTableEndDate": "End Date", "personnelTableName": "Name", "personnelTablePosition": "Position", "personnelTableType": "Type", diff --git a/src/views/EmergencySurge/RapidResponsePerosnnelTable/index.tsx b/src/views/EmergencySurge/RapidResponsePerosnnelTable/index.tsx index f96d79435..63f230c03 100644 --- a/src/views/EmergencySurge/RapidResponsePerosnnelTable/index.tsx +++ b/src/views/EmergencySurge/RapidResponsePerosnnelTable/index.tsx @@ -2,6 +2,8 @@ import { useMemo, useCallback, } from 'react'; +import { isDefined, isNotDefined } from '@togglecorp/fujs'; + import useTranslation from '#hooks/useTranslation'; import { type GoApiResponse, @@ -14,15 +16,21 @@ import { } from '#components/Table/useSorting'; import { createStringColumn, - createDateColumn, createLinkColumn, + createTimelineColumn, } from '#components/Table/ColumnShortcuts'; import Table from '#components/Table'; import Link from '#components/Link'; import { numericIdSelector } from '#utils/selectors'; import useFilterState from '#hooks/useFilterState'; +import { + isValidDate, + maxSafe, + minSafe, +} from '#utils/common'; import i18n from './i18n.json'; +import styles from './styles.module.css'; type PersonnelTableItem = NonNullable['results']>[number]; const now = new Date().toISOString(); @@ -70,13 +78,57 @@ export default function RapidResponsePersonnelTable(props: Props) { }, }); + const dateRange = useMemo( + () => { + if ( + isNotDefined(personnelResponse) + || isNotDefined(personnelResponse.results) + || personnelResponse.results.length === 0 + ) { + return undefined; + } + + const startDateList = personnelResponse.results.map( + ({ start_date }) => ( + isValidDate(start_date) + ? new Date(start_date).getTime() + : undefined + ), + ).filter(isDefined); + + const endDateList = personnelResponse.results.map( + ({ end_date }) => ( + isValidDate(end_date) + ? new Date(end_date).getTime() + : undefined + ), + ).filter(isDefined); + + const start = minSafe([...startDateList, ...endDateList]); + const end = maxSafe([...startDateList, ...endDateList]); + + if (isNotDefined(start) || isNotDefined(end)) { + return undefined; + } + + return { + start: new Date(start), + end: new Date(end), + }; + }, + [personnelResponse], + ); + const columns = useMemo( () => ([ createStringColumn( 'role', strings.personnelTablePosition, (item) => item.role, - { sortable: true }, + { + sortable: true, + columnClassName: styles.role, + }, ), createStringColumn( 'type', @@ -110,17 +162,14 @@ export default function RapidResponsePersonnelTable(props: Props) { (item) => item.name, { sortable: true }, ), - createDateColumn( - 'start_date', - strings.personnelTableStartDate, - (item) => item.start_date, - { sortable: true }, - ), - createDateColumn( - 'end_date', - strings.personnelTableEndDate, - (item) => item.end_date, - { sortable: true }, + createTimelineColumn( + 'timeline', + dateRange, + (item) => ({ + startDate: item.start_date, + endDate: item.end_date, + }), + { columnClassName: styles.timeline }, ), ]), [ @@ -129,14 +178,14 @@ export default function RapidResponsePersonnelTable(props: Props) { strings.personnelTableDeployedParty, strings.personnelTableDeployedTo, strings.personnelTableName, - strings.personnelTableStartDate, - strings.personnelTableEndDate, getTypeName, + dateRange, ], ); return (