diff --git a/.DS_Store b/.DS_Store old mode 100644 new mode 100755 index 381f991b..a821b612 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.editorconfig b/.editorconfig old mode 100644 new mode 100755 diff --git a/.env b/.env old mode 100644 new mode 100755 diff --git a/.eslintignore b/.eslintignore old mode 100644 new mode 100755 diff --git a/.eslintrc b/.eslintrc old mode 100644 new mode 100755 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md old mode 100644 new mode 100755 diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/tx-pull.yml b/.github/workflows/tx-pull.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/tx-push.yml b/.github/workflows/tx-push.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.husky/.gitignore b/.husky/.gitignore old mode 100644 new mode 100755 diff --git a/.prettierignore b/.prettierignore old mode 100644 new mode 100755 diff --git a/.tx/config b/.tx/config old mode 100644 new mode 100755 diff --git a/.vscode/settings.json b/.vscode/settings.json old mode 100644 new mode 100755 diff --git a/.yarnrc.yml b/.yarnrc.yml old mode 100644 new mode 100755 diff --git a/LICENSE.md b/LICENSE.md old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/__mocks__/active-visits.mock.ts b/__mocks__/active-visits.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/address.mock.ts b/__mocks__/address.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/appointments.mock.ts b/__mocks__/appointments.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/auto-generation-options.mock.ts b/__mocks__/auto-generation-options.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/identifier-types.mock.ts b/__mocks__/identifier-types.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/identifiers.mock.ts b/__mocks__/identifiers.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/index.ts b/__mocks__/index.ts old mode 100644 new mode 100755 diff --git a/__mocks__/locations.mock.ts b/__mocks__/locations.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/metrics.mock.ts b/__mocks__/metrics.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/patient-registration.mock.ts b/__mocks__/patient-registration.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/patient-visits.mock.ts b/__mocks__/patient-visits.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/patient.mock.ts b/__mocks__/patient.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/search.mock.ts b/__mocks__/search.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/session.mock.ts b/__mocks__/session.mock.ts old mode 100644 new mode 100755 diff --git a/__mocks__/visits.mock.ts b/__mocks__/visits.mock.ts old mode 100644 new mode 100755 diff --git a/e2e/commands/cohort-operations.ts b/e2e/commands/cohort-operations.ts old mode 100644 new mode 100755 diff --git a/e2e/commands/encounter-operations.ts b/e2e/commands/encounter-operations.ts old mode 100644 new mode 100755 diff --git a/e2e/commands/index.ts b/e2e/commands/index.ts old mode 100644 new mode 100755 diff --git a/e2e/commands/patient-operations.ts b/e2e/commands/patient-operations.ts old mode 100644 new mode 100755 diff --git a/e2e/commands/provider-operations.ts b/e2e/commands/provider-operations.ts old mode 100644 new mode 100755 diff --git a/e2e/commands/visit-operations.ts b/e2e/commands/visit-operations.ts old mode 100644 new mode 100755 diff --git a/e2e/core/global-setup.ts b/e2e/core/global-setup.ts old mode 100644 new mode 100755 diff --git a/e2e/core/index.ts b/e2e/core/index.ts old mode 100644 new mode 100755 diff --git a/e2e/core/test.ts b/e2e/core/test.ts old mode 100644 new mode 100755 diff --git a/e2e/fixtures/api.ts b/e2e/fixtures/api.ts old mode 100644 new mode 100755 diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts old mode 100644 new mode 100755 diff --git a/e2e/pages/appointments-page.ts b/e2e/pages/appointments-page.ts old mode 100644 new mode 100755 diff --git a/e2e/pages/home-page.ts b/e2e/pages/home-page.ts old mode 100644 new mode 100755 diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts old mode 100644 new mode 100755 diff --git a/e2e/pages/patient-lists-page.ts b/e2e/pages/patient-lists-page.ts old mode 100644 new mode 100755 diff --git a/e2e/pages/registration-and-edit-page.ts b/e2e/pages/registration-and-edit-page.ts old mode 100644 new mode 100755 diff --git a/e2e/specs/active-visits.spec.ts b/e2e/specs/active-visits.spec.ts old mode 100644 new mode 100755 diff --git a/e2e/specs/appointments.spec.ts b/e2e/specs/appointments.spec.ts old mode 100644 new mode 100755 diff --git a/e2e/specs/edit-patient.spec.ts b/e2e/specs/edit-patient.spec.ts old mode 100644 new mode 100755 diff --git a/e2e/specs/patient-list.spec.ts b/e2e/specs/patient-list.spec.ts old mode 100644 new mode 100755 diff --git a/e2e/specs/patient-search.spec.ts b/e2e/specs/patient-search.spec.ts old mode 100644 new mode 100755 diff --git a/e2e/specs/register-new-patient.spec.ts b/e2e/specs/register-new-patient.spec.ts old mode 100644 new mode 100755 diff --git a/e2e/specs/return-to-patient-list.spec.ts b/e2e/specs/return-to-patient-list.spec.ts old mode 100644 new mode 100755 diff --git a/e2e/support/bamboo/docker-compose.yml b/e2e/support/bamboo/docker-compose.yml old mode 100644 new mode 100755 diff --git a/e2e/support/bamboo/e2e-test-runner.sh b/e2e/support/bamboo/e2e-test-runner.sh old mode 100644 new mode 100755 diff --git a/e2e/support/bamboo/playwright.Dockerfile b/e2e/support/bamboo/playwright.Dockerfile old mode 100644 new mode 100755 diff --git a/e2e/support/github/Dockerfile b/e2e/support/github/Dockerfile old mode 100644 new mode 100755 diff --git a/e2e/support/github/docker-compose.yml b/e2e/support/github/docker-compose.yml old mode 100644 new mode 100755 diff --git a/e2e/support/github/run-e2e-docker-env.sh b/e2e/support/github/run-e2e-docker-env.sh old mode 100644 new mode 100755 diff --git a/example.env b/example.env old mode 100644 new mode 100755 diff --git a/jest.config.js b/jest.config.js old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 75fdcc74..e6f0b32e --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "start": "openmrs develop --backend http://10.50.80.110:8084 --sources packages/esm-*-app --api-url /amrs --spa-path /amrs/spa/ --port 8040", "dev": "openmrs develop --backend http://10.50.80.110:8084 --sources packages/esm-report-app --api-url /amrs --spa-path /amrs/spa/ --port 8040", - "start:core": "openmrs develop --backend http://10.50.80.110:8084 --sources packages/esm-ampath-core-app --api-url /amrs --spa-path /amrs/spa/ --port 8030", + "dev:core": "openmrs develop --backend http://10.50.80.110:8084 --sources packages/esm-ampath-core-app --api-url /amrs --spa-path /amrs/spa/ --port 8030", "ci:publish": "yarn workspaces foreach --all --topological --exclude @ampath/esm-3.x-app npm publish --access public --tag latest", "ci:prepublish": "yarn workspaces foreach --all --topological --exclude @ampath/esm-3.x-app npm publish --access public --tag next", "release": "yarn workspaces foreach --all --topological version", @@ -38,7 +38,7 @@ "@babel/core": "^7.11.6", "@carbon/react": "~1.37.0", "@ohri/openmrs-esm-ohri-commons-lib": "next", - "@openmrs/esm-framework": "^5.6.1-pre.2075", + "@openmrs/esm-framework": "^5.7.1-pre.2076", "@openmrs/esm-patient-common-lib": "next", "@playwright/test": "1.40.1", "@swc/core": "^1.2.165", @@ -75,7 +75,7 @@ "jest-cli": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.1", - "openmrs": "^5.6.1-pre.2075", + "openmrs": "^5.7.1-pre.2076", "prettier": "^3.1.1", "react": "^18.1.0", "react-dom": "^18.1.0", diff --git a/packages/esm-billing-app/README.md b/packages/esm-billing-app/README.md old mode 100644 new mode 100755 index 37fc4c24..00dc2c05 --- a/packages/esm-billing-app/README.md +++ b/packages/esm-billing-app/README.md @@ -1,3 +1,4 @@ +![Node.js CI](https://github.com/palladiumkenya/kenyaemr-esm-3.x/workflows/Node.js%20CI/badge.svg) # ESM Billing App diff --git a/packages/esm-billing-app/__mocks__/visit.mock.ts b/packages/esm-billing-app/__mocks__/visit.mock.ts old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/jest.config.js b/packages/esm-billing-app/jest.config.js old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/package.json b/packages/esm-billing-app/package.json old mode 100644 new mode 100755 index 10535518..bb437e5c --- a/packages/esm-billing-app/package.json +++ b/packages/esm-billing-app/package.json @@ -1,12 +1,12 @@ { "name": "@ampath/esm-billing-app", - "version": "5.1.1", - "description": "Billing app for AMRS", + "version": "5.2.0", + "description": "Billing app for AMPATH", "browser": "dist/ampath-esm-billing-app.js", "main": "src/index.ts", "source": true, "license": "MPL-2.0", - "homepage": "https://github.com/AMPATH/ampath-esm-3.x#readme", + "homepage": "https://github.com/palladiumkenya/ampath-esm-core#readme", "scripts": { "start": "openmrs develop", "serve": "webpack serve --mode=development", @@ -31,10 +31,10 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/AMPATH/ampath-esm-3.x.git" + "url": "git+https://github.com/palladiumkenya/kenyaemr-esm-core#readme" }, "bugs": { - "url": "git+https://github.com/AMPATH/ampath-esm-3.x.git" + "url": "https://github.com/palladiumkenya/kenyaemr-esm-core/issues" }, "dependencies": { "@carbon/react": "^1.42.1", diff --git a/packages/esm-billing-app/src/bill-history/bill-history.component.tsx b/packages/esm-billing-app/src/bill-history/bill-history.component.tsx old mode 100644 new mode 100755 index e3c19915..811daa8e --- a/packages/esm-billing-app/src/bill-history/bill-history.component.tsx +++ b/packages/esm-billing-app/src/bill-history/bill-history.component.tsx @@ -19,8 +19,14 @@ import { Button, } from '@carbon/react'; import { Add } from '@carbon/react/icons'; -import { isDesktop, useLayoutType, usePagination, launchWorkspace } from '@openmrs/esm-framework'; -import { EmptyDataIllustration, ErrorState, usePaginationInfo, CardHeader } from '@openmrs/esm-patient-common-lib'; +import { isDesktop, useLayoutType, usePagination } from '@openmrs/esm-framework'; +import { + EmptyDataIllustration, + ErrorState, + usePaginationInfo, + CardHeader, + useLaunchWorkspaceRequiringVisit, +} from '@openmrs/esm-patient-common-lib'; import { useBills } from '../billing.resource'; import InvoiceTable from '../invoice/invoice-table.component'; import styles from './bill-history.scss'; @@ -32,12 +38,17 @@ interface BillHistoryProps { const BillHistory: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); const { bills, isLoading, error } = useBills(patientUuid); + const launchPatientWorkspace = useLaunchWorkspaceRequiringVisit('billing-form'); const layout = useLayoutType(); const [pageSize, setPageSize] = React.useState(10); const responsiveSize = isDesktop(layout) ? 'sm' : 'lg'; const { paginated, goTo, results, currentPage } = usePagination(bills, pageSize); const { pageSizes } = usePaginationInfo(pageSize, bills?.length, currentPage, results?.length); + const handleLaunchBillForm = () => { + launchPatientWorkspace({ workspaceTitle: t('billingForm', 'Billing Form') }); + }; + const headerData = [ { header: t('visitTime', 'Visit time'), @@ -58,7 +69,7 @@ const BillHistory: React.FC = ({ patientUuid }) => { ]; const setBilledItems = (bill) => - bill.lineItems.reduce( + bill.lineItems?.reduce( (acc, item) => acc + (acc ? ' & ' : '') + (item.billableService?.split(':')[1] || item.item?.split(':')[1] || ''), '', ); @@ -102,7 +113,7 @@ const BillHistory: React.FC = ({ patientUuid }) => {

