diff --git a/package.json b/package.json index 2da8238..46f7fc4 100755 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "react-router-dom": "^6.14.1", "rxjs": "^6.6.7", "swc-loader": "^0.2.3", + "swr": "^2.2.4", "turbo": "^1.12.4", "typescript": "^4.9.5", "webpack": "^5.88.1", diff --git a/packages/esm-billing-app/package.json b/packages/esm-billing-app/package.json index f3148b6..37ce216 100644 --- a/packages/esm-billing-app/package.json +++ b/packages/esm-billing-app/package.json @@ -1,6 +1,6 @@ { "name": "@ehospital/esm-billing-app", - "version": "1.0.8", + "version": "1.0.9", "description": "Billing frontend module for use in O3", "browser": "dist/ehospital-esm-billing-app.js", "main": "src/index.ts", diff --git a/packages/esm-billing-app/src/autosuggest/autosuggest.component.tsx b/packages/esm-billing-app/src/autosuggest/autosuggest.component.tsx new file mode 100644 index 0000000..ad6a24b --- /dev/null +++ b/packages/esm-billing-app/src/autosuggest/autosuggest.component.tsx @@ -0,0 +1,187 @@ +import React, { type HTMLAttributes, useEffect, useRef, useState } from 'react'; +import { Layer, Search, type SearchProps } from '@carbon/react'; +import classNames from 'classnames'; +import styles from './autosuggest.scss'; + +// FIXME Temporarily included types from Carbon +type InputPropsBase = Omit, 'onChange'>; + +interface SearchProps extends InputPropsBase { + /** + * Specify an optional value for the `autocomplete` property on the underlying + * ``, defaults to "off" + */ + autoComplete?: string; + + /** + * Specify an optional className to be applied to the container node + */ + className?: string; + + /** + * Specify a label to be read by screen readers on the "close" button + */ + closeButtonLabelText?: string; + + /** + * Optionally provide the default value of the `` + */ + defaultValue?: string | number; + + /** + * Specify whether the `` should be disabled + */ + disabled?: boolean; + + /** + * Specify whether or not ExpandableSearch should render expanded or not + */ + isExpanded?: boolean; + + /** + * Specify a custom `id` for the input + */ + id?: string; + + /** + * Provide the label text for the Search icon + */ + labelText: React.ReactNode; + + /** + * Optional callback called when the search value changes. + */ + onChange?(e: { target: HTMLInputElement; type: 'change' }): void; + + /** + * Optional callback called when the search value is cleared. + */ + onClear?(): void; + + /** + * Optional callback called when the magnifier icon is clicked in ExpandableSearch. + */ + onExpand?(e: React.MouseEvent | React.KeyboardEvent): void; + + /** + * Provide an optional placeholder text for the Search. + * Note: if the label and placeholder differ, + * VoiceOver on Mac will read both + */ + placeholder?: string; + + /** + * Rendered icon for the Search. + * Can be a React component class + */ + renderIcon?: React.ComponentType | React.FunctionComponent; + + /** + * Specify the role for the underlying ``, defaults to `searchbox` + */ + role?: string; + + /** + * Specify the size of the Search + */ + size?: 'sm' | 'md' | 'lg'; + + /** + * Optional prop to specify the type of the `` + */ + type?: string; + + /** + * Specify the value of the `` + */ + value?: string | number; +} + +interface AutosuggestProps extends SearchProps { + getDisplayValue: Function; + getFieldValue: Function; + getSearchResults: (query: string) => Promise; + onSuggestionSelected: (field: string, value: string) => void; + invalid?: boolean | undefined; + invalidText?: string | undefined; +} + +export const Autosuggest: React.FC = ({ + getDisplayValue, + getFieldValue, + getSearchResults, + onSuggestionSelected, + invalid, + invalidText, + ...searchProps +}) => { + const [suggestions, setSuggestions] = useState([]); + const searchBox = useRef(null); + const wrapper = useRef(null); + const { id: name, labelText } = searchProps; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutsideComponent); + + return () => { + document.removeEventListener('mousedown', handleClickOutsideComponent); + }; + }, [wrapper]); + + const handleClickOutsideComponent = (e) => { + if (wrapper.current && !wrapper.current.contains(e.target)) { + setSuggestions([]); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + const query = e.target.value; + onSuggestionSelected(name, undefined); + + if (query) { + getSearchResults(query).then((suggestions) => { + setSuggestions(suggestions); + }); + } else { + setSuggestions([]); + } + }; + + const handleClear = (e: React.ChangeEvent) => { + onSuggestionSelected(name, undefined); + }; + + const handleClick = (index: number) => { + const display = getDisplayValue(suggestions[index]); + const value = getFieldValue(suggestions[index]); + searchBox.current.value = display; + onSuggestionSelected(name, value); + setSuggestions([]); + }; + + return ( +
+ + + + + {suggestions.length > 0 && ( +
    + {suggestions.map((suggestion, index) => ( +
  • handleClick(index)} role="presentation"> + {getDisplayValue(suggestion)} +
  • + ))} +
+ )} + {invalid ? : <>} +
+ ); +}; diff --git a/packages/esm-billing-app/src/autosuggest/autosuggest.scss b/packages/esm-billing-app/src/autosuggest/autosuggest.scss new file mode 100644 index 0000000..4be2f43 --- /dev/null +++ b/packages/esm-billing-app/src/autosuggest/autosuggest.scss @@ -0,0 +1,62 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +.label01 { + @include type.type-style('label-01'); +} + +.suggestions { + position: relative; + border-top-width: 0; + list-style: none; + margin-top: 0; + max-height: 143px; + overflow-y: auto; + padding-left: 0; + width: 100%; + position: absolute; + left: 0; + background-color: #fff; + margin-bottom: 20px; + z-index: 99; +} + +.suggestions li { + padding: spacing.$spacing-05; + line-height: 1.29; + color: #525252; + border-bottom: 1px solid #8d8d8d; +} + +.suggestions li:hover { + background-color: #e5e5e5; + color: #161616; + cursor: pointer; +} + +.suggestions li:not(:last-of-type) { + border-bottom: 1px solid #999; +} + +.autocomplete { + position: relative; +} + +.autocompleteSearch { + width: 100%; +} + +.suggestions a { + color: inherit; + text-decoration: none; +} + +.invalid input { + outline: 2px solid var(--cds-support-error, #da1e28); + outline-offset: -2px; +} + +.invalidMsg { + color: var(--cds-text-error, #da1e28); +} diff --git a/packages/esm-billing-app/src/billing-form/billing-form.component.tsx b/packages/esm-billing-app/src/billing-form/billing-form.component.tsx index 63a168d..be7320d 100644 --- a/packages/esm-billing-app/src/billing-form/billing-form.component.tsx +++ b/packages/esm-billing-app/src/billing-form/billing-form.component.tsx @@ -1,352 +1,224 @@ -import React, { useState, useEffect, useMemo } from 'react'; import { - ButtonSet, Button, + ButtonSet, + Column, + Dropdown, Form, - InlineLoading, - RadioButtonGroup, - RadioButton, - Search, Stack, Table, - TableHead, TableBody, + TableCell, + TableHead, TableHeader, TableRow, - TableCell, } from '@carbon/react'; -import styles from './billing-form.scss'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { getPatientUuidFromUrl } from '@openmrs/esm-patient-common-lib'; +import React, { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { restBaseUrl, showSnackbar, showToast, useConfig, useDebounce, useLayoutType } from '@openmrs/esm-framework'; -import { useFetchSearchResults, processBillItems } from '../billing.resource'; -import { mutate } from 'swr'; -import { convertToCurrency } from '../helpers'; import { z } from 'zod'; +import { Autosuggest } from '../autosuggest/autosuggest.component'; +import { billingFormSchema, processBillItems } from '../billing.resource'; +import useBillableServices from '../hooks/useBillableServices'; +import { BillingService } from '../types'; +import styles from './billing-form.scss'; +import { mutate } from 'swr'; +import { showSnackbar, useConfig } from '@openmrs/esm-framework'; +import { BillingConfig } from '../config-schema'; import { TrashCan } from '@carbon/react/icons'; -import fuzzy from 'fuzzy'; -import { type BillabeItem } from '../types'; -import { apiBasePath } from '../constants'; -import isEmpty from 'lodash-es/isEmpty'; type BillingFormProps = { patientUuid: string; closeWorkspace: () => void; }; -const BillingForm: React.FC = ({ patientUuid, closeWorkspace }) => { - const { t } = useTranslation(); - const { defaultCurrency, postBilledItems } = useConfig(); - const isTablet = useLayoutType() === 'tablet'; - - const [grandTotal, setGrandTotal] = useState(0); - const [searchOptions, setSearchOptions] = useState([]); - const [billItems, setBillItems] = useState([]); - const [searchVal, setSearchVal] = useState(''); - const [category, setCategory] = useState(''); - const [saveDisabled, setSaveDisabled] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - const [addedItems, setAddedItems] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); - const debouncedSearchTerm = useDebounce(searchTerm); - const [disableSearch, setDisableSearch] = useState(true); +type FormType = z.infer; - const toggleSearch = (choiceSelected) => { - if (!isEmpty(choiceSelected)) { - setDisableSearch(false); - } - setCategory(choiceSelected === 'Stock Item' ? 'Stock Item' : 'Service'); - }; - - const billItemSchema = z.object({ - Qnty: z.number().min(1, t('quantityGreaterThanZero', 'Quantity must be at least one for all items.')), // zod logic +const BillingForm: React.FC = ({ closeWorkspace }) => { + const { t } = useTranslation(); + const patientUuid = getPatientUuidFromUrl(); + const { billableServices, error, isLoading } = useBillableServices(); + const [searchVal, setsearchVal] = useState(''); + const { cashPointUuid, cashierUuid } = useConfig(); + + const form = useForm({ + resolver: zodResolver(billingFormSchema), + defaultValues: { + cashPoint: cashPointUuid, + cashier: cashierUuid, + patient: patientUuid, + status: 'PENDING', + lineItems: [], + payments: [], + }, }); - const calculateTotal = (event, itemName) => { - const quantity = parseInt(event.target.value); - let isValid = true; - + const onSubmit = async (values: FormType) => { try { - billItemSchema.parse({ Qnty: quantity }); - } catch (error) { - isValid = false; - const parsedErrorMessage = JSON.parse(error.message); - showToast({ - title: t('billItems', 'Save Bill'), - kind: 'error', - description: parsedErrorMessage[0].message, + await processBillItems(values); + mutate((key) => typeof key === 'string' && key.startsWith(`/ws/rest/v1/cashier/bill`), undefined, { + revalidate: true, }); - } - - const updatedItems = billItems.map((item) => { - if (item.Item.toLowerCase().includes(itemName.toLowerCase())) { - return { ...item, Qnty: quantity, Total: quantity > 0 ? item.Price * quantity : 0 }; - } - return item; - }); - - const anyInvalidQuantity = updatedItems.some((item) => item.Qnty <= 0); - - setSaveDisabled(!isValid || anyInvalidQuantity); - - const updatedGrandTotal = updatedItems.reduce((acc, item) => acc + item.Total, 0); - setGrandTotal(updatedGrandTotal); - }; - - const calculateTotalAfterAddBillItem = (items) => { - const sum = items.reduce((acc, item) => acc + item.Price * item.Qnty, 0); - setGrandTotal(sum); - }; - - const addItemToBill = (event, itemid, itemname, itemcategory, itemPrice) => { - const existingItemIndex = billItems.findIndex((item) => item.uuid === itemid); - - let updatedItems = []; - if (existingItemIndex >= 0) { - updatedItems = billItems.map((item, index) => { - if (index === existingItemIndex) { - const updatedQuantity = item.Qnty + 1; - return { ...item, Qnty: updatedQuantity, Total: updatedQuantity * item.Price }; - } - return item; + showSnackbar({ + title: t('billItems', 'Save Bill'), + subtitle: 'Bill processing has been successful', + kind: 'success', + timeoutInMs: 3000, }); - } else { - const newItem = { - uuid: itemid, - Item: itemname, - Qnty: 1, - Price: itemPrice, - Total: itemPrice, - category: itemcategory, - }; - updatedItems = [...billItems, newItem]; - setAddedItems([...addedItems, newItem]); + closeWorkspace(); + } catch (e) { + showSnackbar({ title: 'Bill processing error', kind: 'error', subtitle: e }); } - - setBillItems(updatedItems); - calculateTotalAfterAddBillItem(updatedItems); - (document.getElementById('searchField') as HTMLInputElement).value = ''; - }; - - const removeItemFromBill = (uuid) => { - const updatedItems = billItems.filter((item) => item.uuid !== uuid); - setBillItems(updatedItems); - - // Update the list of added items - setAddedItems(addedItems.filter((item) => item.uuid !== uuid)); - - const updatedGrandTotal = updatedItems.reduce((acc, item) => acc + item.Total, 0); - setGrandTotal(updatedGrandTotal); }; - const { data, error, isLoading, isValidating } = useFetchSearchResults(debouncedSearchTerm, category); - - const handleSearchTermChange = (e: React.ChangeEvent) => setSearchTerm(e.target.value); - - const filterItems = useMemo(() => { - if (!debouncedSearchTerm || isLoading || error) { - return []; - } - - const res = data as { results: BillabeItem[] }; - const existingItemUuids = new Set(billItems.map((item) => item.uuid)); - - const preprocessedData = res?.results - ?.map((item) => { - return { - uuid: item.uuid || '', - Item: item.commonName ? item.commonName : item.name, - Qnty: 1, - Price: item.commonName ? 10 : item.servicePrices[0]?.price, - Total: item.commonName ? 10 : item.servicePrices[0]?.price, - category: item.commonName ? 'StockItem' : 'Service', - }; - }) - .filter((item) => !existingItemUuids.has(item.uuid)); - - return debouncedSearchTerm - ? fuzzy - .filter(debouncedSearchTerm, preprocessedData, { - extract: (o) => `${o.Item}`, - }) - .sort((r1, r2) => r1.score - r2.score) - .map((result) => result.original) - : searchOptions; - }, [debouncedSearchTerm, data, billItems]); - - useEffect(() => { - setSearchOptions(filterItems); - }, [filterItems]); - - const postBillItems = () => { - setIsSubmitting(true); - const bill = { - cashPoint: postBilledItems.cashPoint, - cashier: postBilledItems.cashier, - lineItems: [], - payments: [], - patient: patientUuid, - status: 'PENDING', - }; - - billItems.forEach((item) => { - let lineItem: any = { - quantity: parseInt(item.Qnty), - price: item.Price, - priceName: 'Default', - priceUuid: postBilledItems.priceUuid, - lineItemOrder: 0, - paymentStatus: 'PENDING', - }; - - if (item.category === 'StockItem') { - lineItem.item = item.uuid; - } else { - lineItem.billableService = item.uuid; - } - - bill?.lineItems.push(lineItem); - }); - - const url = `${apiBasePath}bill`; - processBillItems(bill).then( - () => { - setIsSubmitting(false); - - 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) => { - setIsSubmitting(false); - showSnackbar({ title: 'Bill processing error', kind: 'error', subtitle: error?.message }); - }, + const handleSearch = async (searchText: string) => { + setsearchVal(searchText); + return billableServices.filter( + (service) => + service?.name.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()) && + lineItemsOnservable.findIndex((item) => item.billableService === service?.uuid) === -1, ); }; - const handleClearSearchTerm = () => { - setSearchOptions([]); - }; + const lineItemsOnservable = form.watch('lineItems'); return ( -
-
- - - - - - - - + + + setsearchVal('')} + getDisplayValue={(item: BillingService) => item.name} + getFieldValue={(item: BillingService) => item.uuid} + getSearchResults={handleSearch} + onSuggestionSelected={(field, value) => { + if (value) { + form.setValue('lineItems', [ + ...lineItemsOnservable, + { + billableService: value, + lineItemOrder: 0, + quantity: 1, + price: 0, + paymentStatus: 'PENDING', + priceName: 'Default', + }, + ]); + } + setsearchVal(''); + }} + labelText={t('search', 'Search')} + placeholder={t('searchPlaceHolder', 'Find your billables here...')} /> - - -
    - {searchOptions?.length > 0 && - searchOptions?.map((row) => ( -
  • - -
  • - ))} - - {searchOptions?.length === 0 && !isLoading && !!debouncedSearchTerm && ( -

    {t('noResultsFound', 'No results found')}

    - )} -
