diff --git a/src/components/Dashboard/Inventory/PurchaseOrders/NewPurchaseOrder.jsx b/src/components/Dashboard/Inventory/PurchaseOrders/NewPurchaseOrder.jsx index 7ee6d18..4e9728e 100644 --- a/src/components/Dashboard/Inventory/PurchaseOrders/NewPurchaseOrder.jsx +++ b/src/components/Dashboard/Inventory/PurchaseOrders/NewPurchaseOrder.jsx @@ -2,15 +2,15 @@ import PropTypes from 'prop-types' import ObjectInfo from '../../common/ObjectInfo' import NewObjectForm from '../../common/NewObjectForm' import WizardView from '../../common/WizardView' -import { getModelProperty } from '../../../../database/ObjectModels.js' -import ObjectProperty from '../../common/ObjectProperty.jsx' - const NewPurchaseOrder = ({ onOk, reset, defaultValues }) => { return ( {({ handleSubmit, submitLoading, objectData, formValid }) => { const steps = [ @@ -26,24 +26,13 @@ const NewPurchaseOrder = ({ onOk, reset, defaultValues }) => { required={true} objectData={objectData} visibleProperties={{ + _reference: false, items: false, cost: false }} /> ) }, - { - title: 'Items', - key: 'items', - content: ( - - ) - }, { title: 'Summary', key: 'summary', @@ -56,7 +45,15 @@ const NewPurchaseOrder = ({ onOk, reset, defaultValues }) => { _id: false, createdAt: false, updatedAt: false, - items: false + _reference: false, + totalAmount: false, + totalAmountWithTax: false, + totalTaxAmount: false, + postedAt: false, + acknowledgedAt: false, + shippingAmount: false, + shippingAmountWithTax: false, + grandTotalAmount: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx b/src/components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx index f1528ea..3c73a50 100644 --- a/src/components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx +++ b/src/components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx @@ -23,6 +23,12 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx' import NewOrderItem from '../OrderItems/NewOrderItem.jsx' +import NewShipment from '../Shipments/NewShipment.jsx' +import PostPurchaseOrder from './PostPurchaseOrder.jsx' +import AcknowledgePurchaseOrder from './AcknowledgePurchaseOrder.jsx' +import CancelPurchaseOrder from './CancelPurchaseOrder.jsx' +import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' const log = loglevel.getLogger('PurchaseOrderInfo') log.setLevel(config.logLevel) @@ -30,8 +36,15 @@ log.setLevel(config.logLevel) const PurchaseOrderInfo = () => { const location = useLocation() const objectFormRef = useRef(null) + const orderItemsTableRef = useRef(null) + const shipmentsTableRef = useRef(null) const actionHandlerRef = useRef(null) const [newOrderItemOpen, setNewOrderItemOpen] = useState(false) + const [newShipmentOpen, setNewShipmentOpen] = useState(false) + const [postPurchaseOrderOpen, setPostPurchaseOrderOpen] = useState(false) + const [acknowledgePurchaseOrderOpen, setAcknowledgePurchaseOrderOpen] = + useState(false) + const [cancelPurchaseOrderOpen, setCancelPurchaseOrderOpen] = useState(false) const purchaseOrderId = new URLSearchParams(location.search).get( 'purchaseOrderId' ) @@ -59,14 +72,17 @@ const PurchaseOrderInfo = () => { return true }, edit: () => { + orderItemsTableRef?.current?.startEditing?.() objectFormRef?.current?.startEditing?.() return false }, cancelEdit: () => { + orderItemsTableRef?.current?.cancelEditing?.() objectFormRef?.current?.cancelEditing?.() return true }, finishEdit: () => { + orderItemsTableRef?.current?.handleUpdate?.() objectFormRef?.current?.handleUpdate?.() return true }, @@ -77,9 +93,29 @@ const PurchaseOrderInfo = () => { newOrderItem: () => { setNewOrderItemOpen(true) return true + }, + newShipment: () => { + setNewShipmentOpen(true) + return true + }, + post: () => { + setPostPurchaseOrderOpen(true) + return true + }, + acknowledge: () => { + setAcknowledgePurchaseOrderOpen(true) + return true + }, + cancel: () => { + setCancelPurchaseOrderOpen(true) + return true } } + const editDisabled = getModelByName('purchaseOrder') + .actions.find((action) => action.name === 'edit') + .disabled(objectFormState.objectData) + return ( <> { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Purchase Order Information' }, + { key: 'orderItems', label: 'Order Items' }, + { key: 'shipments', label: 'Shipments' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -131,7 +169,11 @@ const PurchaseOrderInfo = () => { }} editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} - disabled={objectFormState.lock?.locked || objectFormState.loading} + disabled={ + objectFormState.lock?.locked || + objectFormState.loading || + editDisabled + } loading={objectFormState.editLoading} /> @@ -169,6 +211,7 @@ const PurchaseOrderInfo = () => { indicator={} isEditing={isEditing} type='purchaseOrder' + labelWidth='225px' objectData={objectData} visibleProperties={{ items: false @@ -178,16 +221,39 @@ const PurchaseOrderInfo = () => { } - active={collapseState.info} + active={collapseState.orderItems} onToggle={(expanded) => - updateCollapseState('info', expanded) + updateCollapseState('orderItems', expanded) } - collapseKey='info' + collapseKey='orderItems' > + + } + active={collapseState.shipments} + onToggle={(expanded) => + updateCollapseState('shipments', expanded) + } + collapseKey='shipments' + > + @@ -248,6 +314,80 @@ const PurchaseOrderInfo = () => { }} /> + { + setNewShipmentOpen(false) + }} + width={800} + footer={null} + destroyOnHidden={true} + > + { + setNewShipmentOpen(false) + }} + reset={newShipmentOpen} + defaultValues={{ + orderType: 'purchaseOrder', + order: { _id: purchaseOrderId } + }} + /> + + { + setPostPurchaseOrderOpen(false) + }} + width={500} + footer={null} + destroyOnHidden={true} + centered={true} + > + { + setPostPurchaseOrderOpen(false) + actions.reload() + }} + objectData={objectFormState.objectData} + /> + + { + setAcknowledgePurchaseOrderOpen(false) + }} + width={515} + footer={null} + destroyOnHidden={true} + centered={true} + > + { + setAcknowledgePurchaseOrderOpen(false) + actions.reload() + }} + objectData={objectFormState.objectData} + /> + + { + setCancelPurchaseOrderOpen(false) + }} + width={515} + footer={null} + destroyOnHidden={true} + centered={true} + > + { + setCancelPurchaseOrderOpen(false) + actions.reload() + }} + objectData={objectFormState.objectData} + /> + ) } diff --git a/src/components/Dashboard/Inventory/Shipments/PurchaseOrderInfo.jsx b/src/components/Dashboard/Inventory/Shipments/PurchaseOrderInfo.jsx new file mode 100644 index 0000000..defc306 --- /dev/null +++ b/src/components/Dashboard/Inventory/Shipments/PurchaseOrderInfo.jsx @@ -0,0 +1,362 @@ +import { useRef, useState } from 'react' +import { useLocation } from 'react-router-dom' +import { Space, Flex, Card, Modal } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config.js' +import useCollapseState from '../../hooks/useCollapseState.js' +import NotesPanel from '../../common/NotesPanel.jsx' +import InfoCollapse from '../../common/InfoCollapse.jsx' +import ObjectInfo from '../../common/ObjectInfo.jsx' +import ViewButton from '../../common/ViewButton.jsx' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import ObjectForm from '../../common/ObjectForm.jsx' +import EditButtons from '../../common/EditButtons.jsx' +import LockIndicator from '../../common/LockIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import ScrollBox from '../../common/ScrollBox.jsx' +import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx' +import NewOrderItem from '../OrderItems/NewOrderItem.jsx' +import NewShipment from './NewShipment.jsx' +import PostPurchaseOrder from '../PurchaseOrders/PostPurchaseOrder.jsx' +import AcknowledgePurchaseOrder from '../PurchaseOrders/AcknowledgePurchaseOrder.jsx' +import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx' + +const log = loglevel.getLogger('PurchaseOrderInfo') +log.setLevel(config.logLevel) + +const PurchaseOrderInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const orderItemsTableRef = useRef(null) + const shipmentsTableRef = useRef(null) + const actionHandlerRef = useRef(null) + const [newOrderItemOpen, setNewOrderItemOpen] = useState(false) + const [newShipmentOpen, setNewShipmentOpen] = useState(false) + const [postPurchaseOrderOpen, setPostPurchaseOrderOpen] = useState(false) + const [acknowledgePurchaseOrderOpen, setAcknowledgePurchaseOrderOpen] = + useState(false) + const purchaseOrderId = new URLSearchParams(location.search).get( + 'purchaseOrderId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'PurchaseOrderInfo', + { + info: true, + notes: true, + auditLogs: true + } + ) + + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + objectData: {} + }) + + const actions = { + reload: () => { + objectFormRef?.current?.handleFetchObject?.() + return true + }, + edit: () => { + orderItemsTableRef?.current?.startEditing?.() + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + orderItemsTableRef?.current?.cancelEditing?.() + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + orderItemsTableRef?.current?.handleUpdate?.() + objectFormRef?.current?.handleUpdate?.() + return true + }, + delete: () => { + objectFormRef?.current?.handleDelete?.() + return true + }, + newOrderItem: () => { + setNewOrderItemOpen(true) + return true + }, + newShipment: () => { + setNewShipmentOpen(true) + return true + }, + post: () => { + setPostPurchaseOrderOpen(true) + return true + }, + acknowledge: () => { + setAcknowledgePurchaseOrderOpen(true) + return true + } + } + + return ( + <> + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={objectFormState.lock?.locked || objectFormState.loading} + loading={objectFormState.editLoading} + /> + + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='purchaseOrder' + labelWidth='225px' + objectData={objectData} + visibleProperties={{ + items: false + }} + /> + + } + active={collapseState.orderItems} + onToggle={(expanded) => + updateCollapseState('orderItems', expanded) + } + collapseKey='orderItems' + > + + + } + active={collapseState.shipments} + onToggle={(expanded) => + updateCollapseState('shipments', expanded) + } + collapseKey='shipments' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + { + setNewOrderItemOpen(false) + }} + width={800} + footer={null} + destroyOnHidden={true} + > + { + setNewOrderItemOpen(false) + }} + reset={newOrderItemOpen} + defaultValues={{ + order: { _id: purchaseOrderId }, + orderType: 'purchaseOrder', + syncAmount: 'itemCost' + }} + /> + + { + setNewShipmentOpen(false) + }} + width={800} + footer={null} + destroyOnHidden={true} + > + { + setNewShipmentOpen(false) + }} + reset={newShipmentOpen} + defaultValues={{ + orderType: 'purchaseOrder', + order: { _id: purchaseOrderId } + }} + /> + + { + setPostPurchaseOrderOpen(false) + }} + width={500} + footer={null} + destroyOnHidden={true} + centered={true} + > + { + setPostPurchaseOrderOpen(false) + actions.reload() + }} + objectData={objectFormState.objectData} + /> + + { + setAcknowledgePurchaseOrderOpen(false) + }} + width={515} + footer={null} + destroyOnHidden={true} + centered={true} + > + { + setAcknowledgePurchaseOrderOpen(false) + actions.reload() + }} + objectData={objectFormState.objectData} + /> + + + ) +} + +export default PurchaseOrderInfo diff --git a/src/database/models/PurchaseOrder.js b/src/database/models/PurchaseOrder.js index ef88ef6..c07069d 100644 --- a/src/database/models/PurchaseOrder.js +++ b/src/database/models/PurchaseOrder.js @@ -1,6 +1,10 @@ import PurchaseOrderIcon from '../../components/Icons/PurchaseOrderIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import PlusIcon from '../../components/Icons/PlusIcon' +import CheckIcon from '../../components/Icons/CheckIcon' +import EditIcon from '../../components/Icons/EditIcon' +import XMarkIcon from '../../components/Icons/XMarkIcon' +import BinIcon from '../../components/Icons/BinIcon' export const PurchaseOrder = { name: 'purchaseOrder', @@ -17,19 +21,168 @@ export const PurchaseOrder = { url: (_id) => `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}` }, + { + name: 'edit', + label: 'Edit', + type: 'button', + icon: EditIcon, + url: (_id) => + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=edit`, + visible: (objectData) => { + return !(objectData?._isEditing && objectData?._isEditing == true) + }, + disabled: (objectData) => { + return objectData?.state?.type != 'draft' + } + }, + { + name: 'cancelEdit', + label: 'Cancel Edit', + type: 'button', + icon: XMarkIcon, + url: (_id) => + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=cancelEdit`, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { + name: 'finishEdit', + label: 'Finish Edit', + type: 'button', + icon: CheckIcon, + url: (_id) => + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=finishEdit`, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { + name: 'delete', + label: 'Delete', + type: 'button', + icon: BinIcon, + danger: true, + url: (_id) => + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=delete`, + visible: (objectData) => { + return !(objectData?._isEditing && objectData?._isEditing == true) + }, + disabled: (objectData) => { + return objectData?.state?.type != 'draft' + } + }, + { type: 'divider' }, { name: 'New Order Item', label: 'New Order Item', type: 'button', icon: PlusIcon, url: (_id) => - `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=newOrderItem` + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=newOrderItem`, + disabled: (objectData) => { + return objectData?.state?.type != 'draft' + } + }, + { + name: 'New Shipment', + label: 'New Shipment', + type: 'button', + icon: PlusIcon, + url: (_id) => + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=newShipment`, + disabled: (objectData) => { + return objectData?.state?.type != 'draft' + } + }, + { + name: 'New Invoice', + label: 'New Invoice', + type: 'button', + icon: PlusIcon, + url: (_id) => + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=newInvoice`, + disabled: (objectData) => { + return objectData?.state?.type != 'received' + } + }, + { + type: 'divider' + }, + { + name: 'post', + label: 'Post', + type: 'button', + icon: CheckIcon, + url: (_id) => + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=post`, + visible: (objectData) => { + return objectData?.state?.type == 'draft' + } + }, + { + name: 'acknowledge', + label: 'Acknowledge', + type: 'button', + icon: CheckIcon, + url: (_id) => + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=acknowledge`, + visible: (objectData) => { + return objectData?.state?.type == 'sent' + } + }, + { + name: 'complete', + label: 'Complete', + type: 'button', + icon: CheckIcon, + url: (_id) => + `/dashboard/inventory/shipments/info?shipmentId=${_id}&action=complete`, + disabled: (objectData) => { + return objectData?.state?.type != 'received' + }, + visible: (objectData) => { + return objectData?.state?.type == 'received' + } + }, + { + name: 'cancel', + label: 'Cancel', + type: 'button', + icon: XMarkIcon, + danger: true, + url: (_id) => + `/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=cancel`, + disabled: (objectData) => { + return objectData?.state?.type == 'cancelled' + }, + visible: (objectData) => { + return ( + objectData?.state?.type != 'draft' && + objectData?.state?.type != 'completed' && + objectData?.state?.type != 'received' + ) + } } ], group: ['vendor'], filters: ['vendor'], sorters: ['createdAt', 'state', 'updatedAt'], - columns: ['_id', 'createdAt', 'state', 'updatedAt', 'vendor'], + columns: [ + '_id', + '_reference', + 'state', + 'vendor', + 'totalAmount', + 'totalAmountWithTax', + 'totalTaxAmount', + 'shippingAmount', + 'shippingAmountWithTax', + 'grandTotalAmount', + 'createdAt', + 'updatedAt', + 'vendor' + ], properties: [ { name: '_id', @@ -62,6 +215,7 @@ export const PurchaseOrder = { readOnly: true }, { name: 'state', label: 'State', type: 'state', readOnly: true }, + { name: 'postedAt', label: 'Posted At', type: 'dateTime', readOnly: true }, { name: 'vendor', label: 'Vendor', @@ -71,17 +225,9 @@ export const PurchaseOrder = { showHyperlink: true }, { - name: 'totalAmount', - label: 'Total Amount', - type: 'number', - prefix: '£', - readOnly: true - }, - { - name: 'totalAmountWithTax', - label: 'Total Amount w/ Tax', - type: 'number', - prefix: '£', + name: 'acknowledgedAt', + label: 'Acknowledged At', + type: 'dateTime', readOnly: true }, { @@ -89,7 +235,104 @@ export const PurchaseOrder = { label: 'Total Tax Amount', type: 'number', prefix: '£', + roundNumber: 2, + readOnly: true, + columnWidth: 175 + }, + { + name: 'CompletedAt', + label: 'Completed At', + type: 'dateTime', readOnly: true + }, + { + name: 'totalAmountWithTax', + label: 'Total Amount w/ Tax', + type: 'number', + prefix: '£', + readOnly: true, + columnWidth: 175, + roundNumber: 2 + }, + { + name: 'shippingAmount', + label: 'Shipping Amount', + type: 'number', + prefix: '£', + roundNumber: 2, + readOnly: true, + columnWidth: 150 + }, + { + name: 'shippingAmountWithTax', + label: 'Shipping Amount w/ Tax', + type: 'number', + prefix: '£', + readOnly: true, + roundNumber: 2, + columnWidth: 200 + }, + { + name: 'totalAmount', + label: 'Total Amount', + type: 'number', + prefix: '£', + roundNumber: 2, + readOnly: true, + columnWidth: 150 + }, + { + name: 'grandTotalAmount', + label: 'Grand Total Amount', + type: 'number', + prefix: '£', + roundNumber: 2, + columnWidth: 175, + readOnly: true + } + ], + stats: [ + { + name: 'draft.count', + label: 'Draft', + type: 'number', + color: 'default' + }, + { + name: 'sent.count', + label: 'Sent', + type: 'number', + color: 'cyan' + }, + { + name: 'acknowledged.count', + label: 'Acknowledged', + type: 'number', + color: 'processing' + }, + { + name: 'partiallyShipped.count', + label: 'Partially Shipped', + type: 'number', + color: 'processing' + }, + { + name: 'shipped.count', + label: 'Shipped', + type: 'number', + color: 'processing' + }, + { + name: 'partiallyReceived.count', + label: 'Partially Received', + type: 'number', + color: 'success' + }, + { + name: 'received.count', + label: 'Received', + type: 'number', + color: 'success' } ] }