There are no bills to display.

- @@ -114,10 +125,7 @@ const BillHistory: React.FC = ({ patientUuid }) => { return (
- diff --git a/packages/esm-billing-app/src/bill-history/bill-history.scss b/packages/esm-billing-app/src/bill-history/bill-history.scss old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services-admin-card-link.component.tsx b/packages/esm-billing-app/src/billable-services-admin-card-link.component.tsx old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services/bill-waiver/bill-selection.component.tsx b/packages/esm-billing-app/src/billable-services/bill-waiver/bill-selection.component.tsx old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver-form.component.tsx b/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver-form.component.tsx old mode 100644 new mode 100755 index 0ca92071..5c0dd522 --- a/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +++ b/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver-form.component.tsx @@ -4,7 +4,7 @@ import { TaskAdd } from '@carbon/react/icons'; import { useTranslation } from 'react-i18next'; import styles from './bill-waiver-form.scss'; import { type LineItem, type MappedBill } from '../../types'; -import { createBillWaiverPayload } from './utils'; +import { createBillWaiverPayload, extractErrorMessagesFromResponse } from '../../utils'; import { convertToCurrency, extractString } from '../../helpers'; import { processBillPayment, usePaymentModes } from '../../billing.resource'; import { showSnackbar } from '@openmrs/esm-framework'; @@ -48,7 +48,7 @@ const BillWaiverForm: React.FC = ({ bill, lineItems, setPat showSnackbar({ title: t('billWaiver', 'Bill waiver'), subtitle: t('billWaiverError', 'Bill waiver failed {{error}}', { - error: error?.responseBody?.error?.message ?? error.message, + error: extractErrorMessagesFromResponse(error?.responseBody), }), kind: 'error', timeoutInMs: 3500, diff --git a/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver-form.scss b/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver-form.scss old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver.component.tsx b/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver.component.tsx old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver.scss b/packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver.scss old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services/bill-waiver/patient-bills.component.tsx b/packages/esm-billing-app/src/billable-services/bill-waiver/patient-bills.component.tsx old mode 100644 new mode 100755 index b459e1e1..93070d0e --- a/packages/esm-billing-app/src/billable-services/bill-waiver/patient-bills.component.tsx +++ b/packages/esm-billing-app/src/billable-services/bill-waiver/patient-bills.component.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { useBills } from '../../billing.resource'; import { Layer, DataTable, @@ -57,7 +56,7 @@ const PatientBills: React.FC = ({ patientUuid, bills, setPati
-

{t('noBilltoDisplay', 'There are no bills to display for this patient')}

+

{t('noBillDisplay', 'There are no bills to display for this patient')}

diff --git a/packages/esm-billing-app/src/billable-services/bill-waiver/utils.ts b/packages/esm-billing-app/src/billable-services/bill-waiver/utils.ts deleted file mode 100644 index f850b352..00000000 --- a/packages/esm-billing-app/src/billable-services/bill-waiver/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { type OpenmrsResource } from '@openmrs/esm-framework'; -import { type LineItem, type MappedBill } from '../../types'; - -export const createBillWaiverPayload = ( - bill: MappedBill, - amountWaived: number, - totalAmount: number, - lineItems: Array, - paymentModes: Array, -) => { - const { cashier } = bill; - - const billPayment = { - amount: parseFloat(totalAmount.toFixed(2)), - amountTendered: parseFloat(Number(amountWaived).toFixed(2)), - attributes: [], - instanceType: paymentModes?.find((mode) => mode.name.toLowerCase().includes('waiver'))?.uuid, - }; - - const processedLineItems = lineItems.map((lineItem) => ({ - ...lineItem, - billableService: processBillItem(lineItem), - item: processBillItem(lineItem), - paymentStatus: 'PAID', - })); - - const processedPayment = { - cashPoint: bill.cashPointUuid, - cashier: cashier.uuid, - lineItems: processedLineItems, - payments: [...bill.payments, billPayment], - patient: bill.patientUuid, - }; - - return processedPayment; -}; - -const processBillItem = (item) => (item.item || item.billableService)?.split(':')[0]; diff --git a/packages/esm-billing-app/src/billable-services/billable-service.resource.tsx b/packages/esm-billing-app/src/billable-services/billable-service.resource.tsx old mode 100644 new mode 100755 index d0296721..c19a27fd --- a/packages/esm-billing-app/src/billable-services/billable-service.resource.tsx +++ b/packages/esm-billing-app/src/billable-services/billable-service.resource.tsx @@ -6,6 +6,10 @@ type ResponseObject = { results: Array; }; +type ServiceTypesResponse = { + setMembers: { uuid: string; display: string }[]; +}; + export const useBillableServices = () => { const url = `/ws/rest/v1/cashier/billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(display),servicePrices:(uuid,name,price))`; const { data, isLoading, isValidating, error, mutate } = useSWR<{ data: ResponseObject }>(url, openmrsFetch, {}); @@ -13,8 +17,8 @@ export const useBillableServices = () => { }; export function useServiceTypes() { - const url = `/ws/rest/v1/concept/d7bd4cc0-90b1-4f22-90f2-ab7fde936727?v=custom:(setMembers:(uuid,display))`; - const { data, error, isLoading } = useSWR<{ data: any }>(url, openmrsFetch, {}); + const url = `/ws/rest/v1/concept/d2ece9e9-3907-440d-b5c3-5d3b148594f5?v=custom:(setMembers:(uuid,display))`; + const { data, error, isLoading } = useSWR<{ data: ServiceTypesResponse }>(url, openmrsFetch, {}); return { serviceTypes: data?.data.setMembers ?? [], error, isLoading }; } diff --git a/packages/esm-billing-app/src/billable-services/billable-services-home.component.tsx b/packages/esm-billing-app/src/billable-services/billable-services-home.component.tsx old mode 100644 new mode 100755 index af50fe2a..ee6c8856 --- a/packages/esm-billing-app/src/billable-services/billable-services-home.component.tsx +++ b/packages/esm-billing-app/src/billable-services/billable-services-home.component.tsx @@ -9,6 +9,8 @@ import BillingHeader from '../billing-header/billing-header.component'; import { Wallet, Money } from '@carbon/react/icons'; import { UserHasAccess, navigate } from '@openmrs/esm-framework'; import BillWaiver from './bill-waiver/bill-waiver.component'; +import BillingTariffs from './billing-tariffs/billing-tariffs.component'; +import AddTariffsService from './billing-tariffs/add-billings-tariffs-service.component'; const basePath = `${window.spaBase}/billable-services`; const BillableServiceHome: React.FC = () => { const { t } = useTranslation(); @@ -28,7 +30,12 @@ const BillableServiceHome: React.FC = () => { handleNavigation('waive-bill')} renderIcon={Money}> - {t('billWaiver', 'Bill waiver')} + {t('billWaiver', 'Bill Waiver')} + + + + handleNavigation('bill-tariffs')} renderIcon={Money}> + {t('billTariffs', 'Insurance Tariffs')} @@ -40,6 +47,8 @@ const BillableServiceHome: React.FC = () => { } /> } /> } /> + } /> + } /> diff --git a/packages/esm-billing-app/src/billable-services/billable-services.component.tsx b/packages/esm-billing-app/src/billable-services/billable-services.component.tsx old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services/billable-services.scss b/packages/esm-billing-app/src/billable-services/billable-services.scss old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/drug-order.component.tsx b/packages/esm-billing-app/src/billable-services/billiable-item/drug-order/drug-order.component.tsx old mode 100644 new mode 100755 similarity index 65% rename from packages/esm-billing-app/src/billable-services/billiable-item/drug-order.component.tsx rename to packages/esm-billing-app/src/billable-services/billiable-item/drug-order/drug-order.component.tsx index c4f2160a..66cd0c63 --- a/packages/esm-billing-app/src/billable-services/billiable-item/drug-order.component.tsx +++ b/packages/esm-billing-app/src/billable-services/billiable-item/drug-order/drug-order.component.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { type Drug } from '@openmrs/esm-patient-common-lib'; -import { type DosingUnit, type MedicationFrequency, type MedicationRoute, type QuantityUnit } from '../../types'; -import { useBillableItem, useSockItemInventory } from './useBilliableItem'; +import { type DosingUnit, type MedicationFrequency, type MedicationRoute, type QuantityUnit } from '../../../types'; +import { useBillableItem, useSockItemInventory } from '../useBillableItem'; import { useTranslation } from 'react-i18next'; import styles from './drug-order.scss'; -import { convertToCurrency } from '../../helpers'; +import { convertToCurrency } from '../../../helpers'; type DrugOrderProps = { order: { @@ -25,21 +25,29 @@ const DrugOrder: React.FC = ({ order }) => { const { t } = useTranslation(); const { stockItem, isLoading: isLoadingInventory } = useSockItemInventory(order?.drug?.uuid); const { billableItem, isLoading } = useBillableItem(order?.drug.concept.uuid); - if (isLoading || isLoadingInventory) { return null; } return (
- {stockItem && ( -
- - {t('inStock', '{{quantityUoM}}(s) In stock ', { quantityUoM: stockItem?.quantityUoM })} - - {Math.round(stockItem?.quantity)} -
+ {stockItem && stockItem.length > 0 ? ( + <> +
{'In Stock'}
+ {stockItem.map((item, index) => ( +
+ {item.partyName} + + {' '} + {Math.round(item.quantity)} {item.quantityUoM}(s){' '} + +
+ ))} + + ) : ( +
{'Drug Is Not Available / Out of Stock'}
)} +
{billableItem && billableItem?.servicePrices.map((item) => ( diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/drug-order.scss b/packages/esm-billing-app/src/billable-services/billiable-item/drug-order/drug-order.scss old mode 100644 new mode 100755 similarity index 89% rename from packages/esm-billing-app/src/billable-services/billiable-item/drug-order.scss rename to packages/esm-billing-app/src/billable-services/billiable-item/drug-order/drug-order.scss index f40a9fd7..d969ad7e --- a/packages/esm-billing-app/src/billable-services/billiable-item/drug-order.scss +++ b/packages/esm-billing-app/src/billable-services/billiable-item/drug-order/drug-order.scss @@ -24,3 +24,8 @@ grid-template-columns: 1fr 1fr; padding-left: spacing.$spacing-03; } + +.red { + color: red; + font-weight: normal; +} diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/lab-order.component.tsx b/packages/esm-billing-app/src/billable-services/billiable-item/lab-order.component.tsx deleted file mode 100644 index 58a93b5f..00000000 --- a/packages/esm-billing-app/src/billable-services/billiable-item/lab-order.component.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { convertToCurrency } from '../../helpers'; -import { useBillableItem } from './useBilliableItem'; - -type LabOrderProps = { - order: { - testType?: { - label: string; - conceptUuid: string; - }; - }; -}; - -const LabOrder: React.FC = ({ order }) => { - // TODO: Implement logic to display whether the lab order service is available to ensure clinicians can order the service - - const { billableItem, error, isLoading } = useBillableItem(order?.testType?.conceptUuid); - - const billItems = billableItem?.servicePrices - .map((servicePrice) => `${servicePrice?.paymentMode?.name} - ${convertToCurrency(servicePrice?.price)}`) - .join(' '); - - if (isLoading) { - return null; - } - - if (error) { - return null; - } - - return

{billItems}

; -}; - -export default LabOrder; diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/test-order/imaging-order.component.tsx b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/imaging-order.component.tsx new file mode 100755 index 00000000..1cb55343 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/imaging-order.component.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useBillableItem } from '../useBillableItem'; +import { useTranslation } from 'react-i18next'; +import { InlineLoading } from '@carbon/react'; +import PriceInfoOrder from './price-info-order.componet'; + +type ImagingOrderProps = { + order: { + testType?: { + label: string; + conceptUuid: string; + }; + }; +}; + +const ImagingOrder: React.FC = ({ order }) => { + const { t } = useTranslation(); + const { billableItem, isLoading, error } = useBillableItem(order?.testType?.conceptUuid); + + if (isLoading) { + return ( + + ); + } + + return ; +}; + +export default ImagingOrder; diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/test-order/lab-order.component.tsx b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/lab-order.component.tsx new file mode 100755 index 00000000..4a0003b2 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/lab-order.component.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useBillableItem } from '../useBillableItem'; +import { useTranslation } from 'react-i18next'; +import { InlineLoading } from '@carbon/react'; +import PriceInfoOrder from './price-info-order.componet'; + +type LabOrderProps = { + order: { + testType?: { + label: string; + conceptUuid: string; + }; + }; +}; + +const LabOrder: React.FC = ({ order }) => { + const { t } = useTranslation(); + const { billableItem, isLoading, error } = useBillableItem(order?.testType?.conceptUuid); + + if (isLoading) { + return ( + + ); + } + + return ; +}; + +export default LabOrder; diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/test-order/price-info-order.componet.tsx b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/price-info-order.componet.tsx new file mode 100755 index 00000000..658cc991 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/price-info-order.componet.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { convertToCurrency } from '../../../helpers'; +import { useTranslation } from 'react-i18next'; +import styles from './price-info-order.scss'; +import { + StructuredListWrapper, + StructuredListHead, + StructuredListRow, + StructuredListCell, + StructuredListBody, + Tile, + InlineNotification, +} from '@carbon/react'; + +type PriceInfoOrderProps = { + billableItem: any; + error?: boolean; +}; + +const PriceInfoOrder: React.FC = ({ billableItem, error }) => { + const { t } = useTranslation(); + + if (error || !billableItem) { + return ( + + ); + } + + return ( + +
+ + + + + {t('paymentMethods', 'Payment methods')} + + + {t('prices', 'Prices(Ksh)')} + + + + + {billableItem.servicePrices.map((priceItem) => ( + + {priceItem.paymentMode.name} + {convertToCurrency(priceItem.price)} + + ))} + + +
+
+ ); +}; + +export default PriceInfoOrder; diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/test-order/price-info-order.scss b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/price-info-order.scss new file mode 100755 index 00000000..00a745b4 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/price-info-order.scss @@ -0,0 +1,20 @@ +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@use '@carbon/layout'; +@use '@carbon/colors'; + +.prices { + justify-content: center; + align-items: center; + margin: layout.$spacing-03; +} + +.listContainer { + display: flex; + flex-direction: column; + align-items: center; +} + +.cell { + padding: spacing.$spacing-05; +} diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/test-order/procedure-order.component.tsx b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/procedure-order.component.tsx new file mode 100755 index 00000000..92291e00 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/procedure-order.component.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useBillableItem } from '../useBillableItem'; +import { useTranslation } from 'react-i18next'; +import { InlineLoading } from '@carbon/react'; +import PriceInfoOrder from './price-info-order.componet'; + +type ProcedureOrderProps = { + order: { + testType?: { + label: string; + conceptUuid: string; + }; + }; +}; + +const ProcedureOrder: React.FC = ({ order }) => { + const { t } = useTranslation(); + const { billableItem, isLoading, error } = useBillableItem(order?.testType?.conceptUuid); + + if (isLoading) { + return ( + + ); + } + + return ; +}; + +export default ProcedureOrder; diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/test-order/test-order-action.component.tsx b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/test-order-action.component.tsx new file mode 100755 index 00000000..1e8b4ee0 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/test-order-action.component.tsx @@ -0,0 +1,50 @@ +import { OverflowMenuItem } from '@carbon/react'; +import { type Order } from '@openmrs/esm-patient-common-lib'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useTestOrderBillStatus } from './test-order-action.resource'; +import { showModal } from '@openmrs/esm-framework'; + +type TestOrderProps = { order: Order }; + +enum FulfillerStatus { + IN_PROGRESS = 'IN_PROGRESS', +} + +const TestOrderAction: React.FC = ({ order }) => { + const { t } = useTranslation(); + const { isLoading, hasPendingPayment } = useTestOrderBillStatus(order.uuid, order.patient.uuid); + + const launchModal = useCallback(() => { + const dispose = showModal('pickup-lab-request-modal', { + closeModal: () => dispose(), + order, + }); + }, [order]); + + // Show the test order if the following conditions are met: + // 1. The current visit is in-patient + // 2. The test order has been paid in full + // 3. The patient is an emergency patient + + // If the order is in progress, do not show the action + if (order.fulfillerStatus === FulfillerStatus.IN_PROGRESS) { + return null; + } + + if (isLoading) { + return ; + } + + return ( + + ); +}; + +export default TestOrderAction; diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/test-order/test-order-action.resource.tsx b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/test-order-action.resource.tsx new file mode 100755 index 00000000..7a23f550 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billiable-item/test-order/test-order-action.resource.tsx @@ -0,0 +1,54 @@ +import { openmrsFetch, restBaseUrl, useConfig, useVisit } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { type LineItem, type QueueEntry } from '../../../types'; +import { type BillingConfig } from '../../../config-schema'; + +export const useTestOrderBillStatus = (orderUuid: string, patientUuid: string) => { + const config = useConfig(); + const { currentVisit } = useVisit(patientUuid); + const { isEmergencyPatient, isLoading: isLoadingQueue } = usePatientQueue(patientUuid); + const { isLoading: isLoadingBill, hasPendingPayment } = usePatientBill(orderUuid); + + if (isLoadingQueue || isLoadingBill) { + return { hasPendingPayment: false, isLoading: true }; + } + + // If current visit type is inpatient or the patient is in the emergency queue, we should allow the patient to receive services without paying the bill + if (currentVisit?.visitType?.uuid === config?.inPatientVisitTypeUuid || isEmergencyPatient) { + return { hasPendingPayment: false, isLoading: false }; + } + + // If the patient is not in the queue then we should check if the patient has a pending bill + return { hasPendingPayment, isLoading: false }; +}; + +export const usePatientQueue = (patientUuid: string) => { + const config = useConfig({ externalModuleName: '@ampath/esm-service-queues-app' }); + const url = `${restBaseUrl}/visit-queue-entry?patient=${patientUuid}`; + const { data, isLoading, error } = useSWR<{ + data: { results: Array }; + }>(url, openmrsFetch); + + const isEmergencyPatient = + data?.data?.results?.[0]?.queueEntry?.priority?.uuid === config?.concepts?.emergencyPriorityConceptUuid; + const isInQueue = data?.data?.results?.length > 0; + return { isInQueue, isLoading, error, isEmergencyPatient }; +}; + +export const usePatientBill = (orderUuid: string) => { + const { billingStatusQueryUrl } = useConfig(); + const billUrl = createUrl(restBaseUrl, orderUuid, billingStatusQueryUrl); + const { data, isLoading, error } = useSWR<{ + data: { results: Array }; + }>(billUrl, openmrsFetch); + + const hasPendingPayment = data?.data?.results?.some( + (lineItem) => lineItem.paymentStatus === 'PENDING' || lineItem.paymentStatus === 'POSTED', + ); + + return { hasPendingPayment, isLoading, error }; +}; + +function createUrl(restBaseUrl: string, orderUuid: string, templateUrl: string): string { + return templateUrl.replace('${restBaseUrl}', restBaseUrl).replace('${orderUuid}', orderUuid); +} diff --git a/packages/esm-billing-app/src/billable-services/billiable-item/useBilliableItem.tsx b/packages/esm-billing-app/src/billable-services/billiable-item/useBillableItem.tsx old mode 100644 new mode 100755 similarity index 83% rename from packages/esm-billing-app/src/billable-services/billiable-item/useBilliableItem.tsx rename to packages/esm-billing-app/src/billable-services/billiable-item/useBillableItem.tsx index 50393f05..44eaa26c --- a/packages/esm-billing-app/src/billable-services/billiable-item/useBilliableItem.tsx +++ b/packages/esm-billing-app/src/billable-services/billiable-item/useBillableItem.tsx @@ -37,13 +37,12 @@ export const useBillableItem = (billableItemId: string) => { export const useSockItemInventory = (stockItemId: string) => { const url = `/ws/rest/v1/stockmanagement/stockiteminventory?v=default&limit=10&totalCount=true&drugUuid=${stockItemId}`; - const { data, error, isLoading } = useSWR<{ data: { results: Array<{ quantityUoM: string; quantity: number }> } }>( - url, - openmrsFetch, - ); - const stockItemsInfo = first(data?.data?.results ?? []); + const { data, error, isLoading } = useSWR<{ + data: { results: Array<{ quantityUoM: string; quantity: number; partyName: string }> }; + }>(url, openmrsFetch); + return { - stockItem: stockItemsInfo, + stockItem: (data?.data?.results as Array) ?? [], isLoading: isLoading, error, }; diff --git a/packages/esm-billing-app/src/billable-services/billing-tariffs/add-billing-tariffs-service.scss b/packages/esm-billing-app/src/billable-services/billing-tariffs/add-billing-tariffs-service.scss new file mode 100755 index 00000000..5c52a1ca --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billing-tariffs/add-billing-tariffs-service.scss @@ -0,0 +1,164 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@use '@carbon/colors'; +@use '@carbon/layout'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.form { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + padding: spacing.$spacing-06; +} + +.subTitle { + font-weight: 600; + font-size: 14px; +} + +.sectionTitle { + @include type.type-style('heading-compact-02'); + color: $text-02; + margin-bottom: spacing.$spacing-04; +} + +.modalBody { + padding-bottom: spacing.$spacing-05; +} + +.container { + margin: 1rem; +} + +.paymentContainer { + margin: layout.$layout-01; + padding: layout.$layout-01; + width: 70%; + border-right: 1px solid colors.$cool-gray-40; +} + +.paymentButtons { + margin: layout.$layout-01 0; +} + +.paymentMethodContainer { + display: grid; + grid-template-columns: repeat(4, minmax(auto, 1fr)); + column-gap: 1rem; + margin: 0.625rem 0; + width: 100%; +} + +.paymentTotals { + margin-top: layout.$spacing-01; +} + +.processPayments { + display: flex; + justify-content: flex-end; + margin: layout.$spacing-05; + column-gap: layout.$spacing-04; +} + +.errorPaymentContainer { + margin: layout.$spacing-04; + min-height: layout.$spacing-09; +} + +.removeButtonContainer { + display: flex; + align-self: center; + cursor: pointer; + margin-left: layout.$spacing-07; +} + +.removeButton { + color: colors.$red-60; +} + +.service { + padding: 1rem 0.75rem; +} + +.conceptsList { + background-color: $ui-02; + max-height: 14rem; + overflow-y: auto; + border: 1px solid $ui-03; + + li:hover { + background-color: $ui-03; + } +} + +.emptyResults { + @include type.type-style('body-compact-01'); + color: $text-02; + min-height: 1rem; + border: 1px solid $ui-03; +} + +.conceptLabel { + @include type.type-style('label-02'); + margin-bottom: 0.6rem; +} + +.errorContainer { + margin: 1rem; +} + +.serviceError { + :global(.cds--search-input):focus { + outline: 2.5px solid $danger; + } + + :global(.cds--search-magnifier) { + svg { + fill: $danger; + } + } +} + +.errorMessage { + @include type.type-style('label-02'); + color: $danger; + margin-top: 0.5rem; +} + +.spinner { + &:global(.cds--inline-loading) { + min-height: 1rem; + } +} + +.loader { + margin-top: 1rem; + margin-bottom: 1rem; + margin-left: auto; + margin-right: auto; + width: max-content; +} + +.searchWrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-items: center; +} + +.nameSection { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.secondSection { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.serviceName { + width: 50%; +} diff --git a/packages/esm-billing-app/src/billable-services/billing-tariffs/add-billings-tariffs-service.component.tsx b/packages/esm-billing-app/src/billable-services/billing-tariffs/add-billings-tariffs-service.component.tsx new file mode 100755 index 00000000..93795538 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billing-tariffs/add-billings-tariffs-service.component.tsx @@ -0,0 +1,183 @@ +import React, { useCallback, useRef, useState } from 'react'; +import styles from './add-billing-tariffs-service.scss'; +import { + Form, + Button, + TextInput, + ComboBox, + Dropdown, + Layer, + InlineLoading, + Search, + Tile, + FormLabel, + NumberInput, +} from '@carbon/react'; +import { useTranslation } from 'react-i18next'; +import { + createBillableService, + useConceptsSearch, + usePaymentModes, + useServiceTypes, +} from '../billable-service.resource'; +import { Controller, useFieldArray, useForm } from 'react-hook-form'; +import { Add, TrashCan, WarningFilled } from '@carbon/react/icons'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { navigate, showSnackbar, useDebounce, useLayoutType } from '@openmrs/esm-framework'; +import { type ServiceConcept } from '../../types'; +import { extractErrorMessagesFromResponse } from '../../utils'; + +const servicePriceSchema = z.object({ + paymentMode: z.string({ required_error: 'Payment method is required' }), + price: z + .string() + .refine((value) => !isNaN(Number(value)), 'Value must be a number') + .refine((value) => parseInt(value) > 0, 'Price should be a number more than zero') + .refine((value) => !!value, 'Price is required'), +}); + +const paymentFormSchema = z.object({ + payment: z.array(servicePriceSchema).min(1, 'At least one payment method is required'), + serviceName: z.string({ + required_error: 'Service name is required', + }), + shortName: z.string({ required_error: 'A valid short name is required.' }), + serviceTypeName: z.string({ required_error: 'A service type is required' }), + concept: z.string({ required_error: 'Concept search is required.' }), +}); + +type FormData = z.infer; +const DEFAULT_PAYMENT_OPTION = { paymentMode: '', price: '1' }; + +const AddTariffsService: React.FC = () => { + const { t } = useTranslation(); + + const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes(); + const { serviceTypes, isLoading: isLoadingServicesTypes } = useServiceTypes(); + + const { + control, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + mode: 'all', + defaultValues: {}, + resolver: zodResolver(paymentFormSchema), + }); + + const { fields, remove, append } = useFieldArray({ name: 'payment', control: control }); + + const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT_OPTION), [append]); + const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]); + + const isTablet = useLayoutType() === 'tablet'; + const searchInputRef = useRef(null); + const handleSearchTermChange = (event: React.ChangeEvent) => setSearchTerm(event.target.value); + + const [selectedConcept, setSelectedConcept] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const debouncedSearchTerm = useDebounce(searchTerm); + const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm); + + const handleConceptChange = useCallback((selectedConcept: ServiceConcept) => { + setSelectedConcept(selectedConcept); + }, []); + + const handleNavigateToServiceDashboard = () => + navigate({ + to: window.getOpenmrsSpaBase() + 'billable-services', + }); + + const onSubmit = (data: FormData) => { + const payload: any = {}; + + let servicePrices = data.payment.map((element) => { + return { + name: paymentModes.filter((p) => p.uuid === element.paymentMode)[0].name, + price: element.price, + paymentMode: element.paymentMode, + }; + }); + + payload.name = data.serviceName; + payload.shortName = data.shortName; + payload.serviceType = data.serviceTypeName; + payload.servicePrices = servicePrices; + payload.serviceStatus = 'ENABLED'; + payload.concept = selectedConcept?.concept?.uuid; + + createBillableService(payload).then( + (resp) => { + showSnackbar({ + title: t('billableService', 'Billable service'), + subtitle: 'Billable service created successfully', + kind: 'success', + isLowContrast: true, + timeoutInMs: 3000, + }); + handleNavigateToServiceDashboard(); + }, + (error) => { + showSnackbar({ + title: 'Error adding billable service', + kind: 'error', + subtitle: extractErrorMessagesFromResponse(error.responseBody), + isLowContrast: true, + }); + }, + ); + }; + + if (isLoadingServicesTypes || isLoadingPaymentModes) { + return ( +
+ +
+ ); + } + + return ( +
+

{t('addTariffsServices', 'Add Insurance Tariffs')}

+ +
+
+ ( + (item ? item.display : '')} + placeholder="Select service type" + required + {...field} + onChange={({ selectedItem }) => field.onChange(selectedItem ? selectedItem.display : '')} + invalidText={errors.serviceTypeName?.message || ''} + invalid={!!errors.serviceTypeName} + /> + )} + /> +
+
+ +
+ + +
+
+ ); +}; + +function ResponsiveWrapper({ children, isTablet }: { children: React.ReactNode; isTablet: boolean }) { + return isTablet ? {children} : <>{children}; +} + +export default AddTariffsService; diff --git a/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs-admin-card.tsx b/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs-admin-card.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs-services.scss b/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs-services.scss new file mode 100755 index 00000000..2f2691ff --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs-services.scss @@ -0,0 +1,219 @@ +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@carbon/styles/scss/spacing'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.container { + margin: 2rem 0; +} + +.emptyStateContainer, +.loaderContainer { + @extend .container; +} + +.serviceContainer { + background-color: $ui-02; + border: 1px solid $ui-03; + width: 100%; + margin: 0 auto; + max-width: 95vw; + padding-bottom: 0; + + :has(.filterEmptyState) { + border-bottom: none; + } +} +.left-justified-items { + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + align-items: center; +} + +.filterContainer { + flex: 1; + + :global(.cds--dropdown__wrapper--inline) { + gap: 0; + } + + :global(.cds--list-box__menu-icon) { + height: 1rem; + } + + :global(.cds--list-box__menu) { + min-width: max-content; + } + + :global(.cds--list-box) { + margin-left: layout.$spacing-03; + } +} + +.menu { + margin-left: layout.$spacing-03; +} + +.headerContainer { + display: flex; + justify-content: space-between; + align-items: center; + padding: layout.$spacing-04 layout.$spacing-05; + background-color: $ui-02; +} + +.backgroundDataFetchingIndicator { + align-items: center; + display: flex; + flex: 1; + justify-content: space-between; + + &:global(.cds--inline-loading) { + max-height: 1rem; + } +} + +.tableContainer section { + position: relative; +} + +.tableContainer a { + text-decoration: none; +} + +.pagination { + overflow: hidden; + + &:global(.cds--pagination) { + border-top: none; + } +} + +.hiddenRow { + display: none; +} + +.emptyRow { + padding: 0 1rem; + display: flex; + align-items: center; +} + +.visitSummaryContainer { + width: 100%; + max-width: 768px; + margin: 1rem auto; +} + +.expandedActiveVisitRow > td > div { + max-height: max-content !important; +} + +.expandedActiveVisitRow td { + padding: 0 2rem; +} + +.expandedActiveVisitRow th[colspan] td[colspan] > div:first-child { + padding: 0 1rem; +} + +.action { + margin-bottom: layout.$spacing-03; +} + +.illo { + margin-top: layout.$spacing-05; +} + +.content { + @include type.type-style('heading-compact-01'); + color: $text-02; + margin-top: layout.$spacing-05; + margin-bottom: layout.$spacing-03; +} + +.desktopHeading, +.tabletHeading { + text-align: left; + text-transform: capitalize; + flex: 1; + + h4 { + @include type.type-style('heading-compact-02'); + color: $text-02; + + &:after { + content: ''; + display: block; + width: 2rem; + padding-top: 3px; + border-bottom: 0.375rem solid; + @include brand-03(border-bottom-color); + } + } +} + +.tile { + text-align: center; + border: 1px solid $ui-03; +} + +.menuitem { + max-width: none; +} + +.filterEmptyState { + display: flex; + justify-content: center; + align-items: center; + padding: layout.$spacing-05; + margin: layout.$spacing-09; + text-align: center; +} + +.filterEmptyStateTile { + margin: auto; +} + +.filterEmptyStateContent { + @include type.type-style('heading-compact-02'); + color: $text-02; + margin-bottom: 0.5rem; +} + +.filterEmptyStateHelper { + @include type.type-style('body-compact-01'); + color: $text-02; +} + +.metricsContainer { + display: flex; + justify-content: space-between; + background-color: $ui-02; + height: spacing.$spacing-10; + align-items: center; + padding: 0 spacing.$spacing-05; +} + +.metricsTitle { + @include type.type-style('heading-03'); + color: $ui-05; +} + +.actionsContainer { + display: flex; + justify-content: space-between; + align-items: center; + background-color: $ui-02; +} +.actionBtn { + display: flex; + column-gap: 0.5rem; +} + +.mainSection { + display: grid; + grid-template-columns: 16rem 1fr; +} diff --git a/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs.component.tsx b/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs.component.tsx new file mode 100755 index 00000000..e1923f91 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs.component.tsx @@ -0,0 +1,286 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { + DataTable, + InlineLoading, + Layer, + Pagination, + Search, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, + Tile, + Button, +} from '@carbon/react'; +import { useLayoutType, isDesktop, useConfig, usePagination, ErrorState, navigate } from '@openmrs/esm-framework'; +import { EmptyState } from '@openmrs/esm-patient-common-lib'; +import styles from './billing-tariffs-services.scss'; +import { useTranslation } from 'react-i18next'; +import { useBillableServices } from './billing-tariffs.resource'; +import { ArrowRight, RadioButton } from '@carbon/react/icons'; +import { RadioButtonGroup } from '@carbon/react'; +import { OverflowMenu, OverflowMenuItem } from '@carbon/react'; + +const BillingTariffs = () => { + const { t } = useTranslation(); + const { billableServices, isLoading, isValidating, error, mutate } = useBillableServices(); + const layout = useLayoutType(); + const config = useConfig(); + const [searchString, setSearchString] = useState(''); + const responsiveSize = isDesktop(layout) ? 'lg' : 'sm'; + const pageSizes = config?.billableServices?.pageSizes ?? [10, 20, 30, 40, 50]; + const [pageSize, setPageSize] = useState(config?.billableServices?.pageSize ?? 10); + + //creating service state + const [showOverlay, setShowOverlay] = useState(false); + const [overlayHeader, setOverlayTitle] = useState(''); + const [category, setCategory] = useState(''); + + const toggleSearch = (choiceSelected) => { + (document.getElementById('searchField') as HTMLInputElement).disabled = false; + + if (choiceSelected == 'Stock Item') { + setCategory('Stock Item'); + } else { + setCategory('Service'); + } + }; + + const headerData = [ + { + header: t('serviceName', 'Service Name'), + key: 'serviceName', + }, + { + header: t('shortName', 'Short Name'), + key: 'shortName', + }, + { + header: t('serviceType', 'Service Type'), + key: 'serviceType', + }, + { + header: t('status', 'Service Status'), + key: 'status', + }, + { + header: t('prices', 'Prices'), + key: 'prices', + }, + { + header: t('tariffs', 'Tariffs'), + key: 'tariffs', + }, + { + header: t('actions', 'Actions'), + key: 'actions', + }, + ]; + + const launchBillableServiceForm = useCallback(() => { + navigate({ to: window.getOpenmrsSpaBase() + 'billable-services/add-service' }); + }, []); + + const searchResults = useMemo(() => { + if (billableServices !== undefined && billableServices.length > 0) { + if (searchString && searchString.trim() !== '') { + const search = searchString.toLowerCase(); + return billableServices?.filter((service) => + Object.entries(service).some(([header, value]) => { + return header === 'uuid' ? false : `${value}`.toLowerCase().includes(search); + }), + ); + } + } + return billableServices; + }, [searchString, billableServices]); + + const { paginated, goTo, results, currentPage } = usePagination(searchResults, pageSize); + + let rowData = []; + if (results) { + results.forEach((service, index) => { + const s = { + id: `${index}`, + uuid: service.uuid, + serviceName: service.name, + shortName: service.shortName, + serviceType: service?.serviceType?.display, + status: service.serviceStatus, + prices: '--', + tariffs: 'T1, T2, T3', + actions: ( + + + + + ), + }; + let cost = ''; + service.servicePrices.forEach((price) => { + cost += `${price.name} (${price.price}) `; + }); + s.prices = cost; + rowData.push(s); + }); + } + + const handleSearch = useCallback( + (e) => { + goTo(1); + setSearchString(e.target.value); + }, + [goTo, setSearchString], + ); + + if (isLoading) { + ; + } + if (error) { + ; + } + if (billableServices.length === 0) { + ; + } + + function filterItems(value: any) { + throw new Error('Function not implemented.'); + } + + return ( + <> + {billableServices?.length > 0 ? ( +
+ + + + + +
+ 1 ? true : false}> + {({ rows, headers, getRowProps, getTableProps }) => ( + + + + + {headers.map((header) => ( + {header.header} + ))} + + + + {rows.map((row) => ( + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + +
+
+ )} +
+ {searchResults?.length === 0 && ( +
+ + +

+ {t('noMatchingServicesToDisplay', 'No matching services to display')} +

+

{t('checkFilters', 'Check the filters above')}

+
+
+
+ )} + {paginated && ( + { + if (newPageSize !== pageSize) { + setPageSize(newPageSize); + } + if (newPage !== currentPage) { + goTo(newPage); + } + }} + /> + )} +
+ ) : ( + + )} + + ); +}; + +function FilterableTableHeader({ layout, handleSearch, isValidating, responsiveSize, t }) { + return ( + <> +
+
+

{t('servicesList', 'Services list')}

+
+
+ {isValidating ? : null} +
+
+
+ + +
+ + ); +} +export default BillingTariffs; diff --git a/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs.resource.tsx b/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs.resource.tsx new file mode 100755 index 00000000..90cba6ed --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billing-tariffs/billing-tariffs.resource.tsx @@ -0,0 +1,55 @@ +import { type OpenmrsResource, openmrsFetch } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { type ServiceConcept } from '../../types'; + +type ResponseObject = { + results: Array; +}; + +type ServiceTypesResponse = { + setMembers: { uuid: string; display: string }[]; +}; + +export const useBillableServices = () => { + const url = `/ws/rest/v1/cashier/billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(display),servicePrices:(uuid,name,price))`; + const { data, isLoading, isValidating, error, mutate } = useSWR<{ data: ResponseObject }>(url, openmrsFetch, {}); + return { billableServices: data?.data.results ?? [], isLoading, isValidating, error, mutate }; +}; + +export function useServiceTypes() { + const url = `/ws/rest/v1/concept/d7bd4cc0-90b1-4f22-90f2-ab7fde936727?v=custom:(setMembers:(uuid,display))`; + const { data, error, isLoading } = useSWR<{ data: ServiceTypesResponse }>(url, openmrsFetch, {}); + return { serviceTypes: data?.data.setMembers ?? [], error, isLoading }; +} + +export const usePaymentModes = () => { + const url = `/ws/rest/v1/cashier/paymentMode`; + const { data, error, isLoading } = useSWR<{ data: ResponseObject }>(url, openmrsFetch, {}); + return { paymentModes: data?.data.results ?? [], error, isLoading }; +}; + +export const createBillableService = (payload: any) => { + const url = `/ws/rest/v1/cashier/api/billable-service`; + return openmrsFetch(url, { + method: 'POST', + body: payload, + headers: { + 'Content-Type': 'application/json', + }, + }); +}; + +export function useConceptsSearch(conceptToLookup: string) { + const conditionsSearchUrl = `/ws/rest/v1/conceptsearch?q=${conceptToLookup}`; + + const { data, error, isLoading } = useSWR<{ data: { results: Array } }, Error>( + conceptToLookup ? conditionsSearchUrl : null, + openmrsFetch, + ); + + return { + searchResults: data?.data?.results ?? [], + error: error, + isSearching: isLoading, + }; +} diff --git a/packages/esm-billing-app/src/billable-services/create-edit/add-billable-service.component.tsx b/packages/esm-billing-app/src/billable-services/create-edit/add-billable-service.component.tsx old mode 100644 new mode 100755 index 83ed659c..aaed930c --- a/packages/esm-billing-app/src/billable-services/create-edit/add-billable-service.component.tsx +++ b/packages/esm-billing-app/src/billable-services/create-edit/add-billable-service.component.tsx @@ -1,4 +1,3 @@ -/* eslint-disable curly */ import React, { useCallback, useRef, useState } from 'react'; import styles from './add-billable-service.scss'; import { @@ -12,6 +11,7 @@ import { Search, Tile, FormLabel, + NumberInput, } from '@carbon/react'; import { useTranslation } from 'react-i18next'; import { @@ -24,43 +24,48 @@ import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { Add, TrashCan, WarningFilled } from '@carbon/react/icons'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; -import { navigate, showSnackbar, useDebounce, useLayoutType, useSession } from '@openmrs/esm-framework'; +import { navigate, showSnackbar, useDebounce, useLayoutType } from '@openmrs/esm-framework'; import { type ServiceConcept } from '../../types'; +import { extractErrorMessagesFromResponse } from '../../utils'; const servicePriceSchema = z.object({ - paymentMode: z.string().refine((value) => !!value, 'Payment method is required'), - price: z.union([ - z.number().refine((value) => !!value, 'Price is required'), - z.string().refine((value) => !!value, 'Price is required'), - ]), + paymentMode: z.string({ required_error: 'Payment method is required' }), + price: z + .string() + .refine((value) => !isNaN(Number(value)), 'Value must be a number') + .refine((value) => parseInt(value) > 0, 'Price should be a number more than zero') + .refine((value) => !!value, 'Price is required'), }); -const paymentFormSchema = z.object({ payment: z.array(servicePriceSchema) }); -type PaymentMode = { - paymentMode: string; - price: string | number; -}; -type PaymentModeFormValue = { - payment: Array; -}; -const DEFAULT_PAYMENT_OPTION = { paymentMode: '', price: 0 }; +const paymentFormSchema = z.object({ + payment: z.array(servicePriceSchema).min(1, 'At least one payment method is required'), + serviceName: z.string({ + required_error: 'Service name is required', + }), + shortName: z.string({ required_error: 'A valid short name is required.' }), + serviceTypeName: z.string({ required_error: 'A service type is required' }), + concept: z.string({ required_error: 'Concept search is required.' }), +}); + +type FormData = z.infer; +const DEFAULT_PAYMENT_OPTION = { paymentMode: '', price: '1' }; const AddBillableService: React.FC = () => { const { t } = useTranslation(); const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes(); const { serviceTypes, isLoading: isLoadingServicesTypes } = useServiceTypes(); - const [billableServicePayload, setBillableServicePayload] = useState({}); const { control, handleSubmit, - formState: { errors }, - } = useForm({ + formState: { errors, isValid }, + } = useForm({ mode: 'all', defaultValues: {}, resolver: zodResolver(paymentFormSchema), }); + const { fields, remove, append } = useFieldArray({ name: 'payment', control: control }); const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT_OPTION), [append]); @@ -74,7 +79,8 @@ const AddBillableService: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm); const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm); - const handleConceptChange = useCallback((selectedConcept: any) => { + + const handleConceptChange = useCallback((selectedConcept: ServiceConcept) => { setSelectedConcept(selectedConcept); }, []); @@ -83,27 +89,20 @@ const AddBillableService: React.FC = () => { to: window.getOpenmrsSpaBase() + 'billable-services', }); - if (isLoadingPaymentModes && isLoadingServicesTypes) { - return ( - - ); - } - - const onSubmit = (data) => { + const onSubmit = (data: FormData) => { const payload: any = {}; - let servicePrices = []; - data.payment.forEach((element) => { - element.name = paymentModes.filter((p) => p.uuid === element.paymentMode)[0].name; - servicePrices.push(element); + let servicePrices = data.payment.map((element) => { + return { + name: paymentModes.filter((p) => p.uuid === element.paymentMode)[0].name, + price: element.price, + paymentMode: element.paymentMode, + }; }); - payload.name = billableServicePayload.serviceName; - payload.shortName = billableServicePayload.shortName; - payload.serviceType = billableServicePayload.serviceType.uuid; + + payload.name = data.serviceName; + payload.shortName = data.shortName; + payload.serviceType = data.serviceTypeName; payload.servicePrices = servicePrices; payload.serviceStatus = 'ENABLED'; payload.concept = selectedConcept?.concept?.uuid; @@ -114,205 +113,233 @@ const AddBillableService: React.FC = () => { title: t('billableService', 'Billable service'), subtitle: 'Billable service created successfully', kind: 'success', + isLowContrast: true, timeoutInMs: 3000, }); handleNavigateToServiceDashboard(); }, (error) => { - showSnackbar({ title: 'Bill payment error', kind: 'error', subtitle: error }); + showSnackbar({ + title: 'Error adding billable service', + kind: 'error', + subtitle: extractErrorMessagesFromResponse(error.responseBody), + isLowContrast: true, + }); }, ); }; + if (isLoadingServicesTypes || isLoadingPaymentModes) { + return ( +
+ +
+ ); + } + return (
-

{t('addBillableServices', 'Add Billable Services')}

-
- - - setBillableServicePayload({ - ...billableServicePayload, - serviceName: e.target.value, - }) - } - placeholder="Enter service name" +

{t('addBillableServices', 'Add Billable Services')}

+
+
+ ( + + + + )} /> - -
-
- - - setBillableServicePayload({ - ...billableServicePayload, - shortName: e.target.value, - }) - } - placeholder="Enter service short name" +
+
+ ( + + + + )} /> - +
-
- Associated Concept - ( - - { - onChange(e); - handleSearchTermChange(e); - }} - renderIcon={errors?.search && } - onBlur={onBlur} - onClear={() => { - setSearchTerm(''); - setSelectedConcept(null); - }} - value={(() => { - if (selectedConcept) { - return selectedConcept.display; - } - if (debouncedSearchTerm) { - return value; - } - })()} - /> - - )} - /> - {(() => { - if (!debouncedSearchTerm || selectedConcept) return null; - if (isSearching) - return ; - if (searchResults && searchResults.length) { +
+
+ Associated Concept + ( + + { + onChange(e); + handleSearchTermChange(e); + }} + renderIcon={errors?.concept && } + onBlur={onBlur} + onClear={() => { + setSearchTerm(''); + setSelectedConcept(null); + }} + value={(() => { + if (selectedConcept) { + return selectedConcept.display; + } + if (debouncedSearchTerm) { + return value; + } + })()} + /> + + )} + /> + {(() => { + if (!debouncedSearchTerm || selectedConcept) { + return null; + } + if (isSearching) { + return ( +
+ +
+ ); + } + if (searchResults && searchResults.length) { + return ( +
    + {searchResults?.map((searchResult, index) => ( +
  • handleConceptChange(searchResult)}> + {searchResult.display} +
  • + ))} +
+ ); + } return ( -
    - {/*TODO: use uuid instead of index as the key*/} - {searchResults?.map((searchResult, index) => ( -
  • handleConceptChange(searchResult)}> - {searchResult.display} -
  • - ))} -
+ + + + {t('noResultsFor', 'No results for')} "{debouncedSearchTerm}" + + + ); - } - return ( - - - - {t('noResultsFor', 'No results for')} "{debouncedSearchTerm}" - - - - ); - })()} -
-
- - item?.display} - onChange={({ selectedItem }) => { - setBillableServicePayload({ - ...billableServicePayload, - display: selectedItem?.display, - serviceType: selectedItem, - }); - }} - placeholder="Select service type" - required + })()} +
+
+ ( + (item ? item.display : '')} + placeholder="Select service type" + required + {...field} + onChange={({ selectedItem }) => field.onChange(selectedItem ? selectedItem.display : '')} + invalidText={errors.serviceTypeName?.message || ''} + invalid={!!errors.serviceTypeName} + /> + )} /> - +
- -
-
- {fields.map((field, index) => ( -
- ( - - field.onChange(selectedItem?.uuid)} - titleText={t('paymentMode', 'Payment Mode')} - label={t('selectPaymentMethod', 'Select payment method')} - items={paymentModes ?? []} - itemToString={(item) => (item ? item.name : '')} - invalid={!!errors?.payment?.[index]?.paymentMode} - invalidText={errors?.payment?.[index]?.paymentMode?.message} - /> - - )} +
+ {fields.map((field, index) => ( +
+ ( + + field.onChange(selectedItem?.uuid)} + titleText={t('paymentMode', 'Payment Mode')} + label={t('selectPaymentMethod', 'Select payment method')} + items={paymentModes ?? []} + itemToString={(item) => (item ? item.name : '')} + invalid={!!errors?.payment?.[index]?.paymentMode} + invalidText={errors?.payment?.[index]?.paymentMode?.message} + /> + + )} + /> + ( + + + + )} + /> +
+ handleRemovePaymentMode(index)} + className={styles.removeButton} + size={20} /> - ( - - - - )} - /> -
- handleRemovePaymentMode(index)} - className={styles.removeButton} - size={20} - /> -
- ))} - -
-
+ + ))} + +
-
diff --git a/packages/esm-billing-app/src/billable-services/create-edit/add-billable-service.scss b/packages/esm-billing-app/src/billable-services/create-edit/add-billable-service.scss old mode 100644 new mode 100755 index 3b1f8c83..5c52a1ca --- a/packages/esm-billing-app/src/billable-services/create-edit/add-billable-service.scss +++ b/packages/esm-billing-app/src/billable-services/create-edit/add-billable-service.scss @@ -9,10 +9,12 @@ flex-direction: column; justify-content: space-between; height: 100%; + padding: spacing.$spacing-06; } -.section { - margin: spacing.$spacing-03; +.subTitle { + font-weight: 600; + font-size: 14px; } .sectionTitle { @@ -43,7 +45,6 @@ .paymentMethodContainer { display: grid; grid-template-columns: repeat(4, minmax(auto, 1fr)); - align-items: flex-start; column-gap: 1rem; margin: 0.625rem 0; width: 100%; @@ -100,7 +101,7 @@ .conceptLabel { @include type.type-style('label-02'); - margin: 1rem; + margin-bottom: 0.6rem; } .errorContainer { @@ -130,3 +131,34 @@ min-height: 1rem; } } + +.loader { + margin-top: 1rem; + margin-bottom: 1rem; + margin-left: auto; + margin-right: auto; + width: max-content; +} + +.searchWrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-items: center; +} + +.nameSection { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.secondSection { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.serviceName { + width: 50%; +} diff --git a/packages/esm-billing-app/src/billable-services/dashboard/dashboard.component.tsx b/packages/esm-billing-app/src/billable-services/dashboard/dashboard.component.tsx old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services/dashboard/dashboard.scss b/packages/esm-billing-app/src/billable-services/dashboard/dashboard.scss old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billable-services/dashboard/service-metrics.component.tsx b/packages/esm-billing-app/src/billable-services/dashboard/service-metrics.component.tsx old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billing-dashboard/billing-dashboard.component.tsx b/packages/esm-billing-app/src/billing-dashboard/billing-dashboard.component.tsx old mode 100644 new mode 100755 index 8235f6c8..6008d2a9 --- a/packages/esm-billing-app/src/billing-dashboard/billing-dashboard.component.tsx +++ b/packages/esm-billing-app/src/billing-dashboard/billing-dashboard.component.tsx @@ -4,6 +4,7 @@ import BillingHeader from '../billing-header/billing-header.component'; import MetricsCards from '../metrics-cards/metrics-cards.component'; import BillsTable from '../bills-table/bills-table.component'; import styles from './billing-dashboard.scss'; +import BillingTabs from '../billing-tabs/billling-tabs.component'; export function BillingDashboard() { const { t } = useTranslation(); @@ -12,9 +13,7 @@ export function BillingDashboard() {
-
- -
+
); } diff --git a/packages/esm-billing-app/src/billing-dashboard/billing-dashboard.scss b/packages/esm-billing-app/src/billing-dashboard/billing-dashboard.scss old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billing-form/billing-checkin-form.component.tsx b/packages/esm-billing-app/src/billing-form/billing-checkin-form.component.tsx old mode 100644 new mode 100755 index c2994c3f..24595d94 --- a/packages/esm-billing-app/src/billing-form/billing-checkin-form.component.tsx +++ b/packages/esm-billing-app/src/billing-form/billing-checkin-form.component.tsx @@ -25,7 +25,7 @@ const BillingCheckInForm: React.FC = ({ patientUuid, se const { lineItems, isLoading: isLoadingLineItems, error: lineError } = useBillableItems(); const [attributes, setAttributes] = useState([]); const [paymentMethod, setPaymentMethod] = useState(); - let lineList = []; + const [isPatientExemptedValue, setIsPatientExemptedValue] = useState(null); const handleCreateBill = useCallback((createBillPayload) => { createPatientBill(createBillPayload).then( @@ -51,7 +51,6 @@ const BillingCheckInForm: React.FC = ({ patientUuid, se : PENDING_PAYMENT_STATUS; const lineItems = selectedItems.map((item, index) => { - // // should default to first price if check returns empty. todo - update backend to return default price const priceForPaymentMode = item.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || item?.servicePrices[0]; return { @@ -84,7 +83,7 @@ const BillingCheckInForm: React.FC = ({ patientUuid, se handleCreateExtraVisitInfo: () => {}, attributes, }); - }, []); + }, [attributes, setExtraVisitInfo]); if (isLoadingLineItems || isLoadingCashPoints) { return ( @@ -96,13 +95,6 @@ const BillingCheckInForm: React.FC = ({ patientUuid, se ); } - if (paymentMethod) { - lineList = []; - lineList = lineItems.filter((e) => - e.servicePrices.some((p) => p.paymentMode && p.paymentMode.uuid === paymentMethod?.uuid), - ); - } - if (cashError || lineError) { return ( = ({ patientUuid, se return ( <> - + -
-
{t('billing', 'Billing')}
-
- (item ? item?.name : '')} - onChange={({ selectedItems }) => handleBillingService(selectedItems)} - /> -
-
+ {paymentMethod && ( +
+
{t('billing', 'Billing')}
+
+ (item ? item?.name : '')} + onChange={({ selectedItems }) => handleBillingService(selectedItems)} + disabled={isPatientExemptedValue === ''} + /> +
+
+ )} ); }; diff --git a/packages/esm-billing-app/src/billing-form/billing-checkin-form.scss b/packages/esm-billing-app/src/billing-form/billing-checkin-form.scss old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billing-form/billing-form.component.tsx b/packages/esm-billing-app/src/billing-form/billing-form.component.tsx old mode 100644 new mode 100755 index ee60e514..cbeb959a --- a/packages/esm-billing-app/src/billing-form/billing-form.component.tsx +++ b/packages/esm-billing-app/src/billing-form/billing-form.component.tsx @@ -97,7 +97,7 @@ const BillingForm: React.FC = ({ closeWorkspace }) => { const filterItems = (val) => { setsearchVal(val); - if (!isLoading) { + if (isLoading) { /* empty */ } else { if (typeof data !== 'undefined') { @@ -109,13 +109,17 @@ const BillingForm: React.FC = ({ closeWorkspace }) => { const res = data as { results: any[] }; res.results.map((o) => { - if (o.commonName && (o.commonName != '' || o.commonName != null)) { + if ( + o.commonName && + (o.commonName != '' || o.commonName != null) && + (o.purchasePrice != '' || o.purchasePrice != null) + ) { searchOptions.push({ uuid: o.uuid, Item: o.commonName, Qnty: 1, - Price: 10, - Total: 10, + Price: o?.purchasePrice, + Total: o?.purchasePrice, category: 'StockItem', }); } else { @@ -124,8 +128,8 @@ const BillingForm: React.FC = ({ closeWorkspace }) => { uuid: o.uuid, Item: o.name, Qnty: 1, - Price: o.servicePrices[0].price, - Total: o.servicePrices[0].price, + Price: o.servicePrices[0]?.price, + Total: o.servicePrices[0]?.price, category: 'Service', }); } diff --git a/packages/esm-billing-app/src/billing-form/billing-form.scss b/packages/esm-billing-app/src/billing-form/billing-form.scss old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billing-form/helper.ts b/packages/esm-billing-app/src/billing-form/helper.ts old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billing-form/social-health-authority/sha-number-validity.component.tsx b/packages/esm-billing-app/src/billing-form/social-health-authority/sha-number-validity.component.tsx old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/billing-form/visit-attributes/visit-attributes-form.component.tsx b/packages/esm-billing-app/src/billing-form/visit-attributes/visit-attributes-form.component.tsx old mode 100644 new mode 100755 index 96a59ec6..71db914c --- a/packages/esm-billing-app/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +++ b/packages/esm-billing-app/src/billing-form/visit-attributes/visit-attributes-form.component.tsx @@ -1,6 +1,5 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import styles from './visit-attributes-form.scss'; import { TextInput, InlineLoading, ComboBox, RadioButtonGroup, RadioButton } from '@carbon/react'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -8,15 +7,17 @@ import { z } from 'zod'; import { useConfig } from '@openmrs/esm-framework'; import { type BillingConfig } from '../../config-schema'; import { usePaymentModes } from '../../billing.resource'; +import styles from './visit-attributes-form.scss'; type VisitAttributesFormProps = { setAttributes: (state) => void; setPaymentMethod?: (value: any) => void; + setIsPatientExempted: (value: string) => void; }; type VisitAttributesFormValue = { isPatientExempted: string; - paymentMethods: { uuid: string; name: string }; + paymentMethods: { uuid: string; name: string } | null; insuranceScheme: string; policyNumber: string; exemptionCategory: string; @@ -24,70 +25,70 @@ type VisitAttributesFormValue = { const visitAttributesFormSchema = z.object({ isPatientExempted: z.string(), - paymentMethods: z.object({ uuid: z.string(), name: z.string() }), - insuranceSchema: z.string(), - policyNumber: z.string(), - exemptionCategory: z.string(), + paymentMethods: z.object({ uuid: z.string(), name: z.string() }).nullable(), + insuranceScheme: z.string().optional(), + policyNumber: z.string().optional(), + exemptionCategory: z.string().optional(), }); -const VisitAttributesForm: React.FC = ({ setAttributes, setPaymentMethod }) => { +const VisitAttributesForm: React.FC = ({ + setAttributes, + setPaymentMethod, + setIsPatientExempted, +}) => { const { t } = useTranslation(); const { visitAttributeTypes, patientExemptionCategories } = useConfig(); const { control, getValues, watch, setValue } = useForm({ mode: 'all', - defaultValues: {}, + defaultValues: { + isPatientExempted: '', + paymentMethods: null, + insuranceScheme: '', + policyNumber: '', + exemptionCategory: '', + }, resolver: zodResolver(visitAttributesFormSchema), }); - const [isPatientExempted, paymentMethods, insuranceSchema, policyNumber, exemptionCategory] = watch([ - 'isPatientExempted', - 'paymentMethods', - 'insuranceScheme', - 'policyNumber', - 'exemptionCategory', - ]); const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes(); + const [isPatientExempted, paymentMethods] = watch(['isPatientExempted', 'paymentMethods']); const resetFormFieldsForNonExemptedPatients = useCallback(() => { - if ((isPatientExempted && paymentMethods !== null) || paymentMethods !== undefined) { - setValue('insuranceScheme', ''); - setValue('policyNumber', ''); + setValue('insuranceScheme', ''); + setValue('policyNumber', ''); + setValue('exemptionCategory', ''); + setValue('paymentMethods', null); + }, [setValue]); + + useEffect(() => { + if (isPatientExempted === 'true') { + resetFormFieldsForNonExemptedPatients(); } - }, [isPatientExempted, paymentMethods, setValue]); + setIsPatientExempted(isPatientExempted); + }, [isPatientExempted, resetFormFieldsForNonExemptedPatients, setIsPatientExempted]); const createVisitAttributesPayload = useCallback(() => { - const { exemptionCategory, paymentMethods, policyNumber, isPatientExempted } = getValues(); - setPaymentMethod(paymentMethods); - resetFormFieldsForNonExemptedPatients(); + const values = getValues(); + setPaymentMethod?.(values.paymentMethods); const formPayload = [ - { uuid: visitAttributeTypes.isPatientExempted, value: isPatientExempted }, - { uuid: visitAttributeTypes.paymentMethods, value: paymentMethods?.uuid }, - { uuid: visitAttributeTypes.policyNumber, value: policyNumber }, - { uuid: visitAttributeTypes.insuranceScheme, value: insuranceSchema }, - { uuid: visitAttributeTypes.exemptionCategory, value: exemptionCategory }, + { uuid: visitAttributeTypes.isPatientExempted, value: values.isPatientExempted }, + { uuid: visitAttributeTypes.paymentMethods, value: values.paymentMethods?.uuid }, + { uuid: visitAttributeTypes.policyNumber, value: values.policyNumber }, + { uuid: visitAttributeTypes.insuranceScheme, value: values.insuranceScheme }, + { uuid: visitAttributeTypes.exemptionCategory, value: values.exemptionCategory }, ]; const visitAttributesPayload = formPayload.filter( (item) => item.value !== undefined && item.value !== null && item.value !== '', ); - return Object.entries(visitAttributesPayload).map(([key, value]) => ({ - attributeType: value.uuid, - value: value.value, + return visitAttributesPayload.map(({ uuid, value }) => ({ + attributeType: uuid, + value, })); - }, [ - visitAttributeTypes.insuranceScheme, - visitAttributeTypes.isPatientExempted, - visitAttributeTypes.exemptionCategory, - visitAttributeTypes.paymentMethods, - visitAttributeTypes.policyNumber, - getValues, - insuranceSchema, - resetFormFieldsForNonExemptedPatients, - setPaymentMethod, - ]); + }, [getValues, visitAttributeTypes, setPaymentMethod]); - React.useEffect(() => { + useEffect(() => { setAttributes(createVisitAttributesPayload()); - }, [paymentMethods, insuranceSchema, policyNumber, exemptionCategory, setAttributes, createVisitAttributesPayload]); + }, [isPatientExempted, paymentMethods, getValues, createVisitAttributesPayload, setAttributes]); if (isLoadingPaymentModes) { return ( @@ -109,17 +110,21 @@ const VisitAttributesForm: React.FC = ({ setAttributes control={control} render={({ field }) => ( field.onChange(selected)} + onChange={(selected) => { + field.onChange(selected); + setValue('isPatientExempted', selected); + }} orientation="horizontal" legendText={t('isPatientExemptedLegend', 'Is patient exempted from payment?')} name="patientExemption"> - - + + )} /> - {isPatientExempted && ( + + {isPatientExempted === 'true' && (
= ({ setAttributes />
)} -
- ( - field.onChange(selectedItem)} - id="paymentMethods" - items={paymentModes} - itemToString={(item) => (item ? item.name : '')} - titleText={t('paymentMethodsTitle', 'Payment methods')} - placeholder={t('selectPaymentMethodPlaceholder', 'Select payment method')} - /> - )} - /> -
- {paymentMethods?.name?.toLocaleLowerCase() === 'insurance' && ( + {isPatientExempted === 'false' && ( +
+ ( + field.onChange(selectedItem)} + id="paymentMethods" + items={paymentModes} + itemToString={(item) => (item ? item.name : '')} + titleText={t('paymentMethodsTitle', 'Payment method')} + placeholder={t('selectPaymentMethod', 'Select payment method')} + /> + )} + /> +
+ )} + + {paymentMethods?.name?.toLowerCase() === 'insurance' && isPatientExempted === 'false' && ( <>
{ + const { t } = useTranslation(); + const [activeTabIndex, setActiveTabIndex] = useState(0); + + const handleTabChange = ({ selectedIndex }: { selectedIndex: number }) => { + setActiveTabIndex(selectedIndex); + }; + + return ( +
+ +
+ + {"Today's bills"} + {t('patientBills', 'Patient Bill')} + +
+ + + + + + + + +
+
+ ); +}; + +export default BillingTabs; diff --git a/packages/esm-billing-app/src/billing.resource.ts b/packages/esm-billing-app/src/billing.resource.ts old mode 100644 new mode 100755 index a8a42ce0..d9c3c1ca --- a/packages/esm-billing-app/src/billing.resource.ts +++ b/packages/esm-billing-app/src/billing.resource.ts @@ -97,7 +97,7 @@ export const useBill = (billUuid: string) => { cashPointUuid: bill?.cashPoint?.uuid, cashPointName: bill?.cashPoint?.name, cashPointLocation: bill?.cashPoint?.location?.display, - dateCreated: bill?.dateCreated ? formatDate(parseDate(bill.dateCreated), { mode: 'wide' }) : '--', + dateCreated: bill?.dateCreated ?? '--', lineItems: bill.lineItems, billingService: bill.lineItems.map((bill) => bill.item).join(' '), payments: bill.payments, @@ -132,7 +132,7 @@ export const processBillPayment = (payload, billUuid: string) => { export function useDefaultFacility() { const { authenticated } = useSession(); - const url = '/ws/rest/v1/amrs/default-facility'; + const url = '/ws/rest/v1/kenyaemr/default-facility'; const { data, isLoading } = useSWR<{ data: FacilityDetail }>(authenticated ? url : null, openmrsFetch, {}); return { data: data?.data, isLoading: isLoading }; } @@ -181,8 +181,9 @@ export const usePaymentModes = (excludeWaiver: boolean = true) => { }); const allowedPaymentModes = excludedPaymentMode?.length > 0 - ? data?.data?.results.filter((mode) => !excludedPaymentMode.some((excluded) => excluded.uuid === mode.uuid)) ?? [] - : data?.data?.results ?? []; + ? (data?.data?.results.filter((mode) => !excludedPaymentMode.some((excluded) => excluded.uuid === mode.uuid)) ?? + []) + : (data?.data?.results ?? []); return { paymentModes: excludeWaiver ? allowedPaymentModes : data?.data?.results, isLoading, diff --git a/packages/esm-billing-app/src/bills-table/bills-table.component.tsx b/packages/esm-billing-app/src/bills-table/bills-table.component.tsx old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/bills-table/bills-table.scss b/packages/esm-billing-app/src/bills-table/bills-table.scss old mode 100644 new mode 100755 diff --git a/packages/esm-billing-app/src/claims/claims-wrap/claims-main-component.tsx b/packages/esm-billing-app/src/claims/claims-wrap/claims-main-component.tsx new file mode 100755 index 00000000..65b5fd69 --- /dev/null +++ b/packages/esm-billing-app/src/claims/claims-wrap/claims-main-component.tsx @@ -0,0 +1,35 @@ +import React, { useEffect, useState } from 'react'; +import styles from './claims-main.scss'; +import { type LineItem, type MappedBill } from '../../types'; +import ClaimsTable from '../dashboard/table/claims-table.component'; +import { useBill } from '../../billing.resource'; +import { useTranslation } from 'react-i18next'; +import ClaimsForm from '../dashboard/form/claims-form.component'; +import MainMetrics from '../metrics/metrics.component'; + +interface ClaimsMainProps { + bill: MappedBill; +} + +const ClaimMainComponent: React.FC = ({ bill }) => { + const [selectedLineItems, setSelectedLineItems] = useState([]); + const { isLoading: isLoadingBill, error } = useBill(bill.uuid); + + const handleSelectItem = (lineItems: Array) => { + setSelectedLineItems(lineItems); + }; + + return ( + <> + +
+
+ + +
+
+ + ); +}; + +export default ClaimMainComponent; diff --git a/packages/esm-billing-app/src/claims/claims-wrap/claims-main.scss b/packages/esm-billing-app/src/claims/claims-wrap/claims-main.scss new file mode 100755 index 00000000..001553fa --- /dev/null +++ b/packages/esm-billing-app/src/claims/claims-wrap/claims-main.scss @@ -0,0 +1,25 @@ +.mainContainer { + display: flex; + margin: 0 auto; + padding: 0 1rem; +} + +.content { + display: flex; + width: 100%; +} + +:global(.omrs-breakpoint-lt-desktop) { + .mainContainer { + flex-direction: column; + } + + .content { + flex-direction: column; + } +} + +.claimContainer, +.form { + flex: 1; +} diff --git a/packages/esm-billing-app/src/invoice/claims/claims-dashboard/claims-breakdown/claims-breakdown.component.tsx b/packages/esm-billing-app/src/claims/dashboard/claims-breakdown/claims-breakdown.component.tsx old mode 100644 new mode 100755 similarity index 100% rename from packages/esm-billing-app/src/invoice/claims/claims-dashboard/claims-breakdown/claims-breakdown.component.tsx rename to packages/esm-billing-app/src/claims/dashboard/claims-breakdown/claims-breakdown.component.tsx diff --git a/packages/esm-billing-app/src/invoice/claims/claims-dashboard/claims-breakdown/claims-breakdown.scss b/packages/esm-billing-app/src/claims/dashboard/claims-breakdown/claims-breakdown.scss old mode 100644 new mode 100755 similarity index 100% rename from packages/esm-billing-app/src/invoice/claims/claims-dashboard/claims-breakdown/claims-breakdown.scss rename to packages/esm-billing-app/src/claims/dashboard/claims-breakdown/claims-breakdown.scss diff --git a/packages/esm-billing-app/src/claims/dashboard/claims-dashboard.component.tsx b/packages/esm-billing-app/src/claims/dashboard/claims-dashboard.component.tsx new file mode 100755 index 00000000..dc87cbeb --- /dev/null +++ b/packages/esm-billing-app/src/claims/dashboard/claims-dashboard.component.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { usePatient, ErrorState } from '@openmrs/esm-framework'; +import styles from './header/claims-header.scss'; +import { InlineLoading } from '@carbon/react'; +import { useBill } from '../../billing.resource'; +import ClaimsHeader from './header/claims-header.component'; + +const ClaimScreen: React.FC = () => { + const { billUuid, patientUuid } = useParams(); + const { t } = useTranslation(); + + const { patient, isLoading: isLoadingPatient } = usePatient(patientUuid); + const { bill, isLoading: isLoadingBill, error } = useBill(billUuid); + + if (isLoadingPatient && isLoadingBill) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ +
+ ); + } + + return ; +}; + +export default ClaimScreen; diff --git a/packages/esm-billing-app/src/claims/dashboard/form/claims-form.component.tsx b/packages/esm-billing-app/src/claims/dashboard/form/claims-form.component.tsx new file mode 100755 index 00000000..8c315d94 --- /dev/null +++ b/packages/esm-billing-app/src/claims/dashboard/form/claims-form.component.tsx @@ -0,0 +1,396 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Column, + TextArea, + Form, + Layer, + Stack, + TextInput, + Row, + ButtonSet, + Button, + FilterableMultiSelect, + MultiSelect, + InlineLoading, +} from '@carbon/react'; +import styles from './claims-form.scss'; +import { type MappedBill, type LineItem } from '../../../types'; +import { navigate, showSnackbar } from '@openmrs/esm-framework'; +import { useSystemSetting } from '../../../hooks/getMflCode'; +import { useParams } from 'react-router-dom'; +import { processClaims, useProviders, useVisit } from './claims-form.resource'; +import { useForm, Controller } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { extractNameString, formatDate } from '../../../helpers/functions'; + +type ClaimsFormProps = { + bill: MappedBill; + selectedLineItems: LineItem[]; +}; + +const ClaimsFormSchema = z.object({ + claimCode: z.string().nonempty({ message: 'Claim code is required' }), + guaranteeId: z.string().nonempty({ message: 'Guarantee Id is required' }), + claimExplanation: z.string().nonempty({ message: 'Claim explanation is required' }), + claimJustification: z.string().nonempty({ message: 'Claim justification is required' }), + providerName: z + .array( + z.object({ + id: z.string(), + text: z.string(), + }), + ) + .nonempty({ message: 'At least one provider is required' }), + diagnoses: z + .array( + z.object({ + id: z.string(), + text: z.string(), + }), + ) + .nonempty({ message: 'At least one diagnosis is required' }), + visitType: z.string().nonempty({ message: 'Visit type is required' }), + facility: z.string().nonempty({ message: 'Facility is required' }), + treatmentStart: z.string().nonempty({ message: 'Treatment start date is required' }), + treatmentEnd: z.string().nonempty({ message: 'Treatment end date is required' }), +}); + +const ClaimsForm: React.FC = ({ bill, selectedLineItems }) => { + const { t } = useTranslation(); + const { mflCodeValue } = useSystemSetting('facility.mflcode'); + const { patientUuid, billUuid } = useParams(); + const { visits: recentVisit } = useVisit(patientUuid); + const visitUuid = recentVisit?.visitType.uuid; + + const { data } = useProviders(); + const [loading, setLoading] = useState(false); + const providers = data?.data.results.map((provider) => ({ id: provider.uuid, text: provider.display })) || []; + + const handleNavigateToBillingOptions = () => + navigate({ + to: window.getOpenmrsSpaBase() + `home/billing/patient/${patientUuid}/${billUuid}`, + }); + + const diagnoses = useMemo(() => { + return ( + recentVisit?.encounters?.flatMap( + (encounter) => + encounter.diagnoses.map((diagnosis) => ({ + id: diagnosis.diagnosis.coded.uuid, + text: diagnosis.display, + certainty: diagnosis.certainty, + })) || [], + ) || [] + ); + }, [recentVisit]); + + const confirmedDiagnoses = useMemo(() => { + return diagnoses.filter((diagnosis) => diagnosis.certainty === 'CONFIRMED'); + }, [diagnoses]); + + const { + control, + handleSubmit, + formState: { errors, isValid }, + setValue, + reset, + } = useForm({ + mode: 'all', + resolver: zodResolver(ClaimsFormSchema), + defaultValues: { + claimCode: '', + guaranteeId: '', + claimExplanation: '', + claimJustification: '', + providerName: [], + diagnoses: [], + visitType: recentVisit?.visitType?.display || '', + facility: `${recentVisit?.location?.display || ''} - ${mflCodeValue || ''}`, + treatmentStart: recentVisit?.startDatetime ? formatDate(recentVisit.startDatetime) : '', + treatmentEnd: recentVisit?.stopDatetime ? formatDate(recentVisit.stopDatetime) : '', + }, + }); + + const onSubmit = async (data) => { + setLoading(true); + const providedItems = selectedLineItems.reduce((acc, item) => { + acc[item.uuid] = { + items: [ + { + uuid: item.itemOrServiceConceptUuid, + price: item.price, + quantity: item.quantity, + }, + ], + explanation: data.claimExplanation, + justification: data.claimJustification, + }; + return acc; + }, {}); + + const payload = { + providedItems, + claimExplanation: data.claimExplanation, + claimJustification: data.claimJustification, + startDate: data.treatmentStart, + endDate: data.treatmentEnd, + location: mflCodeValue, + diagnoses: data.diagnoses.map((diagnosis) => diagnosis.id), + paidInFacility: true, + patient: patientUuid, + visitType: visitUuid, + guaranteeId: data.guaranteeId, + providers: data.providerName.map((provider) => provider.id), + claimCode: data.claimCode, + use: 'claim', + insurer: 'SHA', + billNumber: billUuid, + }; + try { + await processClaims(payload); + showSnackbar({ + kind: 'success', + title: t('processClaim', 'Process Claim'), + subtitle: t('sendClaim', 'Claim sent successfully'), + timeoutInMs: 3000, + isLowContrast: true, + }); + reset(); + setTimeout(() => { + navigate({ + to: window.getOpenmrsSpaBase() + `home/billing/`, + }); + }, 2000); + } catch (err) { + console.error(err); + showSnackbar({ + kind: 'error', + title: t('claimError', 'Claim Error'), + subtitle: t('sendClaimError', 'Request Failed, Please try later........'), + timeoutInMs: 2500, + isLowContrast: true, + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + setValue('diagnoses', confirmedDiagnoses); + setValue('visitType', recentVisit?.visitType?.display || ''); + setValue('facility', `${recentVisit?.location?.display || ''} - ${mflCodeValue || ''}`); + setValue('treatmentStart', recentVisit?.startDatetime ? formatDate(recentVisit.startDatetime) : ''); + setValue('treatmentEnd', recentVisit?.stopDatetime ? formatDate(recentVisit.stopDatetime) : ''); + }, [confirmedDiagnoses, recentVisit, mflCodeValue, setValue]); + return ( + + + {t('formTitle', 'Fill in the form details')} + + + + ( + + )} + /> + + + + + ( + + )} + /> + + + + + + + ( + + )} + /> + + + + + ( + + )} + /> + + + + + + ( + (item ? item.text : '')} + selectionFeedback="top-after-reopen" + selectedItems={field.value} + onChange={({ selectedItems }) => field.onChange(selectedItems)} + /> + )} + /> + + + + + ( + (item ? extractNameString(item.text) : '')} + selectionFeedback="top-after-reopen" + selectedItems={field.value} + onChange={({ selectedItems }) => field.onChange(selectedItems)} + /> + )} + /> + + + + + + ( + + )} + /> + + + + + ( + + )} + /> + + + + + + ( +