dhti-cli 0.8.0 → 1.0.1

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.
@@ -1,13 +1,17 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
+ // eslint-disable-next-line import/default
4
+ import yaml from 'js-yaml';
3
5
  import { exec } from 'node:child_process';
4
6
  import fs from 'node:fs';
5
7
  import os from 'node:os';
6
8
  import path from 'node:path';
7
9
  import { fileURLToPath } from 'node:url';
10
+ import { promisify } from 'node:util';
11
+ const execAsync = promisify(exec);
8
12
  export default class Elixir extends Command {
9
13
  static args = {
10
- op: Args.string({ description: 'Operation to perform (install, uninstall or dev)' }),
14
+ op: Args.string({ description: 'Operation to perform (init, install, uninstall, dev or start)' }),
11
15
  };
12
16
  static description = 'Install or uninstall elixirs to create a Docker image';
13
17
  static examples = ['<%= config.bin %> <%= command.id %>'];
@@ -23,6 +27,15 @@ export default class Elixir extends Command {
23
27
  default: false,
24
28
  description: 'Show what changes would be made without actually making them',
25
29
  }),
30
+ elixir: Flags.string({
31
+ char: 'e',
32
+ description: 'Elixir endpoint URL',
33
+ }),
34
+ fhir: Flags.string({
35
+ char: 'f',
36
+ default: 'http://hapi.fhir.org/baseR4',
37
+ description: 'FHIR endpoint URL',
38
+ }),
26
39
  git: Flags.string({ char: 'g', default: 'none', description: 'Github repository to install' }),
27
40
  local: Flags.string({ char: 'l', default: 'none', description: 'Local directory to install from' }),
28
41
  name: Flags.string({ char: 'n', description: 'Name of the elixir' }),
