@openmrs/esm-fast-data-entry-app 1.0.1-pre.8 → 1.0.1-pre.85

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.
Files changed (86) hide show
  1. package/README.md +21 -2
  2. package/dist/132.js +1 -0
  3. package/dist/168.js +1 -0
  4. package/dist/229.js +1 -0
  5. package/dist/247.js +1 -0
  6. package/dist/255.js +1 -0
  7. package/dist/294.js +2 -0
  8. package/dist/294.js.LICENSE.txt +9 -0
  9. package/dist/32.js +1 -0
  10. package/dist/327.js +1 -0
  11. package/dist/403.js +2 -0
  12. package/dist/403.js.LICENSE.txt +14 -0
  13. package/dist/553.js +2 -0
  14. package/dist/553.js.LICENSE.txt +14 -0
  15. package/dist/574.js +1 -0
  16. package/dist/595.js +2 -0
  17. package/dist/595.js.LICENSE.txt +3 -0
  18. package/dist/617.js +1 -0
  19. package/dist/658.js +2 -0
  20. package/dist/658.js.LICENSE.txt +27 -0
  21. package/dist/68.js +2 -0
  22. package/dist/68.js.LICENSE.txt +21 -0
  23. package/dist/74.js +1 -0
  24. package/dist/757.js +1 -0
  25. package/dist/776.js +1 -0
  26. package/dist/804.js +1 -0
  27. package/dist/820.js +1 -0
  28. package/dist/935.js +2 -0
  29. package/dist/935.js.LICENSE.txt +19 -0
  30. package/dist/main.js +1 -0
  31. package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
  32. package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +612 -0
  33. package/dist/openmrs-esm-fast-data-entry-app.old +1 -0
  34. package/jest.config.json +2 -1
  35. package/package.json +9 -9
  36. package/src/CancelModal.tsx +48 -0
  37. package/src/CompleteModal.tsx +46 -0
  38. package/src/FormBootstrap.tsx +18 -3
  39. package/src/add-group-modal/AddGroupModal.tsx +80 -27
  40. package/src/add-group-modal/styles.scss +14 -4
  41. package/src/config-schema.ts +22 -0
  42. package/src/context/FormWorkflowContext.tsx +13 -1
  43. package/src/context/FormWorkflowReducer.ts +13 -3
  44. package/src/context/GroupFormWorkflowContext.tsx +41 -6
  45. package/src/context/GroupFormWorkflowReducer.ts +170 -12
  46. package/src/form-entry-workflow/FormEntryWorkflow.tsx +67 -101
  47. package/src/form-entry-workflow/styles.scss +2 -1
  48. package/src/forms-page/FormsPage.tsx +8 -3
  49. package/src/forms-page/forms-table/FormsTable.tsx +11 -5
  50. package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +13 -400
  51. package/src/group-form-entry-workflow/GroupSessionWorkspace.tsx +247 -0
  52. package/src/group-form-entry-workflow/SessionDetailsForm.tsx +122 -0
  53. package/src/group-form-entry-workflow/SessionMetaWorkspace.tsx +107 -0
  54. package/src/group-form-entry-workflow/attendance-table/AttendanceTable.tsx +105 -0
  55. package/src/group-form-entry-workflow/attendance-table/index.ts +1 -0
  56. package/src/group-form-entry-workflow/{group-banner/GroupBanner.test.tsx → group-display-header/GroupDisplayHeader.test.tsx} +2 -2
  57. package/src/group-form-entry-workflow/{group-banner/GroupBanner.tsx → group-display-header/GroupDisplayHeader.tsx} +23 -5
  58. package/src/group-form-entry-workflow/group-display-header/index.ts +3 -0
  59. package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +61 -28
  60. package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +5 -0
  61. package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +65 -8
  62. package/src/group-form-entry-workflow/group-search/group-search.scss +8 -6
  63. package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +11 -7
  64. package/src/group-form-entry-workflow/styles.scss +12 -1
  65. package/src/hooks/index.ts +1 -0
  66. package/src/hooks/useGetPatient.ts +1 -1
  67. package/src/hooks/useGetSystemSetting.ts +38 -0
  68. package/src/hooks/usePostEndpoint.ts +70 -0
  69. package/src/hooks/useSearchEndpoint.ts +120 -0
  70. package/src/hooks/useStartVisit.ts +92 -0
  71. package/src/patient-card/styles.scss +1 -0
  72. package/tools/i18next-parser.config.js +93 -0
  73. package/translations/en.json +27 -9
  74. package/translations/fr.json +50 -0
  75. package/.editorconfig +0 -12
  76. package/.eslintignore +0 -2
  77. package/.eslintrc.js +0 -10
  78. package/.husky/pre-push +0 -1
  79. package/.prettierignore +0 -14
  80. package/.yarn/plugins/@yarnpkg/plugin-version.cjs +0 -550
  81. package/.yarn/versions/7ee3eceb.yml +0 -0
  82. package/src/group-form-entry-workflow/group-banner/index.ts +0 -3
  83. package/src/group-form-entry-workflow/group-search/mock-group-data.ts +0 -79
  84. package/src/group-form-entry-workflow/group-search/useGroupSearch.ts +0 -14
  85. package/src/hooks/usePostCohort.ts +0 -18
  86. /package/src/group-form-entry-workflow/{group-banner → group-display-header}/styles.scss +0 -0
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useReducer } from "react";
1
+ import React, { useEffect, useReducer, useRef } from "react";
2
2
  import { SkeletonIcon, SkeletonText } from "@carbon/react";
