@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.
@@ -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
- return (React__default.createElement(InputField$1, { error: hasError, validationText: validationError, type: this.field().type, inputWidth: inputWidth, required: this.field().required, name: this.field().id, label: this.label(), min: this.field().min?.toString(), max: this.field().max?.toString(), placeholder: this.placeholder(), value: value, readOnly: disabled, onChange: (event) => {
6462
- onValueChange(event.value);
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
  }