Add sales module with client and sales order management features

- Introduced new SVG icons for client and sales order.
- Implemented SalesRoutes for navigation.
- Created components for managing clients and sales orders, including overview, client info, and order details.
- Added functionality for creating, editing, and canceling sales orders.
- Integrated sales statistics and actions within the dashboard layout.
This commit is contained in:
Tom Butcher 2025-12-27 20:46:45 +00:00
parent 8f65154691
commit ca7ab55d1e
26 changed files with 1932 additions and 2 deletions

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.993756,0,0,0.993756,4.70027,3)">
<path d="M21.917,58.364L7.977,58.364C2.598,58.364 0,56.673 0,53.004C0,45.534 8.171,35.818 21.434,33.487L21.434,38.985C11.428,41.086 5.926,48.099 5.926,52.158C5.926,52.741 6.224,52.965 6.958,52.965L21.434,52.965L21.434,54.927C21.434,56.202 21.603,57.347 21.917,58.364ZM22.29,28.195C17.315,25.985 13.813,20.695 13.813,14.529C13.813,6.508 19.979,0 27.481,0C35.04,0 41.156,6.405 41.156,14.478C41.156,17.712 40.199,20.706 38.573,23.144L30.955,23.144C30.844,23.144 30.734,23.146 30.625,23.148C33.464,21.732 35.494,18.445 35.494,14.478C35.494,9.257 31.873,5.399 27.481,5.399C23.126,5.399 19.474,9.334 19.474,14.524C19.474,19.579 22.791,23.478 26.82,23.866C25.711,24.303 24.771,24.923 23.999,25.691C23.298,26.389 22.72,27.223 22.29,28.195Z"/>
<g transform="matrix(0.589451,0,0,0.589451,24.45243,26.163338)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.570497,0,0,0.570497,16.522802,13.725853)">
<path d="M6.499,56.236L41.124,56.236C44.294,56.236 46.809,54.281 46.809,50.825C46.809,47.443 44.391,45.464 41.124,45.464L18.117,45.464L18.117,45.062C22.049,43.001 23.774,38.335 23.774,33.596C23.774,32.751 23.669,32.057 23.534,31.399L37.509,31.399C39.584,31.399 41.06,30.032 41.06,28.125C41.06,26.241 39.584,24.905 37.509,24.905L22.134,24.905C21.788,23.604 21.265,21.585 21.265,19.395C21.265,13.243 26.441,10.758 32.681,10.758C34.774,10.758 36.433,10.942 37.882,11.247C38.957,11.394 40.282,11.543 41.544,11.543C44.007,11.543 46.13,10.334 46.13,7.354C46.13,5.297 45.211,3.871 43.447,2.745C39.934,0.628 34.458,0.378 30.367,0.378C18.274,0.378 7.903,5.899 7.903,17.414C7.903,19.396 8.212,21.38 9.112,24.905L3.574,24.905C1.5,24.905 0,26.241 0,28.125C0,30.071 1.514,31.399 3.574,31.399L10.467,31.399C10.73,32.486 10.797,33.332 10.797,34.157C10.797,38.814 8.74,42.769 5.065,44.806C3.057,46.118 0.808,47.931 0.808,50.875C0.808,54.298 3.219,56.236 6.499,56.236Z" style="fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.58577,0,0,0.58577,29.000019,28.999974)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.570497,0,0,0.570497,16.522802,13.725853)">
<path d="M6.499,56.236L41.124,56.236C44.294,56.236 46.809,54.281 46.809,50.825C46.809,47.443 44.391,45.464 41.124,45.464L18.117,45.464L18.117,45.062C22.049,43.001 23.774,38.335 23.774,33.596C23.774,32.751 23.669,32.057 23.534,31.399L37.509,31.399C39.584,31.399 41.06,30.032 41.06,28.125C41.06,26.241 39.584,24.905 37.509,24.905L22.134,24.905C21.788,23.604 21.265,21.585 21.265,19.395C21.265,13.243 26.441,10.758 32.681,10.758C34.774,10.758 36.433,10.942 37.882,11.247C38.957,11.394 40.282,11.543 41.544,11.543C44.007,11.543 46.13,10.334 46.13,7.354C46.13,5.297 45.211,3.871 43.447,2.745C39.934,0.628 34.458,0.378 30.367,0.378C18.274,0.378 7.903,5.899 7.903,17.414C7.903,19.396 8.212,21.38 9.112,24.905L3.574,24.905C1.5,24.905 0,26.241 0,28.125C0,30.071 1.514,31.399 3.574,31.399L10.467,31.399C10.73,32.486 10.797,33.332 10.797,34.157C10.797,38.814 8.74,42.769 5.065,44.806C3.057,46.118 0.808,47.931 0.808,50.875C0.808,54.298 3.219,56.236 6.499,56.236Z" style="fill-rule:nonzero;"/>
</g>
</g>
<g transform="matrix(1,0,0,1,11.133834,5.394156)">
<rect x="0" y="0" width="74.451" height="58.962" style="fill-opacity:0;"/>
<g transform="matrix(0.700206,0,0,0.700206,-5.133834,5.956054)">
<path d="M28.563,58.962L10.588,58.962C3.679,58.962 0,55.326 0,48.481L0,10.522C0,3.667 3.679,0.02 10.588,0.02L63.676,0.02C70.606,0.02 74.264,3.667 74.264,10.522L74.264,20.933C74.047,20.926 73.827,20.922 73.604,20.922L67.305,20.922L67.305,11.251C67.305,8.378 65.813,6.979 63.097,6.979L11.167,6.979C8.429,6.979 6.959,8.378 6.959,11.251L6.959,47.743C6.959,50.615 8.429,52.003 11.167,52.003L28.563,52.003L28.563,58.962ZM28.857,31.097L16.175,31.097C14.981,31.097 14.107,30.204 14.107,29.051C14.107,27.929 14.981,27.055 16.175,27.055L30.296,27.055C29.634,28.235 29.144,29.582 28.857,31.097ZM16.175,19.207C14.981,19.207 14.107,18.313 14.107,17.138C14.107,16.016 14.981,15.164 16.175,15.164L58.13,15.164C59.295,15.164 60.157,16.016 60.157,17.138C60.157,18.313 59.295,19.207 58.13,19.207L16.175,19.207Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.822372,0,0,0.822372,2,2.325529)">
<path d="M25.615,51.541L61.925,51.541C63.571,51.541 65.041,50.302 65.041,48.462C65.041,46.631 63.571,45.412 61.925,45.412L26.434,45.412C25.11,45.412 24.295,44.46 24.093,43.051L19.177,9.045C18.785,6.142 17.522,4.675 13.848,4.675L3.326,4.675C1.539,4.675 0,6.215 0,8.032C0,9.86 1.539,11.411 3.326,11.411L12.627,11.411L17.348,43.696C18.064,48.571 20.686,51.541 25.615,51.541ZM20.362,39.849L62.285,39.849C67.216,39.849 69.857,36.878 70.563,31.952L72.815,16.861C72.877,16.425 72.96,15.886 72.96,15.485C72.96,13.488 71.586,12.07 69.142,12.07L17.504,12.07L17.525,18.229L65.71,18.229L63.868,31.389C63.678,32.83 62.925,33.711 61.558,33.711L20.319,33.711L20.362,39.849ZM28.223,67.493C31.383,67.493 33.901,64.986 33.901,61.805C33.901,58.664 31.383,56.126 28.223,56.126C25.061,56.126 22.513,58.664 22.513,61.805C22.513,64.986 25.061,67.493 28.223,67.493ZM56.875,67.493C60.047,67.493 62.575,64.986 62.575,61.805C62.575,58.664 60.047,56.126 56.875,56.126C53.735,56.126 51.175,58.664 51.175,61.805C51.175,64.986 53.735,67.493 56.875,67.493Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -32,6 +32,7 @@ import {
ProductionRoutes, ProductionRoutes,
InventoryRoutes, InventoryRoutes,
FinanceRoutes, FinanceRoutes,
SalesRoutes,
ManagementRoutes, ManagementRoutes,
DeveloperRoutes DeveloperRoutes
} from './routes' } from './routes'
@ -98,6 +99,7 @@ const AppContent = () => {
{ProductionRoutes} {ProductionRoutes}
{InventoryRoutes} {InventoryRoutes}
{FinanceRoutes} {FinanceRoutes}
{SalesRoutes}
{ManagementRoutes} {ManagementRoutes}
{DeveloperRoutes} {DeveloperRoutes}
</Route> </Route>

View File

@ -5,6 +5,7 @@ import { useLocation } from 'react-router-dom'
import ProductionSidebar from './Production/ProductionSidebar' import ProductionSidebar from './Production/ProductionSidebar'
import InventorySidebar from './Inventory/InventorySidebar' import InventorySidebar from './Inventory/InventorySidebar'
import FinanceSidebar from './Finance/FinanceSidebar' import FinanceSidebar from './Finance/FinanceSidebar'
import SalesSidebar from './Sales/SalesSidebar'
import ManagementSidebar from './Management/ManagementSidebar' import ManagementSidebar from './Management/ManagementSidebar'
import DashboardNavigation from './common/DashboardNavigation' import DashboardNavigation from './common/DashboardNavigation'
import DashboardBreadcrumb from './common/DashboardBreadcrumb' import DashboardBreadcrumb from './common/DashboardBreadcrumb'
@ -19,6 +20,7 @@ const DashboardLayout = ({ children }) => {
const isProduction = location.pathname.startsWith('/dashboard/production') const isProduction = location.pathname.startsWith('/dashboard/production')
const isInventory = location.pathname.startsWith('/dashboard/inventory') const isInventory = location.pathname.startsWith('/dashboard/inventory')
const isFinance = location.pathname.startsWith('/dashboard/finance') const isFinance = location.pathname.startsWith('/dashboard/finance')
const isSales = location.pathname.startsWith('/dashboard/sales')
const isManagement = location.pathname.startsWith('/dashboard/management') const isManagement = location.pathname.startsWith('/dashboard/management')
const isDeveloper = location.pathname.startsWith('/dashboard/developer') const isDeveloper = location.pathname.startsWith('/dashboard/developer')
@ -38,6 +40,8 @@ const DashboardLayout = ({ children }) => {
<InventorySidebar /> <InventorySidebar />
) : isFinance ? ( ) : isFinance ? (
<FinanceSidebar /> <FinanceSidebar />
) : isSales ? (
<SalesSidebar />
) : isManagement ? ( ) : isManagement ? (
<ManagementSidebar /> <ManagementSidebar />
) : isDeveloper ? ( ) : isDeveloper ? (

View File

@ -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: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newClient') {
setNewClientOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='client'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='client'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newClientOpen}
onCancel={() => setNewClientOpen(false)}
footer={null}
destroyOnHidden={true}
width={700}
>
<NewClient
onOk={() => {
setNewClientOpen(false)
tableRef.current?.reload()
}}
reset={!newClientOpen}
/>
</Modal>
</>
)
}
export default Clients