3
3
  import { Events } from "@carbon/react/icons";
4
4
  import styles from "./compact-group-result.scss";
@@ -20,11 +20,64 @@ const reducer = (state, action) => {
20
20
  }
21
21
  };
22
22
 
23
- const CompactGroupResults = ({ groups, selectGroupAction }) => {
23
+ const scrollingOptions = {
24
+ behavior: "smooth",
25
+ block: "nearest",
26
+ };
27
+
28
+ const ResultItem = ({
29
+ index,
30
+ selectGroupAction,
31
+ group,
32
+ dispatch,
33
+ state,
34
+ totalGroups,
35
+ lastRef,
36
+ }) => {
37
+ const ref = useRef(null);
38
+ const { t } = useTranslation();
39
+
40
+ useEffect(() => {
41
+ if (state.selectedIndex === totalGroups - 1) {
42
+ lastRef.current.scrollIntoView(scrollingOptions);
43
+ } else if (state.selectedIndex === index) {
44
+ ref.current.scrollIntoView(scrollingOptions);
45
+ }
46
+ }, [state, index, totalGroups, lastRef]);
47
+
48
+ return (
49
+ <div
50
+ onClick={() => {
51
+ dispatch({ type: "select", payload: index });
52
+ selectGroupAction(group);
53
+ }}
54
+ className={`${styles.patientSearchResult} ${
55
+ index === state.selectedIndex && styles.patientSearchResultSelected
56
+ }`}
57
+ role="button"
58
+ aria-pressed={index === state.selectedIndex}
59
+ tabIndex={0}
60
+ ref={ref}
61
+ >
62
+ <div className={styles.patientAvatar} role="img">
63
+ <Events size={24} />
64
+ </div>
65
+ <div>
66
+ <h2 className={styles.patientName}>{group.name}</h2>
67
+ <p className={styles.demographics}>
68
+ {group.cohortMembers?.length ?? 0} {t("members", "members")}
69
+ <span className={styles.middot}>&middot;</span> {group.description}
70
+ </p>
71
+ </div>
72
+ </div>
73
+ );
74
+ };
75
+
76
+ const CompactGroupResults = ({ groups, selectGroupAction, lastRef }) => {
24
77
  const arrowUpPressed = useKeyPress("ArrowUp");
25
78
  const arrowDownPressed = useKeyPress("ArrowDown");
26
79
  const enterPressed = useKeyPress("Enter");
27
- const { t } = useTranslation();
80
+
28
81
  const [state, dispatch] = useReducer(reducer, { selectedIndex: 0 });
29
82
 
30
83
  useEffect(() => {
@@ -48,31 +101,11 @@ const CompactGroupResults = ({ groups, selectGroupAction }) => {
48
101
  return (
49
102
  <>
50
103
  {groups.map((group, index) => (
51
- <div
52
- onClick={() => {
53
- dispatch({ type: "select", payload: index });
54
- selectGroupAction(group);
55
- }}
56
- key={group.id}
57
- className={`${styles.patientSearchResult} ${
58
- index === state.selectedIndex && styles.patientSearchResultSelected
59
- }`}
60
- role="button"
61
- aria-pressed={index === state.selectedIndex}
62
- tabIndex={0}
63
- >
64
- <div className={styles.patientAvatar} role="img">
65
- <Events size={24} />
66
- </div>
67
- <div>
68
- <h2 className={styles.patientName}>{group.name}</h2>
69
- <p className={styles.demographics}>
70
- {group.members.length} {t("members", "members")}
71
- <span className={styles.middot}>&middot;</span>{" "}
72
- {group.description}
73
- </p>
74
- </div>
75
- </div>
104
+ <ResultItem
105
+ key={index}
106
+ totalGroups={groups.length}
107
+ {...{ lastRef, index, selectGroupAction, group, dispatch, state }}
108
+ />
76
109
  ))}
77
110
  </>
78
111
  );
@@ -4,6 +4,7 @@ import styles from "./compact-group-search.scss";
4
4
  import GroupSearch from "./GroupSearch";
5
5
  import { Button, Search } from "@carbon/react";
6
6
  import { useTranslation } from "react-i18next";
7
+ import debounce from "lodash-es/debounce";
7
8
 
8
9
  interface CompactGroupSearchProps {
9
10
  selectGroupAction?: (group: GroupType) => void;
@@ -23,6 +24,10 @@ const CompactGroupSearch: React.FC<CompactGroupSearchProps> = ({
23
24
  };
24
25
 
25
26
  const handleSearchChange = (e) => {
27
+ debounce((q) => {
28
+ setDropdownShown(!!e.length);
29
+ setQuery(q);
30
+ }, 300);
26
31
  setQuery(e);
27
32
  if (e.length) {
28
33
  setDropdownShown(true);
@@ -1,13 +1,13 @@
1
- import React from "react";
1
+ import React, { useCallback, useRef } from "react";
2
2
  import { useTranslation } from "react-i18next";
3
- import { Layer, Tile } from "@carbon/react";
3
+ import { Layer, Tile, Loading } from "@carbon/react";
4
4
  import styles from "./group-search.scss";
5
5
  import { EmptyDataIllustration } from "../../empty-state/EmptyDataIllustration";
6
- import { useGroupSearch } from "./useGroupSearch";
7
6
  import CompactGroupResults, {
8
7
  SearchResultSkeleton,
9
8
  } from "./CompactGroupResults";
10
9
  import { GroupType } from "../../context/GroupFormWorkflowContext";
10
+ import { useSearchCohortInfinite } from "../../hooks/useSearchEndpoint";
11
11
 
12
12
  interface GroupSearchProps {
13
13
  query: string;
@@ -19,8 +19,48 @@ const GroupSearch: React.FC<GroupSearchProps> = ({
19
19
  selectGroupAction,
20
20
  }) => {
21
21
  const { t } = useTranslation();
22
- const results = useGroupSearch(query);
23
- const error = false;
22
+ const {
23
+ isLoading,
24
+ data: results,
25
+ error,
26
+ loadingNewData,
27
+ setPage,
28
+ hasMore,
29
+ totalResults,
30
+ } = useSearchCohortInfinite({
31
+ searchTerm: query,
32
+ searching: !!query,
33
+ parameters: {
34
+ v: "full",
35
+ },
36
+ });
37
+
38
+ const lastItem = useRef(null);
39
+ const observer = useRef(null);
40
+ const loadingRef = useCallback(
41
+ (node) => {
42
+ if (loadingNewData) {
43
+ return;
44
+ }
45
+ if (observer.current) {
46
+ observer.current.disconnect();
47
+ }
48
+ observer.current = new IntersectionObserver(
49
+ (entries) => {
50
+ if (entries[0].isIntersecting && hasMore) {
51
+ setPage((page) => page + 1);
52
+ }
53
+ },
54
+ {
55
+ threshold: 0.75,
56
+ }
57
+ );
58
+ if (node) {
59
+ observer.current.observe(node);
60
+ }
61
+ },
62
+ [loadingNewData, hasMore, setPage]
63
+ );
24
64
 
25
65
  if (error) {
26
66
  return (
@@ -43,9 +83,19 @@ const GroupSearch: React.FC<GroupSearchProps> = ({
43
83
  );
44
84
  }
45
85
 
46
- if (query.length <= 2) return <SearchResultSkeleton />;
86
+ if (isLoading) {
87
+ return (
88
+ <div className={styles.searchResultsContainer}>
89
+ <SearchResultSkeleton />
90
+ <SearchResultSkeleton />
91
+ <SearchResultSkeleton />
92
+ <SearchResultSkeleton />
93
+ <SearchResultSkeleton />
94
+ </div>
95
+ );
96
+ }
47
97
 
48
- if (results.length === 0) {
98
+ if (results?.length === 0) {
49
99
  return (
50
100
  <div className={styles.searchResults}>
51
101
  <Layer>
@@ -79,12 +129,19 @@ const GroupSearch: React.FC<GroupSearchProps> = ({
79
129
  }}
80
130
  >
81
131
  <p className={styles.resultsText}>
82
- {results.length} {t("searchResultsText", "search result(s)")}
132
+ {totalResults} {t("searchResultsText", "search result(s)")}
83
133
  </p>
84
134
  <CompactGroupResults
85
135
  groups={results}
86
136
  selectGroupAction={selectGroupAction}
137
+ lastRef={lastItem}
87
138
  />
139
+ <div ref={lastItem}>
140
+ <div className={styles.lastItem} ref={loadingRef}>
141
+ {hasMore && <Loading withOverlay={false} small />}
142
+ {!hasMore && <p>{t("noMoreResults", "End of search results")}</p>}
143
+ </div>
144
+ </div>
88
145
  </div>
89
146
  </div>
90
147
  );
@@ -29,12 +29,7 @@
29
29
  width: 100%;
30
30
  }
31
31
 
32
- .loadingIcon {
33
- padding: spacing.$spacing-05 0;
34
- display: flex;
35
- justify-content: center;
36
- align-items: center;
37
- }
32
+
38
33
 
39
34
  .searchTerm {
40
35
  @include type.type-style('heading-03');
@@ -92,3 +87,10 @@
92
87
  @include type.type-style('body-01');
93
88
  color: $text-02;
94
89
  }
90
+
91
+ .lastItem {
92
+ padding: spacing.$spacing-05;
93
+ display: flex;
94
+ justify-content: center;
95
+ align-items: center;
96
+ }
@@ -1,7 +1,6 @@
1
1
  import { Close } from "@carbon/react/icons";
2
2
  import { Button } from "@carbon/react";
3
3
  import React, { useContext } from "react";
4
- import { Link } from "react-router-dom";
5
4
  import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
6
5
  import styles from "./styles.scss";
7
6
  import { useTranslation } from "react-i18next";
@@ -10,7 +9,9 @@ import AddGroupModal from "../../add-group-modal/AddGroupModal";
10
9
 
11
10
  const GroupSearchHeader = () => {
12
11
  const { t } = useTranslation();
13
- const { activeGroupUuid, setGroup } = useContext(GroupFormWorkflowContext);
12
+ const { activeGroupUuid, setGroup, destroySession } = useContext(
13
+ GroupFormWorkflowContext
14
+ );
14
15
  const handleSelectGroup = (group) => {
15
16
  setGroup(group);
16
17
  };
@@ -29,11 +30,14 @@ const GroupSearchHeader = () => {
29
30
  </span>
30
31
  <span style={{ flexGrow: 1 }} />
31
32
  <span>
32
- <Link to="..">
33
- <Button kind="ghost">
34
- {t("cancel", "Cancel")} <Close size={20} />
35
- </Button>
36
- </Link>
33
+ <Button
34
+ kind="ghost"
35
+ onClick={() => {
36
+ destroySession();
37
+ }}
38
+ >
39
+ {t("cancel", "Cancel")} <Close size={20} />
40
+ </Button>
37
41
  </span>
38
42
  </div>
39
43
  );
@@ -19,6 +19,16 @@
19
19
  width: 1100px;
20
20
  }
21
21
 
22
+ :global(.omrs-breakpoint-lt-large-desktop) .workspace {
23
+ width: 1000px;
24
+ }
25
+
26
+ :global(.omrs-breakpoint-lt-small-desktop) .workspace {
27
+ // there's only so much we can do here. Currenlty the design does not support tablet
28
+ width: 100vw;
29
+ padding: 0 spacing.$spacing-04;
30
+ }
31
+
22
32
  .selectPatientMessage {
23
33
  @include type.type-style('productive-heading-03');
24
34
  margin: spacing.$spacing-07;
@@ -36,6 +46,7 @@
36
46
  flex-grow: 1;
37
47
  max-height: calc(100vh - 14rem);
38
48
  overflow-y: scroll;
49
+ text-align: left;
39
50
  }
40
51
 
41
52
  .formContainer :global(.cds--form-item) :global(.question-area) {
@@ -43,7 +54,7 @@
43
54
  }
44
55
 
45
56
  .rightPanel {
46
- width: 13rem;
57
+ min-width: 13rem;
47
58
  text-align: left;
48
59
  overflow-y: scroll;
49
60
  display: flex;
@@ -4,3 +4,4 @@ import useFormState from "./useFormState";
4
4
  import useGetEncounter from "./useGetEncounter";
5
5
 
6
6
  export { useGetAllForms, useGetPatient, useFormState, useGetEncounter };
7
+ export * from "./usePostEndpoint";
@@ -14,7 +14,7 @@ const useGetPatient = (patientUuid) => {
14
14
 
15
15
  const getPatient = async (uuid) => {
16
16
  const result = await fetchCurrentPatient(uuid);
17
- setPatient(result?.data);
17
+ setPatient(result);
18
18
  };
19
19
 
20
20
  return patient;
@@ -0,0 +1,38 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { openmrsFetch } from "@openmrs/esm-framework";
3
+
4
+ const useGetSystemSetting = (settingId) => {
5
+ const [isSubmitting, setIsSubmitting] = useState(false);
6
+ const [result, setResult] = useState(null);
7
+ const [error, setError] = useState(null);
8
+
9
+ const onResult = useCallback((result) => {
10
+ setIsSubmitting(false);
11
+ setError(false);
12
+ setResult(result);
13
+ }, []);
14
+
15
+ const onError = useCallback((error) => {
16
+ setIsSubmitting(false);
17
+ setResult(null);
18
+ setError(error);
19
+ }, []);
20
+
21
+ const getSetting = useCallback(() => {
22
+ openmrsFetch(`/ws/rest/v1/systemsetting?q=${settingId}&v=default`)
23
+ .then(onResult)
24
+ .catch(onError);
25
+ }, [onError, onResult, settingId]);
26
+
27
+ useEffect(() => {
28
+ getSetting();
29
+ }, [getSetting]);
30
+
31
+ return {
32
+ result,
33
+ error,
34
+ isSubmitting,
35
+ };
36
+ };
37
+
38
+ export default useGetSystemSetting;
@@ -0,0 +1,70 @@
1
+ import { openmrsFetch } from "@openmrs/esm-framework";
2
+ import { useCallback, useState } from "react";
3
+
4
+ const usePostEndpoint = ({ endpointUrl }) => {
5
+ const [submissionInProgress, setSubmissionInProgress] = useState(null);
6
+ const [result, setResult] = useState(null);
7
+ const [error, setError] = useState(null);
8
+
9
+ const onFormPosted = useCallback(
10
+ (result) => {
11
+ setSubmissionInProgress(false);
12
+ if (error) {
13
+ setError(null);
14
+ }
15
+ setResult(result.data);
16
+ },
17
+ [error]
18
+ );
19
+
20
+ const onError = useCallback(
21
+ (error) => {
22
+ setSubmissionInProgress(false);
23
+ if (result) {
24
+ setResult(null);
25
+ }
26
+ setError(error?.responseBody?.error ?? error?.responseBody ?? error);
27
+ },
28
+ [result]
29
+ );
30
+
31
+ const post = useCallback(
32
+ async (data) => {
33
+ setSubmissionInProgress(true);
34
+ return openmrsFetch(endpointUrl, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ },
39
+ body: data,
40
+ })
41
+ .then(onFormPosted)
42
+ .catch(onError);
43
+ },
44
+ [endpointUrl, onError, onFormPosted]
45
+ );
46
+
47
+ const reset = () => {
48
+ setSubmissionInProgress(null);
49
+ setResult(null);
50
+ setError(null);
51
+ };
52
+
53
+ return {
54
+ post,
55
+ isPosting: submissionInProgress,
56
+ result,
57
+ error,
58
+ reset,
59
+ };
60
+ };
61
+
62
+ const usePostVisit = () => {
63
+ return usePostEndpoint({ endpointUrl: "/ws/rest/v1/visit" });
64
+ };
65
+
66
+ const usePostCohort = () => {
67
+ return usePostEndpoint({ endpointUrl: "/ws/rest/v1/cohortm/cohort" });
68
+ };
69
+
70
+ export { usePostEndpoint, usePostVisit, usePostCohort };
@@ -0,0 +1,120 @@
1
+ import { openmrsFetch, FetchResponse } from "@openmrs/esm-framework";
2
+ import { useCallback, useMemo } from "react";
3
+ import useSWRInfinite from "swr/infinite";
4
+
5
+ export interface SearchResponse {
6
+ data: Array<Record<string, unknown>> | null;
7
+ isLoading: boolean;
8
+ error: Error;
9
+ loadingNewData: boolean;
10
+ hasMore: boolean;
11
+ currentPage: number;
12
+ totalResults: number;
13
+ setPage: (size: number | ((_size: number) => number)) => Promise<
14
+ FetchResponse<{
15
+ results: Array<Record<string, unknown>>;
16
+ links: Array<{
17
+ rel: "prev" | "next";
18
+ }>;
19
+ }>[]
20
+ >;
21
+ }
22
+
23
+ interface SearchInfiniteProps {
24
+ baseUrl?: string;
25
+ searchTerm: string;
26
+ parameters?: Record<string, unknown> | undefined;
27
+ searching: boolean;
28
+ resultsToFetch?: number;
29
+ }
30
+
31
+ const useSearchEndpointInfinite = (
32
+ arg0: SearchInfiniteProps
33
+ ): SearchResponse => {
34
+ const {
35
+ baseUrl,
36
+ searchTerm,
37
+ parameters,
38
+ searching = true,
39
+ resultsToFetch = 10,
40
+ } = arg0;
41
+
42
+ const getUrl = useCallback(
43
+ (
44
+ page: number,
45
+ prevPageData: FetchResponse<{
46
+ results: Array<Record<string, unknown>>;
47
+ links: Array<{ rel: "prev" | "next" }>;
48
+ }>
49
+ ) => {
50
+ if (
51
+ prevPageData &&
52
+ !prevPageData?.data?.links.some((link) => link.rel === "next")
53
+ ) {
54
+ return null;
55
+ }
56
+ let url = `${baseUrl}?q=${searchTerm}`;
57
+ const params = {
58
+ // merge passed parameters and default parameters
59
+ // this way the defaults can be overriden if needed
60
+ totalCount: true,
61
+ limit: resultsToFetch,
62
+ ...parameters,
63
+ };
64
+ Object.entries(params).forEach(([key, value]) => {
65
+ // don't send null parmeters
66
+ if (value !== null && value !== undefined) {
67
+ url += `&${key}=${value}`;
68
+ }
69
+ });
70
+ if (page) {
71
+ url += `&startIndex=${page * resultsToFetch}`;
72
+ }
73
+ return url;
74
+ },
75
+ [baseUrl, searchTerm, parameters, resultsToFetch]
76
+ );
77
+
78
+ const { data, isValidating, setSize, error, size } = useSWRInfinite<
79
+ FetchResponse<{
80
+ results: Array<Record<string, unknown>>;
81
+ links: Array<{ rel: "prev" | "next" }>;
82
+ totalCount: number;
83
+ }>,
84
+ Error
85
+ >(searching ? getUrl : null, openmrsFetch);
86
+
87
+ const results = useMemo(
88
+ () => ({
89
+ data: data
90
+ ? [].concat(...(data?.map((resp) => resp?.data?.results) ?? []))
91
+ : null,
92
+ isLoading: !data && !error,
93
+ error,
94
+ hasMore: data?.length
95
+ ? !!data[data.length - 1].data?.links?.some(
96
+ (link) => link.rel === "next"
97
+ )
98
+ : false,
99
+ loadingNewData: isValidating,
100
+ setPage: setSize,
101
+ currentPage: size,
102
+ totalResults: data?.[0]?.data?.totalCount,
103
+ }),
104
+ [data, isValidating, error, setSize, size]
105
+ );
106
+
107
+ return results;
108
+ };
109
+
110
+ const useSearchCohortInfinite = ({
111
+ ...props
112
+ }: SearchInfiniteProps): SearchResponse => {
113
+ return useSearchEndpointInfinite({
114
+ baseUrl: "/ws/rest/v1/cohortm/cohort",
115
+ resultsToFetch: 10,
116
+ ...props,
117
+ });
118
+ };
119
+
120
+ export { useSearchEndpointInfinite, useSearchCohortInfinite };
@@ -0,0 +1,92 @@
1
+ import { useCallback, useState } from "react";
2
+ import { useTranslation } from "react-i18next";
3
+ import {
4
+ showNotification,
5
+ showToast,
6
+ openmrsFetch,
7
+ } from "@openmrs/esm-framework";
8
+
9
+ const useStartVisit = ({
10
+ showSuccessNotification = true,
11
+ showErrorNotification = true,
12
+ }) => {
13
+ const { t } = useTranslation();
14
+ const [isSubmitting, setIsSubmitting] = useState(false);
15
+ const [success, setSuccess] = useState(null);
16
+ const [error, setError] = useState(null);
17
+
18
+ const onSave = useCallback(
19
+ (result) => {
20
+ setIsSubmitting(false);
21
+ setError(false);
22
+ setSuccess(result);
23
+ if (showSuccessNotification) {
24
+ showToast({
25
+ critical: true,
26
+ kind: "success",
27
+ description: t(
28
+ "visitStartedSuccessfully",
29
+ `${result?.data?.visitType?.display} started successfully`
30
+ ),
31
+ title: t("visitStarted", "Visit started"),
32
+ });
33
+ }
34
+ },
35
+ [t, showSuccessNotification]
36
+ );
37
+
38
+ const onError = useCallback(
39
+ (error) => {
40
+ setIsSubmitting(false);
41
+ setSuccess(false);
42
+ setError(error);
43
+ if (showErrorNotification) {
44
+ showNotification({
45
+ title: t("startVisitError", "Error starting visit"),
46
+ kind: "error",
47
+ critical: true,
48
+ description: error?.message,
49
+ });
50
+ }
51
+ },
52
+ [t, showErrorNotification]
53
+ );
54
+
55
+ const saveVisit = useCallback(
56
+ (data) => {
57
+ const payload = {
58
+ patient: data.patientUuid,
59
+ startDatetime: data.startDatetime,
60
+ stopDatetime: data.stopDatetime,
61
+ visitType: data.visitType,
62
+ location: data.location,
63
+ };
64
+ openmrsFetch("/ws/rest/v1/visit", {
65
+ method: "POST",
66
+ body: payload,
67
+ headers: { "Content-Type": "application/json" },
68
+ })
69
+ .then(onSave)
70
+ .catch(onError);
71
+ },
72
+ [onError, onSave]
73
+ );
74
+
75
+ const updateEncounter = useCallback((data) => {
76
+ openmrsFetch("/ws/rest/v1/encounter/" + data.uuid, {
77
+ method: "POST",
78
+ body: { visit: data.visit },
79
+ headers: { "Content-Type": "application/json" },
80
+ });
81
+ }, []);
82
+
83
+ return {
84
+ saveVisit,
85
+ updateEncounter,
86
+ success,
87
+ error,
88
+ isSubmitting,
89
+ };
90
+ };
91
+
92
+ export default useStartVisit;
@@ -6,6 +6,7 @@
6
6
  .cardContainer {
7
7
  padding: spacing.$spacing-05;
8
8
  display: flex;
9
+ cursor: pointer;
9
10
  }
10
11
 
11
12
  .skeletonText {