@iapps/ng-dhis2-ui 1.2.129 → 1.2.132
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/iapps-ng-dhis2-ui.mjs +475 -4
- package/fesm2022/iapps-ng-dhis2-ui.mjs.map +1 -1
- package/index.d.ts +27 -8
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ import { Divider } from '@dhis2-ui/divider';
|
|
|
23
23
|
import { Layer } from '@dhis2-ui/layer';
|
|
24
24
|
import { CircularLoader } from '@dhis2-ui/loader';
|
|
25
25
|
import { MenuItem, MenuDivider } from '@dhis2-ui/menu';
|
|
26
|
-
import { Button as Button$1, IconDimensionOrgUnit16, IconClock16, IconDimensionData16, spacers as spacers$1, colors as colors$1, IconEdit16, theme as theme$1, Tooltip, IconChevronRight16, SingleSelectField, SingleSelectOption, InputField as InputField$1, TabBar, Tab, Transfer, Modal as Modal$1, ModalTitle as ModalTitle$1, ModalContent as ModalContent$1, ModalActions as ModalActions$1, ButtonStrip as ButtonStrip$1, Checkbox, OrganisationUnitTree, MultiSelect, MultiSelectOption, IconWarningFilled16, CircularLoader as CircularLoader$1, Chip, DataTableToolbar, DropdownButton, FlyoutMenu, CalendarInput, TableHead, DataTableRow, DataTableColumnHeader, Layer as Layer$1, Popper, MenuItem as MenuItem$1, IconMore24, DataTableCell, TableBody as TableBody$1, Pagination as Pagination$1, TableFoot, DataTable, FileInputField, FileListItem, MultiSelectField, TextAreaField } from '@dhis2/ui';
|
|
26
|
+
import { Button as Button$1, IconDimensionOrgUnit16, IconClock16, IconDimensionData16, spacers as spacers$1, colors as colors$1, IconEdit16, theme as theme$1, Tooltip, IconChevronRight16, SingleSelectField, SingleSelectOption, InputField as InputField$1, TabBar, Tab, Transfer, Modal as Modal$1, ModalTitle as ModalTitle$1, ModalContent as ModalContent$1, ModalActions as ModalActions$1, ButtonStrip as ButtonStrip$1, Checkbox, OrganisationUnitTree, MultiSelect, MultiSelectOption, IconWarningFilled16, CircularLoader as CircularLoader$1, Chip, DataTableToolbar, DropdownButton, FlyoutMenu, CalendarInput, TableHead, DataTableRow, DataTableColumnHeader, Layer as Layer$1, Popper, MenuItem as MenuItem$1, IconMore24, DataTableCell, TableBody as TableBody$1, Pagination as Pagination$1, TableFoot, DataTable, FileInputField, FileListItem, Field, Help, MultiSelectField, TextAreaField, NoticeBox } from '@dhis2/ui';
|
|
27
27
|
import { isObject, isString, debounce, isUndefined, find, sortBy, camelCase, flatten, isArray, isEmpty } from 'lodash';
|
|
28
28
|
import cx from 'classnames';
|
|
29
29
|
import { getNowInCalendar, generateFixedPeriods } from '@dhis2/multi-calendar-dates';
|
|
@@ -34,6 +34,7 @@ import * as XLSX from 'xlsx';
|
|
|
34
34
|
import * as i1$1 from '@angular/forms';
|
|
35
35
|
import { FormControl, Validators, UntypedFormControl, UntypedFormGroup, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
36
36
|
import { toObservable } from '@angular/core/rxjs-interop';
|
|
37
|
+
import { useMapEvents, useMap, MapContainer, TileLayer, Marker } from 'react-leaflet';
|
|
37
38
|
import moment from 'moment';
|
|
38
39
|
|
|
39
40
|
class SharedDhis2UiModule {
|
|
@@ -6268,6 +6269,273 @@ class FieldConfig {
|
|
|
6268
6269
|
}
|
|
6269
6270
|
}
|
|
6270
6271
|
|
|
6272
|
+
// Default focus: Zanzibar (Unguja) and nearby coastline
|
|
6273
|
+
const DEFAULT_CENTER = [-6.1659, 39.2026];
|
|
6274
|
+
const DEFAULT_ZOOM = 8;
|
|
6275
|
+
function normalizeCoordinate(lat, lng) {
|
|
6276
|
+
if (Number.isNaN(lat) || Number.isNaN(lng))
|
|
6277
|
+
return null;
|
|
6278
|
+
if (lat < -90 || lat > 90 || lng < -180 || lng > 180)
|
|
6279
|
+
return null;
|
|
6280
|
+
// Treat Null Island as "no coordinate"
|
|
6281
|
+
if (lat === 0 && lng === 0)
|
|
6282
|
+
return null;
|
|
6283
|
+
return { lat, lng };
|
|
6284
|
+
}
|
|
6285
|
+
/**
|
|
6286
|
+
* Accepts either:
|
|
6287
|
+
* - "[lon,lat]" (JSON-style array)
|
|
6288
|
+
* - "lat, lon" (string)
|
|
6289
|
+
*/
|
|
6290
|
+
function parseCoordinate(value) {
|
|
6291
|
+
if (!value || typeof value !== 'string')
|
|
6292
|
+
return null;
|
|
6293
|
+
const trimmed = value.trim();
|
|
6294
|
+
let lat;
|
|
6295
|
+
let lng;
|
|
6296
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
6297
|
+
try {
|
|
6298
|
+
const arr = JSON.parse(trimmed);
|
|
6299
|
+
if (!Array.isArray(arr) || arr.length < 2)
|
|
6300
|
+
return null;
|
|
6301
|
+
lng = Number(arr[0]);
|
|
6302
|
+
lat = Number(arr[1]);
|
|
6303
|
+
}
|
|
6304
|
+
catch {
|
|
6305
|
+
return null;
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
else {
|
|
6309
|
+
const parts = trimmed.split(',').map((part) => part.trim());
|
|
6310
|
+
if (parts.length !== 2)
|
|
6311
|
+
return null;
|
|
6312
|
+
lat = Number(parts[0]);
|
|
6313
|
+
lng = Number(parts[1]);
|
|
6314
|
+
}
|
|
6315
|
+
return normalizeCoordinate(lat, lng);
|
|
6316
|
+
}
|
|
6317
|
+
function formatCoordinate(coordinate) {
|
|
6318
|
+
if (!coordinate)
|
|
6319
|
+
return null;
|
|
6320
|
+
return `[${coordinate.lng.toFixed(6)},${coordinate.lat.toFixed(6)}]`;
|
|
6321
|
+
}
|
|
6322
|
+
const MapClickListener = ({ disabled, onCoordinateSelected, }) => {
|
|
6323
|
+
useMapEvents({
|
|
6324
|
+
click(event) {
|
|
6325
|
+
if (disabled)
|
|
6326
|
+
return;
|
|
6327
|
+
const { lat, lng } = event.latlng;
|
|
6328
|
+
onCoordinateSelected({ lat, lng });
|
|
6329
|
+
},
|
|
6330
|
+
});
|
|
6331
|
+
return null;
|
|
6332
|
+
};
|
|
6333
|
+
const RecenterOnCoordinate = ({ coordinate }) => {
|
|
6334
|
+
const map = useMap();
|
|
6335
|
+
React__default.useEffect(() => {
|
|
6336
|
+
if (coordinate) {
|
|
6337
|
+
map.flyTo([coordinate.lat, coordinate.lng], map.getZoom(), {
|
|
6338
|
+
duration: 0.4,
|
|
6339
|
+
});
|
|
6340
|
+
}
|
|
6341
|
+
}, [coordinate, map]);
|
|
6342
|
+
return null;
|
|
6343
|
+
};
|
|
6344
|
+
// DHIS2 / react-leaflet typing workaround
|
|
6345
|
+
const CoordinateMapContainer = MapContainer;
|
|
6346
|
+
const CoordinatePickerField = ({ label, name, value, required, disabled, error, validationText, onChange, onBlur, }) => {
|
|
6347
|
+
const parsedCoordinate = React__default.useMemo(() => parseCoordinate(value), [value]);
|
|
6348
|
+
const [latitudeInput, setLatitudeInput] = React__default.useState(parsedCoordinate ? String(parsedCoordinate.lat) : '');
|
|
6349
|
+
const [longitudeInput, setLongitudeInput] = React__default.useState(parsedCoordinate ? String(parsedCoordinate.lng) : '');
|
|
6350
|
+
const [validationMessage, setValidationMessage] = React__default.useState(null);
|
|
6351
|
+
const [isMapDialogOpen, setIsMapDialogOpen] = React__default.useState(false);
|
|
6352
|
+
const [pendingCoordinate, setPendingCoordinate] = React__default.useState(parsedCoordinate ?? null);
|
|
6353
|
+
const [mapCenterPosition, setMapCenterPosition] = React__default.useState(parsedCoordinate
|
|
6354
|
+
? [parsedCoordinate.lat, parsedCoordinate.lng]
|
|
6355
|
+
: DEFAULT_CENTER);
|
|
6356
|
+
React__default.useEffect(() => {
|
|
6357
|
+
const updatedCoordinate = parseCoordinate(value);
|
|
6358
|
+
if (updatedCoordinate) {
|
|
6359
|
+
setLatitudeInput(String(updatedCoordinate.lat));
|
|
6360
|
+
setLongitudeInput(String(updatedCoordinate.lng));
|
|
6361
|
+
setPendingCoordinate(updatedCoordinate);
|
|
6362
|
+
setMapCenterPosition([updatedCoordinate.lat, updatedCoordinate.lng]);
|
|
6363
|
+
}
|
|
6364
|
+
else {
|
|
6365
|
+
setLatitudeInput('');
|
|
6366
|
+
setLongitudeInput('');
|
|
6367
|
+
setPendingCoordinate(null);
|
|
6368
|
+
setMapCenterPosition(DEFAULT_CENTER);
|
|
6369
|
+
}
|
|
6370
|
+
}, [value]);
|
|
6371
|
+
const currentCoordinate = React__default.useMemo(() => {
|
|
6372
|
+
const lat = Number(latitudeInput);
|
|
6373
|
+
const lng = Number(longitudeInput);
|
|
6374
|
+
return normalizeCoordinate(lat, lng);
|
|
6375
|
+
}, [latitudeInput, longitudeInput]);
|
|
6376
|
+
const applyCoordinateChange = React__default.useCallback((coordinate) => {
|
|
6377
|
+
if (!coordinate) {
|
|
6378
|
+
setLatitudeInput('');
|
|
6379
|
+
setLongitudeInput('');
|
|
6380
|
+
setValidationMessage(null);
|
|
6381
|
+
onChange(null);
|
|
6382
|
+
return;
|
|
6383
|
+
}
|
|
6384
|
+
const normalized = normalizeCoordinate(coordinate.lat, coordinate.lng);
|
|
6385
|
+
if (!normalized) {
|
|
6386
|
+
setValidationMessage('Please enter a valid coordinate within the allowed range.');
|
|
6387
|
+
onChange(null);
|
|
6388
|
+
return;
|
|
6389
|
+
}
|
|
6390
|
+
setLatitudeInput(String(normalized.lat));
|
|
6391
|
+
setLongitudeInput(String(normalized.lng));
|
|
6392
|
+
setValidationMessage(null);
|
|
6393
|
+
const formattedValue = formatCoordinate(normalized);
|
|
6394
|
+
onChange(formattedValue);
|
|
6395
|
+
}, [onChange]);
|
|
6396
|
+
const handleLatitudeChange = (event) => {
|
|
6397
|
+
const rawValue = event.value;
|
|
6398
|
+
setLatitudeInput(rawValue);
|
|
6399
|
+
const numericLatitude = Number(rawValue);
|
|
6400
|
+
if (rawValue !== '' &&
|
|
6401
|
+
(Number.isNaN(numericLatitude) ||
|
|
6402
|
+
numericLatitude < -90 ||
|
|
6403
|
+
numericLatitude > 90)) {
|
|
6404
|
+
setValidationMessage('Latitude must be between -90 and 90.');
|
|
6405
|
+
return;
|
|
6406
|
+
}
|
|
6407
|
+
if (rawValue === '' || longitudeInput === '') {
|
|
6408
|
+
applyCoordinateChange(null);
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
const numericLongitude = Number(longitudeInput);
|
|
6412
|
+
if (Number.isNaN(numericLongitude) ||
|
|
6413
|
+
numericLongitude < -180 ||
|
|
6414
|
+
numericLongitude > 180) {
|
|
6415
|
+
setValidationMessage('Longitude must be between -180 and 180.');
|
|
6416
|
+
return;
|
|
6417
|
+
}
|
|
6418
|
+
const updatedCoordinate = {
|
|
6419
|
+
lat: numericLatitude,
|
|
6420
|
+
lng: numericLongitude,
|
|
6421
|
+
};
|
|
6422
|
+
setPendingCoordinate(updatedCoordinate);
|
|
6423
|
+
applyCoordinateChange(updatedCoordinate);
|
|
6424
|
+
};
|
|
6425
|
+
const handleLongitudeChange = (event) => {
|
|
6426
|
+
const rawValue = event.value;
|
|
6427
|
+
setLongitudeInput(rawValue);
|
|
6428
|
+
const numericLongitude = Number(rawValue);
|
|
6429
|
+
if (rawValue !== '' &&
|
|
6430
|
+
(Number.isNaN(numericLongitude) ||
|
|
6431
|
+
numericLongitude < -180 ||
|
|
6432
|
+
numericLongitude > 180)) {
|
|
6433
|
+
setValidationMessage('Longitude must be between -180 and 180.');
|
|
6434
|
+
return;
|
|
6435
|
+
}
|
|
6436
|
+
if (latitudeInput === '' || rawValue === '') {
|
|
6437
|
+
applyCoordinateChange(null);
|
|
6438
|
+
return;
|
|
6439
|
+
}
|
|
6440
|
+
const numericLatitude = Number(latitudeInput);
|
|
6441
|
+
if (Number.isNaN(numericLatitude) ||
|
|
6442
|
+
numericLatitude < -90 ||
|
|
6443
|
+
numericLatitude > 90) {
|
|
6444
|
+
setValidationMessage('Latitude must be between -90 and 90.');
|
|
6445
|
+
return;
|
|
6446
|
+
}
|
|
6447
|
+
const updatedCoordinate = {
|
|
6448
|
+
lat: numericLatitude,
|
|
6449
|
+
lng: numericLongitude,
|
|
6450
|
+
};
|
|
6451
|
+
setPendingCoordinate(updatedCoordinate);
|
|
6452
|
+
applyCoordinateChange(updatedCoordinate);
|
|
6453
|
+
};
|
|
6454
|
+
const handleOpenMapDialog = () => {
|
|
6455
|
+
if (disabled)
|
|
6456
|
+
return;
|
|
6457
|
+
const baseCoordinate = currentCoordinate ?? pendingCoordinate ?? null;
|
|
6458
|
+
if (baseCoordinate) {
|
|
6459
|
+
setPendingCoordinate(baseCoordinate);
|
|
6460
|
+
setMapCenterPosition([baseCoordinate.lat, baseCoordinate.lng]);
|
|
6461
|
+
}
|
|
6462
|
+
else {
|
|
6463
|
+
setMapCenterPosition(DEFAULT_CENTER);
|
|
6464
|
+
}
|
|
6465
|
+
setIsMapDialogOpen(true);
|
|
6466
|
+
if (!baseCoordinate && navigator.geolocation) {
|
|
6467
|
+
navigator.geolocation.getCurrentPosition((position) => {
|
|
6468
|
+
const coordinate = normalizeCoordinate(position.coords.latitude, position.coords.longitude);
|
|
6469
|
+
if (coordinate) {
|
|
6470
|
+
setPendingCoordinate(coordinate);
|
|
6471
|
+
setMapCenterPosition([coordinate.lat, coordinate.lng]);
|
|
6472
|
+
}
|
|
6473
|
+
else {
|
|
6474
|
+
setMapCenterPosition(DEFAULT_CENTER);
|
|
6475
|
+
}
|
|
6476
|
+
}, () => {
|
|
6477
|
+
setValidationMessage((previous) => previous ??
|
|
6478
|
+
'We could not detect your location. Please move the map and click on the correct place.');
|
|
6479
|
+
});
|
|
6480
|
+
}
|
|
6481
|
+
};
|
|
6482
|
+
const handleCloseMapDialog = () => {
|
|
6483
|
+
setIsMapDialogOpen(false);
|
|
6484
|
+
};
|
|
6485
|
+
const handleMapClick = (coordinate) => {
|
|
6486
|
+
const normalized = normalizeCoordinate(coordinate.lat, coordinate.lng);
|
|
6487
|
+
if (!normalized)
|
|
6488
|
+
return;
|
|
6489
|
+
setPendingCoordinate(normalized);
|
|
6490
|
+
setMapCenterPosition([normalized.lat, normalized.lng]);
|
|
6491
|
+
};
|
|
6492
|
+
const handleConfirmFromMap = () => {
|
|
6493
|
+
if (!pendingCoordinate)
|
|
6494
|
+
return;
|
|
6495
|
+
applyCoordinateChange(pendingCoordinate);
|
|
6496
|
+
setIsMapDialogOpen(false);
|
|
6497
|
+
};
|
|
6498
|
+
const combinedErrorMessage = validationMessage || (error ? validationText : null);
|
|
6499
|
+
const markerCoordinate = pendingCoordinate || currentCoordinate || null;
|
|
6500
|
+
const selectedLocationText = markerCoordinate
|
|
6501
|
+
? `Selected location: Lat ${markerCoordinate.lat.toFixed(5)}, Lon ${markerCoordinate.lng.toFixed(5)}`
|
|
6502
|
+
: 'No location selected yet. Click on the street map to drop a marker.';
|
|
6503
|
+
return (React__default.createElement(React__default.Fragment, null,
|
|
6504
|
+
React__default.createElement(Field, { label: label, name: name, required: required, error: Boolean(combinedErrorMessage), validationText: combinedErrorMessage ?? undefined },
|
|
6505
|
+
React__default.createElement("div", { style: {
|
|
6506
|
+
display: 'grid',
|
|
6507
|
+
gridTemplateColumns: 'minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr)',
|
|
6508
|
+
gap: 8,
|
|
6509
|
+
alignItems: 'flex-end',
|
|
6510
|
+
} },
|
|
6511
|
+
React__default.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
6512
|
+
React__default.createElement(Button$1, { name: `${name}-open-map`, disabled: disabled, onClick: handleOpenMapDialog, secondary: true }, currentCoordinate
|
|
6513
|
+
? 'Update location on map'
|
|
6514
|
+
: 'Pick location on map')),
|
|
6515
|
+
React__default.createElement(InputField$1, { label: "Latitude (\u00B0)", name: `${name}-lat`, type: "number", step: "0.000001", disabled: disabled, value: latitudeInput, onChange: handleLatitudeChange, onBlur: onBlur, placeholder: "-6.814527" }),
|
|
6516
|
+
React__default.createElement(InputField$1, { label: "Longitude (\u00B0)", name: `${name}-lng`, type: "number", step: "0.000001", disabled: disabled, value: longitudeInput, onChange: handleLongitudeChange, onBlur: onBlur, placeholder: "39.279557" }))),
|
|
6517
|
+
isMapDialogOpen && (React__default.createElement(Modal$1, { onClose: handleCloseMapDialog, position: "middle", className: "coordinate-modal" },
|
|
6518
|
+
React__default.createElement(ModalTitle$1, null, "Select location on street map"),
|
|
6519
|
+
React__default.createElement(ModalContent$1, null,
|
|
6520
|
+
React__default.createElement("div", { style: {
|
|
6521
|
+
width: '80vw',
|
|
6522
|
+
height: '70vh',
|
|
6523
|
+
maxWidth: '100%',
|
|
6524
|
+
maxHeight: '80vh',
|
|
6525
|
+
} },
|
|
6526
|
+
React__default.createElement(CoordinateMapContainer, { center: mapCenterPosition, zoom: markerCoordinate ? 12 : DEFAULT_ZOOM, style: { width: '100%', height: '100%', borderRadius: 8 }, scrollWheelZoom: !disabled },
|
|
6527
|
+
React__default.createElement(TileLayer, { attribution: "\u00A9 OpenStreetMap contributors", url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" }),
|
|
6528
|
+
React__default.createElement(RecenterOnCoordinate, { coordinate: markerCoordinate }),
|
|
6529
|
+
markerCoordinate && (React__default.createElement(Marker, { position: [markerCoordinate.lat, markerCoordinate.lng] })),
|
|
6530
|
+
React__default.createElement(MapClickListener, { disabled: disabled, onCoordinateSelected: handleMapClick }))),
|
|
6531
|
+
React__default.createElement("p", { style: { marginTop: 8, fontSize: 13 } }, selectedLocationText),
|
|
6532
|
+
React__default.createElement(Help, null, "Use the street map to find the correct facility, building or area. Pan (drag), zoom in/out, then click once to place or move the marker. Click Use Selected Location to save the coordinates.")),
|
|
6533
|
+
React__default.createElement(ModalActions$1, null,
|
|
6534
|
+
React__default.createElement("div", { style: { display: 'flex', gap: 12 } },
|
|
6535
|
+
React__default.createElement(Button$1, { onClick: handleCloseMapDialog, secondary: true }, "Cancel"),
|
|
6536
|
+
React__default.createElement(Button$1, { primary: true, onClick: handleConfirmFromMap, disabled: !pendingCoordinate }, "Use Selected Location")))))));
|
|
6537
|
+
};
|
|
6538
|
+
|
|
6271
6539
|
// Copyright 2024 UDSM DHIS2 Lab. All rights reserved.
|
|
6272
6540
|
// Use of this source code is governed by a BSD-style
|
|
6273
6541
|
// license that can be found in the LICENSE file.
|
|
@@ -6416,6 +6684,26 @@ class BaseFormFieldComponent extends ReactWrapperModule {
|
|
|
6416
6684
|
};
|
|
6417
6685
|
const formFieldContent = () => {
|
|
6418
6686
|
switch (this.field().controlType) {
|
|
6687
|
+
case 'coordinate':
|
|
6688
|
+
return (React__default.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
|
|
6689
|
+
React__default.createElement(NoticeBox, { title: "Location capture required" },
|
|
6690
|
+
React__default.createElement("p", { style: { margin: 0 } }, "This field requires selecting the exact geographic location on the map."),
|
|
6691
|
+
React__default.createElement("ul", { style: { margin: '6px 0 0 18px', padding: 0, fontSize: 13 } },
|
|
6692
|
+
React__default.createElement("li", null,
|
|
6693
|
+
"Click ",
|
|
6694
|
+
React__default.createElement("strong", null, "\u201CPick location on map\u201D"),
|
|
6695
|
+
" to open the street map."),
|
|
6696
|
+
React__default.createElement("li", null, "Pan and zoom until you locate the correct facility, building, house or area."),
|
|
6697
|
+
React__default.createElement("li", null, "Click once on the map to place or move the marker accurately."),
|
|
6698
|
+
React__default.createElement("li", null,
|
|
6699
|
+
"Click ",
|
|
6700
|
+
React__default.createElement("strong", null, "\u201CUse Selected Location\u201D"),
|
|
6701
|
+
" to save the coordinates."))),
|
|
6702
|
+
React__default.createElement(CoordinatePickerField, { error: hasError, validationText: validationError, required: this.field().required, name: this.field().id, disabled: disabled, label: this.label(), value: value, onChange: (newValue) => {
|
|
6703
|
+
onValueChange(newValue);
|
|
6704
|
+
}, onBlur: () => {
|
|
6705
|
+
checkValueUniqueness();
|
|
6706
|
+
} })));
|
|
6419
6707
|
case 'textarea':
|
|
6420
6708
|
return (React__default.createElement(TextAreaField, { error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, required: this.field().required, name: this.field().id, disabled: disabled, label: this.label(), rows: 5, placeholder: this.placeholder(), value: value, onChange: (event) => {
|
|
6421
6709
|
onValueChange(event.value);
|
|
@@ -6456,13 +6744,57 @@ class BaseFormFieldComponent extends ReactWrapperModule {
|
|
|
6456
6744
|
}, onBlur: () => {
|
|
6457
6745
|
checkValueUniqueness();
|
|
6458
6746
|
}, selected: arrayValue }, (this.field().options || []).map((option) => (React__default.createElement(MultiSelectOption, { key: generateUUID(), label: option.label, value: option.value })))));
|
|
6747
|
+
// case 'date':
|
|
6748
|
+
// case 'date-time':
|
|
6749
|
+
// return (
|
|
6750
|
+
// <InputField
|
|
6751
|
+
// error={hasError}
|
|
6752
|
+
// validationText={validationError}
|
|
6753
|
+
// type={this.field().type as any}
|
|
6754
|
+
// inputWidth={inputWidth}
|
|
6755
|
+
// required={this.field().required}
|
|
6756
|
+
// name={this.field().id}
|
|
6757
|
+
// label={this.label()}
|
|
6758
|
+
// min={this.field().min?.toString()}
|
|
6759
|
+
// max={this.field().max?.toString()}
|
|
6760
|
+
// placeholder={this.placeholder()}
|
|
6761
|
+
// value={value}
|
|
6762
|
+
// readOnly={disabled}
|
|
6763
|
+
// onChange={(event: any) => {
|
|
6764
|
+
// onValueChange(event.value);
|
|
6765
|
+
// }}
|
|
6766
|
+
// onBlur={() => {
|
|
6767
|
+
// checkValueUniqueness();
|
|
6768
|
+
// }}
|
|
6769
|
+
// />
|
|
6770
|
+
// );
|
|
6459
6771
|
case 'date':
|
|
6460
|
-
case 'date-time':
|
|
6461
|
-
|
|
6462
|
-
|
|
6772
|
+
case 'date-time': {
|
|
6773
|
+
const field = this.field();
|
|
6774
|
+
const isDateTimeField = field.type === 'date-time';
|
|
6775
|
+
const inputType = isDateTimeField
|
|
6776
|
+
? 'datetime-local'
|
|
6777
|
+
: 'date';
|
|
6778
|
+
// Value coming from DHIS2 store
|
|
6779
|
+
const htmlValue = this.formatValueForHtmlInputFromDhis(value, isDateTimeField);
|
|
6780
|
+
const htmlMin = field.min
|
|
6781
|
+
? this.formatValueForHtmlInputFromDhis(String(field.min), isDateTimeField)
|
|
6782
|
+
: undefined;
|
|
6783
|
+
// Max defined in metadata (if any)
|
|
6784
|
+
const metadataMax = field.max
|
|
6785
|
+
? this.formatValueForHtmlInputFromDhis(String(field.max), isDateTimeField)
|
|
6786
|
+
: undefined;
|
|
6787
|
+
// System max = “now” (no future allowed)
|
|
6788
|
+
const systemMax = this.getNowForHtmlDateInput(isDateTimeField);
|
|
6789
|
+
// Effective max = earliest of metadataMax and systemMax
|
|
6790
|
+
const htmlMax = this.pickEarlierDateLimit(metadataMax, systemMax);
|
|
6791
|
+
return (React__default.createElement(InputField$1, { error: hasError, validationText: validationError, type: inputType, inputWidth: inputWidth, required: field.required, name: field.id, label: this.label(), min: htmlMin, max: htmlMax, placeholder: this.placeholder(), value: htmlValue, readOnly: disabled, onChange: ({ value: newValue }) => {
|
|
6792
|
+
const normalized = this.normalizeDateValueFromHtmlInput(newValue, isDateTimeField, true);
|
|
6793
|
+
onValueChange(normalized);
|
|
6463
6794
|
}, onBlur: () => {
|
|
6464
6795
|
checkValueUniqueness();
|
|
6465
6796
|
} }));
|
|
6797
|
+
}
|
|
6466
6798
|
case 'file':
|
|
6467
6799
|
return (React__default.createElement(React__default.Fragment, null,
|
|
6468
6800
|
React__default.createElement(FileUploadField, { label: this.label(), id: this.field().id, hasError: hasError, required: this.field().required, validationText: validationError, performUpload: true, uploadUrl: 'fileResources', onUploadSuccess: (fileId) => {
|
|
@@ -6495,6 +6827,132 @@ class BaseFormFieldComponent extends ReactWrapperModule {
|
|
|
6495
6827
|
onChange(event) {
|
|
6496
6828
|
this.value.set(event);
|
|
6497
6829
|
}
|
|
6830
|
+
formatValueForHtmlDateInput(rawValue, isDateTimeField) {
|
|
6831
|
+
if (!rawValue)
|
|
6832
|
+
return '';
|
|
6833
|
+
const trimmed = rawValue.trim();
|
|
6834
|
+
if (!trimmed)
|
|
6835
|
+
return '';
|
|
6836
|
+
if (!isDateTimeField) {
|
|
6837
|
+
const datePart = trimmed.split('T')[0].split(' ')[0];
|
|
6838
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(datePart) ? datePart : '';
|
|
6839
|
+
}
|
|
6840
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(trimmed)) {
|
|
6841
|
+
return trimmed;
|
|
6842
|
+
}
|
|
6843
|
+
const date = new Date(trimmed);
|
|
6844
|
+
if (Number.isNaN(date.getTime())) {
|
|
6845
|
+
return '';
|
|
6846
|
+
}
|
|
6847
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
6848
|
+
return (`${date.getFullYear()}-` +
|
|
6849
|
+
`${pad(date.getMonth() + 1)}-` +
|
|
6850
|
+
`${pad(date.getDate())}T` +
|
|
6851
|
+
`${pad(date.getHours())}:` +
|
|
6852
|
+
`${pad(date.getMinutes())}`);
|
|
6853
|
+
}
|
|
6854
|
+
formatValueForHtmlInputFromDhis(rawValue, isDateTimeField) {
|
|
6855
|
+
if (!rawValue)
|
|
6856
|
+
return '';
|
|
6857
|
+
const trimmed = rawValue.trim();
|
|
6858
|
+
if (!trimmed)
|
|
6859
|
+
return '';
|
|
6860
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
6861
|
+
if (!isDateTimeField) {
|
|
6862
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
6863
|
+
return trimmed;
|
|
6864
|
+
}
|
|
6865
|
+
const date = new Date(trimmed);
|
|
6866
|
+
if (Number.isNaN(date.getTime())) {
|
|
6867
|
+
return '';
|
|
6868
|
+
}
|
|
6869
|
+
return (`${date.getFullYear()}-` +
|
|
6870
|
+
`${pad(date.getMonth() + 1)}-` +
|
|
6871
|
+
`${pad(date.getDate())}`);
|
|
6872
|
+
}
|
|
6873
|
+
const parsedDate = new Date(trimmed);
|
|
6874
|
+
if (Number.isNaN(parsedDate.getTime())) {
|
|
6875
|
+
return '';
|
|
6876
|
+
}
|
|
6877
|
+
const year = parsedDate.getFullYear();
|
|
6878
|
+
const month = pad(parsedDate.getMonth() + 1);
|
|
6879
|
+
const day = pad(parsedDate.getDate());
|
|
6880
|
+
const hours = pad(parsedDate.getHours());
|
|
6881
|
+
const minutes = pad(parsedDate.getMinutes());
|
|
6882
|
+
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
6883
|
+
}
|
|
6884
|
+
getNowForHtmlDateInput(isDateTimeField) {
|
|
6885
|
+
const now = new Date();
|
|
6886
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
6887
|
+
const datePart = `${now.getFullYear()}-` +
|
|
6888
|
+
`${pad(now.getMonth() + 1)}-` +
|
|
6889
|
+
`${pad(now.getDate())}`;
|
|
6890
|
+
if (!isDateTimeField) {
|
|
6891
|
+
return datePart;
|
|
6892
|
+
}
|
|
6893
|
+
const timePart = `${pad(now.getHours())}:` + `${pad(now.getMinutes())}`;
|
|
6894
|
+
return `${datePart}T${timePart}`;
|
|
6895
|
+
}
|
|
6896
|
+
pickEarlierDateLimit(metadataMax, systemMax) {
|
|
6897
|
+
if (!metadataMax && !systemMax)
|
|
6898
|
+
return undefined;
|
|
6899
|
+
if (!metadataMax)
|
|
6900
|
+
return systemMax;
|
|
6901
|
+
if (!systemMax)
|
|
6902
|
+
return metadataMax;
|
|
6903
|
+
return metadataMax < systemMax ? metadataMax : systemMax;
|
|
6904
|
+
}
|
|
6905
|
+
/**
|
|
6906
|
+
* Normalizes value from the HTML input into DHIS storage format.
|
|
6907
|
+
*
|
|
6908
|
+
* For date:
|
|
6909
|
+
* - Input: "YYYY-MM-DD"
|
|
6910
|
+
* - Output: "YYYY-MM-DD"
|
|
6911
|
+
*
|
|
6912
|
+
* For datetime:
|
|
6913
|
+
* - Input: "YYYY-MM-DDTHH:mm" (local)
|
|
6914
|
+
* - Output: ISO string in UTC: "YYYY-MM-DDTHH:mm:ss.sssZ"
|
|
6915
|
+
*
|
|
6916
|
+
* If disallowFuture = true, any value beyond "now" is clamped to now.
|
|
6917
|
+
*/
|
|
6918
|
+
normalizeDateValueFromHtmlInput(inputValue, isDateTimeField, disallowFuture) {
|
|
6919
|
+
const trimmed = (inputValue ?? '').trim();
|
|
6920
|
+
if (!trimmed)
|
|
6921
|
+
return null;
|
|
6922
|
+
if (!isDateTimeField) {
|
|
6923
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
6924
|
+
return null;
|
|
6925
|
+
}
|
|
6926
|
+
if (disallowFuture) {
|
|
6927
|
+
const todayStr = this.getNowForHtmlDateInput(false);
|
|
6928
|
+
if (trimmed > todayStr) {
|
|
6929
|
+
return todayStr;
|
|
6930
|
+
}
|
|
6931
|
+
}
|
|
6932
|
+
return trimmed;
|
|
6933
|
+
}
|
|
6934
|
+
// ---- DATETIME (date-time) ----
|
|
6935
|
+
// HTML datetime-local gives "YYYY-MM-DDTHH:mm" (local)
|
|
6936
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(trimmed)) {
|
|
6937
|
+
return null;
|
|
6938
|
+
}
|
|
6939
|
+
// Interpret as LOCAL time
|
|
6940
|
+
const localDate = new Date(trimmed);
|
|
6941
|
+
if (Number.isNaN(localDate.getTime())) {
|
|
6942
|
+
return null;
|
|
6943
|
+
}
|
|
6944
|
+
if (disallowFuture) {
|
|
6945
|
+
const now = new Date();
|
|
6946
|
+
if (localDate.getTime() > now.getTime()) {
|
|
6947
|
+
// Option A: clamp to now
|
|
6948
|
+
return now.toISOString();
|
|
6949
|
+
// Option B: reject:
|
|
6950
|
+
// return null;
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
// Store as UTC ISO string with millis, e.g. "2025-11-26T21:01:00.000Z"
|
|
6954
|
+
return localDate.toISOString();
|
|
6955
|
+
}
|
|
6498
6956
|
async searchForDuplicates(value) {
|
|
6499
6957
|
const d2 = window.d2Web;
|
|
6500
6958
|
if (!value) {
|
|
@@ -7196,6 +7654,7 @@ var FieldType;
|
|
|
7196
7654
|
FieldType["ORG_UNIT"] = "ORGANISATION_UNIT";
|
|
7197
7655
|
FieldType["PHONE_NUMBER"] = "PHONE_NUMBER";
|
|
7198
7656
|
FieldType["EMAIL"] = "EMAIL";
|
|
7657
|
+
FieldType["COORDINATE"] = "COORDINATE";
|
|
7199
7658
|
})(FieldType || (FieldType = {}));
|
|
7200
7659
|
|
|
7201
7660
|
// Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
|
|
@@ -7298,8 +7757,12 @@ class FieldUtil {
|
|
|
7298
7757
|
return 'org-unit';
|
|
7299
7758
|
case FieldType.DATE:
|
|
7300
7759
|
return 'date';
|
|
7760
|
+
case FieldType.DATE_TIME:
|
|
7761
|
+
return 'date-time';
|
|
7301
7762
|
case FieldType.FILE_RESOURCE:
|
|
7302
7763
|
return 'file';
|
|
7764
|
+
case FieldType.COORDINATE:
|
|
7765
|
+
return 'coordinate';
|
|
7303
7766
|
default:
|
|
7304
7767
|
return 'text';
|
|
7305
7768
|
}
|
|
@@ -7329,6 +7792,8 @@ class FieldUtil {
|
|
|
7329
7792
|
return 'image';
|
|
7330
7793
|
case FieldType.FILE_RESOURCE:
|
|
7331
7794
|
return 'file';
|
|
7795
|
+
case FieldType.COORDINATE:
|
|
7796
|
+
return 'coordinate';
|
|
7332
7797
|
default:
|
|
7333
7798
|
return 'textbox';
|
|
7334
7799
|
}
|
|
@@ -7373,6 +7838,8 @@ class FieldControlTypeUtil {
|
|
|
7373
7838
|
return 'image';
|
|
7374
7839
|
case FieldType.FILE_RESOURCE:
|
|
7375
7840
|
return 'file';
|
|
7841
|
+
case FieldType.COORDINATE:
|
|
7842
|
+
return 'coordinate';
|
|
7376
7843
|
default:
|
|
7377
7844
|
return 'textbox';
|
|
7378
7845
|
}
|
|
@@ -7392,6 +7859,10 @@ class FormFieldTypeUtil {
|
|
|
7392
7859
|
return 'image';
|
|
7393
7860
|
case FieldType.FILE_RESOURCE:
|
|
7394
7861
|
return 'file';
|
|
7862
|
+
case FieldType.COORDINATE:
|
|
7863
|
+
return 'coordinate';
|
|
7864
|
+
case FieldType.DATE_TIME:
|
|
7865
|
+
return 'date-time';
|
|
7395
7866
|
default:
|
|
7396
7867
|
return 'text';
|
|
7397
7868
|
}
|