-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #88 from Rugute/main
(feat) added billing modules
Showing
107 changed files
with
7,788 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
# ESM Billing App | ||
|
||
This is a frontend module that provides billing functionality. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
export const mockCurrentVisit = { | ||
uuid: 'ee527f74-7373-4494-98bc-002c979971d1', | ||
encounters: [], | ||
patient: { uuid: '0b25b92a-add3-4d52-8491-778bec556e02' }, | ||
visitType: { uuid: '3371a4d4-f66f-4454-a86d-92c7b3da990c', name: 'Outpatient', display: 'Outpatient' }, | ||
attributes: [ | ||
{ | ||
uuid: 'e1522ecb-d027-4c30-aa25-38698ba18020', | ||
display: 'Source form: 13', | ||
attributeType: { | ||
name: 'Source form', | ||
datatypeClassname: 'org.openmrs.module.kenyaemr.datatype.FormDatatype', | ||
uuid: '8bfab185-6947-4958-b7ab-dfafae1a3e3d', | ||
}, | ||
value: { | ||
uuid: '23b4ebbd-29ad-455e-be0e-04aa6bc30798', | ||
display: 'MOH 257 Visit Summary', | ||
name: 'MOH 257 Visit Summary', | ||
description: null, | ||
encounterType: { | ||
uuid: 'a0034eee-1940-4e35-847f-97537a35d05e', | ||
display: 'HIV Consultation', | ||
}, | ||
version: '1', | ||
build: null, | ||
published: true, | ||
formFields: [], | ||
retired: false, | ||
resourceVersion: '1.9', | ||
}, | ||
}, | ||
{ | ||
uuid: '5dbb093d-b377-4684-9583-05074ae7187a', | ||
display: 'Payment Method: 28989582-e8c3-46b0-96d0-c249cb06d5c6', | ||
attributeType: { | ||
name: 'Payment Method', | ||
datatypeClassname: 'org.openmrs.customdatatype.datatype.FreeTextDatatype', | ||
uuid: 'e6cb0c3b-04b0-4117-9bc6-ce24adbda802', | ||
}, | ||
value: '28989582-e8c3-46b0-96d0-c249cb06d5c6', | ||
}, | ||
{ | ||
uuid: '85abc096-8524-4c2e-863c-b44c99c144f7', | ||
display: 'Patient Type: false', | ||
attributeType: { | ||
name: 'Patient Type', | ||
datatypeClassname: 'org.openmrs.customdatatype.datatype.FreeTextDatatype', | ||
uuid: '3b9dfac8-9e4d-11ee-8c90-0242ac120002', | ||
}, | ||
value: 'false', | ||
}, | ||
{ | ||
uuid: '7eb402bb-2bf8-43b7-91aa-ee3f5c753de1', | ||
display: 'Visit queue number: CLI-090', | ||
attributeType: { | ||
name: 'Visit queue number', | ||
datatypeClassname: 'org.openmrs.customdatatype.datatype.FreeTextDatatype', | ||
uuid: 'c61ce16f-272a-41e7-9924-4c555d0932c5', | ||
}, | ||
value: 'CLI-090', | ||
}, | ||
], | ||
location: { | ||
uuid: '233de33e-2778-4f9a-a398-fa09da9daa14', | ||
name: 'Wamagana Health Centre', | ||
display: 'Wamagana Health Centre', | ||
}, | ||
startDatetime: '2024-05-29T15:19:00.000+0300', | ||
stopDatetime: '', | ||
}; | ||
|
||
export const mockBills = [ | ||
{ | ||
id: 1888, | ||
uuid: '3b784fa7-c124-4710-9152-cad3dbaa19e8', | ||
patientName: ' Test Unit patient', | ||
identifier: 'MGTKYE ', | ||
patientUuid: '0b25b92a-add3-4d52-8491-778bec556e02', | ||
status: 'PENDING', | ||
receiptNumber: '1916-6', | ||
cashier: { | ||
uuid: '693acc9b-734f-488d-b6d2-60368d02cec0', | ||
display: '23797304 - Test Patient', | ||
}, | ||
cashPointUuid: '54065383-b4d4-42d2-af4d-d250a1fd2590', | ||
cashPointName: 'OPD Cash Point', | ||
cashPointLocation: 'Moi Teaching Refferal Hospital', | ||
dateCreated: '29 — May — 2024', | ||
lineItems: [ | ||
{ | ||
uuid: '528c3411-b3b8-41dc-bf88-174b4adc1b5b', | ||
display: 'BillLineItem', | ||
voided: false, | ||
voidReason: null, | ||
item: '', | ||
billableService: '3f5d0684-a280-477e-a67b-2a956a1f6dca:Registration Revist', | ||
quantity: 1, | ||
price: 50, | ||
priceName: 'Default', | ||
priceUuid: '', | ||
lineItemOrder: 0, | ||
paymentStatus: 'PENDING', | ||
order: null, | ||
resourceVersion: '1.8', | ||
}, | ||
], | ||
billingService: '3f5d0684-a280-477e-a67b-2a956a1f6dca:Registration Revist', | ||
payments: [], | ||
display: '1916-6', | ||
totalAmount: 50, | ||
}, | ||
{ | ||
id: 1213, | ||
uuid: '5b633220-9a99-4517-bcdf-c06a7d38dd23', | ||
patientName: ' peter ndungu mairo', | ||
identifier: 'MGTKYE ', | ||
patientUuid: '0b25b92a-add3-4d52-8491-778bec556e02', | ||
status: 'PENDING', | ||
receiptNumber: '1228-6', | ||
cashier: { | ||
uuid: '48b55692-e061-4ffa-b1f2-fd4aaf506224', | ||
display: 'admin - Super User', | ||
}, | ||
cashPointUuid: '54065383-b4d4-42d2-af4d-d250a1fd2590', | ||
cashPointName: 'OPD Cash Point', | ||
cashPointLocation: 'Moi Teaching Refferal Hospital', | ||
dateCreated: '15 — May — 2024', | ||
lineItems: [ | ||
{ | ||
uuid: '16cc8b90-f2d4-4907-a3d5-0f57486e5dcf', | ||
display: 'BillLineItem', | ||
voided: false, | ||
voidReason: null, | ||
item: '', | ||
billableService: '3f5d0684-a280-477e-a67b-2a956a1f6dca:Registration Revist', | ||
quantity: 1, | ||
price: 50, | ||
priceName: 'Default', | ||
priceUuid: '', | ||
lineItemOrder: 0, | ||
paymentStatus: 'PENDING', | ||
order: null, | ||
resourceVersion: '1.8', | ||
}, | ||
], | ||
billingService: '3f5d0684-a280-477e-a67b-2a956a1f6dca:Registration Revist', | ||
payments: [], | ||
display: '1228-6', | ||
totalAmount: 50, | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
const rootConfig = require('../../jest.config.js'); | ||
|
||
const packageConfig = { | ||
...rootConfig, | ||
collectCoverage: false, | ||
}; | ||
|
||
module.exports = packageConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
{ | ||
"name": "@ampath/esm-billing-app", | ||
"version": "5.1.1", | ||
"description": "Billing app for AMRS", | ||
"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", | ||
"scripts": { | ||
"start": "openmrs develop", | ||
"serve": "webpack serve --mode=development", | ||
"debug": "npm run serve", | ||
"build": "webpack --mode production", | ||
"analyze": "webpack --mode=production --env.analyze=true", | ||
"lint": "eslint src --ext ts,tsx", | ||
"typescript": "tsc", | ||
"extract-translations": "i18next 'src/**/*.component.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js", | ||
"test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests", | ||
"test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js", | ||
"coverage": "yarn test --coverage" | ||
}, | ||
"browserslist": [ | ||
"extends browserslist-config-openmrs" | ||
], | ||
"keywords": [ | ||
"openmrs" | ||
], | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/AMPATH/ampath-esm-3.x.git" | ||
}, | ||
"bugs": { | ||
"url": "git+https://github.com/AMPATH/ampath-esm-3.x.git" | ||
}, | ||
"dependencies": { | ||
"@carbon/react": "^1.42.1", | ||
"lodash-es": "^4.17.15", | ||
"react-to-print": "^2.14.13" | ||
}, | ||
"peerDependencies": { | ||
"@openmrs/esm-framework": "5.x", | ||
"react": "^18.1.0", | ||
"react-i18next": "11.x", | ||
"react-router-dom": "6.x", | ||
"swr": "2.x" | ||
}, | ||
"devDependencies": { | ||
"webpack": "^5.74.0" | ||
} | ||
} |
202 changes: 202 additions & 0 deletions
202
packages/esm-billing-app/src/bill-history/bill-history.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import React from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { | ||
DataTableSkeleton, | ||
Layer, | ||
DataTable, | ||
TableContainer, | ||
Table, | ||
TableHead, | ||
TableHeader, | ||
TableRow, | ||
TableBody, | ||
TableCell, | ||
Tile, | ||
Pagination, | ||
TableExpandHeader, | ||
TableExpandRow, | ||
TableExpandedRow, | ||
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 { useBills } from '../billing.resource'; | ||
import InvoiceTable from '../invoice/invoice-table.component'; | ||
import styles from './bill-history.scss'; | ||
|
||
interface BillHistoryProps { | ||
patientUuid: string; | ||
} | ||
|
||
const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => { | ||
const { t } = useTranslation(); | ||
const { bills, isLoading, error } = useBills(patientUuid); | ||
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 headerData = [ | ||
{ | ||
header: t('visitTime', 'Visit time'), | ||
key: 'visitTime', | ||
}, | ||
{ | ||
header: t('identifier', 'Identifier'), | ||
key: 'identifier', | ||
}, | ||
{ | ||
header: t('billedItems', 'Billed Items'), | ||
key: 'billedItems', | ||
}, | ||
{ | ||
header: t('billTotal', 'Bill total'), | ||
key: 'billTotal', | ||
}, | ||
]; | ||
|
||
const setBilledItems = (bill) => | ||
bill.lineItems.reduce( | ||
(acc, item) => acc + (acc ? ' & ' : '') + (item.billableService?.split(':')[1] || item.item?.split(':')[1] || ''), | ||
'', | ||
); | ||
|
||
const rowData = results?.map((bill) => ({ | ||
id: bill.uuid, | ||
uuid: bill.uuid, | ||
billTotal: bill.totalAmount, | ||
visitTime: bill.dateCreated, | ||
identifier: bill.identifier, | ||
billedItems: setBilledItems(bill), | ||
})); | ||
|
||
if (isLoading) { | ||
return ( | ||
<div className={styles.loaderContainer}> | ||
<DataTableSkeleton showHeader={false} showToolbar={false} zebra size={responsiveSize} /> | ||
</div> | ||
); | ||
} | ||
|
||
if (error) { | ||
return ( | ||
<div className={styles.errorContainer}> | ||
<Layer> | ||
<ErrorState error={error} headerTitle={t('billsList', 'Bill list')} /> | ||
</Layer> | ||
</div> | ||
); | ||
} | ||
|
||
if (bills.length === 0) { | ||
return ( | ||
<> | ||
<CardHeader title={t('patientBilling', 'Patient billing')}> | ||
<></> | ||
</CardHeader> | ||
<Layer> | ||
<Tile className={styles.tile}> | ||
<div className={styles.illo}> | ||
<EmptyDataIllustration /> | ||
</div> | ||
<p className={styles.content}>There are no bills to display.</p> | ||
<Button onClick={() => launchWorkspace('billing-form', { workspaceTitle: 'Billing Form' })} kind="ghost"> | ||
{t('launchBillForm', 'Launch bill form')} | ||
</Button> | ||
</Tile> | ||
</Layer> | ||
</> | ||
); | ||
} | ||
|
||
return ( | ||
<div> | ||
<CardHeader title={t('patientBilling', 'Patient billing')}> | ||
<Button | ||
renderIcon={Add} | ||
onClick={() => launchWorkspace('billing-form', { workspaceTitle: 'Billing Form' })} | ||
kind="ghost"> | ||
{t('addBill', 'Add bill item(s)')} | ||
</Button> | ||
</CardHeader> | ||
<div className={styles.billHistoryContainer}> | ||
<DataTable isSortable rows={rowData} headers={headerData} size={responsiveSize} useZebraStyles> | ||
{({ | ||
rows, | ||
headers, | ||
getExpandHeaderProps, | ||
getTableProps, | ||
getTableContainerProps, | ||
getHeaderProps, | ||
getRowProps, | ||
}) => ( | ||
<TableContainer {...getTableContainerProps}> | ||
<Table className={styles.table} {...getTableProps()} aria-label="Bill list"> | ||
<TableHead> | ||
<TableRow> | ||
<TableExpandHeader enableToggle {...getExpandHeaderProps()} /> | ||
{headers.map((header, i) => ( | ||
<TableHeader | ||
key={i} | ||
{...getHeaderProps({ | ||
header, | ||
})}> | ||
{header.header} | ||
</TableHeader> | ||
))} | ||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
{rows.map((row, i) => { | ||
const currentBill = bills?.find((bill) => bill.uuid === row.id); | ||
|
||
return ( | ||
<React.Fragment key={row.id}> | ||
<TableExpandRow {...getRowProps({ row })}> | ||
{row.cells.map((cell) => ( | ||
<TableCell key={cell.id}>{cell.value}</TableCell> | ||
))} | ||
</TableExpandRow> | ||
{row.isExpanded ? ( | ||
<TableExpandedRow className={styles.expandedRow} colSpan={headers.length + 1}> | ||
<div className={styles.container} key={i}> | ||
<InvoiceTable bill={currentBill} isSelectable={false} /> | ||
</div> | ||
</TableExpandedRow> | ||
) : ( | ||
<TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} /> | ||
)} | ||
</React.Fragment> | ||
); | ||
})} | ||
</TableBody> | ||
</Table> | ||
</TableContainer> | ||
)} | ||
</DataTable> | ||
{paginated && ( | ||
<Pagination | ||
forwardText={t('nextPage', 'Next page')} | ||
backwardText={t('previousPage', 'Previous page')} | ||
page={currentPage} | ||
pageSize={pageSize} | ||
pageSizes={pageSizes} | ||
totalItems={bills.length} | ||
className={styles.pagination} | ||
size={responsiveSize} | ||
onChange={({ page: newPage, pageSize }) => { | ||
if (newPage !== currentPage) { | ||
goTo(newPage); | ||
} | ||
setPageSize(pageSize); | ||
}} | ||
/> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default BillHistory; |
165 changes: 165 additions & 0 deletions
165
packages/esm-billing-app/src/bill-history/bill-history.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
@use '@carbon/layout'; | ||
@use '@carbon/type'; | ||
@import '~@openmrs/esm-styleguide/src/vars'; | ||
|
||
.container { | ||
margin: 2rem 0; | ||
} | ||
|
||
.billHistoryContainer { | ||
background-color: $ui-02; | ||
border: 1px solid $ui-03; | ||
border-bottom: none; | ||
width: 100%; | ||
margin: 0 auto; | ||
max-width: 95vw; | ||
padding-bottom: 0; | ||
} | ||
|
||
.headerContainer { | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
padding: layout.$spacing-04 0 layout.$spacing-04 layout.$spacing-05; | ||
background-color: $ui-02; | ||
} | ||
|
||
.backgroundDataFetchingIndicator { | ||
align-items: center; | ||
display: flex; | ||
flex: 1 1 0%; | ||
justify-content: center; | ||
} | ||
|
||
.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; | ||
|
||
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; | ||
} | ||
|
||
.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; | ||
} | ||
|
||
.table { | ||
tr[data-child-row] td { | ||
padding-left: 2rem !important; | ||
} | ||
} | ||
|
||
.billingHeading { | ||
position: relative; | ||
max-width: 400px; | ||
text-align: 'left'; | ||
margin-bottom: 40px; | ||
} | ||
|
||
.billingHeading::after { | ||
position: absolute; | ||
bottom: -10px; | ||
width: 50px; | ||
height: 5px; | ||
background: green; | ||
content: ''; | ||
display: block; | ||
left: 6%; | ||
transform: translatex(-50%); | ||
} |
24 changes: 24 additions & 0 deletions
24
packages/esm-billing-app/src/billable-services-admin-card-link.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import React from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Layer, ClickableTile } from '@carbon/react'; | ||
import { ArrowRight } from '@carbon/react/icons'; | ||
|
||
const BillableServicesCardLink: React.FC = () => { | ||
const { t } = useTranslation(); | ||
const header = t('manageBillableServices', 'Manage billable services'); | ||
return ( | ||
<Layer> | ||
<ClickableTile href={`${window.spaBase}/billable-services`} target="_blank" rel="noopener noreferrer"> | ||
<div> | ||
<div className="heading">{header}</div> | ||
<div className="content">{t('billableServices', 'Billable Services')}</div> | ||
</div> | ||
<div className="iconWrapper"> | ||
<ArrowRight size={16} /> | ||
</div> | ||
</ClickableTile> | ||
</Layer> | ||
); | ||
}; | ||
|
||
export default BillableServicesCardLink; |
74 changes: 74 additions & 0 deletions
74
packages/esm-billing-app/src/billable-services/bill-waiver/bill-selection.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import React from 'react'; | ||
import { | ||
StructuredListHead, | ||
StructuredListRow, | ||
StructuredListCell, | ||
StructuredListBody, | ||
StructuredListWrapper, | ||
Layer, | ||
Checkbox, | ||
} from '@carbon/react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { convertToCurrency, extractString } from '../../helpers'; | ||
import { type MappedBill, type LineItem } from '../../types'; | ||
import styles from './bill-waiver.scss'; | ||
import BillWaiverForm from './bill-waiver-form.component'; | ||
|
||
const PatientBillsSelections: React.FC<{ bills: MappedBill; setPatientUuid: (patientUuid) => void }> = ({ | ||
bills, | ||
setPatientUuid, | ||
}) => { | ||
const { t } = useTranslation(); | ||
const [selectedBills, setSelectedBills] = React.useState<Array<LineItem>>([]); | ||
|
||
const checkBoxLabel = (lineItem) => { | ||
return `${lineItem.item === '' ? lineItem.billableService : lineItem.item} ${convertToCurrency(lineItem.price)}`; | ||
}; | ||
|
||
const handleOnCheckBoxChange = (event, { checked, id }) => { | ||
const selectedLineItem = bills.lineItems.find((lineItem) => lineItem.uuid === id); | ||
if (checked) { | ||
setSelectedBills([...selectedBills, selectedLineItem]); | ||
} else { | ||
setSelectedBills(selectedBills.filter((lineItem) => lineItem.uuid !== id)); | ||
} | ||
}; | ||
return ( | ||
<Layer> | ||
<StructuredListWrapper className={styles.billListContainer} isCondensed selection={true}> | ||
<StructuredListHead> | ||
<StructuredListRow head> | ||
<StructuredListCell head>{t('billItem', 'Bill item')}</StructuredListCell> | ||
<StructuredListCell head>{t('quantity', 'Quantity')}</StructuredListCell> | ||
<StructuredListCell head>{t('unitPrice', 'Unit Price')}</StructuredListCell> | ||
<StructuredListCell head>{t('total', 'Total')}</StructuredListCell> | ||
<StructuredListCell head>{t('actions', 'Actions')}</StructuredListCell> | ||
</StructuredListRow> | ||
</StructuredListHead> | ||
<StructuredListBody> | ||
{bills?.lineItems.map((lineItem) => ( | ||
<StructuredListRow> | ||
<StructuredListCell> | ||
{lineItem.item === '' ? extractString(lineItem.billableService) : extractString(lineItem.item)} | ||
</StructuredListCell> | ||
<StructuredListCell>{lineItem.quantity}</StructuredListCell> | ||
<StructuredListCell>{convertToCurrency(lineItem.price)}</StructuredListCell> | ||
<StructuredListCell>{convertToCurrency(lineItem.price * lineItem.quantity)}</StructuredListCell> | ||
<StructuredListCell> | ||
<Checkbox | ||
hideLabel | ||
onChange={(event, { checked, id }) => handleOnCheckBoxChange(event, { checked, id })} | ||
labelText={checkBoxLabel(lineItem)} | ||
id={lineItem.uuid} | ||
/> | ||
</StructuredListCell> | ||
</StructuredListRow> | ||
))} | ||
</StructuredListBody> | ||
</StructuredListWrapper> | ||
<BillWaiverForm bill={bills} lineItems={selectedBills} setPatientUuid={setPatientUuid} /> | ||
</Layer> | ||
); | ||
}; | ||
|
||
export default PatientBillsSelections; |
105 changes: 105 additions & 0 deletions
105
packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver-form.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import React, { useState } from 'react'; | ||
import { Form, Stack, FormGroup, Layer, Button, NumberInput } from '@carbon/react'; | ||
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 { convertToCurrency, extractString } from '../../helpers'; | ||
import { processBillPayment, usePaymentModes } from '../../billing.resource'; | ||
import { showSnackbar } from '@openmrs/esm-framework'; | ||
import { mutate } from 'swr'; | ||
|
||
type BillWaiverFormProps = { | ||
bill: MappedBill; | ||
lineItems: Array<LineItem>; | ||
setPatientUuid: (patientUuid) => void; | ||
}; | ||
|
||
const BillWaiverForm: React.FC<BillWaiverFormProps> = ({ bill, lineItems, setPatientUuid }) => { | ||
const { t } = useTranslation(); | ||
const [waiverAmount, setWaiverAmount] = useState(0); | ||
|
||
const totalAmount = lineItems.reduce((acc, curr) => acc + curr.price * curr.quantity, 0); | ||
const { paymentModes } = usePaymentModes(false); | ||
|
||
if (lineItems?.length === 0) { | ||
return null; | ||
} | ||
|
||
const handleProcessPayment = () => { | ||
const waiverEndPointPayload = createBillWaiverPayload(bill, waiverAmount, totalAmount, lineItems, paymentModes); | ||
|
||
processBillPayment(waiverEndPointPayload, bill.uuid).then( | ||
(resp) => { | ||
showSnackbar({ | ||
title: t('billWaiver', 'Bill waiver'), | ||
subtitle: t('billWaiverSuccess', 'Bill waiver successful'), | ||
kind: 'success', | ||
timeoutInMs: 3500, | ||
isLowContrast: true, | ||
}); | ||
setPatientUuid(''); | ||
mutate((key) => typeof key === 'string' && key.startsWith('/ws/rest/v1/cashier/bill?v=full'), undefined, { | ||
revalidate: true, | ||
}); | ||
}, | ||
(error) => { | ||
showSnackbar({ | ||
title: t('billWaiver', 'Bill waiver'), | ||
subtitle: t('billWaiverError', 'Bill waiver failed {{error}}', { | ||
error: error?.responseBody?.error?.message ?? error.message, | ||
}), | ||
kind: 'error', | ||
timeoutInMs: 3500, | ||
isLowContrast: true, | ||
}); | ||
}, | ||
); | ||
}; | ||
|
||
return ( | ||
<Form className={styles.billWaiverForm} aria-label={t('waiverForm', 'Waiver form')}> | ||
<hr /> | ||
<Stack gap={7}> | ||
<FormGroup legendText={t('waiverForm', 'Waiver form')}> | ||
<section className={styles.billWaiverDescription}> | ||
<label className={styles.label}>{t('billItems', 'Bill Items')}</label> | ||
<p className={styles.value}> | ||
{t('billName', ' {{billName}} ', { | ||
billName: lineItems.map((item) => extractString(item.item || item.billableService)).join(', ') ?? '--', | ||
})} | ||
</p> | ||
</section> | ||
<section className={styles.billWaiverDescription}> | ||
<label className={styles.label}>{t('billTotal', 'Bill total')}</label> | ||
<p className={styles.value}>{convertToCurrency(totalAmount)}</p> | ||
</section> | ||
|
||
<Layer className={styles.formControlLayer}> | ||
<NumberInput | ||
id="waiverAmount" | ||
label={t('amountToWaiveLabel', 'Amount to Waive')} | ||
helperText={t('amountToWaiveHelper', 'Specify the amount to be deducted from the bill')} | ||
aria-label={t('amountToWaiveAriaLabel', 'Enter amount to waive')} | ||
hideSteppers | ||
disableWheel | ||
min={0} | ||
max={totalAmount} | ||
invalidText={t('invalidWaiverAmountMessage', 'Amount to waive cannot be greater than total amount')} | ||
value={waiverAmount} | ||
onChange={(event) => setWaiverAmount(event.target.value)} | ||
/> | ||
</Layer> | ||
</FormGroup> | ||
<div className={styles.buttonContainer}> | ||
<Button kind="tertiary" renderIcon={TaskAdd} onClick={handleProcessPayment}> | ||
{t('postWaiver', 'Post waiver')} | ||
</Button> | ||
</div> | ||
</Stack> | ||
</Form> | ||
); | ||
}; | ||
|
||
export default BillWaiverForm; |
34 changes: 34 additions & 0 deletions
34
packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver-form.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
@use '@carbon/layout'; | ||
@use '@carbon/type'; | ||
|
||
.billWaiverForm { | ||
margin-top: layout.$spacing-05; | ||
padding: 0 layout.$spacing-05; | ||
} | ||
|
||
.buttonContainer { | ||
display: flex; | ||
justify-content: flex-end; | ||
margin-top: -1rem; | ||
} | ||
|
||
.formControlLayer { | ||
padding: layout.$spacing-05 0; | ||
} | ||
|
||
.billWaiverDescription { | ||
display: grid; | ||
grid-template-columns: 5rem 1fr; | ||
column-gap: 1rem; | ||
align-items: center; | ||
margin-top: 0.125rem; | ||
margin-bottom: 0.5rem; | ||
} | ||
|
||
.label { | ||
@include type.type-style('heading-compact-01'); | ||
} | ||
|
||
.value { | ||
@include type.type-style('body-01'); | ||
} |
33 changes: 33 additions & 0 deletions
33
packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import React from 'react'; | ||
import { ExtensionSlot, UserHasAccess } from '@openmrs/esm-framework'; | ||
import PatientBills from './patient-bills.component'; | ||
import styles from './bill-waiver.scss'; | ||
import BillWaiverForm from './bill-waiver-form.component'; | ||
import { useBills } from '../../billing.resource'; | ||
|
||
type BillWaiverProps = {}; | ||
|
||
const BillWaiver: React.FC<BillWaiverProps> = () => { | ||
const [patientUuid, setPatientUuid] = React.useState<string>(''); | ||
const { bills } = useBills(patientUuid); | ||
const filterBills = bills.filter((bill) => bill.status !== 'PAID' && patientUuid === bill.patientUuid) ?? []; | ||
return ( | ||
<UserHasAccess privilege="coreapps.systemAdministration"> | ||
<div className={styles.billWaiverContainer}> | ||
<ExtensionSlot | ||
name="patient-search-bar-slot" | ||
state={{ | ||
selectPatientAction: (patientUuid) => setPatientUuid(patientUuid), | ||
buttonProps: { | ||
kind: 'primary', | ||
}, | ||
}} | ||
/> | ||
|
||
<PatientBills patientUuid={patientUuid} bills={filterBills} setPatientUuid={setPatientUuid} /> | ||
</div> | ||
</UserHasAccess> | ||
); | ||
}; | ||
|
||
export default BillWaiver; |
10 changes: 10 additions & 0 deletions
10
packages/esm-billing-app/src/billable-services/bill-waiver/bill-waiver.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
@use '@carbon/layout'; | ||
|
||
.billWaiverContainer { | ||
margin: layout.$layout-01; | ||
row-gap: layout.$layout-01; | ||
} | ||
|
||
.billListContainer { | ||
background-color: white; | ||
} |
136 changes: 136 additions & 0 deletions
136
packages/esm-billing-app/src/billable-services/bill-waiver/patient-bills.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import React from 'react'; | ||
import { useBills } from '../../billing.resource'; | ||
import { | ||
Layer, | ||
DataTable, | ||
TableContainer, | ||
Table, | ||
TableHead, | ||
TableRow, | ||
TableExpandHeader, | ||
TableHeader, | ||
TableBody, | ||
TableExpandRow, | ||
TableCell, | ||
TableExpandedRow, | ||
Tile, | ||
} from '@carbon/react'; | ||
import { convertToCurrency, extractString } from '../../helpers'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib'; | ||
import PatientBillsSelections from './bill-selection.component'; | ||
import { type MappedBill } from '../../types'; | ||
import styles from '../../bills-table/bills-table.scss'; | ||
|
||
type PatientBillsProps = { | ||
patientUuid: string; | ||
bills: Array<MappedBill>; | ||
setPatientUuid: (patientUuid: string) => void; | ||
}; | ||
|
||
const PatientBills: React.FC<PatientBillsProps> = ({ patientUuid, bills, setPatientUuid }) => { | ||
const { t } = useTranslation(); | ||
|
||
if (!patientUuid) { | ||
return; | ||
} | ||
|
||
const tableHeaders = [ | ||
{ header: 'Date', key: 'date' }, | ||
{ header: 'Billable Service', key: 'billableService' }, | ||
{ header: 'Total Amount', key: 'totalAmount' }, | ||
]; | ||
|
||
const tableRows = bills.map((bill) => ({ | ||
id: `${bill.uuid}`, | ||
date: bill.dateCreated, | ||
billableService: extractString(bill.billingService), | ||
totalAmount: convertToCurrency(bill.totalAmount), | ||
})); | ||
|
||
if (bills.length === 0 && patientUuid !== '') { | ||
return ( | ||
<> | ||
<div style={{ marginTop: '0.625rem' }}> | ||
<Layer className={styles.emptyStateContainer}> | ||
<Tile className={styles.tile}> | ||
<div className={styles.illo}> | ||
<EmptyDataIllustration /> | ||
</div> | ||
<p className={styles.content}>{t('noBilltoDisplay', 'There are no bills to display for this patient')}</p> | ||
</Tile> | ||
</Layer> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
return ( | ||
<div style={{ marginTop: '1rem' }}> | ||
<DataTable | ||
rows={tableRows} | ||
headers={tableHeaders} | ||
size="sm" | ||
useZebraStyles | ||
render={({ | ||
rows, | ||
headers, | ||
getHeaderProps, | ||
getExpandHeaderProps, | ||
getRowProps, | ||
getExpandedRowProps, | ||
getTableProps, | ||
getTableContainerProps, | ||
}) => ( | ||
<TableContainer | ||
title={t('patientBills', 'Patient bill')} | ||
description={t('patientBillsDescription', 'List of patient bills')} | ||
{...getTableContainerProps()}> | ||
<Table {...getTableProps()} aria-label="sample table"> | ||
<TableHead> | ||
<TableRow> | ||
<TableExpandHeader enableToggle={true} {...getExpandHeaderProps()} /> | ||
{headers.map((header, i) => ( | ||
<TableHeader | ||
key={i} | ||
{...getHeaderProps({ | ||
header, | ||
})}> | ||
{header.header} | ||
</TableHeader> | ||
))} | ||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
{rows.map((row, index) => ( | ||
<React.Fragment key={row.id}> | ||
<TableExpandRow | ||
{...getRowProps({ | ||
row, | ||
})}> | ||
{row.cells.map((cell) => ( | ||
<TableCell key={cell.id}>{cell.value}</TableCell> | ||
))} | ||
</TableExpandRow> | ||
<TableExpandedRow | ||
colSpan={headers.length + 1} | ||
className="demo-expanded-td" | ||
{...getExpandedRowProps({ | ||
row, | ||
})}> | ||
<div> | ||
<PatientBillsSelections bills={bills[index]} setPatientUuid={setPatientUuid} /> | ||
</div> | ||
</TableExpandedRow> | ||
</React.Fragment> | ||
))} | ||
</TableBody> | ||
</Table> | ||
</TableContainer> | ||
)} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default PatientBills; |
38 changes: 38 additions & 0 deletions
38
packages/esm-billing-app/src/billable-services/bill-waiver/utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
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<LineItem>, | ||
paymentModes: Array<OpenmrsResource>, | ||
) => { | ||
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]; |
51 changes: 51 additions & 0 deletions
51
packages/esm-billing-app/src/billable-services/billable-service.resource.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { type OpenmrsResource, openmrsFetch } from '@openmrs/esm-framework'; | ||
import useSWR from 'swr'; | ||
import { type ServiceConcept } from '../types'; | ||
|
||
type ResponseObject = { | ||
results: Array<OpenmrsResource>; | ||
}; | ||
|
||
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: any }>(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<ServiceConcept> } }, Error>( | ||
conceptToLookup ? conditionsSearchUrl : null, | ||
openmrsFetch, | ||
); | ||
|
||
return { | ||
searchResults: data?.data?.results ?? [], | ||
error: error, | ||
isSearching: isLoading, | ||
}; | ||
} |
50 changes: 50 additions & 0 deletions
50
packages/esm-billing-app/src/billable-services/billable-services-home.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React from 'react'; | ||
import { BrowserRouter, Routes, Route } from 'react-router-dom'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { BillableServicesDashboard } from './dashboard/dashboard.component'; | ||
import AddBillableService from './create-edit/add-billable-service.component'; | ||
import { SideNav, SideNavItems, SideNavLink } from '@carbon/react'; | ||
import styles from './billable-services.scss'; | ||
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'; | ||
const basePath = `${window.spaBase}/billable-services`; | ||
const BillableServiceHome: React.FC = () => { | ||
const { t } = useTranslation(); | ||
|
||
const handleNavigation = (path: string) => { | ||
navigate({ to: `${basePath}/${path}` }); | ||
}; | ||
|
||
return ( | ||
<BrowserRouter basename={`${window.spaBase}/billable-services`}> | ||
<main className={styles.mainSection}> | ||
<section> | ||
<SideNav> | ||
<SideNavItems> | ||
<SideNavLink onClick={() => handleNavigation('')} renderIcon={Wallet} isActive> | ||
{t('billableServices', 'Billable Services')} | ||
</SideNavLink> | ||
<UserHasAccess privilege="coreapps.systemAdministration"> | ||
<SideNavLink onClick={() => handleNavigation('waive-bill')} renderIcon={Money}> | ||
{t('billWaiver', 'Bill waiver')} | ||
</SideNavLink> | ||
</UserHasAccess> | ||
</SideNavItems> | ||
</SideNav> | ||
</section> | ||
<section> | ||
<BillingHeader title={t('billServicesManagement', 'Bill services management')} /> | ||
<Routes> | ||
<Route path="/" element={<BillableServicesDashboard />} /> | ||
<Route path="/add-service" element={<AddBillableService />} /> | ||
<Route path="/waive-bill" element={<BillWaiver />} /> | ||
</Routes> | ||
</section> | ||
</main> | ||
</BrowserRouter> | ||
); | ||
}; | ||
|
||
export default BillableServiceHome; |
255 changes: 255 additions & 0 deletions
255
packages/esm-billing-app/src/billable-services/billable-services.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
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 './billable-services.scss'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { useBillableServices } from './billable-service.resource'; | ||
import { ArrowRight } from '@carbon/react/icons'; | ||
|
||
const BillableServices = () => { | ||
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 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('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: '--', | ||
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) { | ||
<InlineLoading status="active" iconDescription="Loading" description="Loading data..." />; | ||
} | ||
if (error) { | ||
<ErrorState headerTitle={t('billableService', 'Billable Service')} error={error} />; | ||
} | ||
if (billableServices.length === 0) { | ||
<EmptyState | ||
displayText={t('billableService', 'Billable Service')} | ||
headerTitle={t('billableService', 'Billable Service')} | ||
launchForm={launchBillableServiceForm} | ||
/>; | ||
} | ||
|
||
return ( | ||
<> | ||
{billableServices?.length > 0 ? ( | ||
<div className={styles.serviceContainer}> | ||
<FilterableTableHeader | ||
handleSearch={handleSearch} | ||
isValidating={isValidating} | ||
layout={layout} | ||
responsiveSize={responsiveSize} | ||
t={t} | ||
/> | ||
<DataTable | ||
isSortable | ||
rows={rowData} | ||
headers={headerData} | ||
size={responsiveSize} | ||
useZebraStyles={rowData?.length > 1 ? true : false}> | ||
{({ rows, headers, getRowProps, getTableProps }) => ( | ||
<TableContainer> | ||
<Table {...getTableProps()} aria-label="service list"> | ||
<TableHead> | ||
<TableRow> | ||
{headers.map((header) => ( | ||
<TableHeader key={header.key}>{header.header}</TableHeader> | ||
))} | ||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
{rows.map((row) => ( | ||
<TableRow | ||
key={row.id} | ||
{...getRowProps({ | ||
row, | ||
})}> | ||
{row.cells.map((cell) => ( | ||
<TableCell key={cell.id}>{cell.value}</TableCell> | ||
))} | ||
</TableRow> | ||
))} | ||
</TableBody> | ||
</Table> | ||
</TableContainer> | ||
)} | ||
</DataTable> | ||
{searchResults?.length === 0 && ( | ||
<div className={styles.filterEmptyState}> | ||
<Layer level={0}> | ||
<Tile className={styles.filterEmptyStateTile}> | ||
<p className={styles.filterEmptyStateContent}> | ||
{t('noMatchingServicesToDisplay', 'No matching services to display')} | ||
</p> | ||
<p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p> | ||
</Tile> | ||
</Layer> | ||
</div> | ||
)} | ||
{paginated && ( | ||
<Pagination | ||
forwardText="Next page" | ||
backwardText="Previous page" | ||
page={currentPage} | ||
pageSize={pageSize} | ||
pageSizes={pageSizes} | ||
totalItems={searchResults?.length} | ||
className={styles.pagination} | ||
size={responsiveSize} | ||
onChange={({ pageSize: newPageSize, page: newPage }) => { | ||
if (newPageSize !== pageSize) { | ||
setPageSize(newPageSize); | ||
} | ||
if (newPage !== currentPage) { | ||
goTo(newPage); | ||
} | ||
}} | ||
/> | ||
)} | ||
</div> | ||
) : ( | ||
<EmptyState | ||
launchForm={launchBillableServiceForm} | ||
displayText={t('noServicesToDisplay', 'There are no services to display')} | ||
headerTitle={t('billableService', 'Billable service')} | ||
/> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
function FilterableTableHeader({ layout, handleSearch, isValidating, responsiveSize, t }) { | ||
return ( | ||
<> | ||
<div className={styles.headerContainer}> | ||
<div | ||
className={classNames({ | ||
[styles.tabletHeading]: !isDesktop(layout), | ||
[styles.desktopHeading]: isDesktop(layout), | ||
})}> | ||
<h4>{t('servicesList', 'Services list')}</h4> | ||
</div> | ||
<div className={styles.backgroundDataFetchingIndicator}> | ||
<span>{isValidating ? <InlineLoading /> : null}</span> | ||
</div> | ||
</div> | ||
<div className={styles.actionsContainer}> | ||
<Search | ||
labelText="" | ||
placeholder={t('filterTable', 'Filter table')} | ||
onChange={handleSearch} | ||
size={responsiveSize} | ||
/> | ||
<Button | ||
size={responsiveSize} | ||
kind="primary" | ||
renderIcon={(props) => <ArrowRight size={16} {...props} />} | ||
onClick={() => { | ||
navigate({ to: window.getOpenmrsSpaBase() + 'billable-services/add-service' }); | ||
}} | ||
iconDescription={t('addNewBillableService', 'Add new billable service')}> | ||
{t('addNewService', 'Add new service')} | ||
</Button> | ||
</div> | ||
</> | ||
); | ||
} | ||
export default BillableServices; |
219 changes: 219 additions & 0 deletions
219
packages/esm-billing-app/src/billable-services/billable-services.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
56 changes: 56 additions & 0 deletions
56
packages/esm-billing-app/src/billable-services/billiable-item/drug-order.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
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 { useTranslation } from 'react-i18next'; | ||
import styles from './drug-order.scss'; | ||
import { convertToCurrency } from '../../helpers'; | ||
|
||
type DrugOrderProps = { | ||
order: { | ||
drug: Drug; | ||
unit: DosingUnit; | ||
commonMedicationName: string; | ||
dosage: number; | ||
frequency: MedicationFrequency; | ||
route: MedicationRoute; | ||
quantityUnits: QuantityUnit; | ||
patientInstructions: string; | ||
asNeeded: boolean; | ||
asNeededCondition: string; | ||
}; | ||
}; | ||
|
||
const DrugOrder: React.FC<DrugOrderProps> = ({ 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 ( | ||
<div className={styles.drugOrderContainer}> | ||
{stockItem && ( | ||
<div className={styles.itemContainer}> | ||
<span className={styles.bold}> | ||
{t('inStock', '{{quantityUoM}}(s) In stock ', { quantityUoM: stockItem?.quantityUoM })} | ||
</span> | ||
<span>{Math.round(stockItem?.quantity)}</span> | ||
</div> | ||
)} | ||
<div> | ||
{billableItem && | ||
billableItem?.servicePrices.map((item) => ( | ||
<div key={item.uuid} className={styles.itemContainer}> | ||
<span className={styles.bold}>{t('unitPrice', 'Unit price ')}</span> | ||
<span>{convertToCurrency(item.price)}</span> | ||
</div> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default DrugOrder; |
26 changes: 26 additions & 0 deletions
26
packages/esm-billing-app/src/billable-services/billiable-item/drug-order.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
@use '@carbon/type'; | ||
@use '@carbon/colors'; | ||
@use '@carbon/layout'; | ||
@use '@carbon/styles/scss/spacing'; | ||
|
||
.drugOrderContainer { | ||
margin: spacing.$spacing-03 0; | ||
|
||
& > div { | ||
margin: spacing.$spacing-03 0; | ||
} | ||
} | ||
|
||
.bold { | ||
font-weight: bold; | ||
} | ||
|
||
.itemInfo { | ||
@include type.type-style('body-01'); | ||
} | ||
|
||
.itemContainer { | ||
display: grid; | ||
grid-template-columns: 1fr 1fr; | ||
padding-left: spacing.$spacing-03; | ||
} |
34 changes: 34 additions & 0 deletions
34
packages/esm-billing-app/src/billable-services/billiable-item/lab-order.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import React from 'react'; | ||
import { convertToCurrency } from '../../helpers'; | ||
import { useBillableItem } from './useBilliableItem'; | ||
|
||
type LabOrderProps = { | ||
order: { | ||
testType?: { | ||
label: string; | ||
conceptUuid: string; | ||
}; | ||
}; | ||
}; | ||
|
||
const LabOrder: React.FC<LabOrderProps> = ({ 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 <p>{billItems}</p>; | ||
}; | ||
|
||
export default LabOrder; |
50 changes: 50 additions & 0 deletions
50
packages/esm-billing-app/src/billable-services/billiable-item/useBilliableItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import useSWRImmutable from 'swr/immutable'; | ||
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; | ||
import useSWR from 'swr'; | ||
import first from 'lodash-es/first'; | ||
|
||
type BillableItemResponse = { | ||
uuid: string; | ||
name: string; | ||
concept: { | ||
uuid: string; | ||
display: string; | ||
}; | ||
servicePrices: Array<{ | ||
uuid: string; | ||
price: number; | ||
paymentMode: { | ||
uuid: string; | ||
name: string; | ||
}; | ||
}>; | ||
}; | ||
|
||
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<BillableItemResponse> } }>( | ||
`${restBaseUrl}/cashier/billableService?${customRepresentation}`, | ||
openmrsFetch, | ||
); | ||
const billableItem = data?.data?.results?.find((item) => item?.concept?.uuid === billableItemId); | ||
|
||
return { | ||
billableItem: billableItem, | ||
isLoading: isLoading, | ||
error, | ||
}; | ||
}; | ||
|
||
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 ?? []); | ||
return { | ||
stockItem: stockItemsInfo, | ||
isLoading: isLoading, | ||
error, | ||
}; | ||
}; |
327 changes: 327 additions & 0 deletions
327
...ages/esm-billing-app/src/billable-services/create-edit/add-billable-service.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,327 @@ | ||
/* eslint-disable curly */ | ||
import React, { useCallback, useRef, useState } from 'react'; | ||
import styles from './add-billable-service.scss'; | ||
import { | ||
Form, | ||
Button, | ||
TextInput, | ||
ComboBox, | ||
Dropdown, | ||
Layer, | ||
InlineLoading, | ||
Search, | ||
Tile, | ||
FormLabel, | ||
} 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, useSession } from '@openmrs/esm-framework'; | ||
import { type ServiceConcept } from '../../types'; | ||
|
||
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'), | ||
]), | ||
}); | ||
const paymentFormSchema = z.object({ payment: z.array(servicePriceSchema) }); | ||
|
||
type PaymentMode = { | ||
paymentMode: string; | ||
price: string | number; | ||
}; | ||
type PaymentModeFormValue = { | ||
payment: Array<PaymentMode>; | ||
}; | ||
const DEFAULT_PAYMENT_OPTION = { paymentMode: '', price: 0 }; | ||
|
||
const AddBillableService: React.FC = () => { | ||
const { t } = useTranslation(); | ||
|
||
const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes(); | ||
const { serviceTypes, isLoading: isLoadingServicesTypes } = useServiceTypes(); | ||
const [billableServicePayload, setBillableServicePayload] = useState<any>({}); | ||
|
||
const { | ||
control, | ||
handleSubmit, | ||
formState: { errors }, | ||
} = useForm<any>({ | ||
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<HTMLInputElement>) => setSearchTerm(event.target.value); | ||
|
||
const [selectedConcept, setSelectedConcept] = useState<ServiceConcept>(null); | ||
const [searchTerm, setSearchTerm] = useState(''); | ||
const debouncedSearchTerm = useDebounce(searchTerm); | ||
const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm); | ||
const handleConceptChange = useCallback((selectedConcept: any) => { | ||
setSelectedConcept(selectedConcept); | ||
}, []); | ||
|
||
const handleNavigateToServiceDashboard = () => | ||
navigate({ | ||
to: window.getOpenmrsSpaBase() + 'billable-services', | ||
}); | ||
|
||
if (isLoadingPaymentModes && isLoadingServicesTypes) { | ||
return ( | ||
<InlineLoading | ||
status="active" | ||
iconDescription={t('loadingDescription', 'Loading')} | ||
description={t('loading', 'Loading data...')} | ||
/> | ||
); | ||
} | ||
|
||
const onSubmit = (data) => { | ||
const payload: any = {}; | ||
|
||
let servicePrices = []; | ||
data.payment.forEach((element) => { | ||
element.name = paymentModes.filter((p) => p.uuid === element.paymentMode)[0].name; | ||
servicePrices.push(element); | ||
}); | ||
payload.name = billableServicePayload.serviceName; | ||
payload.shortName = billableServicePayload.shortName; | ||
payload.serviceType = billableServicePayload.serviceType.uuid; | ||
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', | ||
timeoutInMs: 3000, | ||
}); | ||
handleNavigateToServiceDashboard(); | ||
}, | ||
(error) => { | ||
showSnackbar({ title: 'Bill payment error', kind: 'error', subtitle: error }); | ||
}, | ||
); | ||
}; | ||
|
||
return ( | ||
<Form className={styles.form}> | ||
<h4>{t('addBillableServices', 'Add Billable Services')}</h4> | ||
<section className={styles.section}> | ||
<Layer> | ||
<TextInput | ||
id="serviceName" | ||
type="text" | ||
labelText={t('serviceName', 'Service Name')} | ||
size="md" | ||
onChange={(e) => | ||
setBillableServicePayload({ | ||
...billableServicePayload, | ||
serviceName: e.target.value, | ||
}) | ||
} | ||
placeholder="Enter service name" | ||
/> | ||
</Layer> | ||
</section> | ||
<section className={styles.section}> | ||
<Layer> | ||
<TextInput | ||
id="serviceShortName" | ||
type="text" | ||
labelText={t('serviceShortName', 'Short Name')} | ||
size="md" | ||
onChange={(e) => | ||
setBillableServicePayload({ | ||
...billableServicePayload, | ||
shortName: e.target.value, | ||
}) | ||
} | ||
placeholder="Enter service short name" | ||
/> | ||
</Layer> | ||
</section> | ||
<section> | ||
<FormLabel className={styles.conceptLabel}>Associated Concept</FormLabel> | ||
<Controller | ||
name="search" | ||
control={control} | ||
render={({ field: { onChange, value, onBlur } }) => ( | ||
<ResponsiveWrapper isTablet={isTablet}> | ||
<Search | ||
ref={searchInputRef} | ||
size="md" | ||
id="conceptsSearch" | ||
labelText={t('enterConcept', 'Associated concept')} | ||
placeholder={t('searchConcepts', 'Search associated concept')} | ||
className={errors?.search && styles.serviceError} | ||
onChange={(e) => { | ||
onChange(e); | ||
handleSearchTermChange(e); | ||
}} | ||
renderIcon={errors?.search && <WarningFilled />} | ||
onBlur={onBlur} | ||
onClear={() => { | ||
setSearchTerm(''); | ||
setSelectedConcept(null); | ||
}} | ||
value={(() => { | ||
if (selectedConcept) { | ||
return selectedConcept.display; | ||
} | ||
if (debouncedSearchTerm) { | ||
return value; | ||
} | ||
})()} | ||
/> | ||
</ResponsiveWrapper> | ||
)} | ||
/> | ||
{(() => { | ||
if (!debouncedSearchTerm || selectedConcept) return null; | ||
if (isSearching) | ||
return <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />; | ||
if (searchResults && searchResults.length) { | ||
return ( | ||
<ul className={styles.conceptsList}> | ||
{/*TODO: use uuid instead of index as the key*/} | ||
{searchResults?.map((searchResult, index) => ( | ||
<li | ||
role="menuitem" | ||
className={styles.service} | ||
key={index} | ||
onClick={() => handleConceptChange(searchResult)}> | ||
{searchResult.display} | ||
</li> | ||
))} | ||
</ul> | ||
); | ||
} | ||
return ( | ||
<Layer> | ||
<Tile className={styles.emptyResults}> | ||
<span> | ||
{t('noResultsFor', 'No results for')} <strong>"{debouncedSearchTerm}"</strong> | ||
</span> | ||
</Tile> | ||
</Layer> | ||
); | ||
})()} | ||
</section> | ||
<section className={styles.section}> | ||
<Layer> | ||
<ComboBox | ||
id="serviceType" | ||
items={serviceTypes ?? []} | ||
titleText={t('serviceType', 'Service Type')} | ||
itemToString={(item) => item?.display} | ||
onChange={({ selectedItem }) => { | ||
setBillableServicePayload({ | ||
...billableServicePayload, | ||
display: selectedItem?.display, | ||
serviceType: selectedItem, | ||
}); | ||
}} | ||
placeholder="Select service type" | ||
required | ||
/> | ||
</Layer> | ||
</section> | ||
|
||
<section> | ||
<div className={styles.container}> | ||
{fields.map((field, index) => ( | ||
<div key={field.id} className={styles.paymentMethodContainer}> | ||
<Controller | ||
control={control} | ||
name={`payment.${index}.paymentMode`} | ||
render={({ field }) => ( | ||
<Layer> | ||
<Dropdown | ||
id={`paymentMode-${index}`} | ||
onChange={({ selectedItem }) => 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} | ||
/> | ||
</Layer> | ||
)} | ||
/> | ||
<Controller | ||
control={control} | ||
name={`payment.${index}.price`} | ||
render={({ field }) => ( | ||
<Layer> | ||
<TextInput | ||
id={`price-${index}`} | ||
{...field} | ||
invalid={!!errors?.payment?.[index]?.price} | ||
invalidText={errors?.payment?.[index]?.price?.message} | ||
labelText={t('sellingPrice', 'Selling Price')} | ||
placeholder={t('sellingAmount', 'Enter selling price')} | ||
/> | ||
</Layer> | ||
)} | ||
/> | ||
<div className={styles.removeButtonContainer}> | ||
<TrashCan | ||
aria-label={`delete_${index}`} | ||
id={`delete_${index}`} | ||
onClick={() => handleRemovePaymentMode(index)} | ||
className={styles.removeButton} | ||
size={20} | ||
/> | ||
</div> | ||
</div> | ||
))} | ||
<Button | ||
size="md" | ||
onClick={handleAppendPaymentMode} | ||
className={styles.paymentButtons} | ||
renderIcon={(props) => <Add size={24} {...props} />} | ||
iconDescription="Add"> | ||
{t('addPaymentOptions', 'Add payment option')} | ||
</Button> | ||
</div> | ||
</section> | ||
|
||
<section> | ||
<Button kind="secondary" onClick={handleNavigateToServiceDashboard}> | ||
{t('cancel', 'Cancel')} | ||
</Button> | ||
<Button type="submit" onClick={handleSubmit(onSubmit)}> | ||
{t('save', 'Save')} | ||
</Button> | ||
</section> | ||
</Form> | ||
); | ||
}; | ||
|
||
function ResponsiveWrapper({ children, isTablet }: { children: React.ReactNode; isTablet: boolean }) { | ||
return isTablet ? <Layer>{children} </Layer> : <>{children}</>; | ||
} | ||
|
||
export default AddBillableService; |
132 changes: 132 additions & 0 deletions
132
packages/esm-billing-app/src/billable-services/create-edit/add-billable-service.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
@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%; | ||
} | ||
|
||
.section { | ||
margin: spacing.$spacing-03; | ||
} | ||
|
||
.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)); | ||
align-items: flex-start; | ||
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: 1rem; | ||
} | ||
|
||
.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; | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
packages/esm-billing-app/src/billable-services/dashboard/dashboard.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import React from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import styles from './dashboard.scss'; | ||
import ServiceMetrics from './service-metrics.component'; | ||
import BillableServices from '../billable-services.component'; | ||
|
||
export function BillableServicesDashboard() { | ||
const { t } = useTranslation(); | ||
|
||
return ( | ||
<main className={styles.container}> | ||
<ServiceMetrics /> | ||
<main className={styles.servicesTableContainer}> | ||
<BillableServices /> | ||
</main> | ||
</main> | ||
); | ||
} |
27 changes: 27 additions & 0 deletions
27
packages/esm-billing-app/src/billable-services/dashboard/dashboard.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
@use '@carbon/colors'; | ||
@use '@carbon/layout'; | ||
@use '@carbon/type'; | ||
|
||
.container { | ||
height: calc(100vh - 3rem); | ||
} | ||
|
||
.servicesTableContainer { | ||
margin: 2rem 1rem; | ||
} | ||
|
||
.illo { | ||
margin-top: layout.$spacing-05; | ||
} | ||
|
||
.content { | ||
@include type.type-style('heading-compact-01'); | ||
color: colors.$gray-70; | ||
margin-top: layout.$spacing-05; | ||
} | ||
|
||
.tile { | ||
border: 1px solid colors.$gray-20; | ||
padding: 1.5rem 0; | ||
text-align: center; | ||
} |
40 changes: 40 additions & 0 deletions
40
packages/esm-billing-app/src/billable-services/dashboard/service-metrics.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import React, { useMemo } from 'react'; | ||
import Card from '../../metrics-cards/card.component'; | ||
import styles from '../../metrics-cards/metrics-cards.scss'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { ErrorState } from '@openmrs/esm-patient-common-lib'; | ||
import { InlineLoading } from '@carbon/react'; | ||
import { useBillableServices } from '../billable-service.resource'; | ||
|
||
export default function ServiceMetrics() { | ||
const { t } = useTranslation(); | ||
const { isLoading, error } = useBillableServices(); | ||
|
||
const cards = useMemo( | ||
() => [ | ||
{ title: 'Cash Revenue', count: '--' }, | ||
{ title: 'Insurance Revenue', count: '--' }, | ||
{ title: 'Pending Claims', count: '--' }, | ||
], | ||
[], | ||
); | ||
|
||
if (isLoading) { | ||
return ( | ||
<section className={styles.container}> | ||
<InlineLoading status="active" iconDescription="Loading" description="Loading service metrics..." /> | ||
</section> | ||
); | ||
} | ||
|
||
if (error) { | ||
return <ErrorState headerTitle={t('serviceMetrics', 'Service Metrics')} error={error} />; | ||
} | ||
return ( | ||
<section className={styles.container}> | ||
{cards.map((card) => ( | ||
<Card key={card.title} title={card.title} count={card.count} /> | ||
))} | ||
</section> | ||
); | ||
} |
20 changes: 20 additions & 0 deletions
20
packages/esm-billing-app/src/billing-dashboard/billing-dashboard.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import React from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
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'; | ||
|
||
export function BillingDashboard() { | ||
const { t } = useTranslation(); | ||
|
||
return ( | ||
<main className={styles.container}> | ||
<BillingHeader title={t('home', 'Home')} /> | ||
<MetricsCards /> | ||
<main className={styles.billsTableContainer}> | ||
<BillsTable defaultBillPaymentStatus="PENDING" /> | ||
</main> | ||
</main> | ||
); | ||
} |
27 changes: 27 additions & 0 deletions
27
packages/esm-billing-app/src/billing-dashboard/billing-dashboard.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
@use '@carbon/colors'; | ||
@use '@carbon/layout'; | ||
@use '@carbon/type'; | ||
|
||
.container { | ||
height: calc(100vh - 3rem); | ||
} | ||
|
||
.billsTableContainer { | ||
margin: 2rem 1rem; | ||
} | ||
|
||
.illo { | ||
margin-top: layout.$spacing-05; | ||
} | ||
|
||
.content { | ||
@include type.type-style('heading-compact-01'); | ||
color: colors.$gray-70; | ||
margin-top: layout.$spacing-05; | ||
} | ||
|
||
.tile { | ||
border: 1px solid colors.$gray-20; | ||
padding: 1.5rem 0; | ||
text-align: center; | ||
} |
137 changes: 137 additions & 0 deletions
137
packages/esm-billing-app/src/billing-form/billing-checkin-form.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import React, { useCallback, useEffect, useState } from 'react'; | ||
import { InlineLoading, InlineNotification, FilterableMultiSelect } from '@carbon/react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { showSnackbar, useConfig } from '@openmrs/esm-framework'; | ||
import styles from './billing-checkin-form.scss'; | ||
import VisitAttributesForm from './visit-attributes/visit-attributes-form.component'; | ||
import { type BillingConfig } from '../config-schema'; | ||
import { hasPatientBeenExempted } from './helper'; | ||
import { EXEMPTED_PAYMENT_STATUS, PENDING_PAYMENT_STATUS } from '../constants'; | ||
import { type BillingService } from '../types'; | ||
import SHANumberValidity from './social-health-authority/sha-number-validity.component'; | ||
import { createPatientBill, useBillableItems, useCashPoint } from '../billing.resource'; | ||
|
||
type BillingCheckInFormProps = { | ||
patientUuid: string; | ||
setExtraVisitInfo: (state) => void; | ||
}; | ||
|
||
const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, setExtraVisitInfo }) => { | ||
const { t } = useTranslation(); | ||
const { | ||
visitAttributeTypes: { isPatientExempted }, | ||
} = useConfig<BillingConfig>(); | ||
const { cashPoints, isLoading: isLoadingCashPoints, error: cashError } = useCashPoint(); | ||
const { lineItems, isLoading: isLoadingLineItems, error: lineError } = useBillableItems(); | ||
const [attributes, setAttributes] = useState([]); | ||
const [paymentMethod, setPaymentMethod] = useState<any>(); | ||
let lineList = []; | ||
|
||
const handleCreateBill = useCallback((createBillPayload) => { | ||
createPatientBill(createBillPayload).then( | ||
() => { | ||
showSnackbar({ title: 'Patient Bill', subtitle: 'Patient has been billed successfully', kind: 'success' }); | ||
}, | ||
(error) => { | ||
const errorMessage = JSON.stringify(error?.responseBody?.error?.message?.replace(/\[/g, '').replace(/\]/g, '')); | ||
showSnackbar({ | ||
title: 'Patient Bill Error', | ||
subtitle: `An error has occurred while creating patient bill, Contact system administrator quoting this error ${errorMessage}`, | ||
kind: 'error', | ||
isLowContrast: true, | ||
}); | ||
}, | ||
); | ||
}, []); | ||
|
||
const handleBillingService = (selectedItems: Array<BillingService>) => { | ||
const cashPointUuid = cashPoints?.[0]?.uuid ?? ''; | ||
const billStatus = hasPatientBeenExempted(attributes, isPatientExempted) | ||
? EXEMPTED_PAYMENT_STATUS | ||
: 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 { | ||
billableService: item?.uuid ?? '', | ||
quantity: 1, | ||
price: priceForPaymentMode ? priceForPaymentMode.price : '0.000', | ||
priceName: 'Default', | ||
priceUuid: priceForPaymentMode ? priceForPaymentMode.uuid : '', | ||
lineItemOrder: index, | ||
paymentStatus: billStatus, | ||
}; | ||
}); | ||
|
||
const billPayload = { | ||
lineItems: lineItems, | ||
cashPoint: cashPointUuid, | ||
patient: patientUuid, | ||
status: billStatus, | ||
payments: [], | ||
}; | ||
|
||
setExtraVisitInfo({ | ||
handleCreateExtraVisitInfo: () => handleCreateBill(billPayload), | ||
attributes, | ||
}); | ||
}; | ||
|
||
useEffect(() => { | ||
setExtraVisitInfo({ | ||
handleCreateExtraVisitInfo: () => {}, | ||
attributes, | ||
}); | ||
}, []); | ||
|
||
if (isLoadingLineItems || isLoadingCashPoints) { | ||
return ( | ||
<InlineLoading | ||
status="active" | ||
iconDescription={t('loading', 'Loading')} | ||
description={t('loadingBillingServices', 'Loading billing services...')} | ||
/> | ||
); | ||
} | ||
|
||
if (paymentMethod) { | ||
lineList = []; | ||
lineList = lineItems.filter((e) => | ||
e.servicePrices.some((p) => p.paymentMode && p.paymentMode.uuid === paymentMethod?.uuid), | ||
); | ||
} | ||
|
||
if (cashError || lineError) { | ||
return ( | ||
<InlineNotification | ||
kind="error" | ||
lowContrast | ||
title={t('billErrorService', 'Bill service error')} | ||
subtitle={t('errorLoadingBillServices', 'Error loading bill services')} | ||
/> | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<VisitAttributesForm setAttributes={setAttributes} setPaymentMethod={setPaymentMethod} /> | ||
<SHANumberValidity paymentMethod={paymentMethod} /> | ||
<section className={styles.sectionContainer}> | ||
<div className={styles.sectionTitle}>{t('billing', 'Billing')}</div> | ||
<div className={styles.sectionField}> | ||
<FilterableMultiSelect | ||
id="billing-service" | ||
titleText={t('searchServices', 'Search services')} | ||
items={lineItems ?? []} | ||
itemToString={(item) => (item ? item?.name : '')} | ||
onChange={({ selectedItems }) => handleBillingService(selectedItems)} | ||
/> | ||
</div> | ||
</section> | ||
</> | ||
); | ||
}; | ||
|
||
export default React.memo(BillingCheckInForm); |
14 changes: 14 additions & 0 deletions
14
packages/esm-billing-app/src/billing-form/billing-checkin-form.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
@use '@carbon/layout'; | ||
@use '@carbon/type'; | ||
@use '@carbon/colors'; | ||
|
||
.sectionTitle { | ||
@include type.type-style('heading-compact-02'); | ||
color: colors.$gray-70; | ||
margin: 0 0 layout.$spacing-03 0; | ||
flex-basis: 30%; | ||
} | ||
|
||
.sectionField { | ||
flex-basis: 70%; | ||
} |
297 changes: 297 additions & 0 deletions
297
packages/esm-billing-app/src/billing-form/billing-form.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,297 @@ | ||
import React, { useState } from 'react'; | ||
import { | ||
ButtonSet, | ||
Button, | ||
RadioButtonGroup, | ||
RadioButton, | ||
Search, | ||
Table, | ||
TableHead, | ||
TableBody, | ||
TableHeader, | ||
TableRow, | ||
TableCell, | ||
} from '@carbon/react'; | ||
import styles from './billing-form.scss'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { useFetchSearchResults, processBillItems } from '../billing.resource'; | ||
import { getPatientUuidFromUrl } from '@openmrs/esm-patient-common-lib'; | ||
import { showSnackbar } from '@openmrs/esm-framework'; | ||
import { mutate } from 'swr'; | ||
|
||
type BillingFormProps = { | ||
patientUuid: string; | ||
closeWorkspace: () => void; | ||
}; | ||
|
||
const BillingForm: React.FC<BillingFormProps> = ({ closeWorkspace }) => { | ||
const { t } = useTranslation(); | ||
const patientUuid = getPatientUuidFromUrl(); | ||
|
||
const [GrandTotal, setGrandTotal] = useState(0); | ||
|
||
const [searchOptions, setsearchOptions] = useState([]); | ||
const [defaultSearchItems, setdefaultSearchItems] = useState([]); | ||
|
||
const [BillItems, setBillItems] = useState([]); | ||
|
||
const [searchVal, setsearchVal] = 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 calculateTotal = (event, itemName) => { | ||
const Qnty = event.target.value; | ||
const price = (document.getElementById(event.target.id + 'Price') as HTMLInputElement).innerHTML; | ||
const total = parseInt(price) * Qnty; | ||
(document.getElementById(event.target.id + 'Total') as HTMLInputElement).innerHTML = total.toString(); | ||
|
||
const updateItem = BillItems.filter((o) => o.Item.toLowerCase().includes(itemName.toLowerCase())); | ||
|
||
updateItem.map((o) => (o.Qnty = Qnty)); | ||
updateItem.map((o) => (o.Total = total)); | ||
|
||
const totals = Array.from(document.querySelectorAll('[id$="Total"]')); | ||
|
||
let addUpTotals = 0; | ||
totals.forEach((tot) => { | ||
let getTot = (tot as HTMLInputElement).innerHTML; | ||
addUpTotals += parseInt(getTot); | ||
}); | ||
setGrandTotal(addUpTotals); | ||
}; | ||
|
||
const CalculateTotalAfteraddBillItem = () => { | ||
let sum = 0; | ||
BillItems.map((o) => (sum += o.Price)); | ||
|
||
setGrandTotal(sum); | ||
}; | ||
|
||
const addItemToBill = (event, itemid, itemname, itemcategory, itemPrice) => { | ||
BillItems.push({ | ||
uuid: itemid, | ||
Item: itemname, | ||
Qnty: 1, | ||
Price: itemPrice, | ||
Total: itemPrice, | ||
category: itemcategory, | ||
}); | ||
setBillItems(BillItems); | ||
setsearchOptions([]); | ||
CalculateTotalAfteraddBillItem(); | ||
(document.getElementById('searchField') as HTMLInputElement).value = ''; | ||
}; | ||
|
||
// filter items | ||
const { data, error, isLoading, isValidating } = useFetchSearchResults(searchVal, category); | ||
|
||
const filterItems = (val) => { | ||
setsearchVal(val); | ||
|
||
if (!isLoading) { | ||
/* empty */ | ||
} else { | ||
if (typeof data !== 'undefined') { | ||
//set to null then repopulate | ||
while (searchOptions.length > 0) { | ||
searchOptions.pop(); | ||
} | ||
|
||
const res = data as { results: any[] }; | ||
|
||
res.results.map((o) => { | ||
if (o.commonName && (o.commonName != '' || o.commonName != null)) { | ||
searchOptions.push({ | ||
uuid: o.uuid, | ||
Item: o.commonName, | ||
Qnty: 1, | ||
Price: 10, | ||
Total: 10, | ||
category: 'StockItem', | ||
}); | ||
} else { | ||
if (o.name.toLowerCase().includes(searchVal.toLowerCase())) { | ||
searchOptions.push({ | ||
uuid: o.uuid, | ||
Item: o.name, | ||
Qnty: 1, | ||
Price: o.servicePrices[0].price, | ||
Total: o.servicePrices[0].price, | ||
category: 'Service', | ||
}); | ||
} | ||
} | ||
setsearchOptions(searchOptions); | ||
}); | ||
} | ||
} | ||
}; | ||
|
||
const postBillItems = () => { | ||
const bill = { | ||
cashPoint: '54065383-b4d4-42d2-af4d-d250a1fd2590', | ||
cashier: 'f9badd80-ab76-11e2-9e96-0800200c9a66', | ||
lineItems: [], | ||
payments: [], | ||
patient: patientUuid, | ||
status: 'PENDING', | ||
}; | ||
|
||
BillItems.map((o) => { | ||
if (o.category == 'StockItem') { | ||
bill.lineItems.push({ | ||
item: o.uuid, | ||
quantity: parseInt(o.Qnty), | ||
price: o.Price, | ||
priceName: 'Default', | ||
priceUuid: '7b9171ac-d3c1-49b4-beff-c9902aee5245', | ||
lineItemOrder: 0, | ||
paymentStatus: 'PENDING', | ||
}); | ||
} else { | ||
bill.lineItems.push({ | ||
billableService: o.uuid, | ||
quantity: parseInt(o.Qnty), | ||
price: o.Price, | ||
priceName: 'Default', | ||
priceUuid: '7b9171ac-d3c1-49b4-beff-c9902aee5245', | ||
lineItemOrder: 0, | ||
paymentStatus: 'PENDING', | ||
}); | ||
} | ||
}); | ||
|
||
const url = `/ws/rest/v1/cashier/bill`; | ||
processBillItems(bill).then( | ||
(resp) => { | ||
closeWorkspace(); | ||
mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true }); | ||
showSnackbar({ | ||
title: t('billItems', 'Save Bill'), | ||
subtitle: 'Bill processing has been successful', | ||
kind: 'success', | ||
timeoutInMs: 3000, | ||
}); | ||
}, | ||
(error) => { | ||
showSnackbar({ title: 'Bill processing error', kind: 'error', subtitle: error }); | ||
}, | ||
); | ||
}; | ||
|
||
return ( | ||
<div className={styles.billingFormContainer}> | ||
<RadioButtonGroup | ||
legendText={t('selectCategory', 'Select category')} | ||
name="radio-button-group" | ||
defaultSelected="radio-1" | ||
className={styles.billingItem} | ||
onChange={toggleSearch}> | ||
<RadioButton labelText={t('stockItem', 'Stock Item')} value="Stock Item" id="radio-1" /> | ||
<RadioButton labelText={t('service', 'Service')} value="Service" id="radio-2" /> | ||
</RadioButtonGroup> | ||
<div></div> | ||
|
||
<div> | ||
<Search | ||
id="searchField" | ||
size="lg" | ||
placeholder="Find your drugs here..." | ||
labelText="Search" | ||
disabled | ||
closeButtonLabelText="Clear search input" | ||
onChange={() => {}} | ||
className={styles.billingItem} | ||
onKeyUp={(e) => { | ||
filterItems(e.target.value); | ||
}} | ||
/> | ||
|
||
<ul className={styles.searchContent}> | ||
{searchOptions.map((row) => ( | ||
<li className={styles.searchItem}> | ||
<Button | ||
id={row.uuid} | ||
onClick={(e) => addItemToBill(e, row.uuid, row.Item, row.category, row.Price)} | ||
style={{ background: 'inherit', color: 'black' }}> | ||
{row.Item} Qnty.{row.Qnty} Ksh.{row.Price} | ||
</Button> | ||
</li> | ||
))} | ||
</ul> | ||
</div> | ||
|
||
{/* <NumberInput id="carbon-number" min={0} max={100} value={50} ref={numberRef} | ||
onChange={(e)=> alert((numberRef.current as HTMLInputElement).value)} | ||
className="testingNumberInput" label="NumberInput label" helperText="Optional helper text." invalidText="Number is not valid" /> */} | ||
|
||
<Table aria-label="sample table" className={styles.billingItem}> | ||
<TableHead> | ||
<TableRow> | ||
<TableHeader>Item</TableHeader> | ||
<TableHeader>Quantity</TableHeader> | ||
<TableHeader>Price</TableHeader> | ||
<TableHeader>Total</TableHeader> | ||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
{BillItems && Array.isArray(BillItems) ? ( | ||
BillItems.map((row) => ( | ||
<TableRow> | ||
<TableCell>{row.Item}</TableCell> | ||
<TableCell> | ||
<input | ||
type="number" | ||
className="form-control" | ||
id={row.Item} | ||
min={0} | ||
max={100} | ||
value={row.Qnty} | ||
onChange={(e) => { | ||
calculateTotal(e, row.Item); | ||
row.Qnty = e.target.value; | ||
}} | ||
/> | ||
{/* <NumberInput id={row.Item} min={0} max={100} value={row.Qnty} ref={numberRef} | ||
onChange={(e)=> alert((numberRef.current as HTMLInputElement).value)} /> */} | ||
</TableCell> | ||
<TableCell id={row.Item + 'Price'}>{row.Price}</TableCell> | ||
<TableCell id={row.Item + 'Total'} className="totalValue"> | ||
{row.Total} | ||
</TableCell> | ||
</TableRow> | ||
)) | ||
) : ( | ||
<p>Loading...</p> | ||
)} | ||
<TableRow> | ||
<TableCell></TableCell> | ||
<TableCell></TableCell> | ||
<TableCell style={{ fontWeight: 'bold' }}>Grand Total:</TableCell> | ||
<TableCell id="GrandTotalSum">{GrandTotal}</TableCell> | ||
</TableRow> | ||
</TableBody> | ||
</Table> | ||
|
||
<ButtonSet className={styles.billingItem}> | ||
<Button kind="secondary" onClick={closeWorkspace}> | ||
Discard | ||
</Button> | ||
<Button kind="primary" onClick={postBillItems}> | ||
Save & Close | ||
</Button> | ||
</ButtonSet> | ||
</div> | ||
); | ||
}; | ||
|
||
export default BillingForm; |
28 changes: 28 additions & 0 deletions
28
packages/esm-billing-app/src/billing-form/billing-form.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
@use '@carbon/layout'; | ||
|
||
.billingFormContainer { | ||
padding: layout.$spacing-05; | ||
} | ||
|
||
.billingItem { | ||
// padding: layout.$spacing-05; | ||
margin-top: 30px; | ||
} | ||
|
||
.searchContent { | ||
// display: none; | ||
position: absolute; | ||
background-color: #fff; | ||
min-width: 230px; | ||
overflow: auto; | ||
border: 1px solid #ddd; | ||
z-index: 1; | ||
width: 92%; | ||
} | ||
|
||
.searchItem { | ||
border-bottom: 0.1rem solid; | ||
border-bottom-color: silver; | ||
// padding: 10px; | ||
cursor: pointer; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const hasPatientBeenExempted = (attributes: Array<any>, isPatientExempted: string): boolean => | ||
attributes.find(({ attributeType }) => attributeType === isPatientExempted)?.value === true; |
77 changes: 77 additions & 0 deletions
77
...sm-billing-app/src/billing-form/social-health-authority/sha-number-validity.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import React, { useState } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Form, TextInput, Button, InlineLoading, InlineNotification } from '@carbon/react'; | ||
|
||
type SHANumberValidityProps = { | ||
paymentMethod: any; | ||
}; | ||
|
||
const SHANumberValidity: React.FC<SHANumberValidityProps> = ({ paymentMethod }) => { | ||
const { t } = useTranslation(); | ||
const [shaNumber, setNumber] = useState(''); | ||
const [message, setMessage] = useState(''); | ||
const [isLoading, setIsLoading] = useState(false); | ||
const [validity, setValidity] = useState(false); // TODO: set validity based on api response | ||
const isSHA = paymentMethod?.name === 'Social Health Insurance Fund (SHA)'; | ||
|
||
const handleValidateSHANumber = () => { | ||
setIsLoading(true); | ||
const randomNumber = Math.floor(Math.random() * 10) + 1; | ||
// TODO call api to validate sha number | ||
setTimeout(() => { | ||
// TODO: set validity based on api response and update the expiry date based on the response as visit attribute | ||
randomNumber % 2 === 0 ? setValidity(false) : setValidity(true); | ||
setMessage( | ||
randomNumber % 2 === 0 | ||
? t('invalidSHANumber', 'SHA number is invalid, advice patient to update payment or contact SHA') | ||
: t('validSHANumber', 'SHA number is valid, proceed with care'), | ||
); | ||
setIsLoading(false); | ||
}, 1500); | ||
}; | ||
|
||
if (!isSHA) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<Form> | ||
<TextInput | ||
id="sha-number" | ||
onChange={(e) => setNumber(e.target.value)} | ||
labelText={t('shaNumber', 'SHA Number')} | ||
placeholder={t('enterSHANumber', 'Enter SHA Number')} | ||
/> | ||
{isLoading ? ( | ||
<InlineLoading | ||
style={{ minHeight: '3rem', marginTop: '0.625rem' }} | ||
status="active" | ||
iconDescription="Loading" | ||
description={t('validatingSHANumber', 'Validating SHA Number')} | ||
/> | ||
) : ( | ||
<Button | ||
disabled={shaNumber.length === 0} | ||
kind="tertiary" | ||
style={{ marginTop: '0.625rem' }} | ||
onClick={handleValidateSHANumber}> | ||
{t('checkValidity', 'Check Validity')} | ||
</Button> | ||
)} | ||
{message !== '' && ( | ||
<p style={{ marginTop: '0.625rem' }}> | ||
<InlineNotification | ||
aria-label="closes notification" | ||
kind={validity ? 'success' : 'error'} | ||
statusIconDescription="notification" | ||
subtitle={message} | ||
title={validity ? t('valid', 'Valid SHA Number') : t('shaNotValid', 'Invalid SHA Number')} | ||
lowContrast={true} | ||
/> | ||
</p> | ||
)} | ||
</Form> | ||
); | ||
}; | ||
|
||
export default SHANumberValidity; |
198 changes: 198 additions & 0 deletions
198
...ges/esm-billing-app/src/billing-form/visit-attributes/visit-attributes-form.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import React, { useCallback } 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'; | ||
import { z } from 'zod'; | ||
import { useConfig } from '@openmrs/esm-framework'; | ||
import { type BillingConfig } from '../../config-schema'; | ||
import { usePaymentModes } from '../../billing.resource'; | ||
|
||
type VisitAttributesFormProps = { | ||
setAttributes: (state) => void; | ||
setPaymentMethod?: (value: any) => void; | ||
}; | ||
|
||
type VisitAttributesFormValue = { | ||
isPatientExempted: string; | ||
paymentMethods: { uuid: string; name: string }; | ||
insuranceScheme: string; | ||
policyNumber: string; | ||
exemptionCategory: string; | ||
}; | ||
|
||
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(), | ||
}); | ||
|
||
const VisitAttributesForm: React.FC<VisitAttributesFormProps> = ({ setAttributes, setPaymentMethod }) => { | ||
const { t } = useTranslation(); | ||
const { visitAttributeTypes, patientExemptionCategories } = useConfig<BillingConfig>(); | ||
const { control, getValues, watch, setValue } = useForm<VisitAttributesFormValue>({ | ||
mode: 'all', | ||
defaultValues: {}, | ||
resolver: zodResolver(visitAttributesFormSchema), | ||
}); | ||
const [isPatientExempted, paymentMethods, insuranceSchema, policyNumber, exemptionCategory] = watch([ | ||
'isPatientExempted', | ||
'paymentMethods', | ||
'insuranceScheme', | ||
'policyNumber', | ||
'exemptionCategory', | ||
]); | ||
|
||
const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes(); | ||
|
||
const resetFormFieldsForNonExemptedPatients = useCallback(() => { | ||
if ((isPatientExempted && paymentMethods !== null) || paymentMethods !== undefined) { | ||
setValue('insuranceScheme', ''); | ||
setValue('policyNumber', ''); | ||
} | ||
}, [isPatientExempted, paymentMethods, setValue]); | ||
|
||
const createVisitAttributesPayload = useCallback(() => { | ||
const { exemptionCategory, paymentMethods, policyNumber, isPatientExempted } = getValues(); | ||
setPaymentMethod(paymentMethods); | ||
resetFormFieldsForNonExemptedPatients(); | ||
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 }, | ||
]; | ||
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, | ||
})); | ||
}, [ | ||
visitAttributeTypes.insuranceScheme, | ||
visitAttributeTypes.isPatientExempted, | ||
visitAttributeTypes.exemptionCategory, | ||
visitAttributeTypes.paymentMethods, | ||
visitAttributeTypes.policyNumber, | ||
getValues, | ||
insuranceSchema, | ||
resetFormFieldsForNonExemptedPatients, | ||
setPaymentMethod, | ||
]); | ||
|
||
React.useEffect(() => { | ||
setAttributes(createVisitAttributesPayload()); | ||
}, [paymentMethods, insuranceSchema, policyNumber, exemptionCategory, setAttributes, createVisitAttributesPayload]); | ||
|
||
if (isLoadingPaymentModes) { | ||
return ( | ||
<InlineLoading | ||
status="active" | ||
iconDescription={t('loadingDescription', 'Loading')} | ||
description={t('loading', 'Loading data...')} | ||
/> | ||
); | ||
} | ||
|
||
return ( | ||
<section> | ||
<div className={styles.sectionTitle}>{t('billing', 'Billing')}</div> | ||
<div className={styles.sectionField}> | ||
<div className={styles.sectionFieldLayer}> | ||
<Controller | ||
name="isPatientExempted" | ||
control={control} | ||
render={({ field }) => ( | ||
<RadioButtonGroup | ||
onChange={(selected) => field.onChange(selected)} | ||
orientation="horizontal" | ||
legendText={t('isPatientExemptedLegend', 'Is patient exempted from payment?')} | ||
name="patientExemption"> | ||
<RadioButton labelText="Yes" value={true} id="Yes" /> | ||
<RadioButton labelText="No" value={false} id="No" /> | ||
</RadioButtonGroup> | ||
)} | ||
/> | ||
</div> | ||
{isPatientExempted && ( | ||
<div className={styles.sectionFieldLayer}> | ||
<Controller | ||
control={control} | ||
name="exemptionCategory" | ||
render={({ field }) => ( | ||
<ComboBox | ||
className={styles.sectionField} | ||
onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)} | ||
id="exemptionCategory" | ||
items={patientExemptionCategories} | ||
itemToString={(item) => (item ? item.label : '')} | ||
titleText={t('exemptionCategory', 'Exemption category')} | ||
placeholder={t('selectExemptionCategory', 'Select exemption category')} | ||
/> | ||
)} | ||
/> | ||
</div> | ||
)} | ||
<div className={styles.sectionFieldLayer}> | ||
<Controller | ||
control={control} | ||
name="paymentMethods" | ||
render={({ field }) => ( | ||
<ComboBox | ||
className={styles.sectionField} | ||
onChange={({ selectedItem }) => field.onChange(selectedItem)} | ||
id="paymentMethods" | ||
items={paymentModes} | ||
itemToString={(item) => (item ? item.name : '')} | ||
titleText={t('paymentMethodsTitle', 'Payment methods')} | ||
placeholder={t('selectPaymentMethodPlaceholder', 'Select payment method')} | ||
/> | ||
)} | ||
/> | ||
</div> | ||
|
||
{paymentMethods?.name?.toLocaleLowerCase() === 'insurance' && ( | ||
<> | ||
<div className={styles.sectionFieldLayer}> | ||
<Controller | ||
control={control} | ||
name="insuranceScheme" | ||
render={({ field }) => ( | ||
<TextInput | ||
className={styles.sectionField} | ||
onChange={(e) => field.onChange(e.target.value)} | ||
id="insurance-scheme" | ||
type="text" | ||
labelText={t('insuranceScheme', 'Insurance scheme')} | ||
/> | ||
)} | ||
/> | ||
</div> | ||
<div className={styles.sectionFieldLayer}> | ||
<Controller | ||
control={control} | ||
name="policyNumber" | ||
render={({ field }) => ( | ||
<TextInput | ||
className={styles.sectionField} | ||
onChange={(e) => field.onChange(e.target.value)} | ||
id="policy-number" | ||
type="text" | ||
labelText={t('policyNumber', 'Policy number')} | ||
/> | ||
)} | ||
/> | ||
</div> | ||
</> | ||
)} | ||
</div> | ||
</section> | ||
); | ||
}; | ||
|
||
export default VisitAttributesForm; |
18 changes: 18 additions & 0 deletions
18
packages/esm-billing-app/src/billing-form/visit-attributes/visit-attributes-form.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
@use '@carbon/layout'; | ||
@use '@carbon/type'; | ||
@use '@carbon/colors'; | ||
|
||
.sectionTitle { | ||
@include type.type-style('heading-compact-02'); | ||
color: colors.$gray-70; | ||
margin: 0 0 layout.$spacing-03 0; | ||
flex-basis: 30%; | ||
} | ||
|
||
.sectionField { | ||
flex-basis: 70%; | ||
} | ||
|
||
.sectionFieldLayer { | ||
margin-bottom: 0.5rem; | ||
} |
Oops, something went wrong.