View File

@ -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 (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='client'
id={clientId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Client Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='client'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
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}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='Client Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={clientId}
type='client'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='client'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={clientId} type='client' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': clientId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default ClientInfo

View File

@ -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 (
<NewObjectForm
type={'client'}
defaultValues={{ active: true, ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='client'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='client'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='client'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Client'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewClient.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewClient

View File

@ -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: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newSalesOrder') {
setNewSalesOrderOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='salesOrder'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='salesOrder'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newSalesOrderOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewSalesOrderOpen(false)
}}
destroyOnHidden={true}
>
<NewSalesOrder
onOk={() => {
setNewSalesOrderOpen(false)
tableRef.current?.reload()
}}
reset={newSalesOrderOpen}
/>
</Modal>
</>
)
}
export default SalesOrders

View File

@ -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 (
<MessageDialogView
title={'Are you sure you want to cancel this sales order?'}
description={`Cancelling sales order ${objectData?.name || objectData?._reference || objectData?._id} will update its status to cancelled.`}
onOk={handleCancel}
okText='Cancel'
okLoading={cancelLoading}
/>
)
}
CancelSalesOrder.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default CancelSalesOrder

View File

@ -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 (
<MessageDialogView
title={'Are you sure you want to confirm this sales order?'}
description={`Confirming sales order ${objectData?.name || objectData?._reference || objectData?._id} will update its status to confirmed.`}
onOk={handleConfirm}
okText='Confirm'
okLoading={confirmLoading}
/>
)
}
ConfirmSalesOrder.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default ConfirmSalesOrder

