Skip to content

Commit 02c27b7

Browse files
authored
BN-37 | Add. Clinical Config Loader (#8)
* BN-37 | Add. Schemas Path Alias * BN-37 | Add. AJV JSON Validator Package * BN-37 | Add. Clinical Dashboard Config Loader * BN-37 | Fix. Remove Unused Error Constants * BN-37 | Fix. Clinical App Config Schema * BN-37 | Refactor. Rename App Schema As Clinical Schema * BN-37 | Refactor. Move Config URL To app * BN-37 | Refactor. Rename Config As ClinicalConfig * BN-37 | Add. Clinical Config Provider * BN-37 | Fix. Component Structure In Index * BN-37 | Add. i18n Support For Config Errors * BN-37 | Fix. Incorrect Schema Title
1 parent 8cbefcf commit 02c27b7

27 files changed

+1715
-59
lines changed

.storybook/main.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,19 @@ import type { StorybookConfig } from '@storybook/react-webpack5';
22
import path from 'path';
33

44
const config: StorybookConfig = {
5-
"stories": [
6-
"../src/**/*.mdx",
7-
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
5+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
6+
addons: [
7+
'@storybook/addon-webpack5-compiler-swc',
8+
'@storybook/addon-essentials',
9+
'@storybook/addon-onboarding',
10+
'@chromatic-com/storybook',
11+
'@storybook/addon-interactions',
812
],
9-
"addons": [
10-
"@storybook/addon-webpack5-compiler-swc",
11-
"@storybook/addon-essentials",
12-
"@storybook/addon-onboarding",
13-
"@chromatic-com/storybook",
14-
"@storybook/addon-interactions"
15-
],
16-
"framework": {
17-
"name": "@storybook/react-webpack5",
18-
"options": {}
13+
framework: {
14+
name: '@storybook/react-webpack5',
15+
options: {},
1916
},
20-
"staticDirs": ['../public'],
17+
staticDirs: ['../public'],
2118
webpackFinal: async (config) => {
2219
// Add SCSS support
2320
if (config.module && config.module.rules) {
@@ -33,7 +30,7 @@ const config: StorybookConfig = {
3330
includePaths: [path.resolve(__dirname, '../node_modules')],
3431
},
3532
},
36-
}
33+
},
3734
],
3835
include: path.resolve(__dirname, '../'),
3936
});
@@ -50,12 +47,13 @@ const config: StorybookConfig = {
5047
'@hooks': path.resolve(__dirname, '../src/hooks'),
5148
'@providers': path.resolve(__dirname, '../src/providers'),
5249
'@services': path.resolve(__dirname, '../src/services'),
50+
'@schemas': path.resolve(__dirname, '../src/schemas'),
5351
'@types': path.resolve(__dirname, '../src/types'),
5452
'@utils': path.resolve(__dirname, '../src/utils'),
5553
};
5654
}
5755

5856
return config;
59-
}
57+
},
6058
};
6159
export default config;

jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const config: Config.InitialOptions = {
1919
'@types/(.*)$': ['<rootDir>/src/types/$1'],
2020
'@utils/(.*)$': ['<rootDir>/src/utils/$1'],
2121
'@providers/(.*)$': ['<rootDir>/src/providers/$1'],
22+
'@schemas/(.*)$': ['<rootDir>/src/schemas/$1'],
2223
'@__mocks__/(.*)$': ['<rootDir>/src/__mocks__/$1'],
2324
},
2425
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"dependencies": {
2020
"@carbon/react": "^1.78.2",
2121
"@testing-library/react-hooks": "^8.0.1",
22+
"ajv": "^8.17.1",
2223
"axios": "^1.8.4",
2324
"date-fns": "^4.1.0",
2425
"i18next": "^24.2.3",

public/locales/locale_en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@
2121
"CONDITION_TABLE_ONSET_DATE": "Onset Date",
2222
"CONDITION_TABLE_PROVIDER": "Provider",
2323
"CONDITION_TABLE_RECORDED_DATE": "Recorded Date",
24+
"CONFIG_ERROR_NOT_FOUND": "Configuration not found",
25+
"CONFIG_ERROR_SCHEMA_VALIDATION_FAILED": "Configuration does not match required schema",
26+
"CONFIG_ERROR_VALIDATION_FAILED": "Configuration validation failed",
2427
"DATE_ERROR_EMPTY_OR_INVALID": "Date string is empty or invalid",
2528
"DATE_ERROR_FORMAT": "Date Format Error",
2629
"DATE_ERROR_INVALID_FORMAT": "Invalid date format",
2730
"DATE_ERROR_NULL_OR_UNDEFINED": "Date is null or undefined",
2831
"DATE_ERROR_PARSE": "Date Parse Error",
2932
"ERROR_BAD_REQUEST_MESSAGE": "Invalid input parameters. Please check your request and try again.",
3033
"ERROR_BAD_REQUEST_TITLE": "Bad Request",
34+
"ERROR_CONFIG_TITLE": "Configuration Error",
35+
"ERROR_DASHBOARD_CONFIG_TITLE": "Department Configuration Error",
3136
"ERROR_DEFAULT_MESSAGE": "An unexpected error occurred",
3237
"ERROR_DEFAULT_TITLE": "Error",
3338
"ERROR_NETWORK_MESSAGE": "Unable to connect to the server. Please check your internet connection.",
@@ -39,6 +44,7 @@
3944
"ERROR_UNAUTHORIZED_MESSAGE": "You are not authorized to perform this action. Please log in again.",
4045
"ERROR_UNAUTHORIZED_TITLE": "Unauthorized",
4146
"ERROR_UNKNOWN_MESSAGE": "An unknown error occurred",
47+
"ERROR_VALIDATION_TITLE": "Validation Error",
4248
"EXPANDABLE_TABLE_EMPTY_STATE_MESSAGE": "No data available",
4349
"EXPANDABLE_TABLE_ERROR_MESSAGE": "{{title}}: {{message}}",
4450
"NO_ALLERGIES": "No Allergies recorded for this patient.",

public/locales/locale_es.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@
2121
"CONDITION_TABLE_ONSET_DATE": "Fecha de inicio",
2222
"CONDITION_TABLE_PROVIDER": "Proveedor",
2323
"CONDITION_TABLE_RECORDED_DATE": "Fecha de grabación",
24+
"CONFIG_ERROR_NOT_FOUND": "Configuración no encontrada",
25+
"CONFIG_ERROR_SCHEMA_VALIDATION_FAILED": "La configuración no coincide con el esquema requerido",
26+
"CONFIG_ERROR_VALIDATION_FAILED": "La validación de la configuración falló",
2427
"DATE_ERROR_EMPTY_OR_INVALID": "La cadena de fecha está vacía o no es válida",
2528
"DATE_ERROR_FORMAT": "Error de formato de fecha",
2629
"DATE_ERROR_INVALID_FORMAT": "Formato de fecha no válido",
2730
"DATE_ERROR_NULL_OR_UNDEFINED": "La fecha es nula o no está definida",
2831
"DATE_ERROR_PARSE": "Error al analizar la fecha",
2932
"ERROR_BAD_REQUEST_MESSAGE": "Parámetros de entrada no válidos. Por favor, verifique su solicitud e intente nuevamente.",
3033
"ERROR_BAD_REQUEST_TITLE": "Solicitud incorrecta",
34+
"ERROR_CONFIG_TITLE": "Error de configuración",
35+
"ERROR_DASHBOARD_CONFIG_TITLE": "Error de configuración del departamento",
3136
"ERROR_DEFAULT_MESSAGE": "Se produjo un error inesperado",
3237
"ERROR_DEFAULT_TITLE": "Error",
3338
"ERROR_NETWORK_MESSAGE": "No se puede conectar al servidor. Por favor, compruebe su conexión a Internet.",
@@ -39,6 +44,7 @@
3944
"ERROR_UNAUTHORIZED_MESSAGE": "No está autorizado para realizar esta acción. Por favor, inicie sesión nuevamente.",
4045
"ERROR_UNAUTHORIZED_TITLE": "No autorizado",
4146
"ERROR_UNKNOWN_MESSAGE": "Se produjo un error desconocido",
47+
"ERROR_VALIDATION_TITLE": "Error de validación",
4248
"EXPANDABLE_TABLE_EMPTY_STATE_MESSAGE": "No hay datos disponibles",
4349
"EXPANDABLE_TABLE_ERROR_MESSAGE": "{{title}}: {{message}}",
4450
"NO_ALLERGIES": "No hay alergias registradas para este paciente",

src/__mocks__/configMocks.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// src/__mocks__/configMocks.ts
2+
3+
// Happy Path Mocks
4+
export const validFullClinicalConfig = {
5+
patientInformation: {
6+
displayPatientIdentifiers: true,
7+
showPatientPhoto: true,
8+
additionalAttributes: ['caste', 'education', 'occupation'],
9+
},
10+
actions: [
11+
{
12+
name: 'Start Visit',
13+
url: '/openmrs/ws/rest/v1/visit',
14+
icon: 'fa fa-stethoscope',
15+
requiredPrivilege: 'Start Visit',
16+
},
17+
{
18+
name: 'Add Diagnosis',
19+
url: '/openmrs/ws/rest/v1/diagnosis',
20+
icon: 'fa fa-heartbeat',
21+
requiredPrivilege: 'Add Diagnosis',
22+
},
23+
{
24+
name: 'Record Allergy',
25+
url: '/openmrs/ws/rest/v1/allergy',
26+
icon: 'fa fa-exclamation-triangle',
27+
requiredPrivilege: 'Record Allergy',
28+
},
29+
],
30+
dashboards: [
31+
{
32+
name: 'Patient Information',
33+
url: 'patient-information',
34+
requiredPrivileges: ['View Patient Information'],
35+
icon: 'fa fa-user',
36+
default: true,
37+
},
38+
{
39+
name: 'Conditions',
40+
url: 'conditions',
41+
requiredPrivileges: ['View Conditions'],
42+
icon: 'fa fa-heartbeat',
43+
},
44+
{
45+
name: 'Allergies',
46+
url: 'allergies',
47+
requiredPrivileges: ['View Allergies'],
48+
icon: 'fa fa-exclamation-triangle',
49+
},
50+
],
51+
};
52+
53+
export const minimalClinicalConfig = {
54+
patientInformation: {},
55+
actions: [],
56+
dashboards: [
57+
{
58+
name: 'Basic Information',
59+
url: 'basic-information',
60+
requiredPrivileges: ['View Patient Dashboard'],
61+
},
62+
],
63+
};
64+
65+
export const mixedClinicalConfig = {
66+
patientInformation: {
67+
displayPatientIdentifiers: true,
68+
},
69+
actions: [
70+
{
71+
name: 'Start Visit',
72+
url: '/openmrs/ws/rest/v1/visit',
73+
requiredPrivilege: 'Start Visit',
74+
},
75+
],
76+
dashboards: [
77+
{
78+
name: 'Required Section',
79+
url: 'required-section',
80+
requiredPrivileges: ['View Patient Dashboard'],
81+
},
82+
{
83+
name: 'Optional Section',
84+
url: 'optional-section',
85+
requiredPrivileges: ['View Optional Dashboard'],
86+
icon: 'fa fa-plus',
87+
default: false,
88+
},
89+
],
90+
};
91+
92+
// Sad Path Mocks
93+
export const invalidClinicalConfig = {
94+
// Missing required properties
95+
patientInformation: {},
96+
// Missing actions array
97+
// Missing dashboards array
98+
otherProperty: 'value',
99+
};
100+
101+
export const emptyResponse = null;
102+
103+
export const malformedJsonResponse = '{invalid-json}';
104+
105+
// Edge Case Mocks
106+
export const largeConfig = {
107+
patientInformation: {
108+
displayPatientIdentifiers: true,
109+
showPatientPhoto: true,
110+
additionalAttributes: Array(50)
111+
.fill(0)
112+
.map((_, i) => `attribute${i}`),
113+
},
114+
actions: Array(20)
115+
.fill(0)
116+
.map((_, i) => ({
117+
name: `Action ${i}`,
118+
url: `/openmrs/ws/rest/v1/action${i}`,
119+
icon: 'fa fa-cog',
120+
requiredPrivilege: `Privilege ${i}`,
121+
})),
122+
dashboards: generateLargeDashboards(50), // Generates 50 dashboards
123+
};
124+
125+
export const allOptionalFieldsConfig = {
126+
patientInformation: {
127+
displayPatientIdentifiers: true,
128+
showPatientPhoto: true,
129+
additionalAttributes: ['caste', 'education', 'occupation'],
130+
customSections: [
131+
{
132+
name: 'Demographics',
133+
attributes: ['birthdate', 'gender', 'address'],
134+
},
135+
{
136+
name: 'Contact Information',
137+
attributes: ['phoneNumber', 'email'],
138+
},
139+
],
140+
},
141+
actions: [
142+
{
143+
name: 'Comprehensive Action',
144+
url: '/openmrs/ws/rest/v1/comprehensive',
145+
icon: 'fa fa-th-large',
146+
requiredPrivilege: 'Comprehensive Privilege',
147+
order: 1,
148+
type: 'standard',
149+
additionalParams: {
150+
color: 'blue',
151+
size: 'large',
152+
showInHeader: true,
153+
},
154+
},
155+
],
156+
dashboards: [
157+
{
158+
name: 'Comprehensive Dashboard',
159+
url: 'comprehensive-dashboard',
160+
requiredPrivileges: ['View Comprehensive Dashboard'],
161+
icon: 'fa fa-th-large',
162+
default: true,
163+
order: 1,
164+
displayName: 'Comprehensive View',
165+
description: 'A dashboard with all possible controls and features',
166+
config: {
167+
refreshInterval: 60,
168+
layout: 'grid',
169+
maxItems: 10,
170+
},
171+
},
172+
],
173+
};
174+
175+
// Helper function to generate large config
176+
function generateLargeDashboards(count: number) {
177+
const dashboards = [];
178+
const icons = [
179+
'fa fa-user',
180+
'fa fa-heartbeat',
181+
'fa fa-hospital',
182+
'fa fa-medkit',
183+
];
184+
185+
for (let i = 0; i < count; i++) {
186+
dashboards.push({
187+
name: `Dashboard ${i}`,
188+
url: `dashboard-${i}`,
189+
requiredPrivileges: [`View Dashboard ${i}`],
190+
icon: icons[i % icons.length],
191+
default: i === 0, // First one is default
192+
});
193+
}
194+
195+
return dashboards;
196+
}

src/constants/app.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ export const PATIENT_CONDITION_RESOURCE_URL = (patientUUID: string) =>
1111
OPENMRS_FHIR_R4 + `/Condition?patient=${patientUUID}`;
1212
export const PATIENT_ALLERGY_RESOURCE_URL = (patientUUID: string) =>
1313
OPENMRS_FHIR_R4 + `/AllergyIntolerance?patient=${patientUUID}`;
14-
14+
export const DASHBOARD_CONFIG_URL = (dashboardURL: string) =>
15+
`/bahmni_config/openmrs/apps/clinical/v2/dashboards/${dashboardURL}`;
16+
export const CLINICAL_CONFIG_URL =
17+
'/bahmni_config/openmrs/apps/clinical/v2/app.json';
1518
export const LOGIN_PATH = '/bahmni/home/index.html#/login';
1619
export const DEFAULT_LOCALE = 'en';
1720
export const LOCALE_STORAGE_KEY = 'NG_TRANSLATE_LANG_KEY';

src/constants/errors.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1-
import i18next from 'i18next';
1+
/**
2+
* Configuration-related error messages
3+
* Used for consistent error handling across the application
4+
*/
5+
export const CONFIG_ERROR_MESSAGES = {
6+
CONFIG_NOT_FOUND: 'CONFIG_ERROR_NOT_FOUND',
7+
VALIDATION_FAILED: 'CONFIG_ERROR_VALIDATION_FAILED',
8+
SCHEMA_VALIDATION_FAILED: 'CONFIG_ERROR_SCHEMA_VALIDATION_FAILED',
9+
};
10+
11+
/**
12+
* Error title constants for notifications
13+
*/
14+
export const ERROR_TITLES = {
15+
CONFIG_ERROR: 'ERROR_CONFIG_TITLE',
16+
VALIDATION_ERROR: 'ERROR_VALIDATION_TITLE',
17+
DASHBOARD_ERROR: 'ERROR_DASHBOARD_CONFIG_TITLE',
18+
};
219

320
export const DATE_ERROR_MESSAGES = {
4-
PARSE_ERROR: i18next.t('DATE_ERROR_PARSE'),
5-
FORMAT_ERROR: i18next.t('DATE_ERROR_FORMAT'),
6-
EMPTY_OR_INVALID: i18next.t('DATE_ERROR_EMPTY_OR_INVALID'),
7-
INVALID_FORMAT: i18next.t('DATE_ERROR_INVALID_FORMAT'),
8-
NULL_OR_UNDEFINED: i18next.t('DATE_ERROR_NULL_OR_UNDEFINED'),
9-
} as const;
21+
PARSE_ERROR: 'DATE_ERROR_PARSE',
22+
FORMAT_ERROR: 'DATE_ERROR_FORMAT',
23+
EMPTY_OR_INVALID: 'DATE_ERROR_EMPTY_OR_INVALID',
24+
INVALID_FORMAT: 'DATE_ERROR_INVALID_FORMAT',
25+
NULL_OR_UNDEFINED: 'DATE_ERROR_NULL_OR_UNDEFINED',
26+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createContext } from 'react';
2+
import { ClinicalConfigContextType } from '@types/config';
3+
4+
export const ClinicalConfigContext = createContext<
5+
ClinicalConfigContextType | undefined
6+
>(undefined);

0 commit comments

Comments
 (0)