@epcare/esm-report-builder 0.1.1-pre.13

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 (222) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.js +19 -0
  3. package/.prettierrc +8 -0
  4. package/LICENSE +401 -0
  5. package/README.md +317 -0
  6. package/dist/184.js +2 -0
  7. package/dist/184.js.LICENSE.txt +14 -0
  8. package/dist/184.js.map +1 -0
  9. package/dist/211.js +1 -0
  10. package/dist/211.js.map +1 -0
  11. package/dist/300.js +1 -0
  12. package/dist/311.js +1 -0
  13. package/dist/311.js.map +1 -0
  14. package/dist/44.js +2 -0
  15. package/dist/44.js.LICENSE.txt +30 -0
  16. package/dist/44.js.map +1 -0
  17. package/dist/478.js +2 -0
  18. package/dist/478.js.LICENSE.txt +9 -0
  19. package/dist/478.js.map +1 -0
  20. package/dist/535.js +1 -0
  21. package/dist/535.js.map +1 -0
  22. package/dist/540.js +2 -0
  23. package/dist/540.js.LICENSE.txt +9 -0
  24. package/dist/540.js.map +1 -0
  25. package/dist/710.js +1 -0
  26. package/dist/710.js.map +1 -0
  27. package/dist/729.js +1 -0
  28. package/dist/729.js.map +1 -0
  29. package/dist/872.js +1 -0
  30. package/dist/872.js.map +1 -0
  31. package/dist/917.js +1 -0
  32. package/dist/917.js.map +1 -0
  33. package/dist/961.js +2 -0
  34. package/dist/961.js.LICENSE.txt +19 -0
  35. package/dist/961.js.map +1 -0
  36. package/dist/972.js +2 -0
  37. package/dist/972.js.LICENSE.txt +21 -0
  38. package/dist/972.js.map +1 -0
  39. package/dist/main.js +1 -0
  40. package/dist/main.js.map +1 -0
  41. package/dist/openmrs-esm-report-builder.js +1 -0
  42. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +482 -0
  43. package/dist/openmrs-esm-report-builder.js.map +1 -0
  44. package/dist/routes.json +1 -0
  45. package/e2e/core/env.ts +5 -0
  46. package/e2e/core/global-setup.ts +34 -0
  47. package/e2e/core/test.ts +21 -0
  48. package/e2e/pages/app-shell.page.ts +34 -0
  49. package/e2e/pages/etl-sources.page.ts +35 -0
  50. package/e2e/pages/report-sections.page.ts +20 -0
  51. package/e2e/pages/section-preview.modal.ts +29 -0
  52. package/e2e/specs/etl-sources.spec.ts +51 -0
  53. package/e2e/specs/navigation.spec.ts +13 -0
  54. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  55. package/e2e/specs/section-preview.spec.ts +94 -0
  56. package/e2e/tsconfig.json +11 -0
  57. package/example.env +5 -0
  58. package/jest.config.js +28 -0
  59. package/package.json +102 -0
  60. package/playwright.config.ts +28 -0
  61. package/src/admin-card-link.extension.module.scss +55 -0
  62. package/src/admin-card-link.extension.tsx +24 -0
  63. package/src/components/admin/admin-page.component.tsx +31 -0
  64. package/src/components/admin/age-categories-page.component.tsx +109 -0
  65. package/src/components/admin/age-groups-page.component.tsx +146 -0
  66. package/src/components/admin/etl-sources-page.component.tsx +344 -0
  67. package/src/components/admin/report-categories-page.component.tsx +256 -0
  68. package/src/components/admin/report-library-page.component.tsx +540 -0
  69. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  70. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  71. package/src/components/ai-support/ai.service.ts +21 -0
  72. package/src/components/ai-support/ai.types.ts +12 -0
  73. package/src/components/app-shell/report-builder-shell.component.tsx +120 -0
  74. package/src/components/app-shell/report-builder-shell.scss +15 -0
  75. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  76. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  77. package/src/components/data-themes/data-themes-page.component.tsx +132 -0
  78. package/src/components/data-themes/data-themes-table.component.tsx +91 -0
  79. package/src/components/data-themes/icon-registry.ts +35 -0
  80. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  81. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  82. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  83. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  84. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  85. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  86. package/src/components/indicators/README.md +24 -0
  87. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  88. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  89. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  90. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  91. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  92. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  93. package/src/components/indicators/create-base-indicator-modal.component.tsx +501 -0
  94. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +297 -0
  95. package/src/components/indicators/create-final-indicator-modal.component.tsx +260 -0
  96. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  97. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  98. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  99. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  100. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  101. package/src/components/indicators/indicators-page.component.tsx +481 -0
  102. package/src/components/indicators/indicators-page.scss +121 -0
  103. package/src/components/indicators/indicators-table.component.tsx +166 -0
  104. package/src/components/indicators/preview-table.component.tsx +31 -0
  105. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  106. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +161 -0
  107. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  108. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  109. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  110. package/src/components/indicators/sections/final-indicator-picker.section.tsx +92 -0
  111. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  112. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  113. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  114. package/src/components/indicators/sections/indicator-conditions.section.tsx +194 -0
  115. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +26 -0
  116. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  117. package/src/components/indicators/sql-preview.component.tsx +19 -0
  118. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  119. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  120. package/src/components/indicators/types/composite-indicator.types.ts +21 -0
  121. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  122. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  123. package/src/components/indicators/types/indicator-types.ts +56 -0
  124. package/src/components/indicators/types/sql-builders.ts +43 -0
  125. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +206 -0
  126. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  127. package/src/components/indicators/utils/final-indicator-sql.utils.ts +124 -0
  128. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  129. package/src/components/indicators/utils/indicator-sql.utils.ts +185 -0
  130. package/src/components/landing/landing-action-item.component.tsx +50 -0
  131. package/src/components/landing/landing-action-list.component.tsx +12 -0
  132. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  133. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  134. package/src/components/landing/landing-hero.component.tsx +18 -0
  135. package/src/components/landing/landing-section-card.component.tsx +33 -0
  136. package/src/components/landing/report-builder-landing-page.component.tsx +103 -0
  137. package/src/components/landing/report-builder-landing-page.scss +131 -0
  138. package/src/components/report/dashboard/reports-table.component.tsx +145 -0
  139. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  140. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  141. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  142. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  143. package/src/components/report/definition/report-definition.types.ts +66 -0
  144. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  145. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  146. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  147. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  148. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  149. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  150. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  151. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  152. package/src/components/report/design/report-design-editor.component.tsx +894 -0
  153. package/src/components/report/design/report-design.types.ts +43 -0
  154. package/src/components/report/design/report-design.utils.ts +86 -0
  155. package/src/components/report/panels/report-detail.component.tsx +71 -0
  156. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  157. package/src/components/report/report-dashboard.page.component.tsx +75 -0
  158. package/src/components/report/report-editor.page.component.tsx +466 -0
  159. package/src/components/report/report.routes.tsx +30 -0
  160. package/src/components/report-sections/create-report-section-modal.component.tsx +278 -0
  161. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  162. package/src/components/report-sections/hooks/useSectionEditorState.ts +306 -0
  163. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  164. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  165. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +143 -0
  166. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  167. package/src/components/report-sections/report-section-preview-modal.component.tsx +418 -0
  168. package/src/components/report-sections/report-sections-page.component.tsx +313 -0
  169. package/src/components/report-sections/section-types.ts +55 -0
  170. package/src/components/report-sections/section-utils.ts +46 -0
  171. package/src/components/report-sections/sections-page.scss +79 -0
  172. package/src/components/run-reports/run-reports-page.component.tsx +23 -0
  173. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  174. package/src/components/shared/header/header.component.tsx +85 -0
  175. package/src/components/shared/header/header.scss +150 -0
  176. package/src/components/shared/header/illustration.component.tsx +34 -0
  177. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  178. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  179. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  180. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  181. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  182. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  183. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  184. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  185. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  186. package/src/config-schema.ts +23 -0
  187. package/src/globals.d.ts +14 -0
  188. package/src/hooks/theme/useDataThemes.ts +28 -0
  189. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  190. package/src/hooks/theme/useMambaTables.ts +63 -0
  191. package/src/index.ts +23 -0
  192. package/src/resources/agegroup/agegroups.api.ts +58 -0
  193. package/src/resources/concepts/concept-mapper.ts +21 -0
  194. package/src/resources/concepts/concept-types.ts +31 -0
  195. package/src/resources/concepts/concepts.resource.ts +44 -0
  196. package/src/resources/concepts/useConceptSearch.ts +41 -0
  197. package/src/resources/etl-source/etl-source.api.ts +65 -0
  198. package/src/resources/indicator/indicators.api.ts +125 -0
  199. package/src/resources/openmrs-api.ts +24 -0
  200. package/src/resources/preview/sql-preview.api.ts +54 -0
  201. package/src/resources/report/reports.api.ts +117 -0
  202. package/src/resources/report-category/report-category.api.ts +41 -0
  203. package/src/resources/report-library/report-library.api.ts +63 -0
  204. package/src/resources/report-section/report-sections.api.ts +73 -0
  205. package/src/resources/report-section/section-preview.api.ts +21 -0
  206. package/src/resources/theme/data-theme.api.ts +88 -0
  207. package/src/resources/theme/mamba-schema.api.ts +31 -0
  208. package/src/resources/theme/mamba-table-meta.api.ts +45 -0
  209. package/src/root.component.tsx +20 -0
  210. package/src/routes/report-builder.routes.tsx +66 -0
  211. package/src/routes/report-builder.scss +56 -0
  212. package/src/routes.json +21 -0
  213. package/src/sample-template.ts +121 -0
  214. package/src/template-utils.ts +110 -0
  215. package/src/types/carbon-react-inert.d.ts +13 -0
  216. package/src/types/condition-operators.ts +49 -0
  217. package/src/types/theme/data-theme.types.ts +88 -0
  218. package/src/utils/html-entities.utils.ts +23 -0
  219. package/tools/setup-tests.ts +1 -0
  220. package/translations/en.json +4 -0
  221. package/tsconfig.json +43 -0
  222. package/webpack.config.js +14 -0