View File

@ -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 (
<NewObjectForm
type={'salesOrder'}
reset={reset}
defaultValues={{
state: { type: 'draft' },
...defaultValues
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='salesOrder'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
_reference: false,
items: false,
cost: false
}}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='salesOrder'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
_reference: false,
totalAmount: false,
totalAmountWithTax: false,
totalTaxAmount: false,
postedAt: false,
confirmedAt: false,
shippingAmount: false,
shippingAmountWithTax: false,
grandTotalAmount: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Sales Order'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewSalesOrder.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewSalesOrder

View File

@ -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 (
<MessageDialogView
title={'Are you sure you want to post this sales order?'}
description={`Posting sales order ${objectData?.name || objectData?._reference || objectData?._id} will finalize it and update inventory levels where applicable.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostSalesOrder.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostSalesOrder

View File

@ -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 (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='salesOrder'
id={salesOrderId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Sales Order Information' },
{ key: 'orderItems', label: 'Order Items' },
{ key: 'shipments', label: 'Shipments' },
{ key: 'invoices', label: 'Invoices' },
{ key: 'stockEvents', label: 'Stock Events' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='salesOrder'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
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}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={salesOrderId}
type='salesOrder'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Sales Order Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='salesOrder'
labelWidth='225px'
objectData={objectData}
visibleProperties={{
items: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Sales Order Items'
icon={<OrderItemsIcon />}
active={collapseState.orderItems}
onToggle={(expanded) =>
updateCollapseState('orderItems', expanded)
}
collapseKey='orderItems'
>
<ObjectTable
type='orderItem'
masterFilter={{
'order._id': salesOrderId,
orderType: 'salesOrder'
}}
visibleColumns={{ order: false }}
ref={orderItemsTableRef}
/>
</InfoCollapse>
<InfoCollapse
title='Shipments'
icon={<ShipmentIcon />}
active={collapseState.shipments}
onToggle={(expanded) =>
updateCollapseState('shipments', expanded)
}
collapseKey='shipments'
>
<ObjectTable
type='shipment'
masterFilter={{
'order._id': salesOrderId,
orderType: 'salesOrder'
}}
visibleColumns={{ order: false }}
ref={shipmentsTableRef}
/>
</InfoCollapse>
<InfoCollapse
title='Invoices'
icon={<InvoiceIcon />}
active={collapseState.invoices}
onToggle={(expanded) =>
updateCollapseState('invoices', expanded)
}
collapseKey='invoices'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='invoice'
masterFilter={{
'order._id': salesOrderId,
orderType: 'salesOrder'
}}
visibleColumns={{ order: false }}
/>
)}
</InfoCollapse>
<InfoCollapse
title='Stock Events'
icon={<StockEventIcon />}
active={collapseState.stockEvents}
onToggle={(expanded) =>
updateCollapseState('stockEvents', expanded)
}
collapseKey='stockEvents'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='stockEvent'
masterFilter={{
'owner._id': salesOrderId
}}
/>
)}
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={salesOrderId} type='salesOrder' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': salesOrderId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={newOrderItemOpen}
onCancel={() => {
setNewOrderItemOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewOrderItem
onOk={() => {
setNewOrderItemOpen(false)
}}
reset={newOrderItemOpen}
defaultValues={{
order: { _id: salesOrderId },
orderType: 'salesOrder',
syncAmount: 'itemCost'
}}
/>
</Modal>
<Modal
open={newShipmentOpen}
onCancel={() => {
setNewShipmentOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewShipment
onOk={() => {
setNewShipmentOpen(false)
}}
reset={newShipmentOpen}
defaultValues={{
orderType: 'salesOrder',
order: { _id: salesOrderId }
}}
/>
</Modal>
<Modal
open={postSalesOrderOpen}
onCancel={() => {
setPostSalesOrderOpen(false)
}}
width={500}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostSalesOrder
onOk={() => {
setPostSalesOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={confirmSalesOrderOpen}
onCancel={() => {
setConfirmSalesOrderOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<ConfirmSalesOrder
onOk={() => {
setConfirmSalesOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={cancelSalesOrderOpen}
onCancel={() => {
setCancelSalesOrderOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<CancelSalesOrder
onOk={() => {
setCancelSalesOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</>
)
}
export default SalesOrderInfo

View File

@ -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 (
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<ScrollBox>
<Flex vertical gap={'large'}>
<InfoCollapse
title='Sales Order Statistics'
icon={null}
active={collapseState.salesOrderStats}
onToggle={(isActive) =>
updateCollapseState('salesOrderStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='salesOrderStats'
>
<Flex
justify='flex-start'
gap='middle'
wrap='wrap'
align='flex-start'
>
<StatsDisplay objectType='salesOrder' />
</Flex>
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
)
}
export default SalesOverview

View File

@ -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: <SalesIcon />,
path: '/dashboard/sales/overview'
},
{ type: 'divider' },
{
key: 'clients',
label: 'Clients',
icon: <ClientIcon />,
path: '/dashboard/sales/clients'
},
{
key: 'salesorders',
label: 'Sales Orders',
icon: <SalesOrderIcon />,
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 <DashboardSidebar items={items} selectedKey={selectedKey} {...props} />
}
export default SalesSidebar

View File

@ -11,6 +11,7 @@ const breadcrumbNameMap = {
management: 'Management', management: 'Management',
developer: 'Developer', developer: 'Developer',
finance: 'Finance', finance: 'Finance',
sales: 'Sales',
overview: 'Overview', overview: 'Overview',
info: 'Info', info: 'Info',
design: 'Design', design: 'Design',

View File

@ -31,6 +31,7 @@ import MenuIcon from '../../Icons/MenuIcon'
import ProductionIcon from '../../Icons/ProductionIcon' import ProductionIcon from '../../Icons/ProductionIcon'
import InventoryIcon from '../../Icons/InventoryIcon' import InventoryIcon from '../../Icons/InventoryIcon'
import FinanceIcon from '../../Icons/FinanceIcon' import FinanceIcon from '../../Icons/FinanceIcon'
import SalesIcon from '../../Icons/SalesIcon'
import PersonIcon from '../../Icons/PersonIcon' import PersonIcon from '../../Icons/PersonIcon'
import CloudIcon from '../../Icons/CloudIcon' import CloudIcon from '../../Icons/CloudIcon'
import BellIcon from '../../Icons/BellIcon' import BellIcon from '../../Icons/BellIcon'
@ -71,6 +72,11 @@ const DashboardNavigation = () => {
label: 'Inventory', label: 'Inventory',
icon: <InventoryIcon /> icon: <InventoryIcon />
}, },
{
key: 'sales',
label: 'Sales',
icon: <SalesIcon />
},
{ {
key: 'finance', key: 'finance',
label: 'Finance', label: 'Finance',
@ -141,6 +147,8 @@ const DashboardNavigation = () => {
navigate('/dashboard/inventory/overview') navigate('/dashboard/inventory/overview')
} else if (key === 'finance') { } else if (key === 'finance') {
navigate('/dashboard/finance/overview') navigate('/dashboard/finance/overview')
} else if (key === 'sales') {
navigate('/dashboard/sales/overview')
} else if (key === 'management') { } else if (key === 'management') {
navigate('/dashboard/management/filaments') navigate('/dashboard/management/filaments')
} }

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/clienticon.svg?react'
const ClientIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default ClientIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/salesicon.svg?react'
const SalesIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default SalesIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/salesordericon.svg?react'
const SalesOrderIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default SalesOrderIcon

View File

@ -31,6 +31,8 @@ import { DocumentJob } from './models/DocumentJob.js'
import { TaxRate } from './models/TaxRate.js' import { TaxRate } from './models/TaxRate.js'
import { TaxRecord } from './models/TaxRecord.js' import { TaxRecord } from './models/TaxRecord.js'
import { Invoice } from './models/Invoice.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' import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [ export const objectModels = [
@ -66,7 +68,9 @@ export const objectModels = [
DocumentJob, DocumentJob,
TaxRate, TaxRate,
TaxRecord, TaxRecord,
Invoice Invoice,
Client,
SalesOrder
] ]
// Re-export individual models for direct access // Re-export individual models for direct access
@ -103,7 +107,9 @@ export {
DocumentJob, DocumentJob,
TaxRate, TaxRate,
TaxRecord, TaxRecord,
Invoice Invoice,
Client,
SalesOrder
} }
export function getModelByName(name, ignoreCase = false) { export function getModelByName(name, ignoreCase = false) {

View File

@ -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
}
]
}

View File

@ -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'
}
]
}

View File

@ -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 = [
<Route
key='overview'
path='sales/overview'
element={<SalesOverview />}
/>,
<Route key='clients' path='sales/clients' element={<Clients />} />,
<Route
key='clients-info'
path='sales/clients/info'
element={<ClientInfo />}
/>,
<Route key='salesorders' path='sales/salesorders' element={<SalesOrders />} />,
<Route
key='salesorders-info'
path='sales/salesorders/info'
element={<SalesOrderInfo />}
/>
]
export default SalesRoutes

View File

@ -1,5 +1,6 @@
export { default as ProductionRoutes } from './ProductionRoutes' export { default as ProductionRoutes } from './ProductionRoutes'
export { default as InventoryRoutes } from './InventoryRoutes' export { default as InventoryRoutes } from './InventoryRoutes'
export { default as FinanceRoutes } from './FinanceRoutes' export { default as FinanceRoutes } from './FinanceRoutes'
export { default as SalesRoutes } from './SalesRoutes'
export { default as ManagementRoutes } from './ManagementRoutes' export { default as ManagementRoutes } from './ManagementRoutes'
export { default as DeveloperRoutes } from './DeveloperRoutes' export { default as DeveloperRoutes } from './DeveloperRoutes'