diff --git a/packages/esm-billing-app/package.json b/packages/esm-billing-app/package.json index 0d4e120..e9a2da1 100644 --- a/packages/esm-billing-app/package.json +++ b/packages/esm-billing-app/package.json @@ -1,6 +1,6 @@ { "name": "@ehospital/esm-billing-app", - "version": "1.1.0", + "version": "1.1.3", "description": "Billing frontend module for use in O3", "browser": "dist/ehospital-esm-billing-app.js", "main": "src/index.ts", @@ -120,5 +120,5 @@ "*.{js,jsx,ts,tsx}": "eslint --cache --fix" }, "packageManager": "yarn@4.1.1", - "gitHead": "b5e50a7d0160032b1d3f20dec5d5eaf83717fafc" + "gitHead": "536537e2d9cdf3b676cdbeaaa3c32a9d3189b19a" } diff --git a/packages/esm-billing-app/src/billable-services/billable-item/drug-order/drug-order.component.tsx b/packages/esm-billing-app/src/billable-services/billable-item/drug-order/drug-order.component.tsx index 7ee8cf0..a234b60 100644 --- a/packages/esm-billing-app/src/billable-services/billable-item/drug-order/drug-order.component.tsx +++ b/packages/esm-billing-app/src/billable-services/billable-item/drug-order/drug-order.component.tsx @@ -5,6 +5,7 @@ import { useBillableItem, useSockItemInventory } from '../useBillableItem'; import { useTranslation } from 'react-i18next'; import styles from './drug-order.scss'; import { convertToCurrency } from '../../../helpers'; +import { useConfig } from '@openmrs/esm-framework'; type DrugOrderProps = { order: { @@ -25,6 +26,7 @@ const DrugOrder: React.FC = ({ order }) => { const { t } = useTranslation(); const { stockItem, isLoading: isLoadingInventory } = useSockItemInventory(order?.drug?.uuid); const { billableItem, isLoading } = useBillableItem(order?.drug.concept.uuid); + const {defaultCurrency} = useConfig() if (isLoading || isLoadingInventory) { return null; } @@ -53,7 +55,7 @@ const DrugOrder: React.FC = ({ order }) => { billableItem?.servicePrices.map((item) => (
{t('unitPrice', 'Unit price ')} - {convertToCurrency(item.price)} + {convertToCurrency(item.price, defaultCurrency)}
))} diff --git a/packages/esm-billing-app/src/billable-services/billable-item/test-order/price-info-order.componet.tsx b/packages/esm-billing-app/src/billable-services/billable-item/test-order/price-info-order.componet.tsx index 023a4fe..30c9359 100644 --- a/packages/esm-billing-app/src/billable-services/billable-item/test-order/price-info-order.componet.tsx +++ b/packages/esm-billing-app/src/billable-services/billable-item/test-order/price-info-order.componet.tsx @@ -11,6 +11,7 @@ import { Tile, InlineNotification, } from '@carbon/react'; +import { useConfig } from '@openmrs/esm-framework'; type PriceInfoOrderProps = { billableItem: any; @@ -19,7 +20,7 @@ type PriceInfoOrderProps = { const PriceInfoOrder: React.FC = ({ billableItem, error }) => { const { t } = useTranslation(); - + const {defaultCurrency} = useConfig() const hasPrice = billableItem?.servicePrices?.length > 0; if (error || !hasPrice) { @@ -46,7 +47,7 @@ const PriceInfoOrder: React.FC = ({ billableItem, error }) {billableItem.servicePrices.map((priceItem: any) => ( {priceItem.paymentMode.name} - {convertToCurrency(priceItem.price)} + {convertToCurrency(priceItem.price, defaultCurrency)} ))} diff --git a/packages/esm-billing-app/src/billable-services/billable-item/useBillableItem.tsx b/packages/esm-billing-app/src/billable-services/billable-item/useBillableItem.tsx index 780ebbd..ee29ee4 100644 --- a/packages/esm-billing-app/src/billable-services/billable-item/useBillableItem.tsx +++ b/packages/esm-billing-app/src/billable-services/billable-item/useBillableItem.tsx @@ -22,7 +22,7 @@ type BillableItemResponse = { export const useBillableItem = (billableItemId: string) => { const customRepresentation = `v=custom:(uuid,name,concept:(uuid,display),servicePrices:(uuid,price,paymentMode:(uuid,name)))`; const { data, error, isLoading } = useSWRImmutable<{ data: { results: Array } }>( - `${restBaseUrl}/cashier/billableService?${customRepresentation}`, + `${restBaseUrl}/billing/billableService?${customRepresentation}`, openmrsFetch, ); const billableItem = data?.data?.results?.find((item) => item?.concept?.uuid === billableItemId); diff --git a/packages/esm-billing-app/src/billable-services/billables/charge-summary-table.component.tsx b/packages/esm-billing-app/src/billable-services/billables/charge-summary-table.component.tsx new file mode 100644 index 0000000..210d893 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/charge-summary-table.component.tsx @@ -0,0 +1,219 @@ +import { + Button, + ComboButton, + DataTable, + DataTableSkeleton, + InlineLoading, + MenuItem, + OverflowMenu, + OverflowMenuItem, + Pagination, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, + TableToolbar, + TableToolbarContent, + TableToolbarSearch, +} from '@carbon/react'; +import { Add, CategoryAdd, Download, Upload, WatsonHealthScalpelSelect } from '@carbon/react/icons'; +import { ErrorState, launchWorkspace, showModal, useLayoutType, usePagination, useConfig } from '@openmrs/esm-framework'; +import { EmptyState, usePaginationInfo } from '@openmrs/esm-patient-common-lib'; +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { convertToCurrency } from '../../helpers'; +import styles from './charge-summary-table.scss'; +import { useChargeSummaries } from './charge-summary.resource'; +import { searchTableData } from './form-helper'; + +const defaultPageSize = 10; + +const ChargeSummaryTable: React.FC = () => { + const { t } = useTranslation(); + const layout = useLayoutType(); + const size = layout === 'tablet' ? 'lg' : 'md'; + const { isLoading, isValidating, error, mutate, chargeSummaryItems } = useChargeSummaries(); + const [pageSize, setPageSize] = useState(defaultPageSize); + const [searchString, setSearchString] = useState(''); + const {defaultCurrency} = useConfig() + + const searchResults = useMemo( + () => searchTableData(chargeSummaryItems, searchString), + [chargeSummaryItems, searchString], + ); + + const { results, goTo, currentPage } = usePagination(searchResults, pageSize); + const { pageSizes } = usePaginationInfo(defaultPageSize, chargeSummaryItems.length, currentPage, results.length); + + const headers = [ + { + key: 'name', + header: t('name', 'Name'), + }, + { + key: 'shortName', + header: t('shortName', 'Short Name'), + }, + { + key: 'serviceStatus', + header: t('status', 'Status'), + }, + { + key: 'serviceType', + header: t('type', 'Type'), + }, + { + key: 'servicePrices', + header: t('prices', 'Prices'), + }, + ]; + + const rows = results.map((service) => { + return { + id: service.uuid, + name: service.name, + shortName: service.shortName, + serviceStatus: service.serviceStatus, + serviceType: service?.serviceType?.display ?? t('stockItem', 'Stock Item'), + servicePrices: service.servicePrices + .map((price) => `${price.name} : ${convertToCurrency(price.price, defaultCurrency)}`) + .join(', '), + }; + }); + + // TODO: Implement handleDelete + const handleDelete = (service) => {}; + + const handleEdit = (service) => { + Boolean(service?.serviceType?.display) + ? launchWorkspace('billable-service-form', { + initialValues: service, + workspaceTitle: t('editServiceChargeItem', 'Edit Service Charge Item'), + }) + : launchWorkspace('commodity-form', { + initialValues: service, + workspaceTitle: t('editChargeItem', 'Edit Charge Item'), + }); + }; + + const openBulkUploadModal = () => { + const dispose = showModal('bulk-import-billable-services-modal', { + closeModal: () => dispose(), + }); + }; + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!chargeSummaryItems.length) { + return ( + launchWorkspace('billable-service-form')} + displayText={t('chargeItemsDescription', 'Charge Items')} + /> + ); + } + + return ( + <> + + {({ rows, headers, getHeaderProps, getRowProps, getTableProps, getToolbarProps, getTableContainerProps }) => ( + + + + setSearchString(e.target.value)} + persistent + size={size} + /> + {isValidating && ( + + )} + + launchWorkspace('billable-service-form')} + label={t('addServiceChargeItem', 'Add charge service')} + /> + launchWorkspace('commodity-form')} + label={t('addCommodityChargeItem', 'Add charge item')} + /> + + + + + + + + + {headers.map((header) => ( + + {header.header} + + ))} + + + + + {rows.map((row, index) => ( + + {row.cells.map((cell) => ( + {cell.value} + ))} + + + handleEdit(results[index])} + /> + + + + ))} + +
+
+ )} +
+ { + setPageSize(pageSize); + goTo(page); + }} + page={currentPage} + pageSize={defaultPageSize} + pageSizes={pageSizes} + size="sm" + totalItems={chargeSummaryItems.length} + /> + + ); +}; + +export default ChargeSummaryTable; diff --git a/packages/esm-billing-app/src/billable-services/billables/charge-summary-table.scss b/packages/esm-billing-app/src/billable-services/billables/charge-summary-table.scss new file mode 100644 index 0000000..6be23d0 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/charge-summary-table.scss @@ -0,0 +1,19 @@ +@use '@carbon/layout'; + +.tableContainer { + display: flex; + flex-direction: column; + + & > section { + position: relative; + margin-bottom: layout.$spacing-01; + } +} + +.iconMarginLeft { + margin-left: layout.$spacing-05; +} + +.bulkUploadButton { + margin-right: layout.$spacing-05; +} diff --git a/packages/esm-billing-app/src/billable-services/billables/charge-summary.resource.tsx b/packages/esm-billing-app/src/billable-services/billables/charge-summary.resource.tsx new file mode 100644 index 0000000..1b5a874 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/charge-summary.resource.tsx @@ -0,0 +1,42 @@ +import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import useSWR from 'swr'; + +export type ChargeAble = { + uuid: string; + name: string; + shortName: string; + serviceStatus: 'ENABLED' | 'DISABLED'; + stockItem: string; + serviceType: { + uuid: string; + display: string; + }; + servicePrices: Array<{ + uuid: string; + name: string; + price: number; + }>; + concept: { + uuid: string; + display: string; + }; +}; + +type ChargeAblesResponse = { + results: Array; +}; + +export const useChargeSummaries = () => { + const url = `${restBaseUrl}/billing/billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(uuid,display),servicePrices:(uuid,name,paymentMode,price),concept:(uuid,display))`; + const { data, isLoading, isValidating, error, mutate } = useSWR<{ data: ChargeAblesResponse }>(url, openmrsFetch, { + errorRetryCount: 0, + }); + + return { + chargeSummaryItems: data?.data?.results ?? [], + isLoading, + isValidating, + error, + mutate, + }; +}; diff --git a/packages/esm-billing-app/src/billable-services/billables/commodity/commodity-form.scss b/packages/esm-billing-app/src/billable-services/billables/commodity/commodity-form.scss new file mode 100644 index 0000000..8864ddd --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/commodity/commodity-form.scss @@ -0,0 +1,61 @@ +@use '@carbon/colors'; +@use '@carbon/layout'; +@use '@carbon/type'; + +.form { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; +} + +.formContainer { + margin: layout.$spacing-05; +} + +.tablet { + padding: layout.$spacing-06 layout.$spacing-05; + background-color: colors.$white; +} + +.desktop { + padding: 0; +} + +.paymentMethods { + display: grid; + grid-template-columns: 0.45fr 0.45fr 0.1fr; + column-gap: 0.25rem; + justify-content: space-between; + align-items: flex-end; +} + +.searchResults { + position: absolute; + top: 2.625rem; + z-index: 99; + height: 16rem; + padding: 0.5rem; + overflow-y: scroll; + background-color: colors.$white; + border: 1px solid colors.$gray-30; + width: 100%; +} + +.searchItem { + padding: 0.5rem; + color: colors.$gray-70; + cursor: pointer; + @include type.type-style('label-01'); + border-bottom: 1px solid colors.$gray-30; + &:hover { + background-color: colors.$gray-20; + } +} + +.formGroupWithConcept { + position: relative; +} +.formStackControl { + row-gap: layout.$layout-01; +} diff --git a/packages/esm-billing-app/src/billable-services/billables/commodity/commodity-form.workspace.tsx b/packages/esm-billing-app/src/billable-services/billables/commodity/commodity-form.workspace.tsx new file mode 100644 index 0000000..74920e3 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/commodity/commodity-form.workspace.tsx @@ -0,0 +1,166 @@ +import React, { useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ButtonSet, Button, Stack, Toggle, InlineNotification, InlineLoading } from '@carbon/react'; +import { Add } from '@carbon/react/icons'; +import { useForm, FormProvider, useFieldArray, Controller } from 'react-hook-form'; + +import { useLayoutType, ResponsiveWrapper, showSnackbar, restBaseUrl } from '@openmrs/esm-framework'; +import { DefaultPatientWorkspaceProps } from '@openmrs/esm-patient-common-lib'; +import styles from './commodity-form.scss'; +import StockItemSearch from './stock-search.component'; +import classNames from 'classnames'; +import { zodResolver } from '@hookform/resolvers/zod'; +import PriceField from '../services/price.component'; +import { billableFormSchema, BillableFormSchema } from '../form-schemas'; +import { formatBillableServicePayloadForSubmission, mapInputToPayloadSchema } from '../form-helper'; +import { createBillableSerice } from '../../billable-service.resource'; +import { handleMutate } from '../../utils'; + +type CommodityFormProps = DefaultPatientWorkspaceProps & { + initialValues?: BillableFormSchema; +}; + +const CommodityForm: React.FC = ({ + closeWorkspace, + closeWorkspaceWithSavedChanges, + promptBeforeClosing, + initialValues, +}) => { + const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; + const formMethods = useForm({ + resolver: zodResolver(billableFormSchema), + defaultValues: initialValues + ? mapInputToPayloadSchema(initialValues) + : { servicePrices: [], serviceStatus: 'ENABLED' }, + }); + + const { + setValue, + control, + handleSubmit, + formState: { errors, isDirty, isSubmitting }, + } = formMethods; + + const { fields, append, remove } = useFieldArray({ + control, + name: 'servicePrices', + }); + + const onSubmit = async (formValues: BillableFormSchema) => { + const payload = formatBillableServicePayloadForSubmission(formValues, initialValues?.['uuid']); + try { + const response = await createBillableSerice(payload); + if (response.ok) { + showSnackbar({ + title: t('commodityBillableCreated', 'Commodity price created successfully'), + subtitle: t('commodityBillableCreatedSubtitle', 'The commodity price has been created successfully'), + kind: 'success', + isLowContrast: true, + timeoutInMs: 5000, + }); + handleMutate(`${restBaseUrl}/billing/billableService?v`); + closeWorkspaceWithSavedChanges(); + } + } catch (e) { + showSnackbar({ + title: t('commodityBillableCreationFailed', 'Commodity price creation failed'), + subtitle: t('commodityBillableCreationFailedSubtitle', 'The commodity price creation failed'), + kind: 'error', + isLowContrast: true, + timeoutInMs: 5000, + }); + } + }; + + useEffect(() => { + promptBeforeClosing(() => isDirty); + }, [isDirty, promptBeforeClosing]); + + const renderServicePriceFields = useMemo( + () => + fields.map((field, index) => ( + + )), + [fields, control, remove, errors], + ); + + const handleError = (err) => { + console.error(JSON.stringify(err, null, 2)); + showSnackbar({ + title: t('commodityBillableCreationFailed', 'Commodity price creation failed'), + subtitle: t( + 'commodityBillableCreationFailedSubtitle', + 'The commodity price creation failed, view browser console for more details', + ), + kind: 'error', + isLowContrast: true, + timeoutInMs: 5000, + }); + }; + + return ( + +
+
+ + + + ( + (value ? field.onChange('ENABLED') : field.onChange('DISABLED'))} + /> + )} + /> + + {renderServicePriceFields} + + {!!errors.servicePrices && ( + + )} + +
+ + + + +
+
+ ); +}; + +export default CommodityForm; diff --git a/packages/esm-billing-app/src/billable-services/billables/commodity/stock-search.component.tsx b/packages/esm-billing-app/src/billable-services/billables/commodity/stock-search.component.tsx new file mode 100644 index 0000000..f1b7c4c --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/commodity/stock-search.component.tsx @@ -0,0 +1,72 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FormGroup, Search, InlineLoading, CodeSnippet } from '@carbon/react'; +import { ErrorState, ResponsiveWrapper, useDebounce } from '@openmrs/esm-framework'; +import { useCommodityItem } from './useCommodityItem'; +import { type UseFormSetValue } from 'react-hook-form'; +import { BillableFormSchema } from '../form-schemas'; +import { formatStockItemToPayload } from '../form-helper'; +import styles from './commodity-form.scss'; + +type StockItemSearchProps = { + setValue: UseFormSetValue; + defaultStockItem?: string; +}; + +const StockItemSearch: React.FC = ({ setValue, defaultStockItem }) => { + const { t } = useTranslation(); + const [searchTerm, setSearchTerm] = useState(''); + const debouncedSearchTerm = useDebounce(searchTerm, 500); + const [selectedStockItem, setSelectedStockItem] = useState({}); + const { stockItems, isLoading, isValidating, error, mutate } = useCommodityItem(debouncedSearchTerm); + + const handleStockItemSelect = (stockItem) => { + setSelectedStockItem(stockItem); + setSearchTerm(''); + const payload = formatStockItemToPayload(stockItem); + Object.entries(payload).forEach(([key, value]) => { + setValue(key as keyof BillableFormSchema, value); + }); + }; + + if (error) { + return ; + } + + return ( + + + setSearchTerm(e.target.value)} + value={selectedStockItem?.['commonName']} + defaultValue={defaultStockItem} + disabled={Boolean(defaultStockItem)} + /> + + {isLoading && ( +
+ +
+ )} + {stockItems && stockItems.length > 0 && !isLoading && searchTerm && ( +
+ {stockItems.map((stockItem) => ( +
handleStockItemSelect(stockItem)}> + {stockItem.commonName} +
+ ))} +
+ )} +
+ ); +}; + +export default StockItemSearch; diff --git a/packages/esm-billing-app/src/billable-services/billables/commodity/useCommodityItem.tsx b/packages/esm-billing-app/src/billable-services/billables/commodity/useCommodityItem.tsx new file mode 100644 index 0000000..0c157a1 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/commodity/useCommodityItem.tsx @@ -0,0 +1,59 @@ +import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import useSWR from 'swr'; + +type StockItemResponse = { + results: Array<{ + uuid: string; + drugUuid: string; + drugName: string; + conceptUuid: string; + conceptName: string; + hasExpiration: boolean; + preferredVendorUuid: string; + preferredVendorName: string; + purchasePrice: number | null; + purchasePriceUoMUuid: string | null; + purchasePriceUoMName: string | null; + purchasePriceUoMFactor: number | null; + dispensingUnitName: string; + dispensingUnitUuid: string; + dispensingUnitPackagingUoMUuid: string; + dispensingUnitPackagingUoMName: string; + dispensingUnitPackagingUoMFactor: number; + defaultStockOperationsUoMUuid: string | null; + defaultStockOperationsUoMName: string | null; + defaultStockOperationsUoMFactor: number | null; + categoryUuid: string; + categoryName: string; + commonName: string; + acronym: string | null; + reorderLevel: number | null; + reorderLevelUoMUuid: string | null; + reorderLevelUoMName: string | null; + reorderLevelUoMFactor: number | null; + dateCreated: string; + creatorGivenName: string; + creatorFamilyName: string; + voided: boolean; + expiryNotice: number; + links: { + rel: string; + uri: string; + resourceAlias: string; + }[]; + resourceVersion: string; + }>; +}; + +export const useCommodityItem = (searchTerm: string = '') => { + const url = `${restBaseUrl}/stockmanagement/stockitem?v=default&limit=10&q=${searchTerm}`; + const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: StockItemResponse }>(url, openmrsFetch); + + return { + stockItems: data?.data?.results ?? [], + isLoading, + isValidating, + error, + mutate, + }; +}; diff --git a/packages/esm-billing-app/src/billable-services/billables/form-helper.ts b/packages/esm-billing-app/src/billable-services/billables/form-helper.ts new file mode 100644 index 0000000..792b4c9 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/form-helper.ts @@ -0,0 +1,220 @@ +import { ChargeAble } from './charge-summary.resource'; +import { BillableFormSchema, ServicePriceSchema } from './form-schemas'; + +export type BillableServicePayload = { + name: string; + shortName: string; + serviceType: number | string; + servicePrices: Array<{ + paymentMode: string; + price: string | number; + name: string; + }>; + serviceStatus: string; + concept: string | number; + stockItem: string; + uuid?: string; +}; + +export const formatBillableServicePayloadForSubmission = ( + formData: BillableFormSchema, + uuid?: string, +): BillableServicePayload => { + const formPayload = { + name: formData.name, + shortName: formData.name, + serviceType: formData.serviceType.uuid, + servicePrices: formData.servicePrices.map((servicePrice) => ({ + paymentMode: servicePrice.paymentMode.uuid, + price: servicePrice.price.toFixed(2), + name: servicePrice.paymentMode.name, + })), + serviceStatus: formData.serviceStatus, + concept: formData.concept.concept.uuid, + stockItem: formData?.stockItem ? formData.stockItem : '', + }; + + return uuid ? Object.assign(formPayload, { uuid: uuid }) : formPayload; +}; + +export function mapInputToPayloadSchema(service): BillableFormSchema { + const servicePrices: ServicePriceSchema[] = service.servicePrices.map((price: any) => ({ + price: price.price, + paymentMode: { + uuid: price.paymentMode?.uuid, + name: price.paymentMode?.name, + }, + })); + + const payload: BillableFormSchema = { + name: service.name, + shortName: service.shortName, + serviceType: { + uuid: service?.serviceType?.uuid ?? '', + display: service?.serviceType?.display ?? '', + }, + servicePrices: servicePrices, + serviceStatus: service.serviceStatus, + concept: { + concept: { + uuid: service?.concept?.uuid, + display: service.concept?.display, + }, + conceptName: { + uuid: service?.concept?.uuid, + display: service?.concept?.display, + }, + display: service?.concept?.display, + }, + }; + + return payload; +} + +export const formatStockItemToPayload = (stockItem: any): BillableFormSchema => { + return { + name: stockItem.commonName, + shortName: stockItem.commonName.length > 1 ? stockItem.commonName : `${stockItem.commonName} `, + serviceType: { + uuid: stockItem.serviceType?.uuid || '', + display: stockItem.serviceType?.display || '', + }, + servicePrices: [], + concept: { + concept: { + uuid: stockItem.conceptUuid, + display: stockItem.conceptName, + }, + conceptName: { + uuid: stockItem.conceptUuid, + display: stockItem.conceptName, + }, + display: stockItem.conceptName, + }, + stockItem: stockItem.uuid, + }; +}; + +export const searchTableData = (array: Array, searchString: string) => { + if (array !== undefined && array.length > 0) { + if (searchString && searchString.trim() !== '') { + const search = searchString.toLowerCase(); + return array?.filter((item) => + Object.entries(item).some(([header, value]) => { + if (header === 'patientUuid') { + return false; + } + return `${value}`.toLowerCase().includes(search); + }), + ); + } + } + + return array; +}; + +// export const getBulkUploadPayloadFromExcelFile = ( +// fileData: Uint8Array, +// currentlyExistingBillableServices: Array, +// paymentModes: Array, +// ) => { +// const workbook = XLSX.read(fileData, { type: 'array' }); + +// let jsonData: Array = []; + +// for (let i = 0; i < workbook.SheetNames.length; i++) { +// const sheetName = workbook.SheetNames[i]; +// const worksheet = workbook.Sheets[sheetName]; + +// const sheetJSONData: Array = XLSX.utils.sheet_to_json(worksheet, { defval: '' }); +// jsonData.push(...sheetJSONData); +// } + +// if (jsonData.length === 0) { +// return []; +// } + +// const firstRowKeys = Object.keys(jsonData.at(0)); + +// if ( +// !firstRowKeys.includes('concept_id') || +// !firstRowKeys.includes('name') || +// !firstRowKeys.includes('price') || +// !firstRowKeys.includes('disable') || +// !firstRowKeys.includes('service_type_id') || +// !firstRowKeys.includes('short_name') +// ) { +// return 'INVALID_TEMPLATE'; +// } + +// const rowsWithMissingCategories: Array = []; + +// const payload = jsonData +// .filter((row) => { +// if (row.service_type_id.toString().length > 1) { +// return true; +// } else { +// rowsWithMissingCategories.push(row); +// return false; +// } +// }) +// .filter( +// (row) => !currentlyExistingBillableServices.some((item) => item.name.toLowerCase() === row.name.toLowerCase()), +// ) +// .map((row) => { +// return { +// name: row.name, +// shortName: row.short_name ?? row.name, +// serviceType: row.service_type_id, +// servicePrices: [ +// { +// paymentMode: paymentModes.find((mode) => mode.name === 'Cash').uuid, +// price: row.price ?? 0, +// name: 'Cash', +// }, +// ], +// serviceStatus: row.disable === 'false' ? 'DISABLED' : 'ENABLED', +// concept: row.concept_id, +// stockItem: '', +// }; +// }); + +// return [payload, rowsWithMissingCategories]; +// }; + +// export function createExcelTemplateFile(): Uint8Array { +// const headers = ['concept_id', 'name', 'short_name', 'price', 'disable', 'service_type_id']; + +// const worksheet = XLSX.utils.aoa_to_sheet([headers]); +// const workbook = XLSX.utils.book_new(); +// XLSX.utils.book_append_sheet(workbook, worksheet, 'Charge Items'); + +// // Set column widths for better readability +// const colWidths = [ +// { wch: 15 }, // concept_id +// { wch: 30 }, // name +// { wch: 20 }, // short_name +// { wch: 10 }, // price +// { wch: 10 }, // disable +// { wch: 20 }, // service_type_id +// ]; +// worksheet['!cols'] = colWidths; + +// // Generate the Excel file as a Uint8Array +// const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' }); +// return new Uint8Array(excelBuffer); +// } + +// export const downloadExcelTemplateFile = () => { +// const excelBuffer = createExcelTemplateFile(); +// const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); +// const url = window.URL.createObjectURL(blob); +// const a = document.createElement('a'); +// a.style.display = 'none'; +// a.href = url; +// a.download = 'charge_items_template.xlsx'; +// document.body.appendChild(a); +// a.click(); +// window.URL.revokeObjectURL(url); +// document.body.removeChild(a); +// }; diff --git a/packages/esm-billing-app/src/billable-services/billables/form-schemas.tsx b/packages/esm-billing-app/src/billable-services/billables/form-schemas.tsx new file mode 100644 index 0000000..926ceef --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/form-schemas.tsx @@ -0,0 +1,33 @@ +import { z } from 'zod'; + +const ServiceConceptSchema = z.object({ + concept: z.object({ + uuid: z.string(), + display: z.string(), + }), + conceptName: z.object({ + uuid: z.string(), + display: z.string(), + }), + display: z.string(), +}); + +export const servicePriceSchema = z.object({ + price: z.number().gt(0.01, 'Price must be greater than 0').min(0.01, 'Price must be at least 0.01'), + paymentMode: z.object({ uuid: z.string(), name: z.string() }), +}); + +export const billableFormSchema = z.object({ + name: z.string().min(1, 'Service name is required'), + shortName: z + .string() + .refine((value) => value.length !== 1, { message: 'Short name must be at least 1 character long' }), + serviceType: z.object({ uuid: z.string(), display: z.string() }), + servicePrices: z.array(servicePriceSchema).min(1, 'At least one price is required'), + serviceStatus: z.enum(['ENABLED', 'DISABLED']), + concept: ServiceConceptSchema, + stockItem: z.string().optional(), +}); + +export type BillableFormSchema = z.infer; +export type ServicePriceSchema = z.infer; diff --git a/packages/esm-billing-app/src/billable-services/billables/services/concept-search.component.tsx b/packages/esm-billing-app/src/billable-services/billables/services/concept-search.component.tsx new file mode 100644 index 0000000..1c3c124 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/services/concept-search.component.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import styles from './service-form.scss'; +import { useTranslation } from 'react-i18next'; +import { FormGroup, Search, InlineLoading } from '@carbon/react'; +import { ResponsiveWrapper } from '@openmrs/esm-framework'; + +type ConceptSearchProps = { + setConceptToLookup: (value: string) => void; + conceptToLookup: string; + selectedConcept: any; + handleSelectConcept: (concept: any) => void; + errors: any; + isSearching: boolean; + concepts: any; + defaultValues: any; +}; + +const ConceptSearch: React.FC = ({ + selectedConcept, + setConceptToLookup, + conceptToLookup, + defaultValues, + errors, + isSearching, + concepts, + handleSelectConcept, +}) => { + const { t } = useTranslation(); + return ( + + + setConceptToLookup(e.target.value)} + value={ + selectedConcept + ? selectedConcept?.concept?.display + : conceptToLookup ?? defaultValues?.concept?.concept?.display + } + invalid={!!errors.concept} + invalidText={errors?.concept?.message} + /> + + {isSearching && ( +
+ +
+ )} + {concepts && concepts.length > 0 && !isSearching && ( +
+ {concepts.map((concept) => ( +
handleSelectConcept(concept)} + key={concept.concept.uuid} + className={styles.searchItem}> + {concept.concept.display} +
+ ))} +
+ )} +
+ ); +}; + +export default ConceptSearch; diff --git a/packages/esm-billing-app/src/billable-services/billables/services/price.component.tsx b/packages/esm-billing-app/src/billable-services/billables/services/price.component.tsx new file mode 100644 index 0000000..2a60c78 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/services/price.component.tsx @@ -0,0 +1,84 @@ +import React, { useMemo } from 'react'; +import { BillableFormSchema } from '../form-schemas'; +import { Controller, useFormContext, type Control } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { usePaymentModes } from '../../billable-service.resource'; +import styles from './service-form.scss'; +import { ComboBox, NumberInput, IconButton } from '@carbon/react'; +import { TrashCan } from '@carbon/react/icons'; +import { ResponsiveWrapper } from '@openmrs/esm-framework'; + +interface PriceFieldProps { + field: Record; + index: number; + control: Control; + removeServicePrice: (index: number) => void; + errors: Record; +} + +const PriceField: React.FC = ({ field, index, control, removeServicePrice, errors }) => { + const { t } = useTranslation(); + const { paymentModes, isLoading } = usePaymentModes(); + const { watch } = useFormContext(); + const servicePrices = watch('servicePrices'); + // Filter out the payment modes that are already selected + const availablePaymentModes = useMemo( + () => + paymentModes?.filter( + (paymentMode) => !servicePrices?.some((servicePrice) => servicePrice.paymentMode?.uuid === paymentMode.uuid), + ), + [paymentModes, servicePrices], + ); + + return ( +
+ + ( + field.onChange(selectedItem)} + titleText={t('paymentMethodDescription', 'Payment method {{methodName}}', { + methodName: servicePrices[index]?.paymentMode?.name ?? '', + })} + items={availablePaymentModes ?? []} + itemToString={(item) => (item ? item.name : '')} + placeholder={t('selectPaymentMode', 'Select payment mode')} + disabled={isLoading} + initialSelectedItem={field.value} + invalid={!!errors?.servicePrices?.[index]?.paymentMode} + invalidText={errors?.servicePrices?.[index]?.paymentMode?.message} + /> + )} + /> + + + ( + field.onChange(parseFloat(e.target.value))} + type="number" + labelText={t('price', 'Price')} + placeholder={t('enterPrice', 'Enter price')} + defaultValue={field.value} + invalid={!!errors?.servicePrices?.[index]?.price} + invalidText={errors?.servicePrices?.[index]?.price?.message} + /> + )} + /> + + removeServicePrice(index)}> + + +
+ ); +}; + +export default PriceField; diff --git a/packages/esm-billing-app/src/billable-services/billables/services/service-form.scss b/packages/esm-billing-app/src/billable-services/billables/services/service-form.scss new file mode 100644 index 0000000..6a64375 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/services/service-form.scss @@ -0,0 +1,59 @@ +@use '@carbon/colors'; +@use '@carbon/layout'; +@use '@carbon/type'; + +.form { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; +} + +.formContainer { + margin: layout.$spacing-05; +} + +.tablet { + padding: layout.$spacing-06 layout.$spacing-05; + background-color: colors.$white; +} + +.desktop { + padding: 0; +} + +.paymentMethods { + display: flex; + flex-direction: column; + row-gap: layout.$spacing-05; +} + +.searchResults { + position: absolute; + top: 2.625rem; + z-index: 99; + height: 16rem; + padding: 0.5rem; + overflow-y: scroll; + background-color: colors.$white; + border: 1px solid colors.$gray-30; + width: 100%; +} + +.searchItem { + padding: 0.5rem; + color: colors.$gray-70; + cursor: pointer; + @include type.type-style('label-01'); + border-bottom: 1px solid colors.$gray-30; + &:hover { + background-color: colors.$gray-20; + } +} + +.formGroupWithConcept { + position: relative; +} +.formStackControl { + row-gap: layout.$layout-01; +} diff --git a/packages/esm-billing-app/src/billable-services/billables/services/service-form.workspace.tsx b/packages/esm-billing-app/src/billable-services/billables/services/service-form.workspace.tsx new file mode 100644 index 0000000..2020587 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/billables/services/service-form.workspace.tsx @@ -0,0 +1,269 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ButtonSet, + Button, + Stack, + TextInput, + ComboBox, + Toggle, + InlineNotification, + InlineLoading, +} from '@carbon/react'; +import { Add } from '@carbon/react/icons'; +import { Controller, useFieldArray, useForm, FormProvider } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { useLayoutType, useDebounce, ResponsiveWrapper, showSnackbar, restBaseUrl } from '@openmrs/esm-framework'; +import { DefaultPatientWorkspaceProps } from '@openmrs/esm-patient-common-lib'; + +import { createBillableSerice, useConceptsSearch, useServiceTypes } from '../../billable-service.resource'; +import PriceField from './price.component'; +import { billableFormSchema, BillableFormSchema } from '../form-schemas'; + +import classNames from 'classnames'; +import styles from './service-form.scss'; +import { formatBillableServicePayloadForSubmission, mapInputToPayloadSchema } from '../form-helper'; +import ConceptSearch from './concept-search.component'; +import { handleMutate } from '../../utils'; + +interface AddServiceFormProps extends DefaultPatientWorkspaceProps { + initialValues?: BillableFormSchema; +} + +const AddServiceForm: React.FC = ({ + closeWorkspace, + promptBeforeClosing, + closeWorkspaceWithSavedChanges, + initialValues, +}) => { + const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; + const [conceptToLookup, setConceptToLookup] = useState(''); + const debouncedConceptToLookup = useDebounce(conceptToLookup, 500); + const [selectedConcept, setSelectedConcept] = useState(null); + const inEditMode = !!initialValues; + + const { isLoading: isLoadingServiceTypes, serviceTypes } = useServiceTypes(); + const { isSearching, searchResults: concepts } = useConceptsSearch(debouncedConceptToLookup); + const formMethods = useForm({ + resolver: zodResolver(billableFormSchema), + defaultValues: initialValues + ? mapInputToPayloadSchema(initialValues) + : { servicePrices: [], serviceStatus: 'ENABLED' }, + }); + + const { + setValue, + control, + handleSubmit, + formState: { errors, isDirty, defaultValues, isSubmitting }, + } = formMethods; + + useEffect(() => { + if (initialValues) { + setConceptToLookup(initialValues.concept?.concept?.display); + } + }, [initialValues]); + + const { + fields: servicePriceFields, + append: appendServicePrice, + remove: removeServicePrice, + } = useFieldArray({ + control, + name: 'servicePrices', + }); + + const handleSelectConcept = (concept) => { + setSelectedConcept(concept); + setValue('concept', concept); + setConceptToLookup(''); + }; + + useEffect(() => { + promptBeforeClosing(() => isDirty); + }, [isDirty, promptBeforeClosing]); + + const onSubmit = async (data: BillableFormSchema) => { + const formPayload = formatBillableServicePayloadForSubmission(data, initialValues?.['uuid']); + try { + const response = await createBillableSerice(formPayload); + if (response.ok) { + showSnackbar({ + title: inEditMode + ? t('serviceUpdatedSuccessfully', 'Service updated successfully') + : t('serviceCreated', 'Service created successfully'), + kind: 'success', + subtitle: inEditMode + ? t('serviceUpdatedSuccessfully', 'Service updated successfully') + : t('serviceCreatedSuccessfully', 'Service created successfully'), + isLowContrast: true, + timeoutInMs: 5000, + }); + handleMutate(`${restBaseUrl}/cashier/billableService?v`); + + closeWorkspaceWithSavedChanges(); + } + } catch (e) { + showSnackbar({ + title: t('error', 'Error'), + kind: 'error', + subtitle: inEditMode + ? t('serviceUpdateFailed', 'Service failed to update') + : t('serviceCreationFailed', 'Service creation failed'), + isLowContrast: true, + timeoutInMs: 5000, + }); + } + }; + + const renderServicePriceFields = useMemo( + () => + servicePriceFields.map((field, index) => ( + + )), + [servicePriceFields, control, removeServicePrice, errors], + ); + + const handleError = (err) => { + console.error(JSON.stringify(err, null, 2)); + showSnackbar({ + title: t('serviceCreationFailed', 'Service creation failed'), + subtitle: t( + 'serviceCreationFailedSubtitle', + 'The service creation failed, view browser console for more details', + ), + kind: 'error', + isLowContrast: true, + timeoutInMs: 5000, + }); + }; + + return ( + +
+
+ + + ( + + )} + /> + + + ( + + )} + /> + + + + + + { + return ( + field.onChange(selectedItem)} + titleText={t('serviceType', 'Service type')} + items={serviceTypes ?? []} + itemToString={(item) => (item ? item.display : '')} + placeholder={t('selectServiceType', 'Select service type')} + disabled={isLoadingServiceTypes} + initialSelectedItem={field.value} + invalid={!!errors.serviceType} + invalidText={errors?.serviceType?.message} + /> + ); + }} + /> + + + ( + (value ? field.onChange('ENABLED') : field.onChange('DISABLED'))} + /> + )} + /> + + {renderServicePriceFields} + + {!!errors.servicePrices && ( + + )} + +
+ + + + +
+
+ ); +}; + +export default AddServiceForm; diff --git a/packages/esm-billing-app/src/billable-services/clinical-charges.component.tsx b/packages/esm-billing-app/src/billable-services/clinical-charges.component.tsx new file mode 100644 index 0000000..a0b44de --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/clinical-charges.component.tsx @@ -0,0 +1,30 @@ +import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@carbon/react'; +import { Task } from '@carbon/react/icons'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import ChargeSummaryTable from './billables/charge-summary-table.component'; +import styles from './clinical-charges.scss'; + +const ClinicalCharges = () => { + const { t } = useTranslation(); + + return ( + + + + {t('chargeItems', 'Charge Items')} + + + + + + + + + ); +}; + +export default ClinicalCharges; diff --git a/packages/esm-billing-app/src/billable-services/clinical-charges.scss b/packages/esm-billing-app/src/billable-services/clinical-charges.scss new file mode 100644 index 0000000..7fdf296 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/clinical-charges.scss @@ -0,0 +1,255 @@ +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@carbon/styles/scss/spacing'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +.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; +} +.actionsContainer { + display: flex; + align-items: center; +} +.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; +} + +.tabHeader { + min-width: calc(layout.$layout-05 * 5); +} + +.tabPanel { + padding: layout.$spacing-05 0; +} + +.tabWrapper { + display: flex; + justify-content: space-between; + align-items: flex-end; +} + +.iconMarginLeft { + margin-left: layout.$spacing-05; +} + +.csvreq { + margin-bottom: layout.$spacing-05; +} + +.uploadButtonWrapper { + display: flex; + gap: layout.$spacing-05; + align-items: center; + font-size: medium; +} + +.noFile { + font-size: medium; +} diff --git a/packages/esm-billing-app/src/billable-services/dashboard/charge-items-dashboard.component.tsx b/packages/esm-billing-app/src/billable-services/dashboard/charge-items-dashboard.component.tsx new file mode 100644 index 0000000..d10e43a --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/dashboard/charge-items-dashboard.component.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import BillingHeader from "../../billing-header/billing-header.component"; +import ClinicalCharges from "../clinical-charges.component"; +import styles from './dashboard.scss' + + +export const ChargeItemsDashboard = () => { + const { t } = useTranslation(); + + return ( +
+ +
+ +
+
+ ); + }; \ No newline at end of file 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 index 5cb3395..b673cdf 100644 --- a/packages/esm-billing-app/src/billable-services/dashboard/dashboard.component.tsx +++ b/packages/esm-billing-app/src/billable-services/dashboard/dashboard.component.tsx @@ -12,4 +12,4 @@ export default function BillableServicesDashboard() { ); -} +} \ No newline at end of file diff --git a/packages/esm-billing-app/src/billable-services/utils.ts b/packages/esm-billing-app/src/billable-services/utils.ts new file mode 100644 index 0000000..d287a46 --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/utils.ts @@ -0,0 +1,8 @@ +import { mutate } from 'swr'; + +export const handleMutate = (url: string) => { + mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { + revalidate: true, + }); + }; + \ No newline at end of file diff --git a/packages/esm-billing-app/src/index.ts b/packages/esm-billing-app/src/index.ts index 01d0490..0ae6c89 100644 --- a/packages/esm-billing-app/src/index.ts +++ b/packages/esm-billing-app/src/index.ts @@ -38,6 +38,14 @@ export const billingDashboardLink = getSyncLifecycle( options ); +export const chargeableItemsLink = getSyncLifecycle( + createLeftPanelLink({ + name: 'charge-items', + title: 'Charge Items', + }), + options, +); + export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); export function startupApp() { diff --git a/packages/esm-billing-app/src/root.component.tsx b/packages/esm-billing-app/src/root.component.tsx index 9d433e7..f4a1fec 100644 --- a/packages/esm-billing-app/src/root.component.tsx +++ b/packages/esm-billing-app/src/root.component.tsx @@ -8,6 +8,7 @@ import { SideNav } from '@carbon/react'; import { SideNavItems } from '@carbon/react'; import { SideNavLink } from '@carbon/react'; import { useTranslation } from 'react-i18next'; +import { ChargeItemsDashboard } from './billable-services/dashboard/charge-items-dashboard.component'; const RootComponent: React.FC = () => { const basePath = `${window.spaBase}/billing`; @@ -26,16 +27,19 @@ const RootComponent: React.FC = () => { handleNavigation('')} isActive> {t('billingOverview', 'Billing Overview')} + handleNavigation('charge-items')}> + {t('chargeItems', 'Charge Items')} + } /> + } /> } /> ); }; - export default RootComponent; diff --git a/packages/esm-billing-app/src/routes.json b/packages/esm-billing-app/src/routes.json index 03ae5c2..ab6ae69 100644 --- a/packages/esm-billing-app/src/routes.json +++ b/packages/esm-billing-app/src/routes.json @@ -83,6 +83,11 @@ "slot": "billing-home-tiles-slot", "component": "serviceMetrics" }, + { + "component": "chargeableItemsLink", + "name": "chargeable-items-link", + "slot": "billing-dashboard-link-slot" + }, { "name": "drug-order-billable-item", "component": "drugOrder",