@iapps/ng-dhis2-ui 1.1.46 → 1.1.48

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.
@@ -25,9 +25,9 @@ import { Layer } from '@dhis2-ui/layer';
25
25
  import { CircularLoader } from '@dhis2-ui/loader';
26
26
  import { MenuItem, MenuDivider } from '@dhis2-ui/menu';
27
27
  import parse from 'html-react-parser';
28
- import { Button as Button$1, IconDimensionData16, IconClock16, IconDimensionOrgUnit16, spacers as spacers$1, colors as colors$1, IconEdit16, theme as theme$1, Tooltip, 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, Layer as Layer$1, Popper, FlyoutMenu, MenuItem as MenuItem$1, IconMore24, CircularLoader as CircularLoader$1, IconFilter16, DataTable, TableHead, DataTableRow, DataTableColumnHeader, TableBody, DataTableCell, TableFoot, Pagination, FileInputField, FileListItem, MultiSelectField, TextAreaField } from '@dhis2/ui';
28
+ import { Button as Button$1, IconDimensionData16, IconClock16, IconDimensionOrgUnit16, spacers as spacers$1, colors as colors$1, IconEdit16, theme as theme$1, Tooltip, 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, Layer as Layer$1, Popper, FlyoutMenu, MenuItem as MenuItem$1, IconMore24, CircularLoader as CircularLoader$1, IconFilter16, DataTable, TableHead, DataTableRow, DataTableColumnHeader, TableBody, DataTableCell, TableFoot, Pagination, Chip, FileInputField, FileListItem, MultiSelectField, TextAreaField } from '@dhis2/ui';
29
29
  import { getNowInCalendar, generateFixedPeriods } from '@dhis2/multi-calendar-dates';
30
- import { isObject, isString, sortBy, isArray, find, camelCase, flatten, head, isEmpty } from 'lodash';
30
+ import { isObject, isString, sortBy, isArray, find, camelCase, flatten, debounce, head, isEmpty } from 'lodash';
31
31
  import cx from 'classnames';
32
32
  import { map as map$1, switchMap } from 'rxjs/operators';
33
33
  import * as i2 from '@angular/forms';
