From aea46ba1fe9a8a0d6109af86c4383b1425f71d15 Mon Sep 17 00:00:00 2001 From: Patrick Waweru <97954453+PatrickWaweru@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:22:47 +0300 Subject: [PATCH] O3-4284 - feat/Print item transactions (Bin Card and Stock Card) - Stock Card (#250) * first strike * Added Bin Card Printout on Item Transactions * Fixed recommanded areas * Fixed logo printing * added stock card printout * dedup * cleanup --- .../types/stockItem/StockItemTransaction.ts | 2 + src/index.ts | 9 +- src/routes.json | 8 +- ...-bincard-transaction-header.component.tsx} | 4 +- ...stockcard-transaction-header.component.tsx | 52 ++++++ ...ansactions-bincard-printout.component.tsx} | 8 +- .../transactions-print-action.component.tsx | 67 ++++++- ...nsactions-print-bincard-preview.modal.tsx} | 9 +- ...sactions-print-stockcard-preview.modal.tsx | 41 +++++ ...nsactions-stockcard-printout.component.tsx | 170 ++++++++++++++++++ .../transactions/transactions.component.tsx | 6 +- .../transactions/transactions.resource.tsx | 4 +- src/stock-items/stock-items.resource.ts | 1 + src/stock-lookups/stock-lookups.resource.ts | 8 + 14 files changed, 366 insertions(+), 23 deletions(-) rename src/stock-items/add-stock-item/transactions/printout/{printable-transaction-header.component.tsx => printable-bincard-transaction-header.component.tsx} (96%) create mode 100644 src/stock-items/add-stock-item/transactions/printout/printable-stockcard-transaction-header.component.tsx rename src/stock-items/add-stock-item/transactions/printout/{transactions-printout.component.tsx => transactions-bincard-printout.component.tsx} (86%) rename src/stock-items/add-stock-item/transactions/printout/{transactions-print-preview.modal.tsx => transactions-print-bincard-preview.modal.tsx} (71%) create mode 100644 src/stock-items/add-stock-item/transactions/printout/transactions-print-stockcard-preview.modal.tsx create mode 100644 src/stock-items/add-stock-item/transactions/printout/transactions-stockcard-printout.component.tsx diff --git a/src/core/api/types/stockItem/StockItemTransaction.ts b/src/core/api/types/stockItem/StockItemTransaction.ts index 0eaede87..1e8e0848 100644 --- a/src/core/api/types/stockItem/StockItemTransaction.ts +++ b/src/core/api/types/stockItem/StockItemTransaction.ts @@ -20,4 +20,6 @@ export interface StockItemTransactionDTO { packagingUomName: string; operationSourcePartyName: string; operationDestinationPartyName: string; + patientId: number; + patientUuid: string; } diff --git a/src/index.ts b/src/index.ts index b9ad886e..71c570ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,9 @@ import StockSources from './stock-sources/stock-sources.component'; import StockLocations from './stock-locations/stock-locations.component'; import StockReports from './stock-reports/report-list/stock-reports.component'; import StockSettings from './stock-settings/stock-settings.component'; -import TransactionsPrintPreview from './stock-items/add-stock-item/transactions/printout/transactions-print-preview.modal'; +import TransactionsBincardPrintPreview from './stock-items/add-stock-item/transactions/printout/transactions-print-bincard-preview.modal'; +import TransactionsStockcardPrintPreview from './stock-items/add-stock-item/transactions/printout/transactions-print-stockcard-preview.modal'; + const moduleName = '@openmrs/esm-stock-management-app'; const options = { @@ -127,7 +129,10 @@ export const deletePackagingUnitButton = getSyncLifecycle(deletePackagingUnitMod }); export const stockManagementAppMenuItem = getSyncLifecycle(appMenu, options); -export const transactionPrintPreviewModal = getSyncLifecycle(TransactionsPrintPreview, options) + +export const transactionBincardPrintPreviewModal = getSyncLifecycle(TransactionsBincardPrintPreview, options); +export const transactionStockcardPrintPreviewModal = getSyncLifecycle(TransactionsStockcardPrintPreview, options); + export function startupApp() { defineConfigSchema(moduleName, configSchema); } diff --git a/src/routes.json b/src/routes.json index a0c6c01e..76b41aa7 100644 --- a/src/routes.json +++ b/src/routes.json @@ -200,8 +200,12 @@ "component": "stockOperationModal" }, { - "name": "transactions-print-preview-modal", - "component": "transactionPrintPreviewModal" + "name": "transactions-print-bincard-preview-modal", + "component": "transactionBincardPrintPreviewModal" + }, + { + "name": "transactions-print-stockcard-preview-modal", + "component": "transactionStockcardPrintPreviewModal" } ], "pages": [ diff --git a/src/stock-items/add-stock-item/transactions/printout/printable-transaction-header.component.tsx b/src/stock-items/add-stock-item/transactions/printout/printable-bincard-transaction-header.component.tsx similarity index 96% rename from src/stock-items/add-stock-item/transactions/printout/printable-transaction-header.component.tsx rename to src/stock-items/add-stock-item/transactions/printout/printable-bincard-transaction-header.component.tsx index b741f002..f3e120cd 100644 --- a/src/stock-items/add-stock-item/transactions/printout/printable-transaction-header.component.tsx +++ b/src/stock-items/add-stock-item/transactions/printout/printable-bincard-transaction-header.component.tsx @@ -8,7 +8,7 @@ interface PrintableTransactionHeaderProps { itemName: string; } -const PrintableTransactionHeader: React.FC = ({ itemName }) => { +const PrintableBincardTransactionHeader: React.FC = ({ itemName }) => { const { t } = useTranslation(); const { logo } = useConfig({ externalModuleName: '@kenyaemr/esm-login-app' }); @@ -49,4 +49,4 @@ const PrintableTransactionHeader: React.FC = ({ ); }; -export default PrintableTransactionHeader; +export default PrintableBincardTransactionHeader; diff --git a/src/stock-items/add-stock-item/transactions/printout/printable-stockcard-transaction-header.component.tsx b/src/stock-items/add-stock-item/transactions/printout/printable-stockcard-transaction-header.component.tsx new file mode 100644 index 00000000..42a5a6dc --- /dev/null +++ b/src/stock-items/add-stock-item/transactions/printout/printable-stockcard-transaction-header.component.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import styles from './printable-transaction-header.scss'; +import { useConfig } from '@openmrs/esm-framework'; +import { useTranslation } from 'react-i18next'; +import startCase from 'lodash-es/startCase'; + +interface PrintableTransactionHeaderProps { + itemName: string; +} + +const PrintableStockcardTransactionHeader: React.FC = ({ itemName }) => { + const { t } = useTranslation(); + const { logo } = useConfig({ externalModuleName: '@kenyaemr/esm-login-app' }); + + return ( +
+
+

{t('bincard', 'Stock Card')}

+ {logo?.src ? ( + {logo.alt} + ) : logo?.name ? ( + logo.name + ) : ( + // OpenMRS Logo + + + + )} +
+ +
+
+

{t('itemname', 'Item Name')}

+

{startCase(itemName)}

+
+
+
+ ); +}; + +export default PrintableStockcardTransactionHeader; diff --git a/src/stock-items/add-stock-item/transactions/printout/transactions-printout.component.tsx b/src/stock-items/add-stock-item/transactions/printout/transactions-bincard-printout.component.tsx similarity index 86% rename from src/stock-items/add-stock-item/transactions/printout/transactions-printout.component.tsx rename to src/stock-items/add-stock-item/transactions/printout/transactions-bincard-printout.component.tsx index 12a3a927..c2293fca 100644 --- a/src/stock-items/add-stock-item/transactions/printout/transactions-printout.component.tsx +++ b/src/stock-items/add-stock-item/transactions/printout/transactions-bincard-printout.component.tsx @@ -10,7 +10,7 @@ import { TableCell, } from '@carbon/react'; import { useStockItem } from '../../../stock-items.resource'; -import PrintableTransactionHeader from './printable-transaction-header.component'; +import PrintableBincardTransactionHeader from './printable-bincard-transaction-header.component'; import PrintableTransactionFooter from './printable-transaction-footer.component'; import styles from './printable-transaction.scss'; @@ -20,10 +20,10 @@ type Props = { data: any; }; -const TransactionsPrintout: React.FC = ({ columns, data, title }) => { +const TransactionsBincardPrintout: React.FC = ({ columns, data, title }) => { return (
- +
@@ -61,4 +61,4 @@ const TransactionsPrintout: React.FC = ({ columns, data, title }) => { ); }; -export default TransactionsPrintout; +export default TransactionsBincardPrintout; diff --git a/src/stock-items/add-stock-item/transactions/printout/transactions-print-action.component.tsx b/src/stock-items/add-stock-item/transactions/printout/transactions-print-action.component.tsx index 0bdaf78f..3a2d2e67 100644 --- a/src/stock-items/add-stock-item/transactions/printout/transactions-print-action.component.tsx +++ b/src/stock-items/add-stock-item/transactions/printout/transactions-print-action.component.tsx @@ -5,6 +5,9 @@ import { useTranslation } from 'react-i18next'; import { useStockItem } from '../../../stock-items.resource'; import { showModal } from '@openmrs/esm-framework'; import styles from './printable-transaction.scss'; +import { useEffect, useMemo, useState } from 'react'; +import { StockItemInventoryFilter, useStockItemTransactions } from '../../../stock-items.resource'; +import { ResourceRepresentation } from '../../../../core/api/api'; type Props = { itemUuid: string; @@ -15,11 +18,56 @@ type Props = { const TransactionsPrintAction: React.FC = ({ columns, data, itemUuid }) => { const { t } = useTranslation(); + const [stockCardItemFilter, setStockCardItemFilter] = useState({ + startIndex: 0, + totalCount: true, + v: ResourceRepresentation.Full, + isPatientTransaction: 'true', + }); + const { item: stockItem, isLoading: isStockItemLoading } = useStockItem(itemUuid); + const { items: stockCardData, isLoading: isStockCardLoading, error } = useStockItemTransactions(stockCardItemFilter); + + const stockCardHeaders = useMemo( + () => [ + { + key: 'patientId', + header: 'Patient ID', + }, + { + key: 'patientName', + header: 'Patient Name', + }, + { + key: 'patientIdentifier', + header: 'Patient Identifier', + }, + { + key: 'date', + header: 'Date', + }, + { + key: 'location', + header: 'Location', + }, + { + key: 'transaction', + header: 'Transaction', + }, + { + key: 'totalout', + header: 'OUT', + }, + { + key: 'batch', + header: 'Batch', + }, + ], + [], + ); - const handleClick = () => { - // stockItem.drugName || stockItem.conceptName || '' - const dispose = showModal('transactions-print-preview-modal', { + const handleBincardClick = () => { + const dispose = showModal('transactions-print-bincard-preview-modal', { onClose: () => dispose(), title: stockItem.drugName || stockItem.conceptName || '', columns, @@ -27,6 +75,15 @@ const TransactionsPrintAction: React.FC = ({ columns, data, itemUuid }) = }); }; + const handleStockcardClick = () => { + const dispose = showModal('transactions-print-stockcard-preview-modal', { + onClose: () => dispose(), + title: stockItem.drugName || stockItem.conceptName || '', + columns: stockCardHeaders, + data: stockCardData.results, + }); + }; + return ( <> @@ -34,12 +91,14 @@ const TransactionsPrintAction: React.FC = ({ columns, data, itemUuid }) = label={t('printStockCard', 'Print Stock Card')} renderIcon={(props) => } iconDescription="Print Stock Card" + onClick={handleStockcardClick} + disabled={isStockItemLoading || isStockCardLoading} /> } iconDescription="Print Bin Card" - onClick={handleClick} + onClick={handleBincardClick} disabled={isStockItemLoading} /> diff --git a/src/stock-items/add-stock-item/transactions/printout/transactions-print-preview.modal.tsx b/src/stock-items/add-stock-item/transactions/printout/transactions-print-bincard-preview.modal.tsx similarity index 71% rename from src/stock-items/add-stock-item/transactions/printout/transactions-print-preview.modal.tsx rename to src/stock-items/add-stock-item/transactions/printout/transactions-print-bincard-preview.modal.tsx index 5dfbecdd..e841e72e 100644 --- a/src/stock-items/add-stock-item/transactions/printout/transactions-print-preview.modal.tsx +++ b/src/stock-items/add-stock-item/transactions/printout/transactions-print-bincard-preview.modal.tsx @@ -1,5 +1,5 @@ import React, { useRef } from 'react'; -import TransactionsPrintout from './transactions-printout.component'; +import TransactionsBincardPrintout from './transactions-bincard-printout.component'; import { ModalBody, ModalHeader, ModalFooter, Button } from '@carbon/react'; import { useTranslation } from 'react-i18next'; import { useReactToPrint } from 'react-to-print'; @@ -8,9 +8,10 @@ type Props = { title?: string; columns: any; data: any; + binOrStockCard: number; }; -const TransactionsPrintPreview: React.FC = ({ onClose, title, columns, data }) => { +const TransactionsBincardPrintPreview: React.FC = ({ onClose, title, columns, data, binOrStockCard }) => { const { t } = useTranslation(); const ref = useRef(null); @@ -22,7 +23,7 @@ const TransactionsPrintPreview: React.FC = ({ onClose, title, columns, da
- +
@@ -37,4 +38,4 @@ const TransactionsPrintPreview: React.FC = ({ onClose, title, columns, da ); }; -export default TransactionsPrintPreview; +export default TransactionsBincardPrintPreview; diff --git a/src/stock-items/add-stock-item/transactions/printout/transactions-print-stockcard-preview.modal.tsx b/src/stock-items/add-stock-item/transactions/printout/transactions-print-stockcard-preview.modal.tsx new file mode 100644 index 00000000..eda57426 --- /dev/null +++ b/src/stock-items/add-stock-item/transactions/printout/transactions-print-stockcard-preview.modal.tsx @@ -0,0 +1,41 @@ +import React, { useRef } from 'react'; +import TransactionsBincardPrintout from './transactions-bincard-printout.component'; +import TransactionsStockcardPrintout from './transactions-stockcard-printout.component'; +import { ModalBody, ModalHeader, ModalFooter, Button } from '@carbon/react'; +import { useTranslation } from 'react-i18next'; +import { useReactToPrint } from 'react-to-print'; +type Props = { + onClose?: () => void; + title?: string; + columns: any; + data: any; +}; + +const TransactionsStockcardPrintPreview: React.FC = ({ onClose, title, columns, data }) => { + const { t } = useTranslation(); + const ref = useRef(null); + + const handlePrint = useReactToPrint({ + content: () => ref.current, + }); + return ( + <> + + +
+ +
+
+ + + + + + ); +}; + +export default TransactionsStockcardPrintPreview; diff --git a/src/stock-items/add-stock-item/transactions/printout/transactions-stockcard-printout.component.tsx b/src/stock-items/add-stock-item/transactions/printout/transactions-stockcard-printout.component.tsx new file mode 100644 index 00000000..ae5eec5a --- /dev/null +++ b/src/stock-items/add-stock-item/transactions/printout/transactions-stockcard-printout.component.tsx @@ -0,0 +1,170 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { + DataTable, + TableContainer, + Table, + TableHead, + TableRow, + TableHeader, + TableBody, + TableCell, +} from '@carbon/react'; +import { useStockItem } from '../../../stock-items.resource'; +import PrintableBincardTransactionHeader from './printable-bincard-transaction-header.component'; +import PrintableStockcardTransactionHeader from './printable-stockcard-transaction-header.component'; +import PrintableTransactionFooter from './printable-transaction-footer.component'; +import styles from './printable-transaction.scss'; +import StockOperationReference from '../../../../stock-operations/add-stock-operation/stock-operation-reference.component'; +import { formatDisplayDate } from '../../../../core/utils/datetimeUtils'; +import { ArrowLeft } from '@carbon/react/icons'; +import { usePatient } from '../../../../stock-lookups/stock-lookups.resource'; +import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; + +type Props = { + title: string; + columns: any; + items: any; +}; + +const TransactionsStockcardPrintout: React.FC = ({ columns, items, title }) => { + const [mappedData, setMappedData] = useState([]); + const [patientData, setPatientData] = useState({}); + + useEffect(() => { + const fetchPatients = async () => { + const patientPromises = items + .filter((item) => item.patientUuid) // Only fetch for items with patientUuid + .map(async (item) => { + const customePresentation = 'custom:(uuid,display,identifiers,links)'; + const url = `${restBaseUrl}/patient/${item.patientUuid}?v=${customePresentation}`; + const response = await openmrsFetch(url); // Assume `openmrsFetch` is a fetch utility + return { uuid: item.patientUuid, data: response.data }; + }); + + const resolvedPatients = await Promise.all(patientPromises); + + // Map patient UUIDs to their data + const patientMap = {}; + resolvedPatients.forEach((patient) => { + patientMap[patient.uuid] = patient.data; + }); + + setPatientData(patientMap); // Save patient data in state + }; + + fetchPatients(); + }, [items]); + + useEffect(() => { + // Map items with patient data + const data = items.map((stockItemTransaction) => { + const patient = stockItemTransaction.patientUuid ? patientData[stockItemTransaction.patientUuid] : null; + + return { + ...stockItemTransaction, + id: stockItemTransaction?.uuid, + key: `key-${stockItemTransaction?.uuid}`, + uuid: `${stockItemTransaction?.uuid}`, + date: formatDisplayDate(stockItemTransaction?.dateCreated), + location: + stockItemTransaction.operationSourcePartyName && stockItemTransaction.operationDestinationPartyName ? ( + stockItemTransaction.operationSourcePartyName === stockItemTransaction?.partyName ? ( + stockItemTransaction.quantity > 0 ? ( + <> + {stockItemTransaction.operationSourcePartyName} + {stockItemTransaction.operationDestinationPartyName} + + ) : ( + <> + {stockItemTransaction.operationSourcePartyName} + {stockItemTransaction.operationDestinationPartyName} + + ) + ) : stockItemTransaction.operationDestinationPartyName === stockItemTransaction?.partyName ? ( + stockItemTransaction.quantity > 0 ? ( + <> + {stockItemTransaction.operationDestinationPartyName} + {stockItemTransaction.operationSourcePartyName} + + ) : ( + <> + {stockItemTransaction.operationDestinationPartyName} + {stockItemTransaction.operationSourcePartyName} + + ) + ) : ( + stockItemTransaction?.partyName + ) + ) : ( + stockItemTransaction?.partyName + ), + transaction: stockItemTransaction?.isPatientTransaction + ? 'Patient Dispense' + : stockItemTransaction.stockOperationTypeName, + quantity: `${stockItemTransaction?.quantity?.toLocaleString()} ${stockItemTransaction?.packagingUomName ?? ''}`, + batch: stockItemTransaction.stockBatchNo + ? `${stockItemTransaction.stockBatchNo}${ + stockItemTransaction.expiration ? ` (${formatDisplayDate(stockItemTransaction.expiration)})` : '' + }` + : '', + out: + stockItemTransaction?.quantity < 0 + ? `${(-1 * stockItemTransaction?.quantity)?.toLocaleString()} ${ + stockItemTransaction?.packagingUomName ?? '' + } of ${stockItemTransaction.packagingUomFactor}` + : '', + totalout: + stockItemTransaction?.quantity < 0 + ? `${-1 * stockItemTransaction?.quantity * Number(stockItemTransaction.packagingUomFactor)}` + : '', + patientId: stockItemTransaction?.patientId ?? '', + patientUuid: stockItemTransaction?.patientUuid ?? '', + patientName: patient?.display ?? '', // Use patient display name if available + patientIdentifier: '', + }; + }); + + setMappedData(data); + }, [items, patientData]); + + return ( +
+ + +
+
+ + {({ rows, headers, getHeaderProps, getTableProps, onInputChange }) => ( +
+ + + + + {headers.map((header) => ( + {header.header} + ))} + + + + {rows.map((row) => ( + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + +
+
+
+ )} +
+
+
+ + +
+ ); +}; + +export default TransactionsStockcardPrintout; diff --git a/src/stock-items/add-stock-item/transactions/transactions.component.tsx b/src/stock-items/add-stock-item/transactions/transactions.component.tsx index 201660aa..b6aa0f94 100644 --- a/src/stock-items/add-stock-item/transactions/transactions.component.tsx +++ b/src/stock-items/add-stock-item/transactions/transactions.component.tsx @@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'; import StockOperationReference from '../../../stock-operations/add-stock-operation/stock-operation-reference.component'; import { Add } from '@carbon/react/icons'; import { Printer } from '@carbon/react/icons'; -import TransactionsPrintout from './printout/transactions-printout.component'; +import TransactionsPrintout from './printout/transactions-bincard-printout.component'; import TransactionsPrintAction from './printout/transactions-print-action.component'; interface TransactionsProps { @@ -32,7 +32,7 @@ const Transactions: React.FC = ({ stockItemUuid }) => { setCurrentPage, setStockItemUuid, setLocationUuid, - printHeaders, + binCardHeaders, } = useStockItemsTransactions(stockItemFilter); useEffect(() => { @@ -127,7 +127,7 @@ const Transactions: React.FC = ({ stockItemUuid }) => { ( <> - + { setLocationUuid(q); diff --git a/src/stock-items/add-stock-item/transactions/transactions.resource.tsx b/src/stock-items/add-stock-item/transactions/transactions.resource.tsx index a80695af..9075fd3e 100644 --- a/src/stock-items/add-stock-item/transactions/transactions.resource.tsx +++ b/src/stock-items/add-stock-item/transactions/transactions.resource.tsx @@ -76,7 +76,7 @@ export function useStockItemsTransactions(filter?: StockItemInventoryFilter) { [], ); - const printHeaders = useMemo( + const binCardHeaders = useMemo( () => [ { key: 'date', @@ -130,6 +130,6 @@ export function useStockItemsTransactions(filter?: StockItemInventoryFilter) { setLocationUuid, setPartyUuid, setStockBatchUuid, - printHeaders, + binCardHeaders, }; } diff --git a/src/stock-items/stock-items.resource.ts b/src/stock-items/stock-items.resource.ts index aae691e5..a3854daf 100644 --- a/src/stock-items/stock-items.resource.ts +++ b/src/stock-items/stock-items.resource.ts @@ -39,6 +39,7 @@ export interface StockItemInventoryFilter extends ResourceFilterCriteria { date?: string | null; includeStockItemName?: 'true' | 'false' | '0' | '1'; excludeExpired?: boolean | null; + isPatientTransaction?: 'true' | 'false'; } export interface StockItemPackagingUOMFilter extends ResourceFilterCriteria { diff --git a/src/stock-lookups/stock-lookups.resource.ts b/src/stock-lookups/stock-lookups.resource.ts index 4635dd4e..1f44361c 100644 --- a/src/stock-lookups/stock-lookups.resource.ts +++ b/src/stock-lookups/stock-lookups.resource.ts @@ -267,6 +267,14 @@ export function usePatients(filter: ConceptFilterCriteria) { }; } +// get a Patient +export function usePatient(patientUuid: string) { + const customePresentation = 'custom:(uuid,display,identifiers,links)'; + const url = `${restBaseUrl}/patient/${patientUuid}?v=${customePresentation}`; + const { isLoading, error, data } = useSWR>(url, openmrsFetch); + return { isLoading, error, patient: data?.data }; +} + type UserRole = { results: Array<{ userUuid: string;