@@ -50,6 +63,202 @@ export default class Elixir extends Command {
50
63
  const __filename = fileURLToPath(import.meta.url);
51
64
  const __dirname = path.dirname(__filename);
52
65
  const RESOURCES_DIR = path.resolve(__dirname, '../resources');
66
+ // Handle init operation
67
+ if (args.op === 'init') {
68
+ // Validate required flags
69
+ if (!flags.workdir) {
70
+ console.error(chalk.red('Error: workdir flag is required for init operation'));
71
+ this.exit(1);
72
+ }
73
+ if (!flags.name) {
74
+ console.error(chalk.red('Error: name flag is required for init operation'));
75
+ this.exit(1);
76
+ }
77
+ const targetDir = path.join(flags.workdir, 'dhti-elixir');
78
+ if (flags['dry-run']) {
79
+ console.log(chalk.yellow('[DRY RUN] Would execute init operation:'));
80
+ console.log(chalk.cyan(` npx degit dermatologist/dhti-elixir ${targetDir}`));
81
+ console.log(chalk.cyan(` Copy ${targetDir}/packages/starter to ${targetDir}/packages/${flags.name}`));
82
+ return;
83
+ }
84
+ try {
85
+ // Run npx degit to clone the dhti-elixir template
86
+ console.log(chalk.blue(`Initializing DHTI elixir template in ${targetDir}...`));
87
+ const degitCommand = `npx degit dermatologist/dhti-elixir ${targetDir}`;
88
+ await execAsync(degitCommand);
89
+ console.log(chalk.green('✓ DHTI elixir template cloned successfully'));
90
+ // Copy packages/starter subdirectory to packages/<name>
91
+ const simpleChatSource = path.join(targetDir, 'packages', 'starter');
92
+ const targetPackageDir = path.join(targetDir, 'packages', flags.name);
93
+ if (fs.existsSync(simpleChatSource)) {
94
+ console.log(chalk.blue(`Copying starter to packages/${flags.name}...`));
95
+ fs.cpSync(simpleChatSource, targetPackageDir, { recursive: true });
96
+ console.log(chalk.green(`✓ starter copied to packages/${flags.name}`));
97
+ }
98
+ else {
99
+ console.log(chalk.yellow(`Warning: starter not found at ${simpleChatSource}`));
100
+ }
101
+ console.log(chalk.green(`\n✓ Initialization complete! Your elixir workspace is ready at ${targetDir}`));
102
+ console.log(chalk.blue(`\nNext steps:`));
103
+ console.log(chalk.cyan(` 1. cd ${targetDir}`));
104
+ console.log(chalk.cyan(` 2. Follow the README.md for development instructions`));
105
+ }
106
+ catch (error) {
107
+ console.error(chalk.red('Error during initialization:'), error);
108
+ this.exit(1);
109
+ }
110
+ return;
111
+ }
112
+ // Handle start operation
113
+ if (args.op === 'start') {
114
+ // Determine the elixir endpoint URL
115
+ let elixirUrl = flags.elixir;
116
+ if (!elixirUrl) {
117
+ // If --elixir is not provided, construct it from --name
118
+ if (!flags.name) {
119
+ console.error(chalk.red('Error: Either --elixir or --name flag must be provided for start operation'));
120
+ this.exit(1);
121
+ }
122
+ const nameWithUnderscores = flags.name.replaceAll('-', '_');
123
+ elixirUrl = `http://localhost:8001/langserve/${nameWithUnderscores}/cds-services`;
124
+ }
125
+ const sandboxDir = path.join(flags.workdir, 'cds-hooks-sandbox');
126
+ if (flags['dry-run']) {
127
+ const directoryExists = fs.existsSync(sandboxDir);
128
+ console.log(chalk.yellow('[DRY RUN] Would execute start operation:'));
129
+ if (directoryExists) {
130
+ console.log(chalk.cyan(` [SKIP] Directory already exists: ${sandboxDir}`));
131
+ }
132
+ else {
133
+ console.log(chalk.cyan(` npx degit dermatologist/cds-hooks-sandbox ${sandboxDir}`));
134
+ console.log(chalk.cyan(` cd ${sandboxDir}`));
135
+ console.log(chalk.cyan(` yarn install`));
136
+ }
137
+ console.log(chalk.cyan(` yarn dhti ${elixirUrl} ${flags.fhir}`));
138
+ console.log(chalk.cyan(` Update docker-compose.yml FHIR_BASE_URL=${flags.fhir}`));
139
+ console.log(chalk.cyan(` docker restart ${flags.container}`));
140
+ console.log(chalk.cyan(` yarn dev`));
141
+ return;
142
+ }
143
+ try {
144
+ // Check if the directory already exists
145
+ const directoryExists = fs.existsSync(sandboxDir);
146
+ if (directoryExists) {
147
+ console.log(chalk.blue(`Using existing CDS Hooks Sandbox at ${sandboxDir}...`));
148
+ console.log(chalk.green('✓ Skipped clone and install (directory already exists)'));
149
+ }
150
+ else {
151
+ // Clone the cds-hooks-sandbox repository
152
+ console.log(chalk.blue(`Cloning CDS Hooks Sandbox to ${sandboxDir}...`));
153
+ const degitCommand = `npx degit dermatologist/cds-hooks-sandbox ${sandboxDir}`;
154
+ await execAsync(degitCommand);
155
+ console.log(chalk.green('✓ CDS Hooks Sandbox cloned successfully'));
156
+ // Install dependencies
157
+ console.log(chalk.blue('Installing dependencies...'));
158
+ const installCommand = `cd ${sandboxDir} && yarn install`;
159
+ const { stderr: installError } = await execAsync(installCommand);
160
+ if (installError && !installError.includes('warning')) {
161
+ console.error(chalk.yellow(`Installation warnings: ${installError}`));
162
+ }
163
+ console.log(chalk.green('✓ Dependencies installed successfully'));
164
+ }
165
+ // Configure dhti endpoints
166
+ console.log(chalk.blue('Configuring DHTI endpoints...'));
167
+ const dhtiCommand = `cd ${sandboxDir} && yarn dhti ${elixirUrl} ${flags.fhir}`;
168
+ const { stderr: dhtiError } = await execAsync(dhtiCommand);
169
+ if (dhtiError && !dhtiError.includes('warning')) {
170
+ console.error(chalk.yellow(`Configuration warnings: ${dhtiError}`));
171
+ }
172
+ console.log(chalk.green('✓ DHTI endpoints configured successfully'));
173
+ // Configure Docker container with FHIR_BASE_URL environment variable
174
+ console.log(chalk.blue('Setting up Docker container environment...'));
175
+ try {
176
+ // Update the docker-compose.yml file with the new FHIR_BASE_URL
177
+ const dockerComposeFile = path.join(flags.workdir, 'docker-compose.yml');
178
+ if (fs.existsSync(dockerComposeFile)) {
179
+ const composeContent = fs.readFileSync(dockerComposeFile, 'utf8');
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ const compose = yaml.load(composeContent);
182
+ if (compose.services && compose.services.langserve) {
183
+ if (!compose.services.langserve.environment) {
184
+ compose.services.langserve.environment = [];
185
+ }
186
+ // Update or add the FHIR_BASE_URL environment variable
187
+ const envArray = compose.services.langserve.environment;
188
+ const fhirIndex = envArray.findIndex((env) => {
189
+ if (typeof env === 'string') {
190
+ return env.startsWith('FHIR_BASE_URL=');
191
+ }
192
+ return typeof env === 'object' && env !== null && 'FHIR_BASE_URL' in env;
193
+ });
194
+ if (fhirIndex >= 0) {
195
+ // Update existing environment variable
196
+ const existingEnv = envArray[fhirIndex];
197
+ if (typeof existingEnv === 'string') {
198
+ envArray[fhirIndex] = `FHIR_BASE_URL=${flags.fhir}`;
199
+ }
200
+ else if (typeof existingEnv === 'object' && existingEnv !== null) {
201
+ existingEnv.FHIR_BASE_URL = flags.fhir;
202
+ }
203
+ }
204
+ else {
205
+ // Add new environment variable
206
+ envArray.push(`FHIR_BASE_URL=${flags.fhir}`);
207
+ }
208
+ const updatedCompose = yaml.dump(compose);
209
+ fs.writeFileSync(dockerComposeFile, updatedCompose);
210
+ console.log(chalk.green(`✓ docker-compose.yml updated with FHIR_BASE_URL=${flags.fhir}`));
211
+ }
212
+ else {
213
+ console.warn(chalk.yellow('⚠ Warning: langserve service not found in docker-compose.yml'));
214
+ }
215
+ }
216
+ else {
217
+ console.warn(chalk.yellow(`⚠ Warning: docker-compose.yml not found at ${dockerComposeFile}`));
218
+ }
219
+ // Restart the container to apply the new environment variables
220
+ const upCommand = `docker compose up -d`; // Docker Compose is smart enough to re-create only the services where the configuration, including environment variables, has changed
221
+ await execAsync(upCommand, { cwd: flags.workdir });
222
+ console.log(chalk.green(`✓ Docker container ${flags.container} restarted (compose up -d) successfully`));
223
+ }
224
+ catch (error) {
225
+ const err = error;
226
+ if (err.message?.includes('No such container')) {
227
+ console.warn(chalk.yellow(`⚠ Warning: Docker container ${flags.container} not found. Skipping container restart.`));
228
+ }
229
+ else {
230
+ console.error(chalk.red(`Error setting up Docker container: ${err.message}`));
231
+ this.exit(1);
232
+ }
233
+ }
234
+ // Start the development server
235
+ console.log(chalk.blue('Starting development server...'));
236
+ const { spawn } = await import('node:child_process');
237
+ const devCommand = `yarn dhti ${elixirUrl} ${flags.fhir} && yarn dev`;
238
+ try {
239
+ const child = spawn(devCommand, { cwd: sandboxDir, shell: true, stdio: 'inherit' });
240
+ await new Promise((resolve, reject) => {
241
+ child.on('exit', (code) => {
242
+ if (code === 0)
243
+ resolve(undefined);
244
+ else
245
+ reject(new Error(`Dev server exited with code ${code}`));
246
+ });
247
+ child.on('error', reject);
248
+ });
249
+ }
250
+ catch (error) {
251
+ const err = error;
252
+ console.error(chalk.red('Error starting development server:'), err.message);
253
+ this.exit(1);
254
+ }
255
+ }
256
+ catch (error) {
257
+ console.error(chalk.red('Error during start operation:'), error);
258
+ this.exit(1);
259
+ }
260
+ return;
261
+ }
53
262
  if (!flags.name) {
54
263
  console.log('Please provide a name for the elixir');
55
264
  this.exit(1);
@@ -178,11 +387,17 @@ mcp_server.add_tool(${expoName}_mcp_tool) # type: ignore
178
387
  .replace('# DHTI_CLI_IMPORT', `#DHTI_CLI_IMPORT\n${CliImport}`);
179
388
  }
180
389
  const langfuseRoute = `add_routes(app, ${expoName}_chain.with_config(config), path="/langserve/${expoName}")`;
181
- const newLangfuseRoute = flags['dry-run'] ? '' : newCliImport.replace('# DHTI_LANGFUSE_ROUTE', `#DHTI_LANGFUSE_ROUTE\n ${langfuseRoute}`);
390
+ const newLangfuseRoute = flags['dry-run']
391
+ ? ''
392
+ : newCliImport.replace('# DHTI_LANGFUSE_ROUTE', `#DHTI_LANGFUSE_ROUTE\n ${langfuseRoute}`);
182
393
  const normalRoute = `add_routes(app, ${expoName}_chain, path="/langserve/${expoName}")`;
183
- const newNormalRoute = flags['dry-run'] ? '' : newLangfuseRoute.replace('# DHTI_NORMAL_ROUTE', `#DHTI_NORMAL_ROUTE\n ${normalRoute}`);
394
+ const newNormalRoute = flags['dry-run']
395
+ ? ''
396
+ : newLangfuseRoute.replace('# DHTI_NORMAL_ROUTE', `#DHTI_NORMAL_ROUTE\n ${normalRoute}`);
184
397
  const commonRoutes = `\nadd_invokes(app, path="/langserve/${expoName}")\nadd_services(app, path="/langserve/${expoName}")`;
185
- const finalRoute = flags['dry-run'] ? '' : newNormalRoute.replace('# DHTI_COMMON_ROUTE', `#DHTI_COMMON_ROUTES${commonRoutes}`);
398
+ const finalRoute = flags['dry-run']
399
+ ? ''
400
+ : newNormalRoute.replace('# DHTI_COMMON_ROUTE', `#DHTI_COMMON_ROUTES${commonRoutes}`);
186
401
  // if args.op === install, add the line to the pyproject.toml file
187
402
  if (args.op === 'install') {
188
403
  if (flags['dry-run']) {
@@ -1,4 +1,3 @@
1
- version: "3.7"
2
1
 
3
2
  services:
4
3
  gateway:
@@ -72,6 +71,7 @@ services:
72
71
 
73
72
  langserve:
74
73
  image: beapen/genai:latest
74
+ network_mode: host
75
75
  pull_policy: always
76
76
  ports:
77
77
  - "8001:8001"
@@ -86,6 +86,8 @@ services:
86
86
  - OPENAI_API_KEY=${OPENAI_API_KEY:-openai-api-key}
87
87
  - OPENAI_API_BASE=${OPENAI_API_BASE:-https://openrouter.ai/api/v1}
88
88
  - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-openrouter-api-key}
89
+ - FHIR_BASE_URL=${FHIR_BASE_URL:-http://localhost:8080/openmrs/ws/fhir2/R4}
90
+
89
91
 
90
92
  ollama:
91
93
  image: ollama/ollama:latest
@@ -0,0 +1,3 @@
1
+ ## Notes
2
+
3
+ * spa folder is not used and can be deleted once the old OpenMRS container based invocation is completely phased out.
@@ -0,0 +1,53 @@
1
+ import {Type, validator} from '@openmrs/esm-framework'
2
+
3
+ /**
4
+ * This is the config schema. It expects a configuration object which
5
+ * looks like this:
6
+ *
7
+ * ```json
8
+ * { "casualGreeting": true, "whoToGreet": ["Mom"] }
9
+ * ```
10
+ *
11
+ * In OpenMRS Microfrontends, all config parameters are optional. Thus,
12
+ * all elements must have a reasonable default. A good default is one
13
+ * that works well with the reference application.
14
+ *
15
+ * To understand the schema below, please read the configuration system
16
+ * documentation:
17
+ * https://openmrs.github.io/openmrs-esm-core/#/main/config
18
+ * Note especially the section "How do I make my module configurable?"
19
+ * https://openmrs.github.io/openmrs-esm-core/#/main/config?id=im-developing-an-esm-module-how-do-i-make-it-configurable
20
+ * and the Schema Reference
21
+ * https://openmrs.github.io/openmrs-esm-core/#/main/config?id=schema-reference
22
+ */
23
+ export const configSchema = {
24
+ casualGreeting: {
25
+ _type: Type.Boolean,
26
+ _default: false,
27
+ _description: 'Whether to use a casual greeting (or a formal one).',
28
+ },
29
+ dhtiTitle: {
30
+ _type: Type.String,
31
+ _default: 'DHTI Template Widget',
32
+ _description: 'Title to display for the DHTI Template Widget',
33
+ },
34
+ dhtiRoute: {
35
+ _type: Type.String,
36
+ _default: 'http://localhost:8001/langserve/dhti_elixir_template/cds-services/dhti-service',
37
+ _description: 'Route for the DHTI Template Service',
38
+ },
39
+ whoToGreet: {
40
+ _type: Type.Array,
41
+ _default: ['World'],
42
+ _description: 'Who should be greeted. Names will be separated by a comma and space.',
43
+ _elements: {
44
+ _type: Type.String,
45
+ },
46
+ _validators: [validator((v) => v.length > 0, 'At least one person must be greeted.')],
47
+ },
48
+ }
49
+
50
+ export type Config = {
51
+ casualGreeting: boolean
52
+ whoToGreet: Array<string>
53
+ }
@@ -0,0 +1,86 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { useConfig, openmrsFetch, fhirBaseUrl } from '@openmrs/esm-framework';
3
+ import useSWR from 'swr';
4
+ import styles from './glycemic.scss';
5
+ import { useDhti } from './hooks/useDhti';
6
+
7
+ interface GlycemicWidgetProps {
8
+ patientUuid: string;
9
+ }
10
+
11
+ const GlycemicWidget: React.FC<GlycemicWidgetProps> = ({ patientUuid }) => {
12
+ const config = useConfig();
13
+ const serviceName = config?.dhtiServiceName || 'dhti_elixir_glycemic';
14
+ const { submitMessage, loading: aiLoading, error: aiError } = useDhti();
15
+ const [aiResponse, setAiResponse] = useState<any>(null);
16
+
17
+ // Check for Diabetes (SNOMED 44054006)
18
+ const { data: conditionsData, isLoading: conditionsLoading } = useSWR<any>(
19
+ patientUuid ? `${fhirBaseUrl}/Condition?patient=${patientUuid}&code=44054006` : null,
20
+ openmrsFetch
21
+ );
22
+
23
+ const isDiabetic = conditionsData?.data?.entry?.length > 0;
24
+
25
+ // Fetch HbA1c (4548-4) and Glucose (2339-0) observations
26
+ const { data: obsData, isLoading: obsLoading } = useSWR<any>(
27
+ isDiabetic ? `${fhirBaseUrl}/Observation?patient=${patientUuid}&code=4548-4,2339-0&_sort=-date&_count=5` : null,
28
+ openmrsFetch
29
+ );
30
+
31
+ useEffect(() => {
32
+ if (!isDiabetic && !aiResponse && !aiLoading && !aiError) {
33
+ submitMessage('Analyze glycemic control', patientUuid).then(setAiResponse);
34
+ }
35
+ }, [isDiabetic, patientUuid, submitMessage, aiResponse, aiLoading, aiError]);
36
+
37
+ // if (conditionsLoading) return null;
38
+ // if (!isDiabetic) return null;
39
+
40
+ return (
41
+ <div className={styles.container}>
42
+ <div className={styles.header}>
43
+ <h3>{config?.dhtiTitle || 'DHTI Glycemic Widget'}</h3>
44
+ </div>
45
+
46
+ <div className={styles.content}>
47
+ <div className={styles.observations}>
48
+ <h4>Latest Readings</h4>
49
+ {obsLoading ? (
50
+ <p>Loading observations...</p>
51
+ ) : obsData?.data?.entry?.length > 0 ? (
52
+ <ul>
53
+ {obsData.data.entry.map((entry: any) => {
54
+ const obs = entry.resource;
55
+ const value = obs.valueQuantity ? `${obs.valueQuantity.value} ${obs.valueQuantity.unit}` : obs.valueString;
56
+ const date = new Date(obs.effectiveDateTime).toLocaleDateString();
57
+ const name = obs.code?.text || obs.code?.coding?.[0]?.display || 'Observation';
58
+ return (
59
+ <li key={obs.id}>
60
+ <strong>{name}:</strong> {value} <small>({date})</small>
61
+ </li>
62
+ );
63
+ })}
64
+ </ul>
65
+ ) : (
66
+ <p>No recent HbA1c or Glucose readings found.</p>
67
+ )}
68
+ </div>
69
+
70
+ <div className={styles.aiInsights}>
71
+ <h4>GenAI Interpretation</h4>
72
+ {aiLoading && <p>Generating insights...</p>}
73
+ {aiError && <p className={styles.error}>Error: {aiError}</p>}
74
+ {aiResponse && (
75
+ <div className={styles.aiContent}>
76
+ <p>{aiResponse.summary}</p>
77
+ {aiResponse.detail && <p><small>{aiResponse.detail}</small></p>}
78
+ </div>
79
+ )}
80
+ </div>
81
+ </div>
82
+ </div>
83
+ );
84
+ };
85
+
86
+ export default GlycemicWidget;
@@ -0,0 +1,44 @@
1
+ .container {
2
+ padding: 1rem;
3
+ background-color: var(--ui-01);
4
+ border: 1px solid var(--ui-03);
5
+ margin-bottom: 1rem;
6
+ }
7
+
8
+ .header {
9
+ margin-bottom: 1rem;
10
+ h3 {
11
+ margin: 0;
12
+ }
13
+ }
14
+
15
+ .content {
16
+ display: flex;
17
+ gap: 2rem;
18
+ flex-wrap: wrap;
19
+ }
20
+
21
+ .observations, .aiInsights {
22
+ flex: 1;
23
+ min-width: 300px;
24
+ }
25
+
26
+ .observations ul {
27
+ list-style: none;
28
+ padding: 0;
29
+ li {
30
+ margin-bottom: 0.5rem;
31
+ border-bottom: 1px solid var(--ui-03);
32
+ padding-bottom: 0.25rem;
33
+ }
34
+ }
35
+
36
+ .aiContent {
37
+ background-color: var(--ui-02);
38
+ padding: 1rem;
39
+ border-radius: 4px;
40
+ }
41
+
42
+ .error {
43
+ color: var(--support-01);
44
+ }
@@ -0,0 +1,69 @@
1
+ import {useState} from 'react'
2
+ import axios from 'axios'
3
+ import {CDSHookRequest} from '../models/request'
4
+ import {CDSHookCard} from '../models/card'
5
+ import {useConfig, openmrsFetch, fhirBaseUrl} from '@openmrs/esm-framework'
6
+
7
+ interface UseDhtiReturn {
8
+ submitMessage: (newMessage: string, patientId?: string) => Promise<CDSHookCard | null>
9
+ loading: boolean
10
+ error: string | null
11
+ }
12
+
13
+ /**
14
+ * Custom hook to handle DHTI service submissions
15
+ * @returns Object with submitMessage function, loading state, and error state
16
+ */
17
+ export const useDhti = (): UseDhtiReturn => {
18
+ const [loading, setLoading] = useState<boolean>(false)
19
+ const [error, setError] = useState<string | null>(null)
20
+ const config = useConfig()
21
+
22
+ const submitMessage = async (newMessage: string, patientId?: string): Promise<CDSHookCard | null> => {
23
+ setLoading(true)
24
+ setError(null)
25
+
26
+ try {
27
+ const request = new CDSHookRequest({
28
+ context: {input: newMessage, patientId: patientId || undefined},
29
+ })
30
+
31
+ // TODO: Investigate why nested input is required
32
+ const _request = {
33
+ input: request,
34
+ }
35
+
36
+ const response = await axios.post(
37
+ config?.dhtiRoute || 'http://localhost:8001/langserve/dhti_elixir_template/cds-services/dhti-service',
38
+ {
39
+ input: _request,
40
+ config: {},
41
+ kwargs: {},
42
+ },
43
+ {
44
+ headers: {
45
+ 'Access-Control-Allow-Origin': '*',
46
+ },
47
+ },
48
+ )
49
+
50
+ // Assuming the response contains a card or cards
51
+ if (response.data && response.data.cards && response.data.cards.length > 0) {
52
+ return CDSHookCard.from(response.data.cards[0])
53
+ } else if (response.data && response.data.summary) {
54
+ // Handle case where response is directly a card (must have summary property)
55
+ return CDSHookCard.from(response.data)
56
+ }
57
+
58
+ return null
59
+ } catch (err: any) {
60
+ const errorMessage = err.response?.data?.message || err.message || 'Failed to submit message'
61
+ setError(errorMessage)
62
+ return null
63
+ } finally {
64
+ setLoading(false)
65
+ }
66
+ }
67
+
68
+ return {submitMessage, loading, error}
69
+ }
@@ -0,0 +1,61 @@
1
+ import useSWR from 'swr'
2
+ import {fhirBaseUrl, openmrsFetch} from '@openmrs/esm-framework'
3
+
4
+ /**
5
+ * This hook searches for a patient using the provided search term from the
6
+ * OpenMRS FHIR API. It leverages the useSWR hook from the SWR library
7
+ * https://swr.vercel.app/docs/data-fetching to fetch data. SWR provides a
8
+ * number of benefits over the standard React useEffect hook, including:
9
+ *
10
+ * - Fast, lightweight and reusable data fetching
11
+ * - Built-in cache and request deduplication
12
+ * - Real-time updates
13
+ * - Simplified error and loading state handling, and more.
14
+ *
15
+ * We recommend using SWR for data fetching in your OpenMRS frontend modules.
16
+ *
17
+ * See the docs for the underlying fhir.js Client object: https://github.com/FHIR/fhir.js#api
18
+ * See the OpenMRS FHIR Module docs: https://wiki.openmrs.org/display/projects/OpenMRS+FHIR+Module
19
+ * See the OpenMRS REST API docs: https://rest.openmrs.org/#openmrs-rest-api
20
+ *
21
+ * @param query A patient name or ID
22
+ * @returns The first matching patient
23
+ */
24
+
25
+ export function usePatient(query: string) {
26
+ // If query has a number anywhere in it, treat it as an identifier search
27
+ const isId = /\d/.test(query)
28
+ let url = null
29
+ if (query && query.trim()) {
30
+ if (isId) {
31
+ url = `${fhirBaseUrl}/Patient?identifier=${encodeURIComponent(query.trim())}&_summary=data`
32
+ } else {
33
+ url = `${fhirBaseUrl}/Patient?name=${encodeURIComponent(query.trim())}&_summary=data`
34
+ }
35
+ }
36
+ const {data, error, isLoading} = useSWR<any, Error>(url, openmrsFetch)
37
+
38
+ let patient = null
39
+ // * Use the code below to handle direct ID searches, if needed in future
40
+ // --- IGNORE ---
41
+ // if (isId) {
42
+ // // FHIR /Patient/{id} returns the patient directly
43
+ // if (data && data.resourceType === 'Patient') {
44
+ // patient = data;
45
+ // }
46
+ // } else {
47
+ // if (data && data.data && Array.isArray(data.data.entry) && data.data.entry.length > 0) {
48
+ // patient = data.data.entry[0].resource;
49
+ // }
50
+ // }
51
+
52
+ if (data && data.data && Array.isArray(data.data.entry) && data.data.entry.length > 0) {
53
+ patient = data.data.entry[0].resource
54
+ }
55
+
56
+ return {
57
+ patient,
58
+ error: error,
59
+ isLoading,
60
+ }
61
+ }
@@ -0,0 +1,17 @@
1
+ import {getAsyncLifecycle, defineConfigSchema} from '@openmrs/esm-framework'
2
+ import {configSchema} from './config-schema'
3
+
4
+ const moduleName = '@openmrs/esm-dhti-glycemic'
5
+
6
+ const options = {
7
+ featureName: 'dhti-glycemic',
8
+ moduleName,
9
+ }
10
+
11
+ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy')
12
+
13
+ export function startupApp() {
14
+ defineConfigSchema(moduleName, configSchema)
15
+ }
16
+
17
+ export const glycemic = getAsyncLifecycle(() => import('./glycemic.component'), options)