@@ -1564,6 +1564,62 @@ TransferOption.propTypes = {
1564
1564
  // Use of this source code is governed by a BSD-style
1565
1565
  // license that can be found in the LICENSE file.
1566
1566
 
1567
+ // Helper function to generate a unique class name
1568
+ const generateClassName$2 = (base) => `${base}-${Math.random().toString(36).substr(2, 9)}`;
1569
+ // Helper function to convert camelCased properties to hyphenated CSS properties
1570
+ const convertStylesToCSS$2 = (className, styles) => {
1571
+ let cssString = `.${className} {`;
1572
+ for (const key in styles) {
1573
+ if (typeof styles[key] === 'object' && key.startsWith('@media')) {
1574
+ const mediaStyles = convertStylesToCSS$2(className, styles[key]);
1575
+ cssString += `${key} { ${mediaStyles} }`;
1576
+ }
1577
+ else if (key.startsWith('&:')) {
1578
+ const pseudoClass = key.replace('&', '');
1579
+ cssString += `${pseudoClass} { ${convertStylesToCSS$2('', styles[key])} }`;
1580
+ }
1581
+ else {
1582
+ const cssKey = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
1583
+ cssString += `${cssKey}: ${styles[key]}; `;
1584
+ }
1585
+ }
1586
+ cssString += '}';
1587
+ return cssString;
1588
+ };
1589
+ // Custom hook for dynamic styles with generics
1590
+ const useDynamicStyles$2 = (stylesObj) => {
1591
+ const [classNames, setClassNames] = useState({});
1592
+ useEffect(() => {
1593
+ const convertedStyles = Object.keys(stylesObj).map((key) => {
1594
+ const className = generateClassName$2(key);
1595
+ const styleElement = document.createElement('style');
1596
+ styleElement.innerHTML = convertStylesToCSS$2(className, stylesObj[key]);
1597
+ document.head.appendChild(styleElement);
1598
+ return {
1599
+ key,
1600
+ className,
1601
+ styleElement,
1602
+ };
1603
+ });
1604
+ setClassNames(convertedStyles.reduce((classNameEntity, convertedStyle) => {
1605
+ return {
1606
+ ...classNameEntity,
1607
+ [convertedStyle.key]: convertedStyle.className,
1608
+ };
1609
+ }, {}));
1610
+ return () => {
1611
+ convertedStyles.forEach((convertedStyle) => {
1612
+ document.head.removeChild(convertedStyle.styleElement);
1613
+ });
1614
+ };
1615
+ }, []);
1616
+ return classNames;
1617
+ };
1618
+
1619
+ // Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
1620
+ // Use of this source code is governed by a BSD-style
1621
+ // license that can be found in the LICENSE file.
1622
+
1567
1623
  // Copyright 2023 UDSM DHIS2 Lab. All rights reserved.
1568
1624
  // Use of this source code is governed by a BSD-style
1569
1625
  // license that can be found in the LICENSE file.
@@ -3141,7 +3197,7 @@ class LineListService {
3141
3197
  const filterParams = buildFilters(filters);
3142
3198
  const dateFilter = startDate && endDate ? `&programStartDate=${startDate}&programEndDate=${endDate}` : '';
3143
3199
  const ouModeIdentifier = ouMode ? `&ouMode=${ouMode}` : ``;
3144
- return this.httpClient.get(`trackedEntityInstances.json?program=${programId}&ou=${orgUnit}${ouModeIdentifier}&page=${page}&pageSize=${pageSize}&fields=trackedEntityInstance,attributes[*,displayInList],enrollments[*]&totalPages=true&${filterParams}${dateFilter}&order=created:desc`).pipe(map$1((response) => {
3200
+ return this.httpClient.get(`trackedEntityInstances.json?program=${programId}&ou=${orgUnit}${ouModeIdentifier}&page=${page}&pageSize=${pageSize}&fields=trackedEntityInstance,orgUnit,attributes[*,displayInList],enrollments[*]&totalPages=true&${filterParams}${dateFilter}&order=created:desc`).pipe(map$1((response) => {
3145
3201
  //TODO: use the api to to order by desc
3146
3202
  // response.trackedEntityInstances.reverse();
3147
3203
  return response;
@@ -3217,20 +3273,21 @@ const getTrackedEntityData = (response, programId, pager, filters) => {
3217
3273
  let teis = response.data
3218
3274
  .trackedEntityInstances;
3219
3275
  let programMetaData = response.metadata.programTrackedEntityAttributes;
3220
- //let progrmaMetaDataAttributesMap = programMetaData.reduce
3276
+ //organisation units from metadata
3277
+ let orgUnitsFromMetaData = response.metadata.organisationUnits;
3278
+ //a Map to efficiently look up orgUnit names by ID
3279
+ const orgUnitMap = new Map(orgUnitsFromMetaData.map((ou) => [ou.id, ou.name]));
3221
3280
  const mappedProgramMetadataAttributes = programMetaData.map((attr) => ({
3222
3281
  displayInList: attr.displayInList,
3223
3282
  searchable: attr.searchable,
3224
3283
  id: attr.trackedEntityAttribute.id,
3225
3284
  }));
3226
- console.log('the mapped attributes', mappedProgramMetadataAttributes);
3227
3285
  const programAttributesMap = programMetaData.reduce((acc, attribute) => {
3228
3286
  if (attribute.displayInList) {
3229
3287
  acc[attribute.id] = attribute.displayName;
3230
3288
  }
3231
3289
  return acc;
3232
3290
  }, {});
3233
- console.log('metadat attributes', programMetaData);
3234
3291
  if (filters) {
3235
3292
  teis = getFilteredTrackedEntityInstances(teis, filters);
3236
3293
  }
@@ -3242,11 +3299,6 @@ const getTrackedEntityData = (response, programId, pager, filters) => {
3242
3299
  : tei.attributes;
3243
3300
  attributes.forEach((attr) => allAttributes.add(attr.attribute));
3244
3301
  });
3245
- console.log('all of the attributes', allAttributes);
3246
- // const filteredAttributes = new Set(
3247
- // [...allAttributes].filter((attributeId) => programAttributesMap[attributeId])
3248
- // );
3249
- // console.log('Filtered All Attributes:', filteredAttributes);
3250
3302
  let entityColumns = Array.from(allAttributes).map((attrId) => {
3251
3303
  const foundAttribute = teis
3252
3304
  .flatMap((tei) => {
@@ -3265,6 +3317,15 @@ const getTrackedEntityData = (response, programId, pager, filters) => {
3265
3317
  // Check if the column label is part of any value in programAttributesMap
3266
3318
  return Object.values(programAttributesMap).some((value) => value.includes(column.label));
3267
3319
  });
3320
+ // entityColumns.push({
3321
+ // label: 'Location',
3322
+ // key: 'orgUnit',
3323
+ // });
3324
+ // Add orgUnit as the first column
3325
+ entityColumns.unshift({
3326
+ label: 'Location',
3327
+ key: 'orgUnit',
3328
+ });
3268
3329
  const searchableAttributes = mappedProgramMetadataAttributes
3269
3330
  .filter((attr) => attr.searchable && attr.displayInList)
3270
3331
  .map((attr) => ({
@@ -3275,8 +3336,6 @@ const getTrackedEntityData = (response, programId, pager, filters) => {
3275
3336
  const searchableKeys = new Set(searchableAttributes.map((attr) => attr.key));
3276
3337
  // Filter entityColumns to keep only those whose key exists in searchableAttributes
3277
3338
  const filteredEntityColumns = entityColumns.filter((column) => searchableKeys.has(column.key));
3278
- console.log('show the filtered values', filteredEntityColumns);
3279
- console.log('the answer is ', searchableAttributes);
3280
3339
  const attributesData = teis.map((tei, idx) => {
3281
3340
  let row = {
3282
3341
  trackedEntityInstance: tei.trackedEntityInstance,
@@ -3288,9 +3347,10 @@ const getTrackedEntityData = (response, programId, pager, filters) => {
3288
3347
  ? matchingEnrollment.attributes
3289
3348
  : tei.attributes;
3290
3349
  attributesToUse.forEach((attr) => (row[attr.attribute] = attr.value));
3350
+ // Access the name of the orgUnit using the Map
3351
+ row['orgUnit'] = orgUnitMap.get(tei.orgUnit) || tei.orgUnit || 'N/A';
3291
3352
  return row;
3292
3353
  });
3293
- console.log('attributes to use', entityColumns);
3294
3354
  return { columns: entityColumns, data: attributesData, filteredEntityColumns: filteredEntityColumns };
3295
3355
  };
3296
3356
  const getEventData = (response, pager) => {
@@ -3360,6 +3420,9 @@ class LineListTableComponent extends ReactWrapperModule {
3360
3420
  const [filteredColumns, setFilteredColumns] = useState();
3361
3421
  const [inputValues, setInputValues] = useState({});
3362
3422
  const [filters, setFilters] = useState(false);
3423
+ //TODO: this will be migrated on the parent component
3424
+ const [checkValue, setCheckValue] = useState([]);
3425
+ const [valueMatch, setValuesMatch] = useState(false);
3363
3426
  // Store updaters in refs for Angular to access
3364
3427
  const updateRefs = useRef({
3365
3428
  setProgramIdState,
@@ -3409,10 +3472,16 @@ class LineListTableComponent extends ReactWrapperModule {
3409
3472
  const { columns, data, filteredEntityColumns } = getTrackedEntityData(response, this.programId, pager, this.filters);
3410
3473
  entityColumns = columns;
3411
3474
  entityData = data;
3412
- let firstTei = response.data
3413
- ?.trackedEntityInstances?.[0]?.trackedEntityInstance;
3414
- if (firstTei) {
3415
- this.firstValue.emit(firstTei);
3475
+ //TODO: Should be done on the parent
3476
+ let checkValues = [
3477
+ ...new Set(response.data?.trackedEntityInstances?.[0]?.enrollments?.[0]?.events?.flatMap((event) => event.dataValues
3478
+ .filter((dataValue) => /^[A-Za-z]{3}$/.test(dataValue.value))
3479
+ .map((dataValue) => dataValue.value))),
3480
+ ];
3481
+ if (checkValues) {
3482
+ // this.firstValue.emit(firstTei);
3483
+ setCheckValue(checkValues);
3484
+ setValuesMatch(!checkValues.includes(this.buttonFilter));
3416
3485
  }
3417
3486
  // setFilteredColumns(filteredEntityColumns);
3418
3487
  // Ensure filter inputs do not disappear when no data is returned
@@ -3504,9 +3573,12 @@ class LineListTableComponent extends ReactWrapperModule {
3504
3573
  padding: 8,
3505
3574
  marginBottom: 16,
3506
3575
  } },
3507
- this.dispatchTeis &&
3508
- (isButtonLoading ? (React__default.createElement(CircularLoader$1, { small: true })) : (React__default.createElement(Button$1, { style: {
3509
- backgroundColor: '#3b82f6', // Blue color
3576
+ valueMatch &&
3577
+ this.dispatchTeis &&
3578
+ (isButtonLoading ? (React__default.createElement(CircularLoader$1, { small: true })) : (
3579
+ //TODO: this implementation should be done where the library will be consumed
3580
+ React__default.createElement(Button$1, { style: {
3581
+ backgroundColor: '#3b82f6',
3510
3582
  color: 'white',
3511
3583
  padding: '8px 16px',
3512
3584
  borderRadius: '4px',
@@ -3555,7 +3627,7 @@ class LineListTableComponent extends ReactWrapperModule {
3555
3627
  React__default.createElement(DataTableRow, null, columns.map((col) => (React__default.createElement(DataTableColumnHeader, { key: col.key }, col.label))))),
3556
3628
  React__default.createElement(TableBody, null, data.length > 0 ? (data.map((row) => (React__default.createElement(DataTableRow, { key: row.index }, columns.map((col) => (React__default.createElement(DataTableCell, { key: col.key }, col.key === 'actions' ? (React__default.createElement(DropdownMenu$1, { dropdownOptions: getDropdownOptions(row), onClick: (option) => option.onClick?.() })) : (row[col.key] || '--' // Display '--' if value is undefined or null
3557
3629
  )))))))) : (React__default.createElement(DataTableRow, null,
3558
- React__default.createElement(DataTableCell, { colSpan: columns.length, style: {
3630
+ React__default.createElement(DataTableCell, { colSpan: columns.length?.toString(), style: {
3559
3631
  textAlign: 'center',
3560
3632
  fontWeight: 'bold',
3561
3633
  color: 'grey',
@@ -3608,7 +3680,7 @@ class LineListTableComponent extends ReactWrapperModule {
3608
3680
  this.render();
3609
3681
  }
3610
3682
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.8", ngImport: i0, type: LineListTableComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
3611
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.8", type: LineListTableComponent, isStandalone: false, selector: "app-line-list", inputs: { programId: "programId", orgUnit: "orgUnit", programStageId: "programStageId", actionOptions: "actionOptions", attributeFilters: "attributeFilters", startDate: "startDate", endDate: "endDate", filters: "filters", ouMode: "ouMode", dispatchTeis: "dispatchTeis", showFilterButton: "showFilterButton", isButtonLoading: "isButtonLoading", buttonLabel: "buttonLabel" }, outputs: { actionSelected: "actionSelected", approvalSelected: "approvalSelected", firstValue: "firstValue" }, usesInheritance: true, usesOnChanges: true, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["[data-test=dhis2-uiwidgets-pagination]{display:flex!important;flex-direction:row!important;justify-content:space-between!important;align-items:center!important}\n"] }); }
3683
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.8", type: LineListTableComponent, isStandalone: false, selector: "app-line-list", inputs: { programId: "programId", orgUnit: "orgUnit", programStageId: "programStageId", actionOptions: "actionOptions", attributeFilters: "attributeFilters", startDate: "startDate", endDate: "endDate", filters: "filters", ouMode: "ouMode", dispatchTeis: "dispatchTeis", showFilterButton: "showFilterButton", isButtonLoading: "isButtonLoading", buttonLabel: "buttonLabel", buttonFilter: "buttonFilter" }, outputs: { actionSelected: "actionSelected", approvalSelected: "approvalSelected", firstValue: "firstValue" }, usesInheritance: true, usesOnChanges: true, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["[data-test=dhis2-uiwidgets-pagination]{display:flex!important;flex-direction:row!important;justify-content:space-between!important;align-items:center!important}\n"] }); }
3612
3684
  }
3613
3685
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.8", ngImport: i0, type: LineListTableComponent, decorators: [{
3614
3686
  type: Component,
@@ -3645,6 +3717,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.8", ngImpor
3645
3717
  type: Input
3646
3718
  }], firstValue: [{
3647
3719
  type: Output
3720
+ }], buttonFilter: [{
3721
+ type: Input
3648
3722
  }] } });
3649
3723
 
3650
3724
  class LineListModule {
@@ -3814,7 +3888,7 @@ class DataTableUIComponent extends ReactWrapperModule {
3814
3888
  React__default.createElement(DropdownMenu, { dropdownOptions: getDropdownOptions(row), onClick: (option) => option.onClick?.(row) }))))))),
3815
3889
  React__default.createElement(TableFoot, null,
3816
3890
  React__default.createElement(DataTableRow, null,
3817
- React__default.createElement(DataTableCell, { colSpan: columns.length + 1 })))))));
3891
+ React__default.createElement(DataTableCell, { colSpan: (columns.length || 0 + 1).toString() })))))));
3818
3892
  };
3819
3893
  }
3820
3894
  async ngAfterViewInit() {
@@ -4797,6 +4871,230 @@ class FormValue {
4797
4871
  // Use of this source code is governed by a BSD-style
4798
4872
  // license that can be found in the LICENSE file.
4799
4873
 
4874
+ // Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
4875
+ // Use of this source code is governed by a BSD-style
4876
+ // license that can be found in the LICENSE file.
4877
+ const useFieldValidation = (props) => {
4878
+ const { form, touched, field, value } = props;
4879
+ const hasError = useMemo(() => {
4880
+ return touched && (form.get(field.id) || form.get(field.key))?.invalid;
4881
+ }, [value, touched]);
4882
+ const validationError = useMemo(() => {
4883
+ if (!touched) {
4884
+ return undefined;
4885
+ }
4886
+ const error = (form.get(field.id) || form.get(field.key))?.errors;
4887
+ if (!error) {
4888
+ return undefined;
4889
+ }
4890
+ if (error['min']) {
4891
+ return `${field.label || 'Value'} should not be less than ${error['min'].min}!`;
4892
+ }
4893
+ else if (error['max']) {
4894
+ return `${field.label || 'Value'} should not be greater than ${error['max'].max}!`;
4895
+ }
4896
+ else if (error['pattern']) {
4897
+ if (field.type === 'email') {
4898
+ return `The email address provided is not valid`;
4899
+ }
4900
+ else if (field.type === 'tel') {
4901
+ return 'Phone number provided is not valid';
4902
+ }
4903
+ else {
4904
+ return `${field.label || 'Value'} should have appropriate format`;
4905
+ }
4906
+ }
4907
+ return error['required']
4908
+ ? `${field.label || 'Value'} is required`
4909
+ : undefined;
4910
+ }, [hasError]);
4911
+ return { hasError, validationError };
4912
+ };
4913
+
4914
+ // Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
4915
+ // Use of this source code is governed by a BSD-style
4916
+ // license that can be found in the LICENSE file.
4917
+
4918
+ // Copyright 2025 UDSM DHIS2 Lab. All rights reserved.
4919
+ // Use of this source code is governed by a BSD-style
4920
+ // license that can be found in the LICENSE file.
4921
+ // @flow
4922
+ const orgUnitFieldStyles = {
4923
+ container: {
4924
+ border: '1px solid',
4925
+ borderColor: colors$1.grey400,
4926
+ borderRadius: '3px',
4927
+ height: '100%',
4928
+ width: '100%',
4929
+ zIndex: 0,
4930
+ backgroundColor: 'white',
4931
+ },
4932
+ searchField: {
4933
+ height: '40px',
4934
+ overflow: 'hidden',
4935
+ borderBottomLeftRadius: 0,
4936
+ borderBottomRightRadius: 0,
4937
+ boxShadow: 'none',
4938
+ border: 'none',
4939
+ },
4940
+ debounceFieldContainer: {
4941
+ padding: '8px',
4942
+ background: colors$1.grey100,
4943
+ borderTopLeftRadius: '3px',
4944
+ borderTopRightRadius: '3px',
4945
+ },
4946
+ orgUnitTreeContainer: {
4947
+ borderBottomLeftRadius: '3px',
4948
+ borderBottomRightRadius: '3px',
4949
+ overflow: 'auto',
4950
+ },
4951
+ orgUnitTreeLoader: {
4952
+ padding: '8px',
4953
+ },
4954
+ orgUnitFieldLabel: {
4955
+ marginBottom: '8px',
4956
+ display: 'block',
4957
+ boxSizing: 'border-box',
4958
+ fontSize: '14px',
4959
+ lineHeight: '19px',
4960
+ color: 'rgb(33, 41, 52)',
4961
+ padding: '0',
4962
+ },
4963
+ orgUnitFieldRequiredLabel: {
4964
+ paddingInlineStart: '4px',
4965
+ },
4966
+ };
4967
+ const OrgUnitFormField = (props) => {
4968
+ const { onSelectOrgUnit, label, required, selected, disabled, key } = props;
4969
+ const d2 = window?.d2Web;
4970
+ const classes = useDynamicStyles$2(orgUnitFieldStyles);
4971
+ const config = useMemo(() => {
4972
+ return d2.systemInfo?.toInitObject();
4973
+ }, []);
4974
+ const rootOrgUnits = useMemo(() => {
4975
+ return d2.currentUser?.organisationUnits || [];
4976
+ }, []);
4977
+ const getExpandedItems = () => {
4978
+ if (rootOrgUnits && rootOrgUnits.length === 1) {
4979
+ return [`/${rootOrgUnits[0].id}`];
4980
+ }
4981
+ else if (rootOrgUnits?.length > 1) {
4982
+ return rootOrgUnits.map((root) => root.path);
4983
+ }
4984
+ return undefined;
4985
+ };
4986
+ const initiallyExpanded = getExpandedItems();
4987
+ const [searchText, setSearchText] = useState(undefined);
4988
+ const [loading, setLoading] = useState(true);
4989
+ const [searchLoading, setSearchLoading] = useState();
4990
+ const [searchData, setSearchData] = useState();
4991
+ const [selectedOrgUnit, setSelectedOrgUnit] = React__default.useState();
4992
+ const [expanded, setExpanded] = React__default.useState(initiallyExpanded);
4993
+ const [showOrgUnitTree, setShowOrgUnitTree] = useState(!selected);
4994
+ useEffect(() => {
4995
+ setLoading(true);
4996
+ if (selected) {
4997
+ d2.httpInstance
4998
+ .get(`organisationUnits/${selected}.json?fields=id,name,path,code,ancestors[id,name]`)
4999
+ .then((response) => {
5000
+ if (response.data) {
5001
+ setSelectedOrgUnit(response.data);
5002
+ handleExpand({ path: response.data['path'] });
5003
+ setLoading(false);
5004
+ }
5005
+ });
5006
+ }
5007
+ else {
5008
+ setLoading(false);
5009
+ }
5010
+ }, [selected]);
5011
+ const highlighted = useMemo(() => {
5012
+ if (selectedOrgUnit) {
5013
+ return [selectedOrgUnit['path']];
5014
+ }
5015
+ return undefined;
5016
+ }, [selectedOrgUnit]);
5017
+ const selectedOrgUnitLabel = useMemo(() => {
5018
+ if (selectedOrgUnit) {
5019
+ return [
5020
+ ...(selectedOrgUnit['ancestors'] || []),
5021
+ selectedOrgUnit,
5022
+ ]
5023
+ .map((orgUnit) => orgUnit['name'])
5024
+ .join(' / ');
5025
+ }
5026
+ return '';
5027
+ }, [selectedOrgUnit]);
5028
+ const handleExpand = ({ path }) => {
5029
+ if (expanded && !expanded.includes(path)) {
5030
+ setExpanded([...expanded, path]);
5031
+ }
5032
+ };
5033
+ const handleCollapse = ({ path }) => {
5034
+ const pathIndex = expanded?.indexOf(path);
5035
+ if (pathIndex && pathIndex !== -1 && expanded) {
5036
+ const updatedExpanded = pathIndex === 0
5037
+ ? expanded.slice(1)
5038
+ : [...expanded.slice(0, pathIndex), ...expanded.slice(pathIndex + 1)];
5039
+ setExpanded(updatedExpanded);
5040
+ }
5041
+ };
5042
+ const handleOrgUnitSearch = async (query) => {
5043
+ setSearchLoading(true);
5044
+ try {
5045
+ const searchResponse = await d2.httpInstance.get(`organisationUnits.json?fields=id,displayName,path,publicAccess,access,lastUpdated,children[id,displayName,publicAccess,access,path,children::isNotEmpty]&paging=true&query=${query}&withinUserSearchHierarchy=true&pageSize=15`);
5046
+ setSearchData((searchResponse.data || {})['organisationUnits']);
5047
+ setSearchLoading(false);
5048
+ }
5049
+ catch (e) {
5050
+ setSearchLoading(false);
5051
+ }
5052
+ };
5053
+ const debouncedSearch = useCallback(debounce((value) => {
5054
+ handleOrgUnitSearch(value);
5055
+ }, 500), []);
5056
+ const handleFilterChange = (event) => {
5057
+ setSearchText(event.value);
5058
+ debouncedSearch(event.value);
5059
+ };
5060
+ const ready = useMemo(() => {
5061
+ return searchText?.length ? !searchLoading : !loading;
5062
+ }, [searchText, searchLoading, loading]);
5063
+ const renderOrgUnitTree = () => {
5064
+ if (searchText && searchText?.length > 0) {
5065
+ if (searchLoading) {
5066
+ return (React__default.createElement("div", { className: classes.orgUnitTreeLoader },
5067
+ React__default.createElement(CircularLoader$1, { small: true })));
5068
+ }
5069
+ return (ready && (React__default.createElement(OrganisationUnitTree, { key: `${key}-search`, roots: searchData?.map((orgUnit) => orgUnit.id) || [], singleSelection: true, expanded: expanded, handleExpand: handleExpand, handleCollapse: handleCollapse, selected: highlighted, onChange: (event) => {
5070
+ setSelectedOrgUnit(event);
5071
+ onSelectOrgUnit(event.id);
5072
+ } })));
5073
+ }
5074
+ return (React__default.createElement(OrganisationUnitTree, { key: key, roots: rootOrgUnits?.map((orgUnit) => orgUnit.id) || [], singleSelection: true, expanded: expanded, handleExpand: handleExpand, handleCollapse: handleCollapse, selected: highlighted, onChange: (event) => {
5075
+ setSelectedOrgUnit(event);
5076
+ onSelectOrgUnit(event.id);
5077
+ } }));
5078
+ };
5079
+ const handleBlur = () => {
5080
+ // onBlur && onBlur(null);
5081
+ };
5082
+ return (config && (React__default.createElement(Provider, { config: config, plugin: false, parentAlertsAdd: undefined, showAlertsInPlugin: false },
5083
+ React__default.createElement("div", null,
5084
+ label && (React__default.createElement("div", { className: classes.orgUnitFieldLabel },
5085
+ React__default.createElement("span", null, label),
5086
+ required ? (React__default.createElement("span", { className: classes.orgUnitFieldRequiredLabel }, "*")) : (React__default.createElement(React__default.Fragment, null)))),
5087
+ selected && !showOrgUnitTree ? (selectedOrgUnit ? (React__default.createElement(Tooltip, { content: selectedOrgUnitLabel },
5088
+ React__default.createElement(Chip, { onRemove: () => {
5089
+ setShowOrgUnitTree(true);
5090
+ } }, selectedOrgUnit.name))) : (React__default.createElement("div", { className: classes.orgUnitTreeLoader },
5091
+ React__default.createElement(CircularLoader$1, { small: true })))) : (React__default.createElement("div", { className: classes.container },
5092
+ React__default.createElement("div", { className: classes.debounceFieldContainer },
5093
+ React__default.createElement(InputField$1, { placeholder: "Search", value: searchText, disabled: disabled, onChange: handleFilterChange })),
5094
+ loading ? (React__default.createElement("div", { className: classes.orgUnitTreeLoader },
5095
+ React__default.createElement(CircularLoader$1, { small: true }))) : (React__default.createElement("div", { className: classes.orgUnitTreeContainer, style: { maxHeight: '200px', overflowY: 'auto' } }, renderOrgUnitTree()))))))));
5096
+ };
5097
+
4800
5098
  // Copyright 2024 UDSM DHIS2 Lab. All rights reserved.
4801
5099
  // Use of this source code is governed by a BSD-style
4802
5100
  // license that can be found in the LICENSE file.
@@ -4886,45 +5184,15 @@ class BaseFormFieldComponent extends ReactWrapperModule {
4886
5184
  }
4887
5185
  return [];
4888
5186
  }, [value]);
4889
- const hasError = useMemo(() => {
4890
- return (touched &&
4891
- (this.form().get(this.field().id) ||
4892
- this.form().get(this.field().key))?.invalid);
4893
- }, [value, touched]);
4894
- const validationError = useMemo(() => {
4895
- if (!touched) {
4896
- return undefined;
4897
- }
4898
- const field = this.field();
4899
- const error = (this.form().get(field.id) || this.form().get(field.key))
4900
- ?.errors;
4901
- if (!error) {
4902
- return undefined;
4903
- }
4904
- if (error['min']) {
4905
- return `${this.label() || 'Value'} should not be less than ${error['min'].min}!`;
4906
- }
4907
- else if (error['max']) {
4908
- return `${this.label() || 'Value'} should not be greater than ${error['max'].max}!`;
4909
- }
4910
- else if (error['pattern']) {
4911
- if (field.type === 'email') {
4912
- return `The email address provided is not valid`;
4913
- }
4914
- else if (field.type === 'tel') {
4915
- return 'Phone number provided is not valid';
4916
- }
4917
- else {
4918
- return `${this.label() || 'Value'} should have appropriate format`;
4919
- }
4920
- }
4921
- return error['required']
4922
- ? `${this.label() || 'Value'} is required`
4923
- : undefined;
4924
- }, [hasError]);
5187
+ const { hasError, validationError } = useFieldValidation({
5188
+ field: this.field(),
5189
+ form: this.form(),
5190
+ value,
5191
+ touched,
5192
+ });
4925
5193
  switch (this.field().controlType) {
4926
5194
  case 'textarea':
4927
- return (React__default.createElement(TextAreaField, { error: hasError, validationText: validationError, type: this.field().type, inputWidth: this.fieldConfig()?.inputWidth, required: this.field().required, name: this.field().id, label: this.label(), min: this.field().min?.toString(), max: this.field().max?.toString(), disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, placeholder: this.placeholder(), value: value, onChange: (event) => {
5195
+ return (React__default.createElement(TextAreaField, { error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, required: this.field().required, name: this.field().id, label: this.label(), rows: 5, placeholder: this.placeholder(), value: value, onChange: (event) => {
4928
5196
  this.ngZone.run(() => {
4929
5197
  (this.form().get(this.field().id) ||
4930
5198
  this.form().get(this.field().key))?.setValue(event.value);
@@ -4941,34 +5209,14 @@ class BaseFormFieldComponent extends ReactWrapperModule {
4941
5209
  });
4942
5210
  } }));
4943
5211
  case 'org-unit':
4944
- return (React__default.createElement(React__default.Fragment, null,
4945
- React__default.createElement(InputField$1, { error: hasError, validationText: validationError, type: this.field().type, prefixIcon: React__default.createElement(IconDimensionOrgUnit16, null), required: this.field().required, name: this.field().id, label: this.label(), placeholder: this.placeholder(), value: displayValue, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, onFocus: () => {
4946
- setShowOrgUnit(true);
4947
- }, onBlur: () => {
4948
- this.ngZone.run(() => {
4949
- this.update.emit({ form: this.form(), value });
4950
- });
4951
- setTouched(true);
4952
- } }),
4953
- showOrgUnit && (React__default.createElement(this.FieldOrgUnitSelector, { selectedOrgUnits: [], onSelectOrgUnit: (selectedOrgUnits) => {
4954
- if ((selectedOrgUnits || [])[0]) {
4955
- const selectedOrgUnit = selectedOrgUnits[0];
4956
- setDisplayValue(selectedOrgUnit.name);
4957
- setValue(selectedOrgUnit.id);
4958
- setShowOrgUnit(false);
4959
- setTouched(true);
4960
- this.ngZone.run(() => {
4961
- (this.form().get(this.field().id) ||
4962
- this.form().get(this.field().key))?.setValue(selectedOrgUnit.id);
4963
- this.immediateUpdate.emit({
4964
- form: this.form(),
4965
- value: selectedOrgUnit.id,
4966
- });
4967
- });
4968
- }
4969
- } }))));
5212
+ return (React__default.createElement(OrgUnitFormField, { label: this.label(), key: this.field().id, required: this.field().required, onSelectOrgUnit: (selectedOrgUnit) => {
5213
+ setValue(selectedOrgUnit);
5214
+ this.ngZone.run(() => {
5215
+ this.update.emit({ form: this.form(), value });
5216
+ });
5217
+ }, selected: value, onBlur: (event) => { } }));
4970
5218
  case 'transfer':
4971
- return (React__default.createElement(Transfer, { filterable: true, filterPlaceholder: "Search", selected: selected, error: hasError, validationText: validationError, type: this.field().type, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, required: this.field().required, name: this.field().id, label: this.label(), placeholder: this.placeholder(), value: selected, leftHeader: React__default.createElement("div", { style: {
5219
+ return (React__default.createElement(Transfer, { filterable: true, filterPlaceholder: "Search", selected: selected, leftHeader: React__default.createElement("div", { style: {
4972
5220
  fontSize: 14,
4973
5221
  padding: '8px 4px',
4974
5222
  } }, this.field().availableOptionsLabel), rightHeader: React__default.createElement("div", { style: {
@@ -4985,7 +5233,10 @@ class BaseFormFieldComponent extends ReactWrapperModule {
4985
5233
  this.update.emit({ form: this.form(), value });
4986
5234
  } }));
4987
5235
  case 'checkbox':
4988
- return (React__default.createElement(Checkbox, { checked: Boolean(value), error: hasError, label: this.label(), name: this.field().id, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, onChange: (event) => {
5236
+ return (React__default.createElement(Checkbox, { checked: Boolean(value), error: hasError, label: this.label(), name: this.field().id, disabled: this.field()?.disabled ||
5237
+ this.field()?.generated ||
5238
+ this.field()?.unique ||
5239
+ false, onChange: (event) => {
4989
5240
  this.ngZone.run(() => {
4990
5241
  (this.form().get(this.field().id) ||
4991
5242
  this.form().get(this.field().key))?.setValue(event.checked);
@@ -4998,7 +5249,10 @@ class BaseFormFieldComponent extends ReactWrapperModule {
4998
5249
  });
4999
5250
  } }));
5000
5251
  case 'dropdown':
5001
- return (React__default.createElement(SingleSelectField, { filterable: (this.field().options || []).length > 5, clearable: true, error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, required: this.field().required, name: this.field().id, className: "select", label: this.label(), selected: value, onChange: (event) => {
5252
+ return (React__default.createElement(SingleSelectField, { filterable: (this.field().options || []).length > 5, clearable: true, error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled ||
5253
+ this.field()?.generated ||
5254
+ this.field()?.unique ||
5255
+ false, required: this.field().required, className: "select", label: this.label(), selected: value, onChange: (event) => {
5002
5256
  this.ngZone.run(() => {
5003
5257
  (this.form().get(this.field().id) ||
5004
5258
  this.form().get(this.field().key))?.setValue(event.selected);
@@ -5011,7 +5265,10 @@ class BaseFormFieldComponent extends ReactWrapperModule {
5011
5265
  setTouched(true);
5012
5266
  } }, (this.field().options || []).map((option) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || option.key, label: option.label, value: option.value })))));
5013
5267
  case 'multi-dropdown':
5014
- return (React__default.createElement(MultiSelectField, { clearText: "Clear", clearable: true, empty: "No data found", filterable: (this.field().options || []).length > 5, filterPlaceholder: "Type to filter options", error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, label: this.label(), name: this.field().id, required: this.field().required, loadingText: "Loading options", noMatchText: "No options found", onChange: (event) => {
5268
+ return (React__default.createElement(MultiSelectField, { clearText: "Clear", clearable: true, empty: "No data found", filterable: (this.field().options || []).length > 5, filterPlaceholder: "Type to filter options", error: hasError, validationText: validationError, inputWidth: this.fieldConfig()?.inputWidth, disabled: this.field()?.disabled ||
5269
+ this.field()?.generated ||
5270
+ this.field()?.unique ||
5271
+ false, label: this.label(), required: this.field().required, loadingText: "Loading options", noMatchText: "No options found", onChange: (event) => {
5015
5272
  const selectedValue = (event.selected || []).join(',');
5016
5273
  this.ngZone.run(() => {
5017
5274
  (this.form().get(this.field().id) ||
@@ -5025,7 +5282,7 @@ class BaseFormFieldComponent extends ReactWrapperModule {
5025
5282
  setTouched(true);
5026
5283
  }, selected: arrayValue }, (this.field().options || []).map((option) => (React__default.createElement(MultiSelectOption, { key: option.key, label: option.label, value: option.value })))));
5027
5284
  case 'file':
5028
- return (React__default.createElement(FileInputField, { accept: "*", buttonLabel: "Upload a file", label: this.label(), name: this.field().id, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, files: this.uploadedFiles, onChange: (event) => {
5285
+ return (React__default.createElement(FileInputField, { accept: "*", buttonLabel: "Upload a file", label: this.label(), name: this.field().id, onChange: (event) => {
5029
5286
  this.uploadedFiles.push((event?.files || [])[0]);
5030
5287
  this.ngZone.run(() => {
5031
5288
  this.form().get(this.field().id) ||
@@ -5047,7 +5304,13 @@ class BaseFormFieldComponent extends ReactWrapperModule {
5047
5304
  });
5048
5305
  }, removeText: "Remove" })))));
5049
5306
  default:
5050
- return (React__default.createElement(InputField$1, { error: hasError, validationText: validationError, type: this.field().type, inputWidth: this.fieldConfig()?.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, disabled: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, readOnly: this.field()?.disabled || this.field()?.generated || this.field()?.unique || false, onChange: (event) => {
5307
+ return (React__default.createElement(InputField$1, { error: hasError, validationText: validationError, type: this.field().type, inputWidth: this.fieldConfig()?.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, disabled: this.field()?.disabled ||
5308
+ this.field()?.generated ||
5309
+ this.field()?.unique ||
5310
+ false, readOnly: this.field()?.disabled ||
5311
+ this.field()?.generated ||
5312
+ this.field()?.unique ||
5313
+ false, onChange: (event) => {
5051
5314
  this.ngZone.run(() => {
5052
5315
  (this.form().get(this.field().id) ||
5053
5316
  this.form().get(this.field().key))?.setValue(event.value);
@@ -5877,8 +6140,8 @@ class SelectionFiltersComponent extends ReactWrapperModule {
5877
6140
  setTouched(true);
5878
6141
  }
5879
6142
  } }))),
5880
- React__default.createElement(InputField$1, { className: "input-field", label: "Start Date", type: "DATE", value: filters.startDate, onChange: (event) => setFilters({ ...filters, startDate: event.value }), style: { width: '100%', height: '100%' } }),
5881
- React__default.createElement(InputField$1, { className: "input-field", label: "End Date", type: "DATE", value: filters.endDate, onChange: (event) => setFilters({ ...filters, endDate: event.value }), style: { width: '100%', height: '100%' } }),
6143
+ React__default.createElement(InputField$1, { className: "input-field", label: "Start Date", type: "date", value: filters.startDate, onChange: (event) => setFilters({ ...filters, startDate: event.value }) }),
6144
+ React__default.createElement(InputField$1, { className: "input-field", label: "End Date", type: "date", value: filters.endDate, onChange: (event) => setFilters({ ...filters, endDate: event.value }) }),
5882
6145
  React__default.createElement("div", { style: {
5883
6146
  display: 'flex',
5884
6147
  flexDirection: 'column',
@@ -5897,12 +6160,12 @@ class SelectionFiltersComponent extends ReactWrapperModule {
5897
6160
  display: 'grid',
5898
6161
  gridTemplateColumns: 'repeat(4, 1fr)',
5899
6162
  gap: '16px',
5900
- } }, filters.programAttributesFilters.map((filter, index) => (React__default.createElement("div", { key: index, style: { gridColumn: getAttributeColumnSpan(index) } }, filter.hasOptions && filter.options.length > 0 ? (React__default.createElement(SingleSelectField, { className: "single-select", label: filter.name || 'Select Option', type: filter.valueType, selected: filter.value, onChange: (event) => handleAttributeChange(index, event.selected) }, filter.options.map((option, i) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || i, label: option.name, value: option.code }))))) : (React__default.createElement(InputField$1, { className: "input-field", label: filter.name || 'Program name', type: filter.valueType, value: filter.value, onChange: (event) => handleAttributeChange(index, event.value) })))))),
6163
+ } }, filters.programAttributesFilters.map((filter, index) => (React__default.createElement("div", { key: index, style: { gridColumn: getAttributeColumnSpan(index) } }, filter.hasOptions && filter.options.length > 0 ? (React__default.createElement(SingleSelectField, { className: "single-select", label: filter.name || 'Select Option', selected: filter.value, onChange: (event) => handleAttributeChange(index, event.selected) }, filter.options.map((option, i) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || i, label: option.name, value: option.code }))))) : (React__default.createElement(InputField$1, { className: "input-field", label: filter.name || 'Program name', type: filter.valueType, value: filter.value, onChange: (event) => handleAttributeChange(index, event.value) })))))),
5901
6164
  React__default.createElement("div", { style: {
5902
6165
  display: 'grid',
5903
6166
  gridTemplateColumns: 'repeat(4, 1fr)',
5904
6167
  gap: '16px',
5905
- } }, filters.programStageDataElementFilters.map((filter, index) => (React__default.createElement("div", { key: index, style: { gridColumn: getDataElementColumnSpan(index) } }, filter.hasOptions && filter.options.length > 0 ? (React__default.createElement(SingleSelectField, { className: "single-select", label: filter.name || 'Aggregation type', type: filter.valueType, selected: filter.value, onChange: (event) => handleDataElementChange(index, event.selected) }, filter.options.map((option, i) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || i, label: option.name, value: option.code }))))) : (React__default.createElement(InputField$1, { className: "input-field", type: filter.valueType, label: filter.name || 'Program name', value: filter.value, onChange: (event) => handleDataElementChange(index, event.value) })))))),
6168
+ } }, filters.programStageDataElementFilters.map((filter, index) => (React__default.createElement("div", { key: index, style: { gridColumn: getDataElementColumnSpan(index) } }, filter.hasOptions && filter.options.length > 0 ? (React__default.createElement(SingleSelectField, { className: "single-select", label: filter.name || 'Aggregation type', selected: filter.value, onChange: (event) => handleDataElementChange(index, event.selected) }, filter.options.map((option, i) => (React__default.createElement(SingleSelectOption, { key: crypto.randomUUID() || i, label: option.name, value: option.code }))))) : (React__default.createElement(InputField$1, { className: "input-field", type: filter.valueType, label: filter.name || 'Program name', value: filter.value, onChange: (event) => handleDataElementChange(index, event.value) })))))),
5906
6169
  React__default.createElement("div", { style: { textAlign: 'right', marginTop: '16px' } },
5907
6170
  React__default.createElement(Button$1, { onClick: handleSearch }, "Search"))))));
5908
6171
  };