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:
parent
8f65154691
commit
ca7ab55d1e
13
assets/icons/clienticon.svg
Normal file
13
assets/icons/clienticon.svg
Normal 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 |
16
assets/icons/listingicon.svg
Normal file
16
assets/icons/listingicon.svg
Normal 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 |
7
assets/icons/salesicon.svg
Normal file
7
assets/icons/salesicon.svg
Normal 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 |
@ -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}
|
||||
</Route>
|
||||
|
||||
@ -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 }) => {
|
||||
<InventorySidebar />
|
||||
) : isFinance ? (
|
||||
<FinanceSidebar />
|
||||
) : isSales ? (
|
||||
<SalesSidebar />
|
||||
) : isManagement ? (
|
||||
<ManagementSidebar />
|
||||
) : isDeveloper ? (
|
||||
|
||||
95
src/components/Dashboard/Sales/Clients.jsx
Normal file
95
src/components/Dashboard/Sales/Clients.jsx
Normal 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
|
||||
|
||||
194
src/components/Dashboard/Sales/Clients/ClientInfo.jsx
Normal file
194
src/components/Dashboard/Sales/Clients/ClientInfo.jsx
Normal 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
|
||||
|
||||
87
src/components/Dashboard/Sales/Clients/NewClient.jsx
Normal file
87
src/components/Dashboard/Sales/Clients/NewClient.jsx
Normal 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
|
||||
|
||||
99
src/components/Dashboard/Sales/SalesOrders.jsx
Normal file
99
src/components/Dashboard/Sales/SalesOrders.jsx
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
90
src/components/Dashboard/Sales/SalesOrders/NewSalesOrder.jsx
Normal file
90
src/components/Dashboard/Sales/SalesOrders/NewSalesOrder.jsx
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
440
src/components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx
Normal file
440
src/components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx
Normal 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
|
||||
61
src/components/Dashboard/Sales/SalesOverview.jsx
Normal file
61
src/components/Dashboard/Sales/SalesOverview.jsx
Normal 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
|
||||
54
src/components/Dashboard/Sales/SalesSidebar.jsx
Normal file
54
src/components/Dashboard/Sales/SalesSidebar.jsx
Normal 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
|
||||
@ -11,6 +11,7 @@ const breadcrumbNameMap = {
|
||||
management: 'Management',
|
||||
developer: 'Developer',
|
||||
finance: 'Finance',
|
||||
sales: 'Sales',
|
||||
overview: 'Overview',
|
||||
info: 'Info',
|
||||
design: 'Design',
|
||||
|
||||
@ -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: <InventoryIcon />
|
||||
},
|
||||
{
|
||||
key: 'sales',
|
||||
label: 'Sales',
|
||||
icon: <SalesIcon />
|
||||
},
|
||||
{
|
||||
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')
|
||||
}
|
||||
|
||||
6
src/components/Icons/ClientIcon.jsx
Normal file
6
src/components/Icons/ClientIcon.jsx
Normal 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
|
||||
6
src/components/Icons/SalesIcon.jsx
Normal file
6
src/components/Icons/SalesIcon.jsx
Normal 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
|
||||
6
src/components/Icons/SalesOrderIcon.jsx
Normal file
6
src/components/Icons/SalesOrderIcon.jsx
Normal 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
|
||||
@ -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) {
|
||||
|
||||
215
src/database/models/Client.js
Normal file
215
src/database/models/Client.js
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
337
src/database/models/SalesOrder.js
Normal file
337
src/database/models/SalesOrder.js
Normal 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'
|
||||
}
|
||||
]
|
||||
}
|
||||
41
src/routes/SalesRoutes.jsx
Normal file
41
src/routes/SalesRoutes.jsx
Normal 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
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user