diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index 921bd35678..4f9c918bcf 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 0bf32789ad..a8d01e5977 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 b0c7f4655d..409e95af11 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 0000000000..a5f4f5dbdc --- /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 0000000000..a043f8cef3 --- /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 0000000000..249b8ddc25 --- /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 0000000000..1cd9cca097 --- /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 7428949e5c..e67501cc78 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 25bce0f709..edb8305863 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 6f86c9f14f..a4c9d13da4 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 127135ecee..5bfa21a695 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 adfb1aaea2..b33da8217f 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 01b3a1f674..a56125163e 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 6b5afd920a..3b721003cd 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 0c7a92362b..0dc18aa59a 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 dc1c2c2ffc..39912c53db 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 08af8f59cf..934b051221 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 6f614f7f2c..e7418a31d3 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 07a8064c83..20c79cf7d4 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 422d2e2f40..74ef3c893b 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 b411567461..55f61df257 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 8f38203c4d..99a601a476 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 7a620ebb7d..c2a674bc5f 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 7c0a92230f..6711b39d16 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 0abf2278d9..77d99fea91 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 7933c5d4af..ae0eb298f0 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 0000000000..e71a9c6f41 --- /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 0000000000..92397fad20 --- /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 2fef908180..d90f0b6a0d 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 f55357ed7f..6095c915b4 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 2723b2a164..6e742d4d45 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 b4079b8e42..644faa503e 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 6e2b7651ef..1dacc7c2f0 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 ef612438bf..b5d8d81adc 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 0000000000..5094a51e5f --- /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 0000000000..1243a4f228 --- /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 0000000000..4a88db49ae --- /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 d781e79356..2868bfe0fb 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 d01bc34914..d541a18dd8 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 57cdb0ce4b..a57d495685 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 3685615bf2..be7c76a28c 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 acf05bfd4e..be2a767105 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 196633c68e..2b1a96bb5a 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 5dd841265b..8e0748b487 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 eaa4c148ce..1664ecd1d1 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 ae871679c6..a5c18397ca 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 aaad44d316..8439d18928 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 793993a86c..94b01e3f04 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 4bdae5e54e..6e805e8d0e 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 949d808420..7892a8fcf0 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 a7e8a6da3e..1282025219 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 6e3f9e2343..592c746074 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 d8b27ce871..af312ea03d 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 0443ac69e5..acc8a458d5 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 3a336568e5..812d1a497c 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 9fddea6990..1082718fe8 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 0b75404d04..3ea017aff7 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 f64e067ce4..0578109769 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 a0ae2d10a1..75c194eff4 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 f96d794350..63f230c03a 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 (