-
- - + + +
Item Quantity + Payment Method Price Total - Action + - {billItems && Array.isArray(billItems) ? ( - billItems.map((row) => ( + {lineItemsOnservable.map(({ billableService, quantity, price }, index) => { + const service = billableServices.find((serv) => serv.uuid === billableService); + return ( - {row.Item} + {service?.name} - { - calculateTotal(e, row.Item); - row.Qnty = e.target.value; - }} + ( + { + field.onChange(value); + }} + type="number" + className="form-control" + id={billableService} + min={1} + max={100} + /> + )} /> - {row.Price} - - {row.Total} - - removeItemFromBill(row.uuid)} className={styles.removeButton} /> + ( + { + field.onChange(e.selectedItem); + const price = service?.servicePrices.find((p) => p.uuid === e.selectedItem)?.price; + form.setValue(`lineItems.${index}.price`, price ?? 0); + }} + selectedItem={field.value} + label="Choose method" + items={service?.servicePrices.map((r) => r.uuid) ?? []} + itemToString={(item) => service?.servicePrices.find((r) => r.uuid === item)?.name ?? ''} + /> + )} + /> + + {price} + {price * quantity} + +
-
- - - - - -
+ + + + + + +
); }; diff --git a/packages/esm-billing-app/src/billing-form/billing-form.scss b/packages/esm-billing-app/src/billing-form/billing-form.scss index c54c8b9..49e9f8f 100644 --- a/packages/esm-billing-app/src/billing-form/billing-form.scss +++ b/packages/esm-billing-app/src/billing-form/billing-form.scss @@ -1,87 +1,67 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; @use '@carbon/layout'; -@use '~@openmrs/esm-styleguide/src/vars' as *; +@use '@carbon/colors'; -.tablet { - padding: layout.$spacing-06 layout.$spacing-05; - background-color: $ui-02; +.heading { + @include type.type-style('heading-compact-01'); + margin: spacing.$spacing-05 0 spacing.$spacing-05; } -.desktop { - padding: 0rem; -} - -.button { - height: layout.$spacing-10; - display: flex; - align-content: flex-start; - align-items: baseline; - min-width: 50%; - - :global(.cds--inline-loading) { - min-height: layout.$spacing-05 !important; - } - - :global(.cds--inline-loading__text) { - font-size: unset !important; - } -} - -.mt2 { - margin-top: layout.$spacing-07; +.grid { + margin: spacing.$spacing-05 spacing.$spacing-05; + padding: spacing.$spacing-05 0rem 0rem orem; } -.searchContent { +.buttonSet { + width: 100%; + padding-top: spacing.$spacing-05; + background-color: colors.$white-0; + justify-content: space-between; + bottom: 0; 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; - cursor: pointer; -} - -.invalidInput { - border: 2px solid red; + & > button { + max-width: 50% !important; + width: 50%; + } } -.removeButton { - color: #ee0909; - right: 20px; - cursor: pointer; +.billingItem { + margin-top: spacing.$spacing-10; + overflow-x: auto; } -.form { +.formTitle { + @include type.type-style('heading-02'); display: flex; - flex-direction: column; + align-items: center; justify-content: space-between; - height: calc(100vh - 6rem); -} - -:global(.omrs-breakpoint-lt-desktop) .form { - background-color: #ededed; -} + margin: spacing.$spacing-05; + row-gap: 1.5rem; + position: relative; + + &::after { + content: ''; + display: block; + width: 2rem; + border-bottom: 0.375rem solid var(--brand-03); + position: absolute; + bottom: -0.75rem; + left: 0; + } -:global(.omrs-breakpoint-gt-tablet) .form { - background-color: $ui-02; + & > span { + @include type.type-style('body-01'); + } } -.grid { - margin: layout.$spacing-05; +.sectionHeader { + @include type.type-style('heading-02'); } - -.row { - margin: layout.$spacing-03 0rem 0rem; +.button { + height: layout.$spacing-10; display: flex; - flex-flow: row wrap; - gap: layout.$spacing-05; -} - -.spacer { - margin-top: layout.$spacing-05; + align-content: flex-start; + align-items: baseline; + min-width: 50%; } diff --git a/packages/esm-billing-app/src/billing.resource.ts b/packages/esm-billing-app/src/billing.resource.ts index a4c886f..a19a4b8 100644 --- a/packages/esm-billing-app/src/billing.resource.ts +++ b/packages/esm-billing-app/src/billing.resource.ts @@ -7,6 +7,7 @@ import { apiBasePath, omrsDateFormat } from './constants'; import { useContext } from 'react'; import SelectedDateContext from './hooks/selectedDateContext'; import dayjs from 'dayjs'; +import { z } from 'zod'; export const useBills = (patientUuid: string = '', billStatus: string = '') => { const { selectedDate } = useContext(SelectedDateContext); @@ -158,3 +159,24 @@ export const updateBillItems = (payload) => { }, }); }; + +export const billingFormSchema = z.object({ + cashPoint: z.string().uuid(), + cashier: z.string().uuid(), + patient: z.string().uuid(), + payments: z.array(z.string()), + status: z.enum(['PENDING']), + lineItems: z + .array( + z.object({ + billableService: z.string().uuid(), + quantity: z.number({ coerce: true }).min(1).max(100), + price: z.number({ coerce: true }), + priceName: z.string().optional().default('Default'), + priceUuid: z.string().uuid(), + lineItemOrder: z.number().optional().default(0), + paymentStatus: z.enum(['PENDING']), + }), + ) + .min(1), +}); diff --git a/packages/esm-billing-app/src/config-schema.ts b/packages/esm-billing-app/src/config-schema.ts index e40f524..64e6aa6 100644 --- a/packages/esm-billing-app/src/config-schema.ts +++ b/packages/esm-billing-app/src/config-schema.ts @@ -2,6 +2,8 @@ import { Type } from '@openmrs/esm-framework'; export interface BillingConfig { enforceBillPayment: boolean; + cashPointUuid: string; + cashierUuid: string; } export const configSchema = { @@ -78,6 +80,18 @@ export const configSchema = { _default: true, _description: 'Whether to enforce bill payment or not for patient to receive service', }, + + cashPointUuid: { + _type: Type.String, + _description: 'Where bill is generated from', + _default: '54065383-b4d4-42d2-af4d-d250a1fd2590', + }, + + cashierUuid: { + _type: Type.String, + _description: 'Who Generated the bill', + _default: '54065383-b4d4-42d2-af4d-d250a1fd2590', + }, }; export interface ConfigObject { diff --git a/packages/esm-billing-app/src/hooks/useBillableServices.ts b/packages/esm-billing-app/src/hooks/useBillableServices.ts new file mode 100644 index 0000000..a05f69f --- /dev/null +++ b/packages/esm-billing-app/src/hooks/useBillableServices.ts @@ -0,0 +1,18 @@ +import { FetchResponse, openmrsFetch } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { BillingService } from '../types'; + +const useBillableServices = () => { + const customPresentation = `custom:(uuid,name,shortName,serviceStatus,serviceType:(display),servicePrices:(uuid,name,price,paymentMode))`; + const url = `/ws/rest/v1/billing/billableService?v=${customPresentation}`; + + const { data, error, isLoading } = useSWR }>>(url, openmrsFetch); + + return { + error, + isLoading, + billableServices: data?.data?.results ?? [], + }; +}; + +export default useBillableServices; diff --git a/packages/esm-billing-app/src/types/index.ts b/packages/esm-billing-app/src/types/index.ts index b5dbc4d..bbba7fd 100644 --- a/packages/esm-billing-app/src/types/index.ts +++ b/packages/esm-billing-app/src/types/index.ts @@ -194,3 +194,13 @@ export interface BillableService { price: number; }>; } + +export type BillingService = { + name: string; + servicePrices: Array<{ name: string; paymentMode: { uuid: string; name: string }; price: number; uuid: string }>; + serviceStatus: string; + serviceType: { display: string }; + shortName: string; + uuid: string; + stockItem?: string; +}; \ No newline at end of file