diff --git a/CHANGELOG.md b/CHANGELOG.md index 128448da..0984c604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ 7.0.13-SNAPSHOT / WIP ================== +New functionalities that are backwards-compatible: +* [OLMIS-7987](https://openlmis.atlassian.net/browse/OLMIS-7987): Move Submit Requisitionless Orders functionalities from Angola to Core instance Bug fixes: * [OLMIS-7992](https://openlmis.atlassian.net/browse/OLMIS-7992): Fix passing parameter to hasRoleWithRightForProgramAndSupervisoryNode function diff --git a/src/requisition-order-create/messages_en.json b/src/requisition-order-create/messages_en.json index 66153cb4..330c22e9 100644 --- a/src/requisition-order-create/messages_en.json +++ b/src/requisition-order-create/messages_en.json @@ -1,8 +1,39 @@ { - "requisition.orderCreate": "Create Order", - "requisition.orderCreate.submitted": "Order created successfully", + "requisition.orderCreate": "Create Orders", + "requisition.orderCreate.cancel": "Cancel", + "requisition.orderCreate.loading": "Loading", + "requisition.orderCreate.view": "View", + "requisition.orderCreate.printOrder": "Print Order", + "requisition.orderCreate.edit": "Edit", + "requisition.orderCreate.create": "Create Order", + "requisition.orderCreate.saveDraft": "Save Draft", + "requisition.orderCreate.submitted": "Orders created successfully", "requisition.orderCreate.createdOrderSent.success": "Offline created order sent successfully.", "requisition.orderCreate.createdOrderSent.error": "Error occurred while sending offline created order.", "requisition.orderCreate.draftUpdate.success": "Draft order update success.", - "requisition.orderCreate.draftUpdate.error": "Error occurred while updating draft order." + "requisition.orderCreate.draftUpdate.error": "Error occurred while updating draft order.", + "requisition.orderCreate.delete": "Delete", + "requisition.orderCreate.deleteBatch": "Delete Batch", + "requisition.orderCreate.delete.prompt": "Are you sure you want to delete this order?", + "requisition.orderCreate.delete.prompt.batch": "Are you sure you want to delete available orders?", + "requisition.orderCreate.delete.error": "Error occurred while deleting order(s).", + "requisition.orderCreate.delete.success": "Order(s) deleted successfully.", + "requisition.orderCreate.program": "Program", + "requisition.orderCreate.program.placeholder": "Select Program", + "requisition.orderCreate.reqFacility": "Requesting Facility", + "requisition.orderCreate.reqFacility.placeholder": "Select Requesting Facility", + "requisition.orderCreate.supFacility": "Supplying Facility", + "requisition.orderCreate.supFacility.placeholder": "Select Supplying Facility", + "requisition.orderCreate.table.productCode": "Product Code", + "requisition.orderCreate.table.product": "Product", + "requisition.orderCreate.table.soh": "SOH", + "requisition.orderCreate.table.quantity": "Quantity", + "requisition.orderCreate.table.actions": "Actions", + "requisition.orderCreate.table.facility": "Facility", + "requisition.orderCreate.table.addProduct": "Add", + "requisition.orderCreate.table.productAlreadyAdded": "This product was already added to the table", + "requisition.orderCreate.requisistionInfo.status": "Status", + "requisition.orderCreate.requisistionInfo.dateCreated": "Date Created", + "requisition.orderCreate.searchSelect.placeholder": "Select an option", + "requisition.orderCreate.searchSelect.empty.message": "Not found" } diff --git a/src/requisition-order-create/order-create-form-helper-functions.jsx b/src/requisition-order-create/order-create-form-helper-functions.jsx new file mode 100644 index 00000000..dee9c98b --- /dev/null +++ b/src/requisition-order-create/order-create-form-helper-functions.jsx @@ -0,0 +1,93 @@ +export const getMappedRequestingFacilities = (facilities, userId, selectedProgram, selectedSupplyingFacility) => { + return facilities.map((facilityId) => ({ + emergency: true, + createdBy: { id: userId }, + program: { id: selectedProgram }, + requestingFacility: { id: facilityId }, + receivingFacility: { id: facilityId }, + supplyingFacility: { id: selectedSupplyingFacility }, + facility: { id: facilityId } + })); +}; + +export const goToOrderEdit = (orders, orderService, history) => { + const orderCreationPromises = orders.map(order => orderService.create(order)); + Promise.all(orderCreationPromises).then((createdOrders) => { + const ordersIds = createdOrders.map(order => order.id).join(','); + history.push(`/requisitions/orderCreate/${ordersIds}`); + }); +}; + +const getSupervisoryNodes = (selectedRequestingFacilities, selectedProgram, supervisoryNodeResource) => { + const supervisoryNodeResourcePromisses = selectedRequestingFacilities.map(facilityId => supervisoryNodeResource.query({ + programId: selectedProgram, + facilityId: facilityId + })); + + return Promise.all(supervisoryNodeResourcePromisses); +} + +const assignNodeToArray = (acc, obj, mappedPages) => { + if (!acc.some(existingObj => existingObj.id === obj.id)) { + if (mappedPages.every(page => page.some(item => item.id === obj.id))) { + acc.push(obj); + } + } +}; + +const getNodesValue = (pages) => { + const mappedPages = pages.map(page => page.content); + const nodes = mappedPages.reduce((acc, arr) => { + arr.forEach(obj => { + assignNodeToArray(acc, obj, mappedPages); + }); + return acc; + }, []); + return nodes; +} + +const getSupplyLines = (nodes, supplyLineResource, selectedProgram) => { + return Promise.all(nodes.map((node) => ( + supplyLineResource.query({ + programId: selectedProgram, + supervisoryNodeId: node.id + }) + ))) +} + +const setSupplyingFacilities = (supplyLinesResources, facilityService, setSupplyingFacilityOptions) => { + const supplyLines = _.flatten(supplyLinesResources.map((it) => (it.content))); + const facilityIds = _.uniq(supplyLines.map((it) => (it.supplyingFacility.id))); + + if (facilityIds.length > 0) { + facilityService.query({ + id: facilityIds + }) + .then((resp) => { + const facilities = resp.content; + setSupplyingFacilityOptions(facilities.map(facility => ({ name: facility.name, value: facility.id }))); + }); + } else { + setSupplyingFacilityOptions([]); + } +} + +export const updateSupplyingFacilitiesValue = (selectedProgram, selectedRequestingFacilities, supervisoryNodeResource, supplyLineResource, facilityService, setSupplyingFacilityOptions) => { + if (selectedProgram && selectedRequestingFacilities) { + getSupervisoryNodes(selectedRequestingFacilities, selectedProgram, supervisoryNodeResource) + .then((pages) => { + const nodes = getNodesValue(pages); + + if (nodes.length > 0) { + getSupplyLines(nodes, supplyLineResource, selectedProgram) + .then((supplyLinesResources) => { + setSupplyingFacilities(supplyLinesResources, facilityService, setSupplyingFacilityOptions); + }); + } else { + setSupplyingFacilityOptions([]); + } + }); + } else { + setSupplyingFacilityOptions([]); + } +} \ No newline at end of file diff --git a/src/requisition-order-create/order-create-form.jsx b/src/requisition-order-create/order-create-form.jsx index 057246dc..8f593903 100644 --- a/src/requisition-order-create/order-create-form.jsx +++ b/src/requisition-order-create/order-create-form.jsx @@ -17,6 +17,10 @@ import React, { useState, useEffect, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import getService from '../react-components/utils/angular-utils'; import { SearchSelect } from './search-select'; +import EditableTable from '../react-components/table/editable-table'; +import { getMappedRequestingFacilities, goToOrderEdit, updateSupplyingFacilitiesValue } from './order-create-form-helper-functions'; +import { orderCreateFormTableColumns } from './order-create.constant'; + const OrderCreateForm = () => { @@ -25,62 +29,64 @@ const OrderCreateForm = () => { const [programOptions, setProgramOptions] = useState([]); const [requestingFacilityOptions, setRequestingFacilityOptions] = useState([]); const [supplyingFacilityOptions, setSupplyingFacilityOptions] = useState([]); - const [selectedProgram, selectProgram] = useState(''); - const [selectedRequestingFacility, selectRequestingFacility] = useState(''); - const [selectedSupplyingFacility, selectSupplyingFacility] = useState(''); + const [selectedProgram, setSelectedProgram] = useState(''); + const [selectedRequestingFacilities, setSelectedRequestingFacilities] = useState([]); + const [selectedSupplyingFacility, setSelectedSupplyingFacility] = useState(''); + const [filteredRequestingFacilities, setFilteredRequestingFacilities] = useState([]); - const ADMINISTRATION_RIGHTS = useMemo( - () => { - return getService('ADMINISTRATION_RIGHTS'); - }, - [] - ); + const ADMINISTRATION_RIGHTS = useMemo(() => getService('ADMINISTRATION_RIGHTS'), []); + const programService = useMemo(() => getService('programService'), []); + const facilityService = useMemo(() => getService('facilityService'), []); + const orderService = useMemo(() => getService('orderCreateService'), []); + const columns = useMemo(() => orderCreateFormTableColumns, []); const userId = useMemo( () => { const authorizationService = getService('authorizationService'); return authorizationService.getUser().user_id; - }, - [] - ); - - const programService = useMemo( - () => { - return getService('programService'); - }, - [] - ); - - const facilityService = useMemo( - () => { - return getService('facilityService'); - }, - [] - ); - - const orderService = useMemo( - () => { - return getService('orderCreateService'); - }, - [] + }, [] ); const supervisoryNodeResource = useMemo( () => { const resource = getService('SupervisoryNodeResource'); return new resource(); - }, - [] + }, [] ); const supplyLineResource = useMemo( () => { const resource = getService('SupplyLineResource'); return new resource(); - }, - [] + }, [] ); + const createOrders = () => { + const orders = getMappedRequestingFacilities(selectedRequestingFacilities, userId, selectedProgram, selectedSupplyingFacility); + goToOrderEdit(orders, orderService, history); + }; + + const updateFilteredFacilities = () => { + const facilities = requestingFacilityOptions + .filter(facility => selectedRequestingFacilities.includes(facility.value)); + + setFilteredRequestingFacilities(facilities); + } + + const updateTableData = (updatedData) => { + setFilteredRequestingFacilities(updatedData); + const updatedDataIds = updatedData.map(facility => facility.value) + + setSelectedRequestingFacilities(prevState => { + return prevState.filter(id => updatedDataIds.includes(id)); + }); + } + + const updateSupplyingFacilities = () => { + setSelectedSupplyingFacility(''); + updateSupplyingFacilitiesValue(selectedProgram, selectedRequestingFacilities, supervisoryNodeResource, supplyLineResource, facilityService, setSupplyingFacilityOptions); + }; + useEffect( () => { programService.getUserPrograms(userId) @@ -101,99 +107,34 @@ const OrderCreateForm = () => { [facilityService] ); - const updateSupplyingFacilities = () => { - selectSupplyingFacility(''); - - if (selectedProgram && selectedRequestingFacility) { - supervisoryNodeResource.query({ - programId: selectedProgram, - facilityId: selectedRequestingFacility - }) - .then((page) => { - const nodes = page.content; - - if (nodes.length > 0) { - Promise.all(nodes.map((node) => ( - supplyLineResource.query({ - programId: selectedProgram, - supervisoryNodeId: node.id - }) - ))) - .then((results) => { - const supplyLines = _.flatten(results.map((it) => (it.content))); - const facilityIds = _.uniq(supplyLines.map((it) => (it.supplyingFacility.id))); - - if (facilityIds.length > 0) { - facilityService.query({ - id: facilityIds - }) - .then((resp) => { - const facilities = resp.content; - setSupplyingFacilityOptions(_.map(facilities, facility => ({ name: facility.name, value: facility.id }))); - }); - } else { - setSupplyingFacilityOptions([]); - } - }); - } else { - setSupplyingFacilityOptions([]); - } - }); - } else { - setSupplyingFacilityOptions([]); - } - }; - useEffect( () => { updateSupplyingFacilities(); }, - [selectedProgram, selectedRequestingFacility] + [selectedProgram, selectedRequestingFacilities] ); useEffect( () => { if (programOptions && programOptions.length === 1) { - selectProgram(programOptions[0].value); + setSelectedProgram(programOptions[0].value); } }, [programOptions] ); - useEffect( - () => { - if (requestingFacilityOptions && requestingFacilityOptions.length === 1) { - selectRequestingFacility(requestingFacilityOptions[0].value); - } - }, - [requestingFacilityOptions] - ); - useEffect( () => { if (supplyingFacilityOptions && supplyingFacilityOptions.length === 1) { - selectSupplyingFacility(supplyingFacilityOptions[0].value); + setSelectedSupplyingFacility(supplyingFacilityOptions[0].value); } }, [supplyingFacilityOptions] ); - const createOrder = () => { - const order = { - emergency: true, - createdBy: { id: userId }, - program: { id: selectedProgram }, - requestingFacility: { id: selectedRequestingFacility }, - receivingFacility: { id: selectedRequestingFacility }, - supplyingFacility: { id: selectedSupplyingFacility }, - facility: { id: selectedRequestingFacility } - }; - - orderService.create(order) - .then((createdOrder) => { - history.push(`/requisitions/orderCreate/${createdOrder.id}`); - }); - }; + useEffect(() => { + updateFilteredFacilities(); + }, [selectedRequestingFacilities]); return (
@@ -206,7 +147,7 @@ const OrderCreateForm = () => { selectProgram(value)} + onChange={value => setSelectedProgram(value)} placeholder="Select program" />
@@ -214,28 +155,35 @@ const OrderCreateForm = () => {
Requesting Facility
selectRequestingFacility(value)} + value={selectedRequestingFacilities.at(-1)} + onChange={value => setSelectedRequestingFacilities(prevState => [...prevState, value])} placeholder="Select requesting facility" /> +
Supplying Facility
selectSupplyingFacility(value)} + onChange={value => setSelectedSupplyingFacility(value)} placeholder="Select supplying facility" - disabled={!selectedProgram || !selectedRequestingFacility} + disabled={!selectedProgram || !selectedRequestingFacilities} />
diff --git a/src/requisition-order-create/order-create-page.jsx b/src/requisition-order-create/order-create-page.jsx index 208725e3..2f159185 100644 --- a/src/requisition-order-create/order-create-page.jsx +++ b/src/requisition-order-create/order-create-page.jsx @@ -13,38 +13,46 @@ * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  */ -import React from 'react'; -import { HashRouter as Router, Route, Switch } from 'react-router-dom'; - +import React, { useMemo } from 'react'; +import { HashRouter as Router, Route, Switch, useLocation } from 'react-router-dom'; import OrderCreateTable from './order-create-table'; import OrderCreateForm from './order-create-form'; import Breadcrumbs from '../react-components/breadcrumbs/breadcrumbs'; +import getService from '../react-components/utils/angular-utils'; -const OrderCreatePage = () => { +const OrderCreateRouting = () => { + const location = useLocation(); + const queryParams = new URLSearchParams(location.search); + const isReadOnly = queryParams.get('isReadOnly') === 'true'; + const { formatMessage } = useMemo(() => getService('messageService'), []); return (
- - - - - - - - - - - + + + + + + + + +
); }; +const OrderCreatePage = () => { + return ( + + + + ); +}; + export default OrderCreatePage; diff --git a/src/requisition-order-create/order-create-requisition-info.jsx b/src/requisition-order-create/order-create-requisition-info.jsx new file mode 100644 index 00000000..12eed557 --- /dev/null +++ b/src/requisition-order-create/order-create-requisition-info.jsx @@ -0,0 +1,36 @@ +import React, { useMemo } from 'react'; +import { formatDate } from '../react-components/utils/format-utils'; +import getService from '../react-components/utils/angular-utils'; + +const OrderCreateRequisitionInfo = ({ order }) => { + const { formatMessage } = useMemo(() => getService('messageService'), []); + + return ( + + ); +}; + +export default OrderCreateRequisitionInfo; diff --git a/src/requisition-order-create/order-create-summary-modal.jsx b/src/requisition-order-create/order-create-summary-modal.jsx new file mode 100644 index 00000000..84755d0a --- /dev/null +++ b/src/requisition-order-create/order-create-summary-modal.jsx @@ -0,0 +1,67 @@ +import React, { useMemo, useState } from 'react'; +import Modal from '../react-components/modals/Modal'; +import EditableTable from '../react-components/table/editable-table'; +import { orderTableColumns } from './order-create.constant'; +import TabNavigation from '../react-components/tab-navigation/tab-navigation'; +import getService from '../react-components/utils/angular-utils'; + +const OrderCreateSummaryModal = ({ isOpen, orders, onSaveClick, onModalClose }) => { + const { formatMessage } = useMemo(() => getService('messageService'), []); + const columns = useMemo(() => orderTableColumns(true, formatMessage), []); + const [currentTab, setCurrentTab] = useState(0); + + return ( + +
+ Orders Summary +
+
+ ({ + header: order.facility.name, + key: order.id, + isActive: currentTab === index + })), + onTabChange: (index) => { + setCurrentTab(index); + } + } + } + > +
+
+ { + currentTab !== undefined && + + } +
+
+
+
+ + +
+ } + > +
+ ); +}; + +export default OrderCreateSummaryModal; diff --git a/src/requisition-order-create/order-create-tab.jsx b/src/requisition-order-create/order-create-tab.jsx new file mode 100644 index 00000000..0a742a73 --- /dev/null +++ b/src/requisition-order-create/order-create-tab.jsx @@ -0,0 +1,141 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import Tippy from '@tippyjs/react'; + +import EditableTable from '../react-components/table/editable-table'; +import getService from '../react-components/utils/angular-utils'; +import OrderCreateRequisitionInfo from './order-create-requisition-info'; + +import { SearchSelect } from './search-select'; +import { getUpdatedOrder } from './order-create-table-helper-functions'; +import { orderTableColumns } from './order-create.constant'; +import { validateOrderItem } from './order-create-validation-helper-functions'; + +const OrderCreateTab = ({ passedOrder, + updateOrderArray, + showValidationErrors, + isTableReadOnly, + stockCardSummaryRepositoryImpl, + tabIndex, + cacheOrderableOptions, + cachedOrderableOptions }) => { + const [order, setOrder] = useState({ orderLineItems: [], ...passedOrder }); + const [selectedOrderable, setSelectedOrderable] = useState(''); + const [orderableOptions, setOrderableOptions] = useState(cachedOrderableOptions); + const columns = useMemo(() => orderTableColumns(isTableReadOnly), []); + const orderCreatePrintService = useMemo(() => getService('orderCreatePrintService'), []); + + useMemo(() => { + if (cachedOrderableOptions?.length) { + setOrderableOptions(cachedOrderableOptions); + return; + } + + stockCardSummaryRepositoryImpl.query({ + programId: order.program.id, + facilityId: order.requestingFacility.id + }).then((page) => { + const fetchedOrderableOptions = page.content + const orderableOptionsValue = fetchedOrderableOptions.map(stockItem => ({ + name: stockItem.orderable.fullProductName, + value: { ...stockItem.orderable, soh: stockItem.stockOnHand } + })); + + setOrderableOptions(orderableOptionsValue); + }); + }, []); + + useEffect(() => { + if (orderableOptions.length > 0) { + cacheOrderableOptions(orderableOptions, tabIndex); + } + }, [orderableOptions]); + + const updateData = (changedItems) => { + const updatedOrder = { + ...order, + orderLineItems: changedItems + }; + + setOrder(updatedOrder); + updateOrderArray(updatedOrder); + }; + + const addOrderable = () => { + const updatedOrder = getUpdatedOrder(selectedOrderable, order); + setOrder(updatedOrder); + setSelectedOrderable(''); + updateOrderArray(updatedOrder); + }; + + const validateRow = (row) => { + const errors = validateOrderItem(row); + return !errors.length; + }; + + const printOrder = () => { + orderCreatePrintService.print(order.id); + }; + + const isProductAdded = selectedOrderable && _.find(order.orderLineItems, item => (item.orderable.id === selectedOrderable.id)); + + return ( + <> + +
+
+
+
+ { + !isTableReadOnly && + setSelectedOrderable(value)} + objectKey={'id'} + disabled={isTableReadOnly} + >Product + } +
+ + { + !isTableReadOnly && +
+ +
+ } +
+ { + isTableReadOnly && + + } +
+
+ +
+
+
+ + + ); +}; + +export default OrderCreateTab; diff --git a/src/requisition-order-create/order-create-table-helper-functions.jsx b/src/requisition-order-create/order-create-table-helper-functions.jsx new file mode 100644 index 00000000..e5bdd2e9 --- /dev/null +++ b/src/requisition-order-create/order-create-table-helper-functions.jsx @@ -0,0 +1,78 @@ +export const getOrderValue = (fetchedOrder, stockCardSummaryRepositoryImpl) => { + const orderableIds = fetchedOrder.orderLineItems.map((lineItem) => lineItem.orderable.id); + + if (orderableIds?.length) { + return stockCardSummaryRepositoryImpl.query({ + programId: fetchedOrder.program.id, + facilityId: fetchedOrder.requestingFacility.id, + orderableId: orderableIds + }).then(function (page) { + return getOrdersWithSoh(page, fetchedOrder); + }).catch(function () { + return fetchedOrder; + }); + } else { + return Promise.resolve(fetchedOrder); + } +}; + +const getOrdersWithSoh = (page, fetchedOrder) => { + const stockItems = page.content; + const orderWithSoh = { + ...fetchedOrder, + orderLineItems: fetchedOrder.orderLineItems.map((lineItem) => { + const stockItem = stockItems.find((item) => (item.orderable.id === lineItem.orderable.id)); + + return { + ...lineItem, + soh: stockItem ? stockItem.stockOnHand : 0 + }; + }) + }; + + return orderWithSoh; +}; + +export const getUpdatedOrder = (selectedOrderable, order) => { + const newLineItem = { + orderedQuantity: '', + soh: selectedOrderable.soh, + orderable: { + id: selectedOrderable.id, + productCode: selectedOrderable.productCode, + fullProductName: selectedOrderable.fullProductName, + meta: { + versionNumber: selectedOrderable.meta.versionNumber + } + } + }; + + const orderNewLineItems = [...order.orderLineItems, newLineItem]; + + const updatedOrder = { + ...order, + orderLineItems: orderNewLineItems + }; + + return updatedOrder; +}; + +export const getIsOrderValidArray = (orders) => { + return orders.map((order) => { + if (!order?.orderLineItems.length) { + return false; + } + const mappedOrderLineItems = order.orderLineItems.map(item => item.orderedQuantity !== '' && item.orderedQuantity > 0); + return Boolean(!mappedOrderLineItems.includes(false) && order.id !== undefined) + }); +}; + +export const createOrderDisabled = (orders) => { + const mappedOrders = getIsOrderValidArray(orders); + return mappedOrders.length === 0 || mappedOrders.includes(false); +} + +export const saveDraftDisabled = (orders) => { + const mappedOrders = orders.map((order) => order.id !== undefined); + return mappedOrders.length === 0 || mappedOrders.includes(false); +} diff --git a/src/requisition-order-create/order-create-table.jsx b/src/requisition-order-create/order-create-table.jsx index 4e0e264b..c4d8ef97 100644 --- a/src/requisition-order-create/order-create-table.jsx +++ b/src/requisition-order-create/order-create-table.jsx @@ -14,41 +14,32 @@ */ import React, { useState, useEffect, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import { useParams, useHistory } from 'react-router-dom'; import { toast } from 'react-toastify'; -import { useDispatch } from 'react-redux'; -import Tippy from '@tippyjs/react'; - -import TrashButton from '../react-components/buttons/trash-button'; -import EditableTable from '../react-components/table/editable-table'; -import InputCell from '../react-components/table/input-cell'; - -import { formatDate } from '../react-components/utils/format-utils'; import getService from '../react-components/utils/angular-utils'; -import { SearchSelect } from './search-select'; +import { createOrderDisabled, getIsOrderValidArray, getOrderValue, saveDraftDisabled } from './order-create-table-helper-functions'; +import OrderCreateTab from './order-create-tab'; import { saveDraft, createOrder } from './reducers/orders.reducer'; +import { isOrderInvalid } from './order-create-validation-helper-functions'; +import OrderCreateSummaryModal from './order-create-summary-modal'; +import TabNavigation from '../react-components/tab-navigation/tab-navigation'; -const OrderCreateTable = () => { +const OrderCreateTable = ({ isReadOnly }) => { + const [orders, setOrders] = useState([]); + const [currentTab, setCurrentTab] = useState(0); + const [showValidationErrors, setShowValidationErrors] = useState(false); + const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false); + const [cachedOrderableOptions, setCachedOrderableOptions] = useState([]); - const history = useHistory(); - const { orderId } = useParams(); + const { orderIds } = useParams(); const dispatch = useDispatch(); + const history = useHistory(); - const [order, setOrder] = useState({ orderLineItems: [] }); - const [orderParams, setOrderParams] = useState({ programId: null , requestingFacilityId: null }); - - const [orderableOptions, setOrderableOptions] = useState([]); - const [selectedOrderable, selectOrderable] = useState(''); - - const [showValidationErrors, setShowValidationErrors] = useState(false); - - const orderService = useMemo( - () => { - return getService('orderCreateService'); - }, - [] - ); + const orderService = useMemo(() => getService('orderCreateService'), []); + const notificationService = useMemo(() => getService('notificationService'), []); + const offlineService = useMemo(() => getService('offlineService'), []); const stockCardSummaryRepositoryImpl = useMemo( () => { @@ -58,319 +49,148 @@ const OrderCreateTable = () => { [] ); - const notificationService = useMemo( - () => { - return getService('notificationService'); - }, - [] - ); - - const offlineService = useMemo( - () => { - return getService('offlineService'); - }, - [] - ); - useEffect( () => { - orderService.get(orderId) - .then((fetchedOrder) => { - const orderParams = { - programId: fetchedOrder.program.id, - facilityId: fetchedOrder.requestingFacility.id - }; - - setOrderParams(orderParams); - - const orderableIds = fetchedOrder.orderLineItems.map((lineItem) => { - return lineItem.orderable.id; - }); - - if (orderableIds && orderableIds.length) { - stockCardSummaryRepositoryImpl.query({ - programId: fetchedOrder.program.id, - facilityId: fetchedOrder.requestingFacility.id, - orderableId: orderableIds - }) - .then(function(page) { - const stockItems = page.content; - - const orderWithSoh = { - ...fetchedOrder, - orderLineItems: fetchedOrder.orderLineItems.map((lineItem) => { - const stockItem =_.find(stockItems, (item) => (item.orderable.id === lineItem.orderable.id)); - - return { - ...lineItem, - soh: stockItem ? stockItem.stockOnHand : 0 - }; - }) - }; - - setOrder(orderWithSoh); - }) - .catch(function() { - setOrder(fetchedOrder); + if (orderIds) { + const orderIdsArray = orderIds.split(','); + const ordersPromises = orderIdsArray.map(orderId => orderService.get(orderId)); + setCachedOrderableOptions(orderIdsArray.map(() => [])); + Promise.all(ordersPromises) + .then((orders) => { + const orderValuePromisses = orders.map(order => getOrderValue(order, stockCardSummaryRepositoryImpl)); + Promise.all(orderValuePromisses) + .then((orders) => { + setOrders(orders); }); - } else { - setOrder(fetchedOrder); - } - }); - }, - [orderService] - ); - - useEffect( - () => { - if (orderParams.programId !== null && orderParams.requestingFacilityId !== null) { - stockCardSummaryRepositoryImpl.query({ - programId: orderParams.programId, - facilityId: orderParams.facilityId - }) - .then(function(page) { - const stockItems = page.content; - - setOrderableOptions(_.map(stockItems, stockItem => ({ - name: stockItem.orderable.fullProductName, - value: { ...stockItem.orderable, soh: stockItem.stockOnHand } - }))); }); } }, - [orderParams] - ); - - const columns = useMemo( - () => [ - { - Header: 'Product Code', - accessor: 'orderable.productCode' - }, - { - Header: 'Product', - accessor: 'orderable.fullProductName' - }, - { - Header: 'SOH', - accessor: 'soh', - Cell: ({ value }) => (
{value}
) - }, - { - Header: 'Quantity', - accessor: 'orderedQuantity', - Cell: (props) => ( - - ) - }, - { - Header: 'Actions', - accessor: 'id', - Cell: ({ row: { index }, deleteRow }) => ( - deleteRow(index)} /> - ) - } - ], - [] + [orderService] ); - const validateOrderItem = (item) => { - const errors = []; - - if (item.orderedQuantity === null || item.orderedQuantity === undefined - || item.orderedQuantity === '') { - errors.push('Order quantity is required'); - } else if (item.orderedQuantity < 0) { - errors.push('Order quantity cannot be negative'); - } - - return errors; - }; - - const validateRow = (row) => { - const errors = validateOrderItem(row); - - return !errors.length; - }; - - const validateOrder = (orderToValidate) => { - const lineItems = orderToValidate.orderLineItems; - let errors = []; - - if (!lineItems) { - return errors; - } - - lineItems.forEach(item => { - errors = errors.concat(validateOrderItem(item)); + const onProductAdded = (updatedOrder) => { + setOrders(prevOrders => { + const updatedOrders = prevOrders.map(order => { + if (order.id === updatedOrder.id) { + return updatedOrder; + } + return order; + }); + return updatedOrders; }); + } - return _.uniq(errors); - }; - - const updateData = (changedItems) => { - const updatedOrder = { - ...order, - orderLineItems: changedItems - }; - - setOrder(updatedOrder); - }; - - const updateOrder = () => { - const validationErrors = validateOrder(order); - - if (validationErrors.length) { - validationErrors.forEach(error => { - toast.error(error); - }); - setShowValidationErrors(true); + const sendOrders = () => { + if (isOrderInvalid(orders, setShowValidationErrors, toast)) { return; } if (offlineService.isOffline()) { - dispatch(saveDraft(order)); - toast.success("Draft order saved offline"); + dispatch(createOrder(orders[currentTab])); + notificationService.success("Offline order created successfully. It will be sent when you are online."); + history.push('/'); } else { - setShowValidationErrors(false); - - orderService.update(order) - .then(() => { - toast.success("Order saved successfully"); - }); + const orderCreatePromisses = orders.map(order => orderService.send(order)); + Promise.all(orderCreatePromisses).then(() => { + notificationService.success('requisition.orderCreate.submitted'); + history.push('/orders/fulfillment'); + }); } }; - const addOrderable = () => { - const newLineItem = { - orderedQuantity: '', - soh: selectedOrderable.soh, - orderable: { - id: selectedOrderable.id, - productCode: selectedOrderable.productCode, - fullProductName: selectedOrderable.fullProductName, - meta: { - versionNumber: selectedOrderable.meta.versionNumber - } - } - }; - - let orderNewLineItems = [...order.orderLineItems, newLineItem]; - - const updatedOrder = { - ...order, - orderLineItems: orderNewLineItems - }; - - setOrder(updatedOrder); - selectOrderable(''); - }; - - const sendOrder = () => { - const validationErrors = validateOrder(order); - - if (validationErrors.length) { - validationErrors.forEach(error => { - toast.error(error); - }); - setShowValidationErrors(true); + const updateOrders = () => { + if (isOrderInvalid(orders, setShowValidationErrors, toast)) { return; } if (offlineService.isOffline()) { - dispatch(createOrder(order)); - notificationService.success("Offline order created successfully. It will be sent when you are online."); - history.push('/'); + dispatch(saveDraft(orders[currentTab])); + toast.success("Draft order saved offline"); } else { - orderService.send(order) - .then(() => { - notificationService.success('requisition.orderCreate.submitted'); - history.push('/orders/fulfillment'); - }); + setShowValidationErrors(false); + const updateOrdersPromises = orders.map(order => orderService.update(order)); + Promise.all(updateOrdersPromises).then(() => { + toast.success("Orders saved successfully"); + }); } }; - const isProductAdded = selectedOrderable && _.find(order.orderLineItems, item => (item.orderable.id === selectedOrderable.id)); - return (
+ { + isSummaryModalOpen && + setIsSummaryModalOpen(false)} + /> + }

Create Order

-
- -
-
-
- selectOrderable(value)} - objectKey={'id'} - >Product - -
- -
-
-
- -
+ { + orders.length > 0 && +
+ ({ + header: order.facility.name, + key: order.id, + isActive: currentTab === index + })), + onTabChange: (index) => { + setCurrentTab(index); + }, + isTabValidArray: !isReadOnly ? getIsOrderValidArray(orders) : undefined + } + } + >
+ } +
+ {(orders.length > 0) ? ( + { + const updatedCachedOrderableOptions = cachedOrderableOptions; + updatedCachedOrderableOptions[tabIndex] = orderableOptions; + setCachedOrderableOptions(updatedCachedOrderableOptions); + }} + updateOrderArray={ + (updatedOrder) => { + onProductAdded(updatedOrder); + } + } /> + ) : ( +

Loading...

+ )}
- - + { + isReadOnly || + <> + + + + }
); diff --git a/src/requisition-order-create/order-create-validation-helper-functions.jsx b/src/requisition-order-create/order-create-validation-helper-functions.jsx new file mode 100644 index 00000000..1ba44ac5 --- /dev/null +++ b/src/requisition-order-create/order-create-validation-helper-functions.jsx @@ -0,0 +1,45 @@ +export const validateOrderItem = (item) => { + const errors = []; + + if (item.orderedQuantity === null || item.orderedQuantity === undefined + || item.orderedQuantity === '') { + errors.push('Order quantity is required'); + } else if (item.orderedQuantity < 0) { + errors.push('Order quantity cannot be negative'); + } + + return errors; +}; + +const validateOrder = (orderToValidate) => { + const lineItems = orderToValidate.orderLineItems; + let errors = []; + + if (!lineItems) { + return errors; + } + + lineItems.forEach(item => { + errors = errors.concat(validateOrderItem(item)); + }); + + return _.uniq(errors); +}; + +export const isOrderInvalid = (orders, setShowValidationErrors, toast) => { + let validationErrors = []; + + orders.forEach(order => { + validationErrors = validationErrors.concat(validateOrder(order)); + }); + + if (validationErrors.length) { + validationErrors.forEach(error => { + toast.error(error); + }); + setShowValidationErrors(true); + return true; + } + + return false; +} diff --git a/src/requisition-order-create/order-create.constant.jsx b/src/requisition-order-create/order-create.constant.jsx new file mode 100644 index 00000000..e9d1b4a5 --- /dev/null +++ b/src/requisition-order-create/order-create.constant.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import TrashButton from '../react-components/buttons/trash-button'; +import InputCell from '../react-components/table/input-cell'; + +const orderTableDefaultColumns = (formatMessage) => [ + { + Header: formatMessage('requisition.orderCreate.table.productCode'), + accessor: 'orderable.productCode' + }, + { + Header: formatMessage('requisition.orderCreate.table.product'), + accessor: 'orderable.fullProductName' + }, + { + Header: formatMessage('requisition.orderCreate.table.soh'), + accessor: 'soh', + Cell: ({ value }) => (
{value}
) + }, + { + Header: formatMessage('requisition.orderCreate.table.quantity'), + accessor: 'orderedQuantity', + Cell: (props) => ( + + ) + }, + { + Header: formatMessage('requisition.orderCreate.table.actions'), + accessor: 'id', + Cell: ({ row: { index }, deleteRow }) => ( + deleteRow(index)} /> + ) + } +]; + +const orderReadonlyTableColumns = (formatMessage) => [ + { + Header: formatMessage('requisition.orderCreate.table.productCode'), + accessor: 'orderable.productCode' + }, + { + Header: formatMessage('requisition.orderCreate.table.product'), + accessor: 'orderable.fullProductName' + }, + { + Header: formatMessage('requisition.orderCreate.table.soh'), + accessor: 'soh', + Cell: ({ value }) => (
{value}
) + }, + { + Header: formatMessage('requisition.orderCreate.table.quantity'), + accessor: 'orderedQuantity', + } +]; + +export const orderTableColumns = (isTableReadOnly, formatMessage) => { + return isTableReadOnly ? orderReadonlyTableColumns(formatMessage) : + orderTableDefaultColumns(formatMessage); +} + +export const orderCreateFormTableColumns = (formatMessage) => [ + { + Header: formatMessage('requisition.orderCreate.table.facility'), + accessor: 'name' + }, + { + Header: formatMessage('requisition.orderCreate.table.actions'), + accessor: 'value', + Cell: ({ row: { index }, deleteRow }) => ( + deleteRow(index)} /> + ) + } +]; + +export const ORDER_STATUS = { + CREATING: 'CREATING', +}; diff --git a/src/requisition-order-create/order-create.scss b/src/requisition-order-create/order-create.scss index 6b6df6f6..7f1052f8 100644 --- a/src/requisition-order-create/order-create.scss +++ b/src/requisition-order-create/order-create.scss @@ -2,6 +2,20 @@ $accent-color: #49baeb; $light-grey: #bebebe; + .tabs-container { + width: calc(100% - 2em); + margin-left: 1em; + } + + .requisition-info { + width: calc(100% - 2em); + margin: 1em 0 0 1em; + position: relative; + background-color: $background-color-alt; + border: 1px solid $border-color; + border-radius: $border-radius; + } + .order-create-form { .section { margin: 0.5em 0; @@ -19,6 +33,10 @@ min-width: 400px; } } + + .facilities-table { + width: fit-content; + } } .order-create-table-container { @@ -34,13 +52,25 @@ justify-content: space-between; width: 100%; margin-bottom: 1em; + + .buttons-container { + @include flex-layout(row, $gap: 1rem); + + .order-print { + @include icon('print'); + + &::before { + margin-right: .5em; + } + } + } } } } .page-footer { button { - @include margin-right(1em); + margin-right: 1em; } justify-content: flex-end; @@ -52,16 +82,15 @@ max-height: 40vh; .search { - .input-wrapper { - display:inline-block; + display: inline-block; position: relative; &:before { - font-family: 'FONTAWESOME'; - content: '\f0d7'; // fa-caret-down + font-family: "FONTAWESOME"; + content: "\f0d7"; // fa-caret-down position: absolute; - @include left(1em); + left: 1em; color: $light-grey; top: 50%; transform: translateY(-50%); @@ -72,7 +101,7 @@ border: 1px solid $light-grey; background-color: inherit; border-radius: 4px; - @include padding(0.5em, 0.5em, 0.5em, 2em); + padding: 0.5em 0.5em 0.5em 2em; &:focus { outline: none; @@ -82,7 +111,7 @@ } .clear-icon { - @include margin-left(-2em); + margin-left: -2em; color: $light-grey; &:hover { @@ -101,7 +130,7 @@ .select { position: absolute; - z-index:100; + z-index: 100; border: $light-grey 1px solid; box-shadow: 0 0.25em 1em #e4e4e4; background-color: #fff; @@ -136,12 +165,12 @@ font: inherit; outline: inherit; padding: 0.5em; - @include text-align(left); + text-align: left; width: 100%; &.is-selected { - background-color: $accent-color; - color: #fff; + background-color: $accent-color; + color: #fff; } } }