Skip to content

Commit

Permalink
(feat) Billable Exemption config (#503)
Browse files Browse the repository at this point in the history
* (feat) Add Billable exemption config

* Sync yarn.lock

* Sync yarn.lock

* Sync yarn.lock update package.json

---------

Co-authored-by: Donald Kibet <[email protected]>
  • Loading branch information
Ogollah and donaldkibet authored Dec 6, 2024
1 parent 60767a4 commit b40fe59
Show file tree
Hide file tree
Showing 20 changed files with 893 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@hookform/resolvers": "^3.1.1",
"classnames": "^2.3.2",
"fuzzy": "^0.1.3",
"react-ace": "^13.0.0",
"react-hook-form": "^7.45.1",
"ts-dotenv": "^0.9.1",
"xlsx": "^0.18.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import BillingHeader from '../billing-header/billing-header.component';
import { BillableExemptionsViewer } from '../billable-services/billable-exemptions/billable-exemptions-viewer.component';

export const BillableExemptions = () => {
const { t } = useTranslation();
return (
<div>
<BillingHeader title={t('billableExemptionAdministration', 'Exemption Administration')} />
<BillableExemptionsViewer />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import SaveSchemamModal from '../modals/save-schema.modal';
import type { Schema } from '../../../types';
import styles from './action-buttons.scss';

interface ActionButtonsProps {
schema: Schema;
}

const ActionButtons: React.FC<ActionButtonsProps> = ({ schema }) => {
return (
<div className={styles.actionButtons}>
<SaveSchemamModal schema={schema} />
</div>
);
};

export default ActionButtons;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@use '@carbon/layout';

.actionButtons {
display: flex;
align-items: center;
justify-content: flex-end;
margin: layout.$spacing-05 0;

> button {
margin-left: layout.$spacing-05;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React, { useCallback, useEffect, useState } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import type { IMarker } from 'react-ace';
import {
Button,
Column,
Grid,
InlineLoading,
InlineNotification,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from '@carbon/react';
import SchemaEditor from '../billable-exemptions/schema-editor/schema-editor.component';
import SchemaViewer from '../billable-exemptions/schema-editor/schema-viewer-component';
import { useSystemBillableSetting } from '../../hooks/useSystemBillableSetting';
import ActionButtons from '../billable-exemptions/action-buttons/action-buttons.component';
import { EmptyState } from '@openmrs/esm-patient-common-lib';
import type { Schema } from '../../types';
import styles from './billable-exemptions.scss';

interface MarkerProps extends IMarker {
text: string;
}

const ErrorNotification = ({ error, title }: { error: Error; title: string }) => (
<InlineNotification
className={styles.errorNotification}
kind="error"
lowContrast
subtitle={error?.message}
title={title}
/>
);

export const BillableExemptionsViewer = () => {
const { t } = useTranslation();
const { billableExceptionResource, isLoading, error } = useSystemBillableSetting('kenyaemr.billing.exemptions');
const billableExceptionSchema = billableExceptionResource?.value ?? '';

const [schema, setSchema] = useState<Schema | null>(null);
const [stringifiedSchema, setStringifiedSchema] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const [invalidJsonErrorMessage, setInvalidJsonErrorMessage] = useState('');
const [errors, setErrors] = useState<Array<MarkerProps>>([]);
const [validationOn, setValidationOn] = useState(true);

const resetErrorMessage = useCallback(() => setInvalidJsonErrorMessage(''), []);

const handleSchemaChange = useCallback(
(updatedSchema: string) => {
resetErrorMessage();
setStringifiedSchema(updatedSchema);

try {
const parsedSchema = JSON.parse(updatedSchema);
setSchema(parsedSchema);
} catch (error) {
setInvalidJsonErrorMessage(t('invalidJsonError', 'Invalid JSON input.'));
}
},
[resetErrorMessage],
);

const updateSchema = useCallback((updatedSchema: Schema) => {
try {
setSchema(updatedSchema);
setStringifiedSchema(JSON.stringify(updatedSchema, null, 2));
} catch (error) {
setInvalidJsonErrorMessage(t('saveError', 'Failed to save schema.'));
}
}, []);

useEffect(() => {
if (billableExceptionSchema) {
try {
const parsedSchema: Schema = JSON.parse(billableExceptionSchema);
setSchema(parsedSchema);
setStringifiedSchema(JSON.stringify(parsedSchema, null, 2));
} catch (error) {
setInvalidJsonErrorMessage(t('invalidJsonError', 'Invalid JSON received for the schema.'));
}
}
}, [billableExceptionSchema, t]);

const inputDummySchema = useCallback(() => {
const dummySchema = {
services: {
all: [
{ concept: '856000001122243', description: 'HIV Viral Load' },
{ concept: '167441', description: 'PCR' },
{ concept: '162202', description: 'GeneXpert' },
],
'program:HIV': [{ concept: '1000051', description: 'Registration' }],
'program:TB': [{ concept: '162202', description: 'GeneXpert' }],
'age<5': [{ concept: '32', description: 'Malaria Smear' }],
'visitAttribute:prisoner': [{ concept: '32', description: 'Malaria Smear' }],
},
commodities: {},
};

setStringifiedSchema(JSON.stringify(dummySchema, null, 2));
updateSchema(dummySchema);
}, [updateSchema]);

const handleTabChange = (event) => {
setSelectedIndex(event.selectedIndex);
};

return (
<div className={styles.container}>
<Grid className={classNames(styles.grid)}>
<Column lg={16} md={16} className={styles.column}>
<div className={styles.actionButtons}>
{isLoading ? (
<InlineLoading description={`${t('loadingSchema', 'Loading schema')}...`} />
) : (
<h1 className={styles.schemaName}>{t('exemptionSchema', 'Exemption Schema')}</h1>
)}
</div>
<div className={styles.heading}>
<span className={styles.tabHeading}>{t('schemaEditor', 'Exemptions Schema Editor')}</span>
<div className={styles.topBtns}>
{!schema && selectedIndex === 1 && (
<Button kind="ghost" onClick={inputDummySchema}>
{t('inputSampleSchema', 'Input sample schema')}
</Button>
)}
{schema && selectedIndex === 1 && <ActionButtons schema={schema} />}
</div>
</div>
{error && <ErrorNotification error={error} title={t('schemaLoadError', 'Error loading schema')} />}
<Tabs onChange={handleTabChange} selected={selectedIndex}>
<TabList aria-label="Schema previews">
<Tab>{t('preview', 'Schema Preview')}</Tab>
<Tab>{schema ? t('editSchema', 'Edit Schema') : t('addSchema', 'Add Schema')}</Tab>
</TabList>
<TabPanels>
<TabPanel>
{stringifiedSchema ? (
<SchemaViewer data={stringifiedSchema} />
) : (
<div className={styles.emptyStateWrapper}>
<EmptyState
displayText={t('noSchemaExemption', 'No schema available add exemption schema')}
headerTitle={t('noSchema', 'No schema available')}
/>
</div>
)}
</TabPanel>
<TabPanel>
<div className={styles.editorContainer}>
<SchemaEditor
errors={errors}
isLoading={isLoading}
onSchemaChange={handleSchemaChange}
setErrors={setErrors}
setValidationOn={setValidationOn}
stringifiedSchema={stringifiedSchema}
validationOn={validationOn}
/>
</div>
</TabPanel>
</TabPanels>
</Tabs>
</Column>
</Grid>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
@use '@carbon/colors';
@use '@carbon/layout';
@use '@carbon/type';

.container {
padding: layout.$spacing-05 layout.$spacing-10;
display: flex;
flex-direction: column;
}

.grid {
margin-left: 0;
margin-right: 0;
padding-left: 0;
padding-right: 0;
max-width: 100%;

:global(.cds--tabs__nav-item--selected) {
border-bottom: 2px solid var(--cds-border-interactive, colors.$teal-70);
outline: none !important;
}
}

.column {
margin-left: 0;
margin-right: 0;
}
.errorNotification {
min-width: 100%;
margin: 0;
padding: 0;
}

.actionButtons {
display: flex;
align-items: center;
justify-content: space-between;

button {
margin-left: layout.$spacing-05;
}
}

.schemaName {
@include type.type-style('heading-03');
}

.editorContainer {
padding: layout.$spacing-05;

:global(.ace_editor .ace_content .ace_layer .error) {
position: absolute;
background-color: colors.$red-60;
border-radius: 0;
}
}

.heading {
display: flex;
margin-right: 1rem;
align-items: center;
}
.tabHeading {
display: flex;
align-items: center;
@include type.type-style('heading-compact-01');
min-height: 2.5rem;
width: 100%;
padding: 0.75rem;
}

.topBtns {
display: flex;
align-items: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@use '@carbon/layout';

.spinner {
&:global(.cds--inline-loading) {
min-height: 1rem;
}

:global(.cds--inline-loading__text) {
font-size: unset;
}
}

.modalHeader {
:global {
.cds--modal-close-button {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
margin: 0;
margin-top: calc(-1 * #{layout.$spacing-05});
}

.cds--modal-close {
background-color: rgba(0, 0, 0, 0);

&:hover {
background-color: var(--cds-layer-hover);
}
}

.cds--popover--left > .cds--popover > .cds--popover-content {
transform: translate(-4rem, 0.85rem);
}

.cds--popover--left > .cds--popover > .cds--popover-caret {
transform: translate(-3.75rem, 1.25rem);
}
}
}
Loading

0 comments on commit b40fe59

Please sign in to comment.