diff --git a/assets/icons/clienticon.svg b/assets/icons/clienticon.svg
new file mode 100644
index 0000000..cbf4d00
--- /dev/null
+++ b/assets/icons/clienticon.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/assets/icons/listingicon.svg b/assets/icons/listingicon.svg
new file mode 100644
index 0000000..25fc30b
--- /dev/null
+++ b/assets/icons/listingicon.svg
@@ -0,0 +1,16 @@
+
+
+
diff --git a/assets/icons/salesicon.svg b/assets/icons/salesicon.svg
new file mode 100644
index 0000000..01207be
--- /dev/null
+++ b/assets/icons/salesicon.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/App.jsx b/src/App.jsx
index fbee6a5..a4fcfc6 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -32,6 +32,7 @@ import {
ProductionRoutes,
InventoryRoutes,
FinanceRoutes,
+ SalesRoutes,
ManagementRoutes,
DeveloperRoutes
} from './routes'
@@ -98,6 +99,7 @@ const AppContent = () => {
{ProductionRoutes}
{InventoryRoutes}
{FinanceRoutes}
+ {SalesRoutes}
{ManagementRoutes}
{DeveloperRoutes}
diff --git a/src/components/Dashboard/Layout.jsx b/src/components/Dashboard/Layout.jsx
index 54f1d8f..56160aa 100644
--- a/src/components/Dashboard/Layout.jsx
+++ b/src/components/Dashboard/Layout.jsx
@@ -5,6 +5,7 @@ import { useLocation } from 'react-router-dom'
import ProductionSidebar from './Production/ProductionSidebar'
import InventorySidebar from './Inventory/InventorySidebar'
import FinanceSidebar from './Finance/FinanceSidebar'
+import SalesSidebar from './Sales/SalesSidebar'
import ManagementSidebar from './Management/ManagementSidebar'
import DashboardNavigation from './common/DashboardNavigation'
import DashboardBreadcrumb from './common/DashboardBreadcrumb'
@@ -19,6 +20,7 @@ const DashboardLayout = ({ children }) => {
const isProduction = location.pathname.startsWith('/dashboard/production')
const isInventory = location.pathname.startsWith('/dashboard/inventory')
const isFinance = location.pathname.startsWith('/dashboard/finance')
+ const isSales = location.pathname.startsWith('/dashboard/sales')
const isManagement = location.pathname.startsWith('/dashboard/management')
const isDeveloper = location.pathname.startsWith('/dashboard/developer')
@@ -38,6 +40,8 @@ const DashboardLayout = ({ children }) => {
) : isFinance ? (
+ ) : isSales ? (
+
) : isManagement ? (
) : isDeveloper ? (
diff --git a/src/components/Dashboard/Sales/Clients.jsx b/src/components/Dashboard/Sales/Clients.jsx
new file mode 100644
index 0000000..49c799f
--- /dev/null
+++ b/src/components/Dashboard/Sales/Clients.jsx
@@ -0,0 +1,95 @@
+import { useState, useRef } from 'react'
+import { Button, Flex, Space, Modal, Dropdown } from 'antd'
+import NewClient from './Clients/NewClient'
+import ObjectTable from '../common/ObjectTable'
+import PlusIcon from '../../Icons/PlusIcon'
+import ReloadIcon from '../../Icons/ReloadIcon'
+import useColumnVisibility from '../hooks/useColumnVisibility'
+import GridIcon from '../../Icons/GridIcon'
+import ListIcon from '../../Icons/ListIcon'
+import useViewMode from '../hooks/useViewMode'
+import ColumnViewButton from '../common/ColumnViewButton'
+
+const Clients = () => {
+ const [newClientOpen, setNewClientOpen] = useState(false)
+ const tableRef = useRef()
+
+ const [viewMode, setViewMode] = useViewMode('client')
+
+ const [columnVisibility, setColumnVisibility] = useColumnVisibility('client')
+
+ const actionItems = {
+ items: [
+ {
+ label: 'New Client',
+ key: 'newClient',
+ icon:
+ },
+ { type: 'divider' },
+ {
+ label: 'Reload List',
+ key: 'reloadList',
+ icon:
+ }
+ ],
+ onClick: ({ key }) => {
+ if (key === 'reloadList') {
+ tableRef.current?.reload()
+ } else if (key === 'newClient') {
+ setNewClientOpen(true)
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ : }
+ onClick={() =>
+ setViewMode(viewMode === 'cards' ? 'list' : 'cards')
+ }
+ />
+
+
+
+
+ setNewClientOpen(false)}
+ footer={null}
+ destroyOnHidden={true}
+ width={700}
+ >
+ {
+ setNewClientOpen(false)
+ tableRef.current?.reload()
+ }}
+ reset={!newClientOpen}
+ />
+
+ >
+ )
+}
+
+export default Clients
+
diff --git a/src/components/Dashboard/Sales/Clients/ClientInfo.jsx b/src/components/Dashboard/Sales/Clients/ClientInfo.jsx
new file mode 100644
index 0000000..454023d
--- /dev/null
+++ b/src/components/Dashboard/Sales/Clients/ClientInfo.jsx
@@ -0,0 +1,194 @@
+import { useRef, useState } from 'react'
+import { useLocation } from 'react-router-dom'
+import { Space, Flex, Card } from 'antd'
+import loglevel from 'loglevel'
+import config from '../../../../config'
+import useCollapseState from '../../hooks/useCollapseState'
+import NotesPanel from '../../common/NotesPanel'
+import InfoCollapse from '../../common/InfoCollapse'
+import ObjectInfo from '../../common/ObjectInfo'
+import ViewButton from '../../common/ViewButton'
+import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
+import NoteIcon from '../../../Icons/NoteIcon.jsx'
+import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
+import ObjectForm from '../../common/ObjectForm'
+import EditButtons from '../../common/EditButtons'
+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'
+
+const log = loglevel.getLogger('ClientInfo')
+log.setLevel(config.logLevel)
+
+const ClientInfo = () => {
+ const location = useLocation()
+ const objectFormRef = useRef(null)
+ const actionHandlerRef = useRef(null)
+ const clientId = new URLSearchParams(location.search).get('clientId')
+ const [collapseState, updateCollapseState] = useCollapseState('ClientInfo', {
+ 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: () => {
+ objectFormRef?.current?.startEditing?.()
+ return false
+ },
+ cancelEdit: () => {
+ objectFormRef?.current?.cancelEditing?.()
+ return true
+ },
+ finishEdit: () => {
+ objectFormRef?.current?.handleUpdate?.()
+ return true
+ },
+ delete: () => {
+ objectFormRef?.current?.handleDelete?.()
+ 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}
+ />
+
+
+
+
+
+ }
+ active={collapseState.info}
+ onToggle={(expanded) => updateCollapseState('info', expanded)}
+ collapseKey='info'
+ >
+ {
+ setEditFormState((prev) => ({ ...prev, ...state }))
+ }}
+ >
+ {({ loading, isEditing, objectData }) => (
+
+ )}
+
+
+
+ }
+ active={collapseState.notes}
+ onToggle={(expanded) => updateCollapseState('notes', expanded)}
+ collapseKey='notes'
+ >
+
+
+
+
+ }
+ active={collapseState.auditLogs}
+ onToggle={(expanded) =>
+ updateCollapseState('auditLogs', expanded)
+ }
+ collapseKey='auditLogs'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ >
+ )
+}
+
+export default ClientInfo
+
diff --git a/src/components/Dashboard/Sales/Clients/NewClient.jsx b/src/components/Dashboard/Sales/Clients/NewClient.jsx
new file mode 100644
index 0000000..d79fbca
--- /dev/null
+++ b/src/components/Dashboard/Sales/Clients/NewClient.jsx
@@ -0,0 +1,87 @@
+import PropTypes from 'prop-types'
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import WizardView from '../../common/WizardView'
+
+const NewClient = ({ onOk, defaultValues }) => {
+ return (
+
+ {({ handleSubmit, submitLoading, objectData, formValid }) => {
+ const steps = [
+ {
+ title: 'Required',
+ key: 'required',
+ content: (
+
+ )
+ },
+ {
+ title: 'Optional',
+ key: 'optional',
+ content: (
+
+ )
+ },
+ {
+ title: 'Summary',
+ key: 'summary',
+ content: (
+
+ )
+ }
+ ]
+ return (
+ {
+ const result = await handleSubmit()
+ if (result) {
+ onOk()
+ }
+ }}
+ />
+ )
+ }}
+
+ )
+}
+
+NewClient.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool,
+ defaultValues: PropTypes.object
+}
+
+export default NewClient
+
diff --git a/src/components/Dashboard/Sales/SalesOrders.jsx b/src/components/Dashboard/Sales/SalesOrders.jsx
new file mode 100644
index 0000000..e3f3bfe
--- /dev/null
+++ b/src/components/Dashboard/Sales/SalesOrders.jsx
@@ -0,0 +1,99 @@
+import { useState, useRef } from 'react'
+import { Button, Flex, Space, Modal, Dropdown } from 'antd'
+import NewSalesOrder from './SalesOrders/NewSalesOrder'
+import ObjectTable from '../common/ObjectTable'
+import PlusIcon from '../../Icons/PlusIcon'
+import ReloadIcon from '../../Icons/ReloadIcon'
+import useColumnVisibility from '../hooks/useColumnVisibility'
+import GridIcon from '../../Icons/GridIcon'
+import ListIcon from '../../Icons/ListIcon'
+import useViewMode from '../hooks/useViewMode'
+import ColumnViewButton from '../common/ColumnViewButton'
+
+const SalesOrders = () => {
+ const [newSalesOrderOpen, setNewSalesOrderOpen] = useState(false)
+ const tableRef = useRef()
+
+ const [viewMode, setViewMode] = useViewMode('salesOrders')
+
+ const [columnVisibility, setColumnVisibility] =
+ useColumnVisibility('salesOrders')
+
+ const actionItems = {
+ items: [
+ {
+ label: 'New Sales Order',
+ key: 'newSalesOrder',
+ icon:
+ },
+ { type: 'divider' },
+ {
+ label: 'Reload List',
+ key: 'reloadList',
+ icon:
+ }
+ ],
+ onClick: ({ key }) => {
+ if (key === 'reloadList') {
+ tableRef.current?.reload()
+ } else if (key === 'newSalesOrder') {
+ setNewSalesOrderOpen(true)
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ : }
+ onClick={() =>
+ setViewMode(viewMode === 'cards' ? 'list' : 'cards')
+ }
+ />
+
+
+
+
+ {
+ setNewSalesOrderOpen(false)
+ }}
+ destroyOnHidden={true}
+ >
+ {
+ setNewSalesOrderOpen(false)
+ tableRef.current?.reload()
+ }}
+ reset={newSalesOrderOpen}
+ />
+
+ >
+ )
+}
+
+export default SalesOrders
+
diff --git a/src/components/Dashboard/Sales/SalesOrders/CancelSalesOrder.jsx b/src/components/Dashboard/Sales/SalesOrders/CancelSalesOrder.jsx
new file mode 100644
index 0000000..f6adb7b
--- /dev/null
+++ b/src/components/Dashboard/Sales/SalesOrders/CancelSalesOrder.jsx
@@ -0,0 +1,47 @@
+import { useState, useContext } from 'react'
+import PropTypes from 'prop-types'
+import { ApiServerContext } from '../../context/ApiServerContext'
+import { message } from 'antd'
+import MessageDialogView from '../../common/MessageDialogView.jsx'
+
+const CancelSalesOrder = ({ onOk, objectData }) => {
+ const [cancelLoading, setCancelLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handleCancel = async () => {
+ setCancelLoading(true)
+ try {
+ const result = await sendObjectFunction(
+ objectData._id,
+ 'SalesOrder',
+ 'cancel'
+ )
+ if (result) {
+ message.success('Sales order cancelled successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error cancelling sales order:', error)
+ } finally {
+ setCancelLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+CancelSalesOrder.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default CancelSalesOrder
+
diff --git a/src/components/Dashboard/Sales/SalesOrders/ConfirmSalesOrder.jsx b/src/components/Dashboard/Sales/SalesOrders/ConfirmSalesOrder.jsx
new file mode 100644
index 0000000..bedb1be
--- /dev/null
+++ b/src/components/Dashboard/Sales/SalesOrders/ConfirmSalesOrder.jsx
@@ -0,0 +1,47 @@
+import { useState, useContext } from 'react'
+import PropTypes from 'prop-types'
+import { ApiServerContext } from '../../context/ApiServerContext'
+import { message } from 'antd'
+import MessageDialogView from '../../common/MessageDialogView.jsx'
+
+const ConfirmSalesOrder = ({ onOk, objectData }) => {
+ const [confirmLoading, setConfirmLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handleConfirm = async () => {
+ setConfirmLoading(true)
+ try {
+ const result = await sendObjectFunction(
+ objectData._id,
+ 'SalesOrder',
+ 'confirm'
+ )
+ if (result) {
+ message.success('Sales order confirmed successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error confirming sales order:', error)
+ } finally {
+ setConfirmLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+ConfirmSalesOrder.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default ConfirmSalesOrder
+
diff --git a/src/components/Dashboard/Sales/SalesOrders/NewSalesOrder.jsx b/src/components/Dashboard/Sales/SalesOrders/NewSalesOrder.jsx
new file mode 100644
index 0000000..da2c683
--- /dev/null
+++ b/src/components/Dashboard/Sales/SalesOrders/NewSalesOrder.jsx
@@ -0,0 +1,90 @@
+import PropTypes from 'prop-types'
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import WizardView from '../../common/WizardView'
+const NewSalesOrder = ({ onOk, reset, defaultValues }) => {
+ return (
+
+ {({ handleSubmit, submitLoading, objectData, formValid }) => {
+ const steps = [
+ {
+ title: 'Required',
+ key: 'required',
+ content: (
+
+ )
+ },
+ {
+ title: 'Summary',
+ key: 'summary',
+ content: (
+
+ )
+ }
+ ]
+ return (
+ {
+ const result = await handleSubmit()
+ if (result) {
+ onOk()
+ }
+ }}
+ />
+ )
+ }}
+
+ )
+}
+
+NewSalesOrder.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool,
+ defaultValues: PropTypes.object
+}
+
+export default NewSalesOrder
+
diff --git a/src/components/Dashboard/Sales/SalesOrders/PostSalesOrder.jsx b/src/components/Dashboard/Sales/SalesOrders/PostSalesOrder.jsx
new file mode 100644
index 0000000..d906ddb
--- /dev/null
+++ b/src/components/Dashboard/Sales/SalesOrders/PostSalesOrder.jsx
@@ -0,0 +1,47 @@
+import { useState, useContext } from 'react'
+import PropTypes from 'prop-types'
+import { ApiServerContext } from '../../context/ApiServerContext'
+import { message } from 'antd'
+import MessageDialogView from '../../common/MessageDialogView.jsx'
+
+const PostSalesOrder = ({ onOk, objectData }) => {
+ const [postLoading, setPostLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handlePost = async () => {
+ setPostLoading(true)
+ try {
+ const result = await sendObjectFunction(
+ objectData._id,
+ 'SalesOrder',
+ 'post'
+ )
+ if (result) {
+ message.success('Sales order posted successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error posting sales order:', error)
+ } finally {
+ setPostLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+PostSalesOrder.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default PostSalesOrder
+
diff --git a/src/components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx b/src/components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx
new file mode 100644
index 0000000..76ac1bf
--- /dev/null
+++ b/src/components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx
@@ -0,0 +1,440 @@
+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 '../../Inventory/OrderItems/NewOrderItem.jsx'
+import NewShipment from '../../Inventory/Shipments/NewShipment.jsx'
+import PostSalesOrder from './PostSalesOrder.jsx'
+import ConfirmSalesOrder from './ConfirmSalesOrder.jsx'
+import CancelSalesOrder from './CancelSalesOrder.jsx'
+import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx'
+import InvoiceIcon from '../../../Icons/InvoiceIcon.jsx'
+import StockEventIcon from '../../../Icons/StockEventIcon.jsx'
+import { getModelByName } from '../../../../database/ObjectModels.js'
+
+const log = loglevel.getLogger('SalesOrderInfo')
+log.setLevel(config.logLevel)
+
+const SalesOrderInfo = () => {
+ 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 [postSalesOrderOpen, setPostSalesOrderOpen] = useState(false)
+ const [confirmSalesOrderOpen, setConfirmSalesOrderOpen] = useState(false)
+ const [cancelSalesOrderOpen, setCancelSalesOrderOpen] = useState(false)
+ const salesOrderId = new URLSearchParams(location.search).get('salesOrderId')
+ const [collapseState, updateCollapseState] = useCollapseState(
+ 'SalesOrderInfo',
+ {
+ info: true,
+ notes: true,
+ auditLogs: true,
+ invoices: true,
+ stockEvents: 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: () => {
+ setPostSalesOrderOpen(true)
+ return true
+ },
+ confirm: () => {
+ setConfirmSalesOrderOpen(true)
+ return true
+ },
+ cancel: () => {
+ setCancelSalesOrderOpen(true)
+ return true
+ }
+ }
+
+ const editDisabled = getModelByName('salesOrder')
+ .actions.find((action) => action.name === 'edit')
+ .disabled(objectFormState.objectData)
+
+ 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 ||
+ editDisabled
+ }
+ loading={objectFormState.editLoading}
+ />
+
+
+
+
+
+
+ {
+ setEditFormState((prev) => ({ ...prev, ...state }))
+ }}
+ >
+ {({ loading, isEditing, objectData }) => (
+
+ }
+ active={collapseState.info}
+ onToggle={(expanded) =>
+ updateCollapseState('info', expanded)
+ }
+ collapseKey='info'
+ >
+ }
+ isEditing={isEditing}
+ type='salesOrder'
+ 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.invoices}
+ onToggle={(expanded) =>
+ updateCollapseState('invoices', expanded)
+ }
+ collapseKey='invoices'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+
+ }
+ active={collapseState.stockEvents}
+ onToggle={(expanded) =>
+ updateCollapseState('stockEvents', expanded)
+ }
+ collapseKey='stockEvents'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+
+ }
+ 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: salesOrderId },
+ orderType: 'salesOrder',
+ syncAmount: 'itemCost'
+ }}
+ />
+
+ {
+ setNewShipmentOpen(false)
+ }}
+ width={800}
+ footer={null}
+ destroyOnHidden={true}
+ >
+ {
+ setNewShipmentOpen(false)
+ }}
+ reset={newShipmentOpen}
+ defaultValues={{
+ orderType: 'salesOrder',
+ order: { _id: salesOrderId }
+ }}
+ />
+
+ {
+ setPostSalesOrderOpen(false)
+ }}
+ width={500}
+ footer={null}
+ destroyOnHidden={true}
+ centered={true}
+ >
+ {
+ setPostSalesOrderOpen(false)
+ actions.reload()
+ }}
+ objectData={objectFormState.objectData}
+ />
+
+ {
+ setConfirmSalesOrderOpen(false)
+ }}
+ width={515}
+ footer={null}
+ destroyOnHidden={true}
+ centered={true}
+ >
+ {
+ setConfirmSalesOrderOpen(false)
+ actions.reload()
+ }}
+ objectData={objectFormState.objectData}
+ />
+
+ {
+ setCancelSalesOrderOpen(false)
+ }}
+ width={515}
+ footer={null}
+ destroyOnHidden={true}
+ centered={true}
+ >
+ {
+ setCancelSalesOrderOpen(false)
+ actions.reload()
+ }}
+ objectData={objectFormState.objectData}
+ />
+
+ >
+ )
+}
+
+export default SalesOrderInfo
diff --git a/src/components/Dashboard/Sales/SalesOverview.jsx b/src/components/Dashboard/Sales/SalesOverview.jsx
new file mode 100644
index 0000000..a1736ee
--- /dev/null
+++ b/src/components/Dashboard/Sales/SalesOverview.jsx
@@ -0,0 +1,61 @@
+import { useContext } from 'react'
+import { Flex } from 'antd'
+import useCollapseState from '../hooks/useCollapseState'
+import StatsDisplay from '../common/StatsDisplay'
+import InfoCollapse from '../common/InfoCollapse'
+import ScrollBox from '../common/ScrollBox'
+
+import { ApiServerContext } from '../context/ApiServerContext'
+
+const SalesOverview = () => {
+ const { connected } = useContext(ApiServerContext)
+
+ const [collapseState, updateCollapseState] = useCollapseState(
+ 'SalesOverview',
+ {
+ clientStats: true,
+ salesOrderStats: true
+ }
+ )
+
+ if (!connected) {
+ return null
+ }
+
+ return (
+
+
+
+
+ updateCollapseState('salesOrderStats', isActive)
+ }
+ className='no-t-padding-collapse'
+ collapseKey='salesOrderStats'
+ >
+
+
+
+
+
+
+
+ )
+}
+
+export default SalesOverview
diff --git a/src/components/Dashboard/Sales/SalesSidebar.jsx b/src/components/Dashboard/Sales/SalesSidebar.jsx
new file mode 100644
index 0000000..9b89a29
--- /dev/null
+++ b/src/components/Dashboard/Sales/SalesSidebar.jsx
@@ -0,0 +1,54 @@
+import { useLocation } from 'react-router-dom'
+import DashboardSidebar from '../common/DashboardSidebar'
+import ClientIcon from '../../Icons/ClientIcon'
+import SalesIcon from '../../Icons/SalesIcon'
+
+import SalesOrderIcon from '../../Icons/SalesOrderIcon'
+
+const items = [
+ {
+ key: 'overview',
+ label: 'Overview',
+ icon: ,
+ path: '/dashboard/sales/overview'
+ },
+ { type: 'divider' },
+ {
+ key: 'clients',
+ label: 'Clients',
+ icon: ,
+ path: '/dashboard/sales/clients'
+ },
+ {
+ key: 'salesorders',
+ label: 'Sales Orders',
+ icon: ,
+ path: '/dashboard/sales/salesorders'
+ }
+]
+
+const routeKeyMap = {
+ '/dashboard/sales/overview': 'overview',
+ '/dashboard/sales/clients': 'clients',
+ '/dashboard/sales/salesorders': 'salesorders'
+}
+
+const SalesSidebar = (props) => {
+ const location = useLocation()
+ const selectedKey = (() => {
+ const match = Object.keys(routeKeyMap).find((path) => {
+ const pathSplit = path.split('/')
+ const locationPathSplit = location.pathname.split('/')
+ if (pathSplit.length > locationPathSplit.length) return false
+ for (let i = 0; i < pathSplit.length; i++) {
+ if (pathSplit[i] !== locationPathSplit[i]) return false
+ }
+ return true
+ })
+ return match ? routeKeyMap[match] : 'overview'
+ })()
+
+ return
+}
+
+export default SalesSidebar
diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
index 5a62ca9..90b99b8 100644
--- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx
+++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
@@ -11,6 +11,7 @@ const breadcrumbNameMap = {
management: 'Management',
developer: 'Developer',
finance: 'Finance',
+ sales: 'Sales',
overview: 'Overview',
info: 'Info',
design: 'Design',
diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx
index 597f7d3..abde559 100644
--- a/src/components/Dashboard/common/DashboardNavigation.jsx
+++ b/src/components/Dashboard/common/DashboardNavigation.jsx
@@ -31,6 +31,7 @@ import MenuIcon from '../../Icons/MenuIcon'
import ProductionIcon from '../../Icons/ProductionIcon'
import InventoryIcon from '../../Icons/InventoryIcon'
import FinanceIcon from '../../Icons/FinanceIcon'
+import SalesIcon from '../../Icons/SalesIcon'
import PersonIcon from '../../Icons/PersonIcon'
import CloudIcon from '../../Icons/CloudIcon'
import BellIcon from '../../Icons/BellIcon'
@@ -71,6 +72,11 @@ const DashboardNavigation = () => {
label: 'Inventory',
icon:
},
+ {
+ key: 'sales',
+ label: 'Sales',
+ icon:
+ },
{
key: 'finance',
label: 'Finance',
@@ -141,6 +147,8 @@ const DashboardNavigation = () => {
navigate('/dashboard/inventory/overview')
} else if (key === 'finance') {
navigate('/dashboard/finance/overview')
+ } else if (key === 'sales') {
+ navigate('/dashboard/sales/overview')
} else if (key === 'management') {
navigate('/dashboard/management/filaments')
}
diff --git a/src/components/Icons/ClientIcon.jsx b/src/components/Icons/ClientIcon.jsx
new file mode 100644
index 0000000..78e37d4
--- /dev/null
+++ b/src/components/Icons/ClientIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/clienticon.svg?react'
+
+const ClientIcon = (props) =>
+
+export default ClientIcon
diff --git a/src/components/Icons/SalesIcon.jsx b/src/components/Icons/SalesIcon.jsx
new file mode 100644
index 0000000..482e4c9
--- /dev/null
+++ b/src/components/Icons/SalesIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/salesicon.svg?react'
+
+const SalesIcon = (props) =>
+
+export default SalesIcon
diff --git a/src/components/Icons/SalesOrderIcon.jsx b/src/components/Icons/SalesOrderIcon.jsx
new file mode 100644
index 0000000..c899eb6
--- /dev/null
+++ b/src/components/Icons/SalesOrderIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/salesordericon.svg?react'
+
+const SalesOrderIcon = (props) =>
+
+export default SalesOrderIcon
diff --git a/src/database/ObjectModels.js b/src/database/ObjectModels.js
index 337e1d7..62f56cd 100644
--- a/src/database/ObjectModels.js
+++ b/src/database/ObjectModels.js
@@ -31,6 +31,8 @@ import { DocumentJob } from './models/DocumentJob.js'
import { TaxRate } from './models/TaxRate.js'
import { TaxRecord } from './models/TaxRecord.js'
import { Invoice } from './models/Invoice.js'
+import { Client } from './models/Client.js'
+import { SalesOrder } from './models/SalesOrder.js'
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [
@@ -66,7 +68,9 @@ export const objectModels = [
DocumentJob,
TaxRate,
TaxRecord,
- Invoice
+ Invoice,
+ Client,
+ SalesOrder
]
// Re-export individual models for direct access
@@ -103,7 +107,9 @@ export {
DocumentJob,
TaxRate,
TaxRecord,
- Invoice
+ Invoice,
+ Client,
+ SalesOrder
}
export function getModelByName(name, ignoreCase = false) {
diff --git a/src/database/models/Client.js b/src/database/models/Client.js
new file mode 100644
index 0000000..1481111
--- /dev/null
+++ b/src/database/models/Client.js
@@ -0,0 +1,215 @@
+import ClientIcon from '../../components/Icons/ClientIcon'
+import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
+import EditIcon from '../../components/Icons/EditIcon'
+import CheckIcon from '../../components/Icons/CheckIcon'
+import XMarkIcon from '../../components/Icons/XMarkIcon'
+import ReloadIcon from '../../components/Icons/ReloadIcon'
+import BinIcon from '../../components/Icons/BinIcon'
+
+export const Client = {
+ name: 'client',
+ label: 'Client',
+ prefix: 'CLI',
+ icon: ClientIcon,
+ actions: [
+ {
+ name: 'info',
+ label: 'Info',
+ default: true,
+ row: true,
+ icon: InfoCircleIcon,
+ url: (_id) => `/dashboard/sales/clients/info?clientId=${_id}`
+ },
+ {
+ name: 'reload',
+ label: 'Reload',
+ icon: ReloadIcon,
+ url: (_id) =>
+ `/dashboard/sales/clients/info?clientId=${_id}&action=reload`
+ },
+ {
+ name: 'edit',
+ label: 'Edit',
+ row: true,
+ icon: EditIcon,
+ url: (_id) => `/dashboard/sales/clients/info?clientId=${_id}&action=edit`,
+ visible: (objectData) => {
+ return !(objectData?._isEditing && objectData?._isEditing == true)
+ }
+ },
+ {
+ name: 'finishEdit',
+ label: 'Save Edits',
+ icon: CheckIcon,
+ url: (_id) =>
+ `/dashboard/sales/clients/info?clientId=${_id}&action=finishEdit`,
+ visible: (objectData) => {
+ return objectData?._isEditing && objectData?._isEditing == true
+ }
+ },
+ {
+ name: 'cancelEdit',
+ label: 'Cancel Edits',
+ icon: XMarkIcon,
+ url: (_id) =>
+ `/dashboard/sales/clients/info?clientId=${_id}&action=cancelEdit`,
+ visible: (objectData) => {
+ return objectData?._isEditing && objectData?._isEditing == true
+ }
+ },
+ { type: 'divider' },
+ {
+ name: 'delete',
+ label: 'Delete',
+ icon: BinIcon,
+ danger: true,
+ url: (_id) =>
+ `/dashboard/sales/clients/info?clientId=${_id}&action=delete`
+ }
+ ],
+ columns: [
+ 'name',
+ '_id',
+ 'country',
+ 'email',
+ 'phone',
+ 'active',
+ 'createdAt',
+ 'updatedAt'
+ ],
+ filters: [
+ 'name',
+ '_id',
+ 'country',
+ 'email',
+ 'phone',
+ 'active',
+ 'createdAt',
+ 'updatedAt'
+ ],
+ sorters: [
+ 'name',
+ 'country',
+ 'email',
+ 'phone',
+ 'active',
+ 'createdAt',
+ 'updatedAt',
+ '_id'
+ ],
+ group: [],
+ properties: [
+ {
+ name: '_id',
+ label: 'ID',
+ columnFixed: 'left',
+ type: 'id',
+ objectType: 'client',
+ showCopy: true
+ },
+ {
+ name: 'createdAt',
+ label: 'Created At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: 'name',
+ label: 'Name',
+ columnFixed: 'left',
+ required: true,
+ type: 'text'
+ },
+ {
+ name: 'updatedAt',
+ label: 'Updated At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: 'active',
+ label: 'Active',
+ type: 'bool',
+ readOnly: false,
+ required: true
+ },
+ {
+ name: 'country',
+ label: 'Country',
+ type: 'country',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'email',
+ label: 'Email',
+ columnWidth: 300,
+ type: 'email',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'phone',
+ label: 'Phone',
+ type: 'phone',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'tags',
+ label: 'Tags',
+ type: 'array',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'address.building',
+ label: 'Building',
+ type: 'text',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'address.addressLine1',
+ label: 'Address Line 1',
+ type: 'text',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'address.addressLine2',
+ label: 'Address Line 2',
+ type: 'text',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'address.city',
+ label: 'City',
+ type: 'text',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'address.state',
+ label: 'State',
+ type: 'text',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'address.postcode',
+ label: 'Postcode',
+ type: 'text',
+ readOnly: false,
+ required: false
+ },
+ {
+ name: 'address.country',
+ label: 'Country',
+ type: 'country',
+ readOnly: false,
+ required: false
+ }
+ ]
+}
diff --git a/src/database/models/SalesOrder.js b/src/database/models/SalesOrder.js
new file mode 100644
index 0000000..87c170f
--- /dev/null
+++ b/src/database/models/SalesOrder.js
@@ -0,0 +1,337 @@
+import SalesOrderIcon from '../../components/Icons/SalesOrderIcon'
+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 SalesOrder = {
+ name: 'salesOrder',
+ label: 'Sales Order',
+ prefix: 'SOR',
+ icon: SalesOrderIcon,
+ actions: [
+ {
+ name: 'info',
+ label: 'Info',
+ default: true,
+ row: true,
+ icon: InfoCircleIcon,
+ url: (_id) => `/dashboard/sales/salesorders/info?salesOrderId=${_id}`
+ },
+ {
+ name: 'edit',
+ label: 'Edit',
+ type: 'button',
+ icon: EditIcon,
+ url: (_id) =>
+ `/dashboard/sales/salesorders/info?salesOrderId=${_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/sales/salesorders/info?salesOrderId=${_id}&action=cancelEdit`,
+ visible: (objectData) => {
+ return objectData?._isEditing && objectData?._isEditing == true
+ }
+ },
+ {
+ name: 'finishEdit',
+ label: 'Finish Edit',
+ type: 'button',
+ icon: CheckIcon,
+ url: (_id) =>
+ `/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=finishEdit`,
+ visible: (objectData) => {
+ return objectData?._isEditing && objectData?._isEditing == true
+ }
+ },
+ {
+ name: 'delete',
+ label: 'Delete',
+ type: 'button',
+ icon: BinIcon,
+ danger: true,
+ url: (_id) =>
+ `/dashboard/sales/salesorders/info?salesOrderId=${_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/sales/salesorders/info?salesOrderId=${_id}&action=newOrderItem`,
+ disabled: (objectData) => {
+ return objectData?.state?.type != 'draft'
+ }
+ },
+ {
+ name: 'New Shipment',
+ label: 'New Shipment',
+ type: 'button',
+ icon: PlusIcon,
+ url: (_id) =>
+ `/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=newShipment`,
+ disabled: (objectData) => {
+ return objectData?.state?.type != 'draft'
+ }
+ },
+ {
+ name: 'New Invoice',
+ label: 'New Invoice',
+ type: 'button',
+ icon: PlusIcon,
+ url: (_id) =>
+ `/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=newInvoice`,
+ disabled: (objectData) => {
+ return objectData?.state?.type != 'delivered'
+ }
+ },
+ {
+ type: 'divider'
+ },
+ {
+ name: 'post',
+ label: 'Post',
+ type: 'button',
+ icon: CheckIcon,
+ url: (_id) =>
+ `/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=post`,
+ visible: (objectData) => {
+ return objectData?.state?.type == 'draft'
+ }
+ },
+ {
+ name: 'confirm',
+ label: 'Confirm',
+ type: 'button',
+ icon: CheckIcon,
+ url: (_id) =>
+ `/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=confirm`,
+ visible: (objectData) => {
+ return objectData?.state?.type == 'sent'
+ }
+ },
+ {
+ name: 'complete',
+ label: 'Complete',
+ type: 'button',
+ icon: CheckIcon,
+ url: (_id) =>
+ `/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=complete`,
+ disabled: (objectData) => {
+ return objectData?.state?.type != 'delivered'
+ },
+ visible: (objectData) => {
+ return objectData?.state?.type == 'delivered'
+ }
+ },
+ {
+ name: 'cancel',
+ label: 'Cancel',
+ type: 'button',
+ icon: XMarkIcon,
+ danger: true,
+ url: (_id) =>
+ `/dashboard/sales/salesorders/info?salesOrderId=${_id}&action=cancel`,
+ disabled: (objectData) => {
+ return objectData?.state?.type == 'cancelled'
+ },
+ visible: (objectData) => {
+ return (
+ objectData?.state?.type != 'draft' &&
+ objectData?.state?.type != 'completed' &&
+ objectData?.state?.type != 'delivered'
+ )
+ }
+ }
+ ],
+ group: ['client'],
+ filters: ['client'],
+ sorters: ['createdAt', 'state', 'updatedAt'],
+ columns: [
+ '_id',
+ '_reference',
+ 'state',
+ 'client',
+ 'totalAmount',
+ 'totalAmountWithTax',
+ 'totalTaxAmount',
+ 'shippingAmount',
+ 'shippingAmountWithTax',
+ 'grandTotalAmount',
+ 'createdAt',
+ 'updatedAt',
+ 'client'
+ ],
+ properties: [
+ {
+ name: '_id',
+ label: 'ID',
+ type: 'id',
+ columnFixed: 'left',
+ objectType: 'salesOrder',
+ columnWidth: 140,
+ showCopy: true
+ },
+ {
+ name: 'createdAt',
+ label: 'Created At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: '_reference',
+ label: 'Reference',
+ type: 'reference',
+ required: true,
+ objectType: 'salesOrder',
+ showCopy: true,
+ readOnly: true
+ },
+ {
+ name: 'updatedAt',
+ label: 'Updated At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ { name: 'state', label: 'State', type: 'state', readOnly: true },
+ { name: 'postedAt', label: 'Posted At', type: 'dateTime', readOnly: true },
+ {
+ name: 'client',
+ label: 'Client',
+ required: true,
+ type: 'object',
+ objectType: 'client',
+ showHyperlink: true
+ },
+ {
+ name: 'confirmedAt',
+ label: 'Confirmed At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: 'totalTaxAmount',
+ 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: 'confirmed.count',
+ label: 'Confirmed',
+ type: 'number',
+ color: 'purple'
+ },
+ {
+ name: 'partiallyShipped.count',
+ label: 'Partially Shipped',
+ type: 'number',
+ color: 'processing'
+ },
+ {
+ name: 'shipped.count',
+ label: 'Shipped',
+ type: 'number',
+ color: 'processing'
+ },
+ {
+ name: 'partiallyDelivered.count',
+ label: 'Partially Delivered',
+ type: 'number',
+ color: 'success'
+ },
+ {
+ name: 'delivered.count',
+ label: 'Delivered',
+ type: 'number',
+ color: 'success'
+ }
+ ]
+}
diff --git a/src/routes/SalesRoutes.jsx b/src/routes/SalesRoutes.jsx
new file mode 100644
index 0000000..89d5a38
--- /dev/null
+++ b/src/routes/SalesRoutes.jsx
@@ -0,0 +1,41 @@
+import { lazy } from 'react'
+import { Route } from 'react-router-dom'
+
+const Clients = lazy(
+ () => import('../components/Dashboard/Sales/Clients.jsx')
+)
+const ClientInfo = lazy(
+ () => import('../components/Dashboard/Sales/Clients/ClientInfo.jsx')
+)
+const SalesOrders = lazy(
+ () => import('../components/Dashboard/Sales/SalesOrders.jsx')
+)
+const SalesOrderInfo = lazy(
+ () => import('../components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx')
+)
+const SalesOverview = lazy(
+ () => import('../components/Dashboard/Sales/SalesOverview.jsx')
+)
+
+const SalesRoutes = [
+ }
+ />,
+ } />,
+ }
+ />,
+ } />,
+ }
+ />
+]
+
+export default SalesRoutes
+
diff --git a/src/routes/index.js b/src/routes/index.js
index 57c46ab..241e8e9 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -1,5 +1,6 @@
export { default as ProductionRoutes } from './ProductionRoutes'
export { default as InventoryRoutes } from './InventoryRoutes'
export { default as FinanceRoutes } from './FinanceRoutes'
+export { default as SalesRoutes } from './SalesRoutes'
export { default as ManagementRoutes } from './ManagementRoutes'
export { default as DeveloperRoutes } from './DeveloperRoutes'