Skip to content

Commit 50dc791

Browse files
conor-jhericboucherCopilot
authored
Initial batch maps cadence control (#1773)
* Initial batch maps cadence control * Fix lint error * Update snapshot * Implement coverage-aware cadences * Normalize start/end dates to prevent excluding layer timestamps * Restrict cadence options based on coverage window * Update logic for enabling batch maps * Style updates; fix logic for cadence availability * Exclude quarterly/monthly maps without a full coverage window * Support quarterly map format * Fix date formatting * Add UI messaging for invalid layer and date selections * Update snapshot * Ensure error banner stays in print dialog context * Security bump * Fix security vulnerabilities in API and alerting dependencies Agent-Logs-Url: https://github.com/WFP-VAM/prism-app/sessions/d1011152-bb2d-4853-a12e-7b8decd6dd8d Co-authored-by: ericboucher <16843267+ericboucher@users.noreply.github.com> * Update dekadal selector to single line layout * Revert enabling batch maps --------- Co-authored-by: Eric Boucher <eric.p.boucher@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ericboucher <16843267+ericboucher@users.noreply.github.com>
1 parent 6f22f3b commit 50dc791

9 files changed

Lines changed: 536 additions & 46 deletions

File tree

frontend/src/components/MapExport/MapExportLayout.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import MapGL, { Layer, MapRef, Marker, Source } from 'react-map-gl/maplibre';
1313
import { useTranslation } from 'react-i18next';
1414
import useResizeObserver from 'utils/useOnResizeObserver';
1515
import { getFormattedDate, formatCoverageText } from 'utils/date-utils';
16-
import { DateFormat } from 'utils/name-utils';
1716
import { lightGrey } from 'muiTheme';
1817
import { FloodStationMarker } from 'components/MapView/Layers/AnticipatoryActionFloodLayer/FloodStationMarker';
1918
import LegendItemsList from 'components/MapView/Legends/LegendItemsList';
@@ -152,14 +151,10 @@ function MapExportLayout({
152151
);
153152
}
154153

155-
// Replace {coverage} with formatted coverage ranges (using DayFirstHyphenMonthName)
154+
// Replace {coverage} with formatted coverage ranges (using localized numeric date)
156155
if (layersCoverage && result.includes('{date_coverage}')) {
157156
const coverageText =
158-
formatCoverageText(
159-
layersCoverage,
160-
t,
161-
DateFormat.DayFirstHyphenMonthName,
162-
) ?? '';
157+
formatCoverageText(layersCoverage, t, 'localeNumericUTC') ?? '';
163158
result = result.replace(/\{date_coverage\}/g, coverageText);
164159
}
165160

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { useContext } from 'react';
2+
import {
3+
Box,
4+
TextField,
5+
Typography,
6+
createStyles,
7+
makeStyles,
8+
} from '@material-ui/core';
9+
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
10+
import ToggleButton from '@material-ui/lab/ToggleButton';
11+
import { getFormattedDate } from 'utils/date-utils';
12+
import { BatchCadence } from 'utils/batchCadenceUtils';
13+
import { useSafeTranslation } from '../../../i18n';
14+
import PrintConfigContext from './printConfig.context';
15+
16+
const useStyles = makeStyles(() =>
17+
createStyles({
18+
root: {
19+
display: 'flex',
20+
flexDirection: 'column',
21+
gap: '0.5rem',
22+
},
23+
row: {
24+
display: 'flex',
25+
flexDirection: 'row',
26+
alignItems: 'center',
27+
gap: '0.5rem',
28+
flexWrap: 'wrap',
29+
},
30+
header: {
31+
fontSize: '14px',
32+
fontWeight: 400,
33+
marginTop: '8px',
34+
},
35+
toggleButton: {
36+
fontSize: '0.75rem',
37+
textTransform: 'none',
38+
padding: '4px 10px',
39+
marginBottom: '8px',
40+
},
41+
intervalInput: {
42+
zIndex: 0,
43+
marginBottom: '8px',
44+
'& input': {
45+
fontSize: '0.875rem',
46+
},
47+
},
48+
dateList: {
49+
maxHeight: '120px',
50+
overflowY: 'auto',
51+
display: 'flex',
52+
flexWrap: 'wrap',
53+
gap: '4px',
54+
padding: '4px',
55+
backgroundColor: 'white',
56+
borderRadius: '4px',
57+
border: '1px solid #ccc',
58+
},
59+
dateChip: {
60+
color: '#000',
61+
fontSize: '0.7rem',
62+
padding: '2px 6px',
63+
backgroundColor: '#e0e0e0',
64+
borderRadius: '4px',
65+
whiteSpace: 'nowrap',
66+
},
67+
}),
68+
);
69+
70+
const CADENCE_LABELS: Record<BatchCadence, string> = {
71+
'every-n-dekads': 'Every N dekads',
72+
monthly: 'Monthly',
73+
quarterly: 'Quarterly',
74+
};
75+
76+
function getDateFormat(
77+
cadence: BatchCadence,
78+
): 'localeShortUTC' | 'monthYearUTC' | 'quarterYearUTC' {
79+
switch (cadence) {
80+
case 'every-n-dekads':
81+
return 'localeShortUTC';
82+
case 'monthly':
83+
return 'monthYearUTC';
84+
case 'quarterly':
85+
return 'quarterYearUTC';
86+
default:
87+
return 'localeShortUTC';
88+
}
89+
}
90+
91+
export default function CadenceSelector() {
92+
const classes = useStyles();
93+
const { t } = useSafeTranslation();
94+
const { printConfig } = useContext(PrintConfigContext);
95+
96+
if (!printConfig) {
97+
return null;
98+
}
99+
100+
const {
101+
cadence,
102+
setCadence,
103+
dekadInterval,
104+
setDekadInterval,
105+
filteredBatchDates,
106+
availableCadences,
107+
disabledCadences,
108+
} = printConfig;
109+
110+
return (
111+
<Box className={classes.root}>
112+
<Typography variant="h4" className={classes.header}>
113+
{t('Cadence')}
114+
</Typography>
115+
<Box className={classes.row}>
116+
<ToggleButtonGroup
117+
value={cadence}
118+
exclusive
119+
size="small"
120+
onChange={(_e, value: BatchCadence | null) => {
121+
if (value !== null) {
122+
if (!disabledCadences.has(value)) {
123+
setCadence(value);
124+
}
125+
}
126+
}}
127+
>
128+
{availableCadences.map(option => {
129+
const isDisabled = disabledCadences.has(option);
130+
return (
131+
<ToggleButton
132+
key={option}
133+
value={option}
134+
disabled={isDisabled}
135+
className={classes.toggleButton}
136+
>
137+
{t(CADENCE_LABELS[option])}
138+
</ToggleButton>
139+
);
140+
})}
141+
</ToggleButtonGroup>
142+
{cadence === 'every-n-dekads' && (
143+
<TextField
144+
className={classes.intervalInput}
145+
type="number"
146+
size="small"
147+
variant="outlined"
148+
label="Dekad interval"
149+
value={dekadInterval}
150+
inputProps={{ min: 1 }}
151+
onChange={e => {
152+
const val = parseInt(e.target.value, 10);
153+
setDekadInterval(val);
154+
}}
155+
/>
156+
)}
157+
</Box>
158+
{filteredBatchDates.length > 0 && (
159+
<Box className={classes.dateList}>
160+
{filteredBatchDates.map(ts => (
161+
<span key={ts} className={classes.dateChip}>
162+
{getFormattedDate(ts, getDateFormat(cadence))}
163+
</span>
164+
))}
165+
</Box>
166+
)}
167+
</Box>
168+
);
169+
}

frontend/src/components/NavBar/PrintImage/DateRangePicker.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ function DateRangePicker() {
2222

2323
useEffect(() => {
2424
if (selectedDateFromMap) {
25-
setDateRange({
25+
setDateRange(prev => ({
26+
...prev,
2627
startDate: selectedDateFromMap,
27-
endDate: new Date().getTime(),
28-
});
28+
}));
2929
}
3030
}, [selectedDateFromMap]);
3131

@@ -103,13 +103,7 @@ function DateRangePicker() {
103103
<DatePicker
104104
locale={t('date_locale')}
105105
dateFormat="dd/MM/yyyy"
106-
selected={
107-
endDate
108-
? new Date(endDate)
109-
: selectedDateFromMap
110-
? new Date()
111-
: null
112-
}
106+
selected={endDate ? new Date(endDate) : null}
113107
onChange={handleEndDateChange}
114108
minDate={startDate ? new Date(startDate) : minDate}
115109
includeDates={includedDates}

0 commit comments

Comments
 (0)