@@ -0,0 +1,44 @@
1
+ import type { ConceptSummary } from './concept-types';
2
+
3
+ import { omrsGet } from '../openmrs-api';
4
+
5
+
6
+ const RESOURCE = '/concept';
7
+ const cutomConceptRepresentation = 'custom:(id,uuid,display,datatype:(uuid,name),conceptClass:(uuid,name),answers:(id,uuid,display),mappings:(display,uuid,conceptMapType:(display)))';
8
+
9
+ type ConceptSearchResponse = {
10
+ results: ConceptSummary[];
11
+ };
12
+
13
+ type ConceptAnswersResponse = {
14
+ answers?: Array<{ uuid: string; display: string; id?: number; mappings?: any[]; conceptClass?: any; datatype?: any }>;
15
+ };
16
+
17
+ export async function searchConcepts(
18
+ query: string,
19
+ signal?: AbortSignal,
20
+ ): Promise<ConceptSummary[]> {
21
+ const res = await omrsGet<ConceptSearchResponse>(
22
+ `${RESOURCE}/?q=${encodeURIComponent(query)}&v=${cutomConceptRepresentation}`,
23
+ signal);
24
+
25
+ return res?.results ?? [];
26
+ }
27
+
28
+ export async function getConceptByUuid(uuid: string, signal?: AbortSignal): Promise<ConceptSummary> {
29
+ const u = String(uuid ?? '').trim();
30
+ if (!u) {
31
+ throw new Error('Concept uuid is required');
32
+ }
33
+
34
+ // /concept/{uuid} returns a single Concept object
35
+ return omrsGet<ConceptSummary>(`${RESOURCE}/${encodeURIComponent(u)}?v=${cutomConceptRepresentation}`, signal);
36
+ }
37
+
38
+
39
+ export async function getConceptAnswers(questionUuid: string, signal?: AbortSignal): Promise<ConceptSummary[]> {
40
+ const res = await omrsGet<ConceptAnswersResponse>(`${RESOURCE}/${questionUuid}?v=${cutomConceptRepresentation}`, signal );
41
+
42
+ const answers = (res as any)?.answers ?? [];
43
+ return answers as ConceptSummary[];
44
+ }
@@ -0,0 +1,41 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { searchConcepts } from './concepts.resource';
3
+ import type { ConceptSummary } from './concept-types';
4
+
5
+ export function useConceptSearch(query: string) {
6
+ const [loading, setLoading] = useState(false);
7
+ const [results, setResults] = useState<ConceptSummary[]>([]);
8
+ const [error, setError] = useState<string | null>(null);
9
+
10
+ useEffect(() => {
11
+ const q = query.trim();
12
+ if (q.length < 2) {
13
+ setResults([]);
14
+ setError(null);
15
+ return;
16
+ }
17
+
18
+ const ctrl = new AbortController();
19
+
20
+ const t = window.setTimeout(async () => {
21
+ try {
22
+ setLoading(true);
23
+ setError(null);
24
+
25
+ const res = await searchConcepts(q, ctrl.signal); // ✅ returns ConceptSummary[]
26
+ setResults(res);
27
+ } catch (e: any) {
28
+ if (e?.name !== 'AbortError') setError('Failed to search concepts');
29
+ } finally {
30
+ setLoading(false);
31
+ }
32
+ }, 300);
33
+
34
+ return () => {
35
+ ctrl.abort();
36
+ window.clearTimeout(t);
37
+ };
38
+ }, [query]);
39
+
40
+ return { loading, results, error };
41
+ }
@@ -0,0 +1,65 @@
1
+ import { omrsDelete, omrsGet, omrsPost } from '../openmrs-api';
2
+
3
+ type RestList<T> = { results?: T[] } & Record<string, any>;
4
+
5
+ function unwrapRestList<T>(data: RestList<T> | T[] | undefined): T[] {
6
+ if (!data) return [];
7
+ if (Array.isArray(data)) return data;
8
+ return Array.isArray(data.results) ? data.results : [];
9
+ }
10
+
11
+ const RESOURCE = '/reportbuilder/etlsource';
12
+
13
+ export type ETLSourceDto = {
14
+ uuid: string;
15
+ display?: string;
16
+ name: string;
17
+ code?: string;
18
+ description?: string;
19
+ tablePatterns?: string;
20
+ schemaName?: string;
21
+ sourceType?: string;
22
+ active?: boolean;
23
+ voided?: boolean;
24
+ };
25
+
26
+ export type SaveETLSourcePayload = {
27
+ name: string;
28
+ code?: string;
29
+ description?: string;
30
+ tablePatterns?: string;
31
+ schemaName?: string;
32
+ sourceType?: string;
33
+ active?: boolean;
34
+ };
35
+
36
+ export async function listETLSources(q?: string, signal?: AbortSignal): Promise<ETLSourceDto[]> {
37
+ const qs = new URLSearchParams();
38
+ qs.set('v', 'custom:(uuid,name,code,description,tablePatterns,schemaName,sourceType,active,retired)');
39
+ if (q?.trim()) qs.set('q', q.trim());
40
+
41
+ const data = await omrsGet<RestList<ETLSourceDto> | ETLSourceDto[] | undefined>(
42
+ `${RESOURCE}?${qs.toString()}`,
43
+ signal,
44
+ );
45
+
46
+ return unwrapRestList(data).filter((x) => Boolean(x?.uuid));
47
+ }
48
+
49
+ export async function createETLSource(payload: SaveETLSourcePayload, signal?: AbortSignal) {
50
+ return omrsPost<ETLSourceDto>(RESOURCE, payload, signal);
51
+ }
52
+
53
+ export async function updateETLSource(uuid: string, payload: SaveETLSourcePayload, signal?: AbortSignal) {
54
+ return omrsPost<ETLSourceDto>(`${RESOURCE}/${encodeURIComponent(uuid)}`, payload, signal);
55
+ }
56
+
57
+ export async function deleteETLSource(
58
+ uuid: string,
59
+ reason = 'Retired from Report Builder Admin',
60
+ signal?: AbortSignal,
61
+ ) {
62
+ const qs = new URLSearchParams();
63
+ qs.set('reason', reason);
64
+ return omrsDelete(`${RESOURCE}/${encodeURIComponent(uuid)}?${qs.toString()}`, signal);
65
+ }
@@ -0,0 +1,125 @@
1
+ import { omrsDelete, omrsGet, omrsPost } from '../openmrs-api';
2
+ import { decodeHtmlEntities } from '../../utils/html-entities.utils';
3
+
4
+ /**
5
+ * REST resource:
6
+ * /ws/rest/v1/reportbuilderindicator
7
+ */
8
+ const RESOURCE = '/reportbuilder/indicator';
9
+
10
+ type RestList<T> = { results?: T[] } & Record<string, any>;
11
+
12
+ function unwrapRestList<T>(data: RestList<T> | T[] | undefined): T[] {
13
+ if (!data) return [];
14
+ if (Array.isArray(data)) return data;
15
+ return Array.isArray(data.results) ? data.results : [];
16
+ }
17
+
18
+ function normalizeIndicatorDto(ind: IndicatorDto): IndicatorDto {
19
+ // OpenMRS can return HTML-escaped strings; decode before UI uses them.
20
+ return {
21
+ ...ind,
22
+ configJson: ind.configJson ? decodeHtmlEntities(ind.configJson) : ind.configJson,
23
+ metaJson: ind.metaJson ? decodeHtmlEntities(ind.metaJson) : ind.metaJson,
24
+ sqlTemplate: ind.sqlTemplate ? decodeHtmlEntities(ind.sqlTemplate) : ind.sqlTemplate,
25
+ denominatorSqlTemplate: ind.denominatorSqlTemplate
26
+ ? decodeHtmlEntities(ind.denominatorSqlTemplate)
27
+ : ind.denominatorSqlTemplate,
28
+ };
29
+ }
30
+
31
+ export type IndicatorKind = 'BASE' | 'COMPOSITE' | 'FINAL';
32
+ export type IndicatorValueType = 'NUMBER' | 'TABLE' | 'PATIENT_SET';
33
+ export type RestRep = 'default' | 'full';
34
+
35
+ export type IndicatorDto = {
36
+ uuid: string;
37
+ name: string;
38
+ description?: string;
39
+ code?: string;
40
+
41
+ kind: IndicatorKind;
42
+ defaultValueType?: IndicatorValueType;
43
+
44
+ // new-ish fields you’re adding backend-side (safe optional)
45
+ themeUuid?: string | null;
46
+ configJson?: string;
47
+ metaJson?: string;
48
+ sqlTemplate?: string | null;
49
+ denominatorSqlTemplate?: string | null;
50
+
51
+ retired?: boolean;
52
+ retireReason?: string | null;
53
+ };
54
+
55
+ export type MambaIndicatorDto = {
56
+ id: string,
57
+ uuid: string;
58
+ name: string;
59
+ description?: string;
60
+ code?: string;
61
+
62
+ kind: IndicatorKind;
63
+ defaultValueType?: IndicatorValueType;
64
+ themeName?: string;
65
+ themeColor?: string;
66
+
67
+ // new-ish fields you’re adding backend-side (safe optional)
68
+ themeUuid?: string | null;
69
+ configJson?: string;
70
+ metaJson?: string;
71
+ sqlTemplate?: string | null;
72
+ denominatorSqlTemplate?: string | null;
73
+
74
+ retired?: boolean;
75
+ retireReason?: string | null;
76
+ };
77
+
78
+ export type ListIndicatorsParams = {
79
+ q?: string;
80
+ kind?: IndicatorKind;
81
+ includeRetired?: boolean;
82
+ v?: RestRep;
83
+ };
84
+
85
+ export async function listIndicators(params?: ListIndicatorsParams, signal?: AbortSignal): Promise<IndicatorDto[]> {
86
+ const q = params?.q?.trim();
87
+ const kind = params?.kind?.trim?.() ? params.kind : params?.kind;
88
+ const includeRetired = params?.includeRetired === true;
89
+ const v = params?.v ?? 'default';
90
+
91
+ const qs = new URLSearchParams();
92
+ qs.set('v', v);
93
+ if (q) qs.set('q', q);
94
+ if (kind) qs.set('kind', kind);
95
+ if (includeRetired) qs.set('includeRetired', 'true');
96
+
97
+ const data = await omrsGet<RestList<IndicatorDto> | IndicatorDto[] | undefined>(`${RESOURCE}?${qs.toString()}`, signal);
98
+
99
+ return unwrapRestList(data)
100
+ .filter((x) => Boolean(x?.uuid))
101
+ .map(normalizeIndicatorDto);
102
+ }
103
+
104
+ export async function getIndicator(uuid: string, signal?: AbortSignal, v: RestRep = 'full'): Promise<IndicatorDto> {
105
+ const res = await omrsGet<IndicatorDto>(`${RESOURCE}/${encodeURIComponent(uuid)}?v=${v}`, signal);
106
+ return normalizeIndicatorDto(res);
107
+ }
108
+
109
+ export async function createIndicator(payload: Partial<IndicatorDto>, signal?: AbortSignal): Promise<IndicatorDto> {
110
+ return omrsPost<IndicatorDto>(RESOURCE, payload, signal);
111
+ }
112
+
113
+ export async function updateIndicator(uuid: string, payload: Partial<IndicatorDto>, signal?: AbortSignal): Promise<IndicatorDto> {
114
+ return omrsPost<IndicatorDto>(`${RESOURCE}/${encodeURIComponent(uuid)}`, payload, signal);
115
+ }
116
+
117
+ export async function deleteIndicator(
118
+ uuid: string,
119
+ purge = false,
120
+ reason = 'Retired via UI',
121
+ signal?: AbortSignal,
122
+ ): Promise<void> {
123
+ const qs = purge ? `purge=true` : `purge=false&reason=${encodeURIComponent(reason)}`;
124
+ await omrsDelete<void>(`${RESOURCE}/${encodeURIComponent(uuid)}?${qs}`, signal);
125
+ }
@@ -0,0 +1,24 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+
3
+ export async function omrsGet<T>(path: string, signal?: AbortSignal): Promise<T> {
4
+ const url = `${restBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
5
+ const res = await openmrsFetch<T>(url, { signal });
6
+ return res.data;
7
+ }
8
+
9
+ export async function omrsPost<T>(path: string, body: any, signal?: AbortSignal): Promise<T> {
10
+ const url = `${restBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
11
+ const res = await openmrsFetch<T>(url, {
12
+ method: 'POST',
13
+ headers: { 'Content-Type': 'application/json' },
14
+ body,
15
+ signal,
16
+ });
17
+ return res.data;
18
+ }
19
+
20
+ export async function omrsDelete<T>(path: string, signal?: AbortSignal): Promise<T> {
21
+ const url = `${restBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
22
+ const res = await openmrsFetch<T>(url, { method: 'DELETE', signal });
23
+ return res.data;
24
+ }
@@ -0,0 +1,54 @@
1
+ export type SqlPreviewRequest = {
2
+ /**
3
+ * Prefer plain sql for now.
4
+ * If you later add sqlEncoded, you can extend this type.
5
+ */
6
+ sql: string;
7
+ params?: Record<string, any>;
8
+ maxRows?: number;
9
+ };
10
+
11
+ export type SqlPreviewResponse = {
12
+ columns: string[];
13
+ rows: any[][];
14
+ rowCount: number;
15
+ truncated: boolean;
16
+
17
+ // optional echo fields from RESTWS
18
+ sql?: string;
19
+ params?: Record<string, any>;
20
+ maxRows?: number;
21
+ resourceVersion?: string;
22
+ };
23
+
24
+ function restBase() {
25
+ // OpenMRS ESM convention
26
+ const base = (window as any).openmrsBaseUrl ?? '/openmrs/';
27
+ return `${base}ws/rest/v1`;
28
+ }
29
+
30
+ export async function previewSql(request: SqlPreviewRequest, signal?: AbortSignal): Promise<SqlPreviewResponse> {
31
+ const url = `${restBase()}/reportbuilder/sqlpreview`;
32
+
33
+ const res = await fetch(url, {
34
+ method: 'POST',
35
+ credentials: 'include',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ Accept: 'application/json',
39
+ },
40
+ body: JSON.stringify({
41
+ sql: request.sql,
42
+ params: request.params ?? {},
43
+ maxRows: request.maxRows ?? 200,
44
+ }),
45
+ signal,
46
+ });
47
+
48
+ if (!res.ok) {
49
+ const text = await res.text().catch(() => '');
50
+ throw new Error(`SQL preview failed (${res.status}). ${text}`);
51
+ }
52
+
53
+ return (await res.json()) as SqlPreviewResponse;
54
+ }
@@ -0,0 +1,117 @@
1
+ import { omrsGet, omrsPost } from '../openmrs-api';
2
+ import { decodeHtmlEntities } from '../../utils/html-entities.utils';
3
+
4
+ const RESOURCE = '/reportbuilder/report';
5
+ const COMPILE_RESOURCE = '/reportbuilder/reportcompile';
6
+
7
+ type RestList<T> = { results?: T[] } & Record<string, any>;
8
+
9
+ function unwrapRestList<T>(data: RestList<T> | T[] | undefined): T[] {
10
+ if (!data) return [];
11
+ if (Array.isArray(data)) return data;
12
+ return Array.isArray(data.results) ? data.results : [];
13
+ }
14
+
15
+ export type RestRep = 'default' | 'full';
16
+
17
+ export type ReportDto = {
18
+ uuid: string;
19
+ name: string;
20
+ description?: string;
21
+ code?: string;
22
+ configJson?: string;
23
+ metaJson?: string;
24
+ retired?: boolean;
25
+ };
26
+
27
+ export type SaveReportPayload = {
28
+ name: string;
29
+ description?: string;
30
+ code?: string;
31
+ configJson?: string;
32
+ metaJson?: string;
33
+ };
34
+
35
+ export type ListReportsParams = {
36
+ q?: string;
37
+ includeRetired?: boolean;
38
+ v?: RestRep;
39
+ };
40
+
41
+ export type CompileReportPayload = {
42
+ reportUuid: string;
43
+ };
44
+
45
+ export type CompileReportResult = {
46
+ reportUuid: string;
47
+ reportDefinitionUuid?: string;
48
+ reportDefinitionName?: string;
49
+ reportDesignPath?: string;
50
+ compiled?: boolean;
51
+ };
52
+
53
+ function normalizeReportDto(r: ReportDto): ReportDto {
54
+ return {
55
+ ...r,
56
+ configJson: r.configJson ? decodeHtmlEntities(r.configJson) : r.configJson,
57
+ metaJson: r.metaJson ? decodeHtmlEntities(r.metaJson) : r.metaJson,
58
+ };
59
+ }
60
+
61
+ export async function listReports(
62
+ params?: ListReportsParams,
63
+ signal?: AbortSignal,
64
+ ): Promise<ReportDto[]> {
65
+ const q = params?.q?.trim();
66
+ const includeRetired = params?.includeRetired === true;
67
+ const v = params?.v ?? 'default';
68
+
69
+ const qs = new URLSearchParams();
70
+ qs.set('v', v);
71
+
72
+ if (q) qs.set('q', q);
73
+ if (includeRetired) qs.set('includeRetired', 'true');
74
+
75
+ const data = await omrsGet<RestList<ReportDto> | ReportDto[] | undefined>(
76
+ `${RESOURCE}?${qs.toString()}`,
77
+ signal,
78
+ );
79
+
80
+ return unwrapRestList(data)
81
+ .filter((x) => Boolean(x?.uuid))
82
+ .map(normalizeReportDto);
83
+ }
84
+
85
+ export async function getReport(
86
+ uuid: string,
87
+ signal?: AbortSignal,
88
+ v: RestRep = 'full',
89
+ ): Promise<ReportDto> {
90
+ const data = await omrsGet<ReportDto>(`${RESOURCE}/${encodeURIComponent(uuid)}?v=${v}`, signal);
91
+ return normalizeReportDto(data);
92
+ }
93
+
94
+ export async function createReport(
95
+ payload: SaveReportPayload,
96
+ signal?: AbortSignal,
97
+ ): Promise<ReportDto> {
98
+ const data = await omrsPost<ReportDto>(RESOURCE, payload, signal);
99
+ return normalizeReportDto(data);
100
+ }
101
+
102
+ export async function updateReport(
103
+ uuid: string,
104
+ payload: SaveReportPayload,
105
+ signal?: AbortSignal,
106
+ ): Promise<ReportDto> {
107
+ const data = await omrsPost<ReportDto>(`${RESOURCE}/${encodeURIComponent(uuid)}`, payload, signal);
108
+ return normalizeReportDto(data);
109
+ }
110
+
111
+ export async function compileReport(
112
+ reportUuid: string,
113
+ signal?: AbortSignal,
114
+ ): Promise<CompileReportResult> {
115
+ const payload: CompileReportPayload = { reportUuid };
116
+ return omrsPost<CompileReportResult>(COMPILE_RESOURCE, payload, signal);
117
+ }
@@ -0,0 +1,41 @@
1
+ import { omrsDelete, omrsGet, omrsPost } from '../openmrs-api';
2
+
3
+ type RestList<T> = { results?: T[] } & Record<string, any>;
4
+
5
+ function unwrapRestList<T>(data: RestList<T> | T[] | undefined): T[] {
6
+ if (!data) return [];
7
+ if (Array.isArray(data)) return data;
8
+ return Array.isArray(data.results) ? data.results : [];
9
+ }
10
+
11
+ const RESOURCE = '/reportbuilder/reportcategory';
12
+
13
+ export type ReportCategoryDto = {
14
+ uuid: string;
15
+ display?: string;
16
+ name: string;
17
+ description?: string;
18
+ retired?: boolean;
19
+ };
20
+
21
+ export async function listReportCategories(q?: string, signal?: AbortSignal): Promise<ReportCategoryDto[]> {
22
+ const qs = new URLSearchParams();
23
+ qs.set('v', 'full');
24
+ if (q?.trim()) qs.set('q', q.trim());
25
+ const data = await omrsGet<RestList<ReportCategoryDto> | ReportCategoryDto[] | undefined>(`${RESOURCE}?${qs.toString()}`, signal);
26
+ return unwrapRestList(data).filter((x) => Boolean(x?.uuid));
27
+ }
28
+
29
+ export async function createReportCategory(payload: Pick<ReportCategoryDto, 'name' | 'description'>, signal?: AbortSignal) {
30
+ return omrsPost<ReportCategoryDto>(RESOURCE, payload, signal);
31
+ }
32
+
33
+ export async function updateReportCategory(uuid: string, payload: Pick<ReportCategoryDto, 'name' | 'description'>, signal?: AbortSignal) {
34
+ return omrsPost<ReportCategoryDto>(`${RESOURCE}/${encodeURIComponent(uuid)}`, payload, signal);
35
+ }
36
+
37
+ export async function deleteReportCategory(uuid: string, reason = 'Retired from Report Builder Admin', signal?: AbortSignal) {
38
+ const qs = new URLSearchParams();
39
+ qs.set('reason', reason);
40
+ return omrsDelete<any>(`${RESOURCE}/${encodeURIComponent(uuid)}?${qs.toString()}`, signal);
41
+ }
@@ -0,0 +1,63 @@
1
+ import { omrsDelete, omrsGet, omrsPost } from '../openmrs-api';
2
+
3
+ type RestList<T> = { results?: T[] } & Record<string, any>;
4
+
5
+ function unwrapRestList<T>(data: RestList<T> | T[] | undefined): T[] {
6
+ if (!data) return [];
7
+ if (Array.isArray(data)) return data;
8
+ return Array.isArray(data.results) ? data.results : [];
9
+ }
10
+
11
+ const RESOURCE = '/reportbuilder/reportlibrary';
12
+
13
+ export type ReportSourceType = 'LEGACY' | 'BUILDER';
14
+ export type ReportType = 'AGGREGATE' | 'LINE_LIST';
15
+
16
+ export type ReportLibraryDto = {
17
+ uuid: string;
18
+ display?: string;
19
+ name: string;
20
+ description?: string;
21
+ code?: string;
22
+ sourceType?: ReportSourceType;
23
+ reportDefinitionUuid?: string;
24
+ reportBuilderReportUuid?: string;
25
+ reportType?: ReportType;
26
+ migrated?: boolean;
27
+ metaJson?: string;
28
+ retired?: boolean;
29
+ category?: { uuid: string; name?: string } | string;
30
+ };
31
+
32
+ export type SaveReportLibraryPayload = {
33
+ name: string;
34
+ description?: string;
35
+ code?: string;
36
+ sourceType?: ReportSourceType;
37
+ reportDefinitionUuid?: string;
38
+ reportBuilderReportUuid?: string;
39
+ reportType?: ReportType;
40
+ migrated?: boolean;
41
+ metaJson?: string;
42
+ category?: string;
43
+ };
44
+
45
+ export async function listReportLibraries(q?: string, signal?: AbortSignal): Promise<ReportLibraryDto[]> {
46
+ const qs = new URLSearchParams();
47
+ qs.set('v', 'full');
48
+ if (q?.trim()) qs.set('q', q.trim());
49
+ const data = await omrsGet<RestList<ReportLibraryDto> | ReportLibraryDto[]>(`${RESOURCE}?${qs.toString()}`, signal);
50
+ return unwrapRestList(data).filter((x) => Boolean(x?.uuid));
51
+ }
52
+
53
+ export async function createReportLibrary(payload: SaveReportLibraryPayload, signal?: AbortSignal): Promise<ReportLibraryDto> {
54
+ return omrsPost<ReportLibraryDto>(RESOURCE, payload, signal);
55
+ }
56
+
57
+ export async function updateReportLibrary(uuid: string, payload: SaveReportLibraryPayload, signal?: AbortSignal): Promise<ReportLibraryDto> {
58
+ return omrsPost<ReportLibraryDto>(`${RESOURCE}/${encodeURIComponent(uuid)}`, payload, signal);
59
+ }
60
+
61
+ export async function deleteReportLibrary(uuid: string, reason = 'Retired from report library', signal?: AbortSignal): Promise<any> {
62
+ return omrsDelete<any>(`${RESOURCE}/${encodeURIComponent(uuid)}?reason=${encodeURIComponent(reason)}`, signal);
63
+ }
@@ -0,0 +1,73 @@
1
+ import { omrsGet, omrsPost } from '../openmrs-api';
2
+ import { decodeHtmlEntities } from '../../utils/html-entities.utils';
3
+
4
+ /**
5
+ * REST resource:
6
+ * /ws/rest/v1/reportbuildersection
7
+ */
8
+ const RESOURCE = '/reportbuilder/section';
9
+
10
+ type RestList<T> = { results?: T[] } & Record<string, any>;
11
+
12
+ function unwrapRestList<T>(data: RestList<T> | T[] | undefined): T[] {
13
+ if (!data) return [];
14
+ if (Array.isArray(data)) return data;
15
+ return Array.isArray(data.results) ? data.results : [];
16
+ }
17
+
18
+ export type RestRep = 'default' | 'full';
19
+
20
+ export type ReportSectionDto = {
21
+ uuid: string;
22
+ name: string;
23
+ description?: string;
24
+ code?: string;
25
+ configJson?: string;
26
+ metaJson?: string;
27
+ retired?: boolean;
28
+ };
29
+
30
+ function normalizeSectionDto(s: ReportSectionDto): ReportSectionDto {
31
+ return {
32
+ ...s,
33
+ configJson: s.configJson ? decodeHtmlEntities(s.configJson) : s.configJson,
34
+ metaJson: s.metaJson ? decodeHtmlEntities(s.metaJson) : s.metaJson,
35
+ };
36
+ }
37
+
38
+ export type ListSectionsParams = {
39
+ q?: string;
40
+ includeRetired?: boolean;
41
+ v?: RestRep;
42
+ };
43
+
44
+ export async function listSections(params?: ListSectionsParams, signal?: AbortSignal): Promise<ReportSectionDto[]> {
45
+ const q = params?.q?.trim();
46
+ const includeRetired = params?.includeRetired === true;
47
+ const v = params?.v ?? 'default';
48
+
49
+ const qs = new URLSearchParams();
50
+ qs.set('v', v);
51
+ if (q) qs.set('q', q);
52
+ if (includeRetired) qs.set('includeRetired', 'true');
53
+
54
+ const data = await omrsGet<RestList<ReportSectionDto> | ReportSectionDto[] | undefined>(`${RESOURCE}?${qs.toString()}`, signal);
55
+ return unwrapRestList(data)
56
+ .filter((x) => Boolean(x?.uuid))
57
+ .map(normalizeSectionDto);
58
+ }
59
+
60
+ export async function getSection(uuid: string, signal?: AbortSignal, v: RestRep = 'full'): Promise<ReportSectionDto> {
61
+ const s = await omrsGet<ReportSectionDto>(`${RESOURCE}/${encodeURIComponent(uuid)}?v=${v}`, signal);
62
+ return normalizeSectionDto(s);
63
+ }
64
+
65
+ export async function createSection(payload: Partial<ReportSectionDto>, signal?: AbortSignal): Promise<ReportSectionDto> {
66
+ const s = await omrsPost<ReportSectionDto>(RESOURCE, payload, signal);
67
+ return normalizeSectionDto(s);
68
+ }
69
+
70
+ export async function updateSection(uuid: string, payload: Partial<ReportSectionDto>, signal?: AbortSignal): Promise<ReportSectionDto> {
71
+ const s = await omrsPost<ReportSectionDto>(`${RESOURCE}/${encodeURIComponent(uuid)}`, payload, signal);
72
+ return normalizeSectionDto(s);
73
+ }
@@ -0,0 +1,21 @@
1
+ import { openmrsFetch } from '@openmrs/esm-framework';
2
+
3
+ export type SectionPreviewRequest = {
4
+ sectionUuid: string;
5
+ indicatorUuid?: string;
6
+ startDate: string;
7
+ endDate: string;
8
+ maxRows?: number;
9
+ params?: Record<string, any>;
10
+ };
11
+
12
+ export async function previewSection(body: SectionPreviewRequest, signal?: AbortSignal) {
13
+ const { data } = await openmrsFetch('/ws/rest/v1/reportbuilder/sectionpreview', {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body,
17
+ signal,
18
+ });
19
+
20
+ return data;
21
+ }