diff --git a/kenyaemr-esm-3.x b/kenyaemr-esm-3.x new file mode 160000 index 000000000..a0a11015b --- /dev/null +++ b/kenyaemr-esm-3.x @@ -0,0 +1 @@ +Subproject commit a0a11015b4da58ebdb873afe6d61c8c2ff125688 diff --git a/packages/esm-billing-app/src/billable-services/billable-services.component.tsx b/packages/esm-billing-app/src/billable-services/billable-services.component.tsx index 58053c2b0..8c8b2d263 100644 --- a/packages/esm-billing-app/src/billable-services/billable-services.component.tsx +++ b/packages/esm-billing-app/src/billable-services/billable-services.component.tsx @@ -24,12 +24,13 @@ import { ErrorState, navigate, WorkspaceContainer, + launchWorkspace, } 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'; +import { ArrowRight, Edit } from '@carbon/react/icons'; const BillableServices = () => { const { t } = useTranslation(); @@ -45,6 +46,12 @@ const BillableServices = () => { const [showOverlay, setShowOverlay] = useState(false); const [overlayHeader, setOverlayTitle] = useState(''); + const handleEditClick = (service) => { + launchWorkspace('update-billable-services-workspace', { + service, + }); + }; + const headerData = [ { header: t('serviceName', 'Service Name'), @@ -103,7 +110,7 @@ const BillableServices = () => { serviceType: service?.serviceType?.display, status: service.serviceStatus, prices: '--', - actions: '--', + actions: handleEditClick(service)} style={{ cursor: 'pointer' }} />, }; let cost = ''; service.servicePrices.forEach((price) => { @@ -138,6 +145,7 @@ const BillableServices = () => { return ( <> + {billableServices?.length > 0 ? (
!isNaN(Number(value)), 'Value must be a number') + .refine((value) => parseInt(value) > 0, 'Price should be a number more than zero') + .refine((value) => !!value, 'Price is required'), +}); + +const paymentFormSchema = z.object({ + payment: z.array(servicePriceSchema).min(1, 'At least one payment method is required'), + serviceName: z.string({ + required_error: 'Service name is required', + }), + shortName: z.string({ required_error: 'A valid short name is required.' }), + serviceTypeName: z.string({ required_error: 'A service type is required' }), + concept: z.string({ required_error: 'Concept search is required.' }), +}); + +type FormData = z.infer; +const DEFAULT_PAYMENT_OPTION = { paymentMode: '', price: '1' }; + +const UpdateBillableServicesDialog: React.FC<{ closeWorkspace: () => void; service: any }> = ({ + closeWorkspace, + service, +}) => { + const { t } = useTranslation(); + + const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes(); + const { serviceTypes, isLoading: isLoadingServicesTypes } = useServiceTypes(); + + const { + control, + handleSubmit, + formState: { errors, isValid }, + setValue, + } = useForm({ + mode: 'all', + resolver: zodResolver(paymentFormSchema), + }); + + const { fields, remove, append } = useFieldArray({ name: 'payment', control: control }); + + const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT_OPTION), [append]); + const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]); + + const isTablet = useLayoutType() === 'tablet'; + const searchInputRef = useRef(null); + const handleSearchTermChange = (event: React.ChangeEvent) => setSearchTerm(event.target.value); + + const [selectedConcept, setSelectedConcept] = useState(service?.concept); + const [searchTerm, setSearchTerm] = useState(''); + const debouncedSearchTerm = useDebounce(searchTerm); + const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm); + + const handleConceptChange = useCallback((selectedConcept: ServiceConcept) => { + setSelectedConcept(selectedConcept); + }, []); + + const handleNavigateToServiceDashboard = () => + navigate({ + to: window.getOpenmrsSpaBase() + 'billable-services', + }); + + const onSubmit = (data: FormData) => { + const payload: any = {}; + + let servicePrices = data.payment + ? data.payment.map((element) => { + return { + name: paymentModes.find((p) => p.uuid === element.paymentMode)?.name || '', + price: element.price.toString(), + paymentMode: element.paymentMode, + }; + }) + : []; + + payload.name = data.serviceName; + payload.shortName = data.shortName; + payload.serviceType = data.serviceTypeName; + payload.servicePrices = servicePrices; + payload.serviceStatus = 'ENABLED'; + payload.concept = selectedConcept?.concept?.uuid; + payload.uuid = service.uuid; + + createBillableService(payload).then( + (resp) => { + showSnackbar({ + title: t('billableService', 'Billable service'), + subtitle: 'Billable service updated successfully', + kind: 'success', + isLowContrast: true, + timeoutInMs: 3000, + }); + closeWorkspace(); + }, + (error) => { + showSnackbar({ + title: 'Error updating billable service', + kind: 'error', + subtitle: extractErrorMessagesFromResponse(error.responseBody), + isLowContrast: true, + }); + }, + ); + }; + + useEffect(() => { + if (service) { + setValue('serviceName', service.name); + setValue('shortName', service.shortName); + setValue('serviceTypeName', service.serviceType.display); + setValue('concept', service.concept?.display || ''); + setValue( + 'payment', + service.servicePrices.map((price) => ({ + paymentMode: paymentModes.find((mode) => mode.uuid === price.paymentMode), + price: price.price.toString(), + })), + ); + } + }, [service, setValue, paymentModes]); + + if (isLoadingServicesTypes || isLoadingPaymentModes) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ + ( + + + + )} + /> + + + ( + + + + )} + /> + + + ( + (item ? item.display : '')} + placeholder="Select service type" + required + {...field} + onChange={({ selectedItem }) => field.onChange(selectedItem ? selectedItem.display : '')} + invalidText={errors.serviceTypeName?.message || ''} + invalid={!!errors.serviceTypeName} + /> + )} + /> + + Associated Concept + ( + + { + onChange(e); + handleSearchTermChange(e); + }} + renderIcon={errors?.concept && } + onBlur={onBlur} + onClear={() => { + setSearchTerm(''); + setSelectedConcept(null); + }} + value={(() => { + if (selectedConcept) { + return selectedConcept.display; + } + if (debouncedSearchTerm) { + return value; + } + })()} + /> + + )} + /> + {(() => { + if (!debouncedSearchTerm || selectedConcept) { + return null; + } + if (isSearching) { + return ; + } + if (searchResults && searchResults.length) { + return ( +
    + {searchResults?.map((searchResult, index) => ( +
  • handleConceptChange(searchResult)}> + {searchResult.display} +
  • + ))} +
+ ); + } + return ( + + + + {t('noResultsFor', 'No results for')} "{debouncedSearchTerm}" + + + + ); + })()} + {fields.map((field, index) => ( +
+ ( + + field.onChange(selectedItem?.uuid)} + titleText={t('paymentMode', 'Payment Mode')} + label={t('selectPaymentMethod', 'Select payment method')} + items={paymentModes ?? []} + itemToString={(item) => (item ? item.name : '')} + invalid={!!errors?.payment?.[index]?.paymentMode} + invalidText={errors?.payment?.[index]?.paymentMode?.message} + initialSelectedItem={paymentModes.find((mode) => mode.uuid === field.value)} + /> + + )} + /> + ( + + + + )} + /> +
+
+ handleRemovePaymentMode(index)} + className={styles.removeButton} + size={20} + /> +
+
+ ))} +
+ +
+
+ + +
+
+
+ ); +}; + +function ResponsiveWrapper({ children, isTablet }: { children: React.ReactNode; isTablet: boolean }) { + return isTablet ? {children} : <>{children}; +} + +export default UpdateBillableServicesDialog; diff --git a/packages/esm-billing-app/src/billable-services/create-edit/update-billable-services.scss b/packages/esm-billing-app/src/billable-services/create-edit/update-billable-services.scss new file mode 100644 index 000000000..466c65b0c --- /dev/null +++ b/packages/esm-billing-app/src/billable-services/create-edit/update-billable-services.scss @@ -0,0 +1,38 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; + +.radioButton { + padding: spacing.$spacing-02 spacing.$spacing-02; + margin: spacing.$spacing-03 0; +} + +.section { + margin: spacing.$spacing-03; +} + +.sectionTitle { + margin-bottom: spacing.$spacing-04; +} + +.modalBody { + padding-bottom: spacing.$spacing-05; +} + +.container { + display: 'flex'; + align-items: 'center'; + justify-content: 'space-between'; + align-content: 'stretch'; +} + +.specimenContainer { + display: 'flex'; + align-items: 'center'; + justify-content: 'space-between'; + column-gap: '10px'; +} + +.inputText { + display: 'flex'; + align-items: 'center'; +} diff --git a/packages/esm-billing-app/src/index.ts b/packages/esm-billing-app/src/index.ts index f62057bea..590f54d3f 100644 --- a/packages/esm-billing-app/src/index.ts +++ b/packages/esm-billing-app/src/index.ts @@ -22,6 +22,7 @@ import { DeleteBillModal } from './billable-services/bill-manager/modals/delete- import PriceInfoOrder from './billable-services/billiable-item/test-order/price-info-order.componet'; import ProcedureOrder from './billable-services/billiable-item/test-order/procedure-order.component'; import ImagingOrder from './billable-services/billiable-item/test-order/imaging-order.component'; +import UpdateBillableServicesDialog from './billable-services/create-edit/update-billable-service.component'; const moduleName = '@kenyaemr/esm-billing-app'; @@ -62,15 +63,4 @@ export const procedureOrder = getSyncLifecycle(ProcedureOrder, options); export const imagingOrder = getSyncLifecycle(ImagingOrder, options); export const drugOrder = getSyncLifecycle(DrugOrder, options); export const testOrderAction = getSyncLifecycle(TestOrderAction, options); - -// bill manager modals -export const cancelBillModal = getSyncLifecycle(CancelBillModal, options); -export const deleteBillModal = getSyncLifecycle(DeleteBillModal, options); - -// bill manager extensions -export const waiveBillForm = getSyncLifecycle(WaiveBillForm, options); -export const editBillForm = getSyncLifecycle(EditBillForm, options); - -export function startupApp() { - defineConfigSchema(moduleName, configSchema); -} +export const updateBillableServicesWorkspace = getSyncLifecycle(UpdateBillableServicesDialog, options); diff --git a/packages/esm-billing-app/src/routes.json b/packages/esm-billing-app/src/routes.json index 168c9c269..bce529736 100644 --- a/packages/esm-billing-app/src/routes.json +++ b/packages/esm-billing-app/src/routes.json @@ -118,6 +118,15 @@ "slot": "tests-ordered-actions-slot", "order": 0 } + + ], + "workspaces": [ + { + "name": "update-billable-services-workspace", + "title": "Update Billable Services", + "component": "updateBillableServicesWorkspace", + "type": "workspace" + } ], "workspaces": [ {