Compare commits

..

3 Commits

42 changed files with 2137 additions and 360 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.887143,0,0,0.887143,3,3.211722)">
<path d="M37.605,64.798C35.97,65.05 34.296,65.181 32.59,65.181C14.599,65.181 0,50.582 0,32.59C0,14.599 14.599,0 32.59,0C50.592,0 65.191,14.599 65.191,32.59C65.191,32.92 65.186,33.248 65.176,33.575L64.728,33.251L64.711,33.241L63.625,32.757L62.474,32.491L61.304,32.455L60.16,32.642L59.079,33.04L58.094,33.629L58.006,33.706C58.022,33.336 58.03,32.964 58.03,32.59C58.03,18.529 46.652,7.161 32.59,7.161C18.529,7.161 7.161,18.529 7.161,32.59C7.161,46.652 18.529,58.02 32.59,58.02C36.258,58.02 39.743,57.247 42.892,55.854L38.593,62.35C38.366,62.692 38.175,63.045 38.019,63.4L37.639,64.54L37.605,64.798Z"/>
</g>
<g transform="matrix(0.887143,0,0,0.887143,3,3.211722)">
<path d="M33.627,46.224L22.957,46.224C21.791,46.224 20.916,45.429 20.916,44.203C20.916,43.087 21.687,42.386 22.622,41.935C24.986,40.884 26.28,38.794 26.28,36.193C26.28,35.474 26.152,34.744 25.961,33.93L22.25,33.93C21.359,33.93 20.726,33.348 20.726,32.529C20.726,31.742 21.359,31.16 22.25,31.16L25.284,31.16C24.896,29.781 24.7,28.625 24.7,27.447C24.7,21.845 29.228,18.881 35.049,18.881C37.066,18.881 38.335,19.006 39.843,19.534C41.021,19.845 41.964,20.474 41.964,21.695C41.964,22.719 41.283,23.327 40.22,23.327C39.785,23.327 39.274,23.232 38.771,23.127C38.008,22.948 36.92,22.811 35.732,22.811C32.123,22.811 29.593,24.555 29.593,27.791C29.593,28.786 29.707,29.656 30.074,31.16L38.186,31.16C39.066,31.16 39.71,31.753 39.71,32.529C39.71,33.338 39.066,33.93 38.186,33.93L30.783,33.93C30.911,34.534 30.975,35.213 30.975,36.008C30.975,38.529 29.988,41.074 27.758,42.081L27.758,42.226L33.619,42.226C33.535,42.851 33.491,43.509 33.491,44.2C33.491,44.907 33.538,45.582 33.627,46.224Z"/>
</g>
<g transform="matrix(0.48434,0,0,0.48434,31.429473,34.991997)">
<path d="M20.943,29.037C28.325,29.037 33.046,23.873 33.046,15.317C33.046,6.658 28.272,1.806 20.943,1.806C13.64,1.806 8.841,6.658 8.841,15.343C8.841,23.925 13.588,29.037 20.943,29.037ZM20.943,22.073C18.883,22.073 17.918,19.908 17.918,15.343C17.918,10.831 18.883,8.744 20.943,8.744C23.004,8.744 23.969,10.831 23.969,15.343C23.969,19.908 23.004,22.073 20.943,22.073ZM55.145,57.616C62.526,57.616 67.247,52.478 67.247,43.923C67.247,35.263 62.474,30.386 55.145,30.386C47.868,30.386 43.042,35.263 43.042,43.949C43.042,52.53 47.789,57.616 55.145,57.616ZM55.145,50.678C53.084,50.678 52.119,48.513 52.119,43.949C52.119,39.436 53.084,37.35 55.145,37.35C57.205,37.35 58.17,39.436 58.17,43.949C58.17,48.513 57.205,50.678 55.145,50.678ZM18.469,59.162C20.812,60.537 23.938,59.943 25.469,57.599L58.25,7.881C59.938,5.318 59.344,2.131 56.781,0.662C54.406,-0.682 51.562,0.068 49.938,2.475L17.156,52.006C15.438,54.599 15.906,57.662 18.469,59.162Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,14 @@
<?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.887143,0,0,0.887143,3,3.211722)">
<path d="M29.615,65.047C13.017,63.543 0,49.579 0,32.59C0,14.599 14.599,0 32.59,0C50.592,0 65.191,14.599 65.191,32.59C65.191,36.04 64.654,39.365 63.66,42.486C63.098,42.436 62.495,42.411 61.843,42.411L56.068,42.411C57.332,39.391 58.03,36.073 58.03,32.59C58.03,18.529 46.652,7.161 32.59,7.161C18.529,7.161 7.161,18.529 7.161,32.59C7.161,45.539 16.801,56.204 29.308,57.811L29.308,61.604C29.308,62.941 29.41,64.07 29.615,65.047Z"/>
</g>
<g transform="matrix(0.887143,0,0,0.887143,3,3.211722)">
<path d="M30.87,46.224L22.957,46.224C21.791,46.224 20.916,45.429 20.916,44.203C20.916,43.087 21.687,42.386 22.622,41.935C24.986,40.884 26.28,38.794 26.28,36.193C26.28,35.474 26.152,34.744 25.961,33.93L22.25,33.93C21.359,33.93 20.726,33.348 20.726,32.529C20.726,31.742 21.359,31.16 22.25,31.16L25.284,31.16C24.896,29.781 24.7,28.625 24.7,27.447C24.7,21.845 29.228,18.881 35.049,18.881C37.066,18.881 38.335,19.006 39.843,19.534C41.021,19.845 41.964,20.474 41.964,21.695C41.964,22.719 41.283,23.327 40.22,23.327C39.785,23.327 39.274,23.232 38.771,23.127C38.008,22.948 36.92,22.811 35.732,22.811C32.123,22.811 29.593,24.555 29.593,27.791C29.593,28.786 29.707,29.656 30.074,31.16L38.186,31.16C39.066,31.16 39.71,31.753 39.71,32.529C39.71,33.338 39.066,33.93 38.186,33.93L30.783,33.93C30.911,34.534 30.975,35.213 30.975,36.008C30.975,38.529 29.988,41.074 27.758,42.081L27.758,42.226L35.078,42.226C35.08,42.226 35.082,42.226 35.084,42.226C35.222,42.228 35.338,42.331 35.355,42.468C35.373,42.605 35.287,42.734 35.154,42.771C34.259,43.164 32.987,43.909 31.885,45.011C31.503,45.392 31.165,45.792 30.87,46.224Z"/>
</g>
<g transform="matrix(0.317224,0,0,0.317224,32,43.836431)">
<path d="M19.344,63.562L81.531,63.562C88.781,63.562 93.094,62.406 96.438,59.062C99.75,55.75 100.875,51.562 100.875,44.219L100.875,19.344C100.875,12 99.75,7.781 96.438,4.5C93.062,1.156 88.781,0 81.531,0L19.344,0C12.094,0 7.781,1.156 4.438,4.5C1.125,7.812 0,12 0,19.344L0,44.219C0,51.562 1.125,55.781 4.438,59.062C7.812,62.406 12.094,63.562 19.344,63.562ZM18.469,52.469C15.125,52.469 13.656,51.969 12.594,50.938C11.562,49.844 11.094,48.469 11.094,45.094L11.094,18.469C11.094,15.094 11.562,13.688 12.594,12.625C13.656,11.594 15.125,11.094 18.469,11.094L82.406,11.094C85.75,11.094 87.219,11.594 88.281,12.625C89.312,13.688 89.781,15.094 89.781,18.469L89.781,45.094C89.781,48.469 89.312,49.844 88.281,50.938C87.219,51.969 85.75,52.469 82.406,52.469L18.469,52.469Z" style="fill-rule:nonzero;"/>
<path d="M20.531,30.188L28.031,30.188C30.531,30.188 32.25,28.469 32.25,25.969L32.25,20.469C32.25,17.969 30.531,16.25 28.031,16.25L20.531,16.25C18.031,16.25 16.312,17.969 16.312,20.469L16.312,25.969C16.312,28.469 18.031,30.188 20.531,30.188ZM18.75,47.281L82.125,47.281C83.469,47.281 84.531,46.156 84.531,44.844C84.531,43.5 83.469,42.469 82.125,42.469L18.75,42.469C17.406,42.469 16.344,43.5 16.344,44.844C16.344,46.156 17.406,47.281 18.75,47.281Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

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.887143,0,0,0.887143,3,3.211722)">
<path d="M40.292,64.266C37.823,64.864 35.243,65.181 32.59,65.181C14.599,65.181 0,50.582 0,32.59C0,14.599 14.599,0 32.59,0C48.085,0 61.059,10.816 64.372,25.302C62.551,23.444 59.987,22.309 57.084,22.306L57.08,22.306C56.676,22.306 56.281,22.325 55.894,22.364C51.96,13.406 43.012,7.161 32.59,7.161C18.529,7.161 7.161,18.529 7.161,32.59C7.161,46.652 18.529,58.02 32.59,58.02C35.805,58.02 38.88,57.426 41.71,56.341C40.562,58.022 40.04,59.878 40.04,61.96C40.04,62.761 40.127,63.532 40.292,64.266Z"/>
</g>
<g transform="matrix(0.887143,0,0,0.887143,3,3.211722)">
<path d="M35.033,46.224L22.957,46.224C21.791,46.224 20.916,45.429 20.916,44.203C20.916,43.087 21.687,42.386 22.622,41.935C24.986,40.884 26.28,38.794 26.28,36.193C26.28,35.474 26.152,34.744 25.961,33.93L22.25,33.93C21.359,33.93 20.726,33.348 20.726,32.529C20.726,31.742 21.359,31.16 22.25,31.16L25.284,31.16C24.896,29.781 24.7,28.625 24.7,27.447C24.7,21.845 29.228,18.881 35.049,18.881C37.066,18.881 38.335,19.006 39.843,19.534C41.021,19.845 41.964,20.474 41.964,21.695C41.964,22.719 41.283,23.327 40.22,23.327C39.785,23.327 39.274,23.232 38.771,23.127C38.008,22.948 36.92,22.811 35.732,22.811C32.123,22.811 29.593,24.555 29.593,27.791C29.593,28.786 29.707,29.656 30.074,31.16L38.186,31.16C39.066,31.16 39.71,31.753 39.71,32.529C39.71,33.338 39.066,33.93 38.186,33.93L30.783,33.93C30.911,34.534 30.975,35.213 30.975,36.008C30.975,38.529 29.988,41.074 27.758,42.081L27.758,42.226L35.079,42.226C34.947,42.877 34.876,43.548 34.87,44.233C34.863,44.909 34.917,45.574 35.033,46.224Z"/>
</g>
<g transform="matrix(0.465962,0,0,0.465962,36.933857,26)">
<path d="M0.001,35.371C-0.038,39.202 1.473,42.727 4.441,45.671L16.349,57.61C11.913,60.839 9.844,64.493 9.844,69.059C9.844,76.236 15.431,81.432 22.42,81.549C26.022,81.612 29.343,80.305 31.751,77.938L53.883,55.695C59.804,49.751 59.453,42.361 52.939,35.83L41.879,24.794L44.775,21.89C47.234,19.407 48.675,16.156 48.692,12.936C48.785,5.665 43.16,0.007 35.85,0C31.88,0 28.667,1.489 25.293,4.83L3.649,26.387C1.324,28.753 0.032,31.925 0.001,35.371ZM18.613,24.255L31.665,11.203C33.369,9.474 34.298,9.018 35.85,9.018C37.991,9.018 39.664,10.692 39.664,12.825C39.664,13.85 39.304,14.61 38.379,15.518L24.077,29.836C23.14,27.191 21.326,25.315 18.613,24.255ZM9.135,35.594C9.135,33.425 10.709,31.799 12.756,31.799C14.857,31.799 16.468,33.464 16.468,35.669C16.468,36.68 16.444,37.157 16.216,37.941C15.03,43.335 21.775,44.881 24.421,42.268L35.475,31.174L46.542,42.227C49.615,45.227 49.817,47.01 47.511,49.299L33.566,63.268C32.715,61.558 31.327,59.892 29.126,57.643L11.994,40.417C9.701,38.148 9.135,37.157 9.135,35.594ZM18.887,69.149C18.887,67.295 20.04,65.793 22.846,64.06L23.303,64.516C25.504,66.773 26.164,67.845 26.172,69.173C26.22,71.078 24.636,72.476 22.444,72.476C20.445,72.476 18.887,71.13 18.887,69.149Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -347,6 +347,26 @@ body {
} }
.ant-select-selection-item .ant-tag, .ant-select-selection-item .ant-tag,
.ant-select-tree-title .ant-tag { .ant-select-tree-title .object-display-tag {
background: transparent !important; background: transparent !important;
padding: 0;
margin-right: 1px !important;
padding-top: 3px;
}
.ant-select-outlined.ant-select-multiple
.ant-select-selection-item
.object-display-tag {
padding-top: 0px;
}
.ant-select-outlined.ant-select-multiple
.ant-select-selection-item
.object-display-tag
.ant-flex {
margin-top: -0.75px;
}
.object-info-descriptions table {
table-layout: fixed !important;
} }

View File

@ -19,6 +19,8 @@ import DocumentJobIcon from '../../Icons/DocumentJobIcon'
import FileIcon from '../../Icons/FileIcon' import FileIcon from '../../Icons/FileIcon'
import CourierIcon from '../../Icons/CourierIcon' import CourierIcon from '../../Icons/CourierIcon'
import CourierServiceIcon from '../../Icons/CourierServiceIcon' import CourierServiceIcon from '../../Icons/CourierServiceIcon'
import TaxRateIcon from '../../Icons/TaxRateIcon'
import TaxRecordIcon from '../../Icons/TaxRecordIcon'
const items = [ const items = [
{ {
@ -65,6 +67,19 @@ const items = [
path: '/dashboard/management/courierservices' path: '/dashboard/management/courierservices'
}, },
{ type: 'divider' }, { type: 'divider' },
{
key: 'taxRates',
icon: <TaxRateIcon />,
label: 'Tax Rates',
path: '/dashboard/management/taxrates'
},
{
key: 'taxRecords',
icon: <TaxRecordIcon />,
label: 'Tax Records',
path: '/dashboard/management/taxrecords'
},
{ type: 'divider' },
{ {
key: 'noteTypes', key: 'noteTypes',
icon: <NoteTypeIcon />, icon: <NoteTypeIcon />,
@ -156,6 +171,8 @@ const routeKeyMap = {
'/dashboard/management/vendors': 'vendors', '/dashboard/management/vendors': 'vendors',
'/dashboard/management/couriers': 'couriers', '/dashboard/management/couriers': 'couriers',
'/dashboard/management/courierservices': 'courierServices', '/dashboard/management/courierservices': 'courierServices',
'/dashboard/management/taxrates': 'taxRates',
'/dashboard/management/taxrecords': 'taxRecords',
'/dashboard/management/materials': 'materials', '/dashboard/management/materials': 'materials',
'/dashboard/management/notetypes': 'noteTypes', '/dashboard/management/notetypes': 'noteTypes',
'/dashboard/management/settings': 'settings', '/dashboard/management/settings': 'settings',

View File

@ -25,8 +25,12 @@ const NewPart = ({ onOk }) => {
visibleProperties={{ visibleProperties={{
file: false, file: false,
priceMode: false, priceMode: false,
globalPricing: false, cost: false,
amount: false, costTaxRate: false,
costWithTax: false,
price: false,
priceTaxRate: false,
priceWithTax: false,
margin: false margin: false
}} }}
/> />
@ -45,9 +49,13 @@ const NewPart = ({ onOk }) => {
objectData={objectData} objectData={objectData}
visibleProperties={{ visibleProperties={{
priceMode: true, priceMode: true,
globalPricing: true, margin: true,
amount: true, cost: true,
margin: true costTaxRate: true,
costWithTax: true,
price: true,
priceTaxRate: true,
priceWithTax: true
}} }}
/> />
) )

View File

@ -0,0 +1,97 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import NewTaxRate from './TaxRates/NewTaxRate'
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 TaxRates = () => {
const [messageApi, contextHolder] = message.useMessage()
const [newTaxRateOpen, setNewTaxRateOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('taxRate')
const [columnVisibility, setColumnVisibility] = useColumnVisibility('taxRate')
const actionItems = {
items: [
{
label: 'New Tax Rate',
key: 'newTaxRate',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newTaxRate') {
setNewTaxRateOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='taxRate'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='taxRate'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newTaxRateOpen}
onCancel={() => setNewTaxRateOpen(false)}
footer={null}
destroyOnHidden={true}
width={700}
>
<NewTaxRate
onOk={() => {
setNewTaxRateOpen(false)
messageApi.success('New tax rate created successfully.')
tableRef.current?.reload()
}}
reset={!newTaxRateOpen}
/>
</Modal>
</>
)
}
export default TaxRates

View File

@ -0,0 +1,80 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewTaxRate = ({ onOk }) => {
return (
<NewObjectForm type={'taxRate'}>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='taxRate'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='taxRate'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='taxRate'
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 Tax Rate'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>
)
}
NewTaxRate.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewTaxRate

View File

@ -0,0 +1,193 @@
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('TaxRateInfo')
log.setLevel(config.logLevel)
const TaxRateInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const taxRateId = new URLSearchParams(location.search).get('taxRateId')
const [collapseState, updateCollapseState] = useCollapseState('TaxRateInfo', {
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='taxRate'
id={taxRateId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Tax Rate Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='taxRate'
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='Tax Rate Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={taxRateId}
type='taxRate'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='taxRate'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={taxRateId} type='taxRate' />
</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': taxRateId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default TaxRateInfo

View File

@ -0,0 +1,98 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import NewTaxRecord from './TaxRecords/NewTaxRecord'
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 TaxRecords = () => {
const [messageApi, contextHolder] = message.useMessage()
const [newTaxRecordOpen, setNewTaxRecordOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('taxRecord')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('taxRecord')
const actionItems = {
items: [
{
label: 'New Tax Record',
key: 'newTaxRecord',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newTaxRecord') {
setNewTaxRecordOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='taxRecord'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='taxRecord'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newTaxRecordOpen}
onCancel={() => setNewTaxRecordOpen(false)}
footer={null}
destroyOnHidden={true}
width={700}
>
<NewTaxRecord
onOk={() => {
setNewTaxRecordOpen(false)
messageApi.success('New tax record created successfully.')
tableRef.current?.reload()
}}
reset={!newTaxRecordOpen}
/>
</Modal>
</>
)
}
export default TaxRecords

View File

@ -0,0 +1,80 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewTaxRecord = ({ onOk }) => {
return (
<NewObjectForm type={'taxRecord'}>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='taxRecord'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='taxRecord'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='taxRecord'
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 Tax Record'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>
)
}
NewTaxRecord.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewTaxRecord

View File

@ -0,0 +1,196 @@
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('TaxRecordInfo')
log.setLevel(config.logLevel)
const TaxRecordInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const taxRecordId = new URLSearchParams(location.search).get('taxRecordId')
const [collapseState, updateCollapseState] = useCollapseState(
'TaxRecordInfo',
{
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='taxRecord'
id={taxRecordId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Tax Record Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='taxRecord'
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='Tax Record Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={taxRecordId}
type='taxRecord'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='taxRecord'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={taxRecordId} type='taxRecord' />
</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': taxRecordId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default TaxRecordInfo

View File

@ -172,6 +172,7 @@ const GCodeFileInfo = () => {
visibleProperties={{ visibleProperties={{
parts: false parts: false
}} }}
labelWidth='200px'
/> />
</InfoCollapse> </InfoCollapse>
<InfoCollapse <InfoCollapse

View File

@ -19,7 +19,7 @@ import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel.jsx' import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel.jsx'
import PrinterPositionPanel from '../../common/PrinterPositionPanel.jsx' import PrinterPositionPanel from '../../common/PrinterPositionPanel.jsx'
import PrinterMovementPanel from '../../common/PrinterMovementPanel.jsx' import PrinterMovementPanel from '../../common/PrinterMovementPanel.jsx'
import PrinterMiscPanel from '../../common/PrinterMiscPanel.jsx'
import JobIcon from '../../../Icons/JobIcon.jsx' import JobIcon from '../../../Icons/JobIcon.jsx'
import SubJobIcon from '../../../Icons/SubJobIcon.jsx' import SubJobIcon from '../../../Icons/SubJobIcon.jsx'
import FilamentStockIcon from '../../../Icons/FilamentStockIcon.jsx' import FilamentStockIcon from '../../../Icons/FilamentStockIcon.jsx'
@ -50,7 +50,8 @@ const ControlPrinter = () => {
filamentStock: true, filamentStock: true,
temperature: true, temperature: true,
position: true, position: true,
movement: true movement: true,
misc: true
} }
) )
@ -58,7 +59,8 @@ const ControlPrinter = () => {
const [sideBarVisible, setSideBarVisible] = useState( const [sideBarVisible, setSideBarVisible] = useState(
collapseState.temperature || collapseState.temperature ||
collapseState.position || collapseState.position ||
collapseState.movement collapseState.movement ||
collapseState.misc
) )
const [loadFilamentStockOpen, setLoadFilamentStockOpen] = useState(false) const [loadFilamentStockOpen, setLoadFilamentStockOpen] = useState(false)
@ -68,7 +70,8 @@ const ControlPrinter = () => {
setSideBarVisible( setSideBarVisible(
collapseState.temperature || collapseState.temperature ||
collapseState.position || collapseState.position ||
collapseState.movement collapseState.movement ||
collapseState.misc
) )
}, [collapseState]) }, [collapseState])
@ -168,18 +171,43 @@ const ControlPrinter = () => {
const sideBarItems = ( const sideBarItems = (
<Flex gap={isMobile ? 'large' : 'middle'} vertical> <Flex gap={isMobile ? 'large' : 'middle'} vertical>
{collapseState.temperature && ( {collapseState.temperature && (
<Card style={{ width: '100%' }}> <Card
style={{ width: '100%' }}
title='Temperature'
size='small'
styles={{ body: { padding: '25px' } }}
>
<PrinterTemperaturePanel id={printerId} /> <PrinterTemperaturePanel id={printerId} />
</Card> </Card>
)} )}
{collapseState.position && ( {collapseState.position && (
<Card style={{ width: '100%' }}> <Card
<PrinterPositionPanel /> style={{ width: '100%' }}
title='Position'
size='small'
styles={{ body: { padding: '25px' } }}
>
<PrinterPositionPanel id={printerId} />
</Card> </Card>
)} )}
{collapseState.movement && ( {collapseState.movement && (
<Card style={{ width: '100%' }}> <Card
<PrinterMovementPanel /> style={{ width: '100%' }}
title='Movement'
size='small'
styles={{ body: { padding: '25px' } }}
>
<PrinterMovementPanel id={printerId} />
</Card>
)}
{collapseState.misc && (
<Card
style={{ width: '100%' }}
title='Misc'
size='small'
styles={{ body: { padding: '25px' } }}
>
<PrinterMiscPanel id={printerId} />
</Card> </Card>
)} )}
</Flex> </Flex>
@ -236,6 +264,10 @@ const ControlPrinter = () => {
key: 'movement', key: 'movement',
label: 'Movement' label: 'Movement'
}, },
{
key: 'misc',
label: 'Misc'
},
{ key: 'notes', label: 'Notes' } { key: 'notes', label: 'Notes' }
]} ]}
visibleState={collapseState} visibleState={collapseState}
@ -330,6 +362,7 @@ const ControlPrinter = () => {
}} }}
objectData={printerObjectData} objectData={printerObjectData}
type='printer' type='printer'
labelWidth='100px'
/> />
) )
}} }}
@ -361,7 +394,8 @@ const ControlPrinter = () => {
showHyperlink={true} showHyperlink={true}
visibleProperties={{ visibleProperties={{
printers: false, printers: false,
createdAt: false createdAt: false,
finishedAt: false
}} }}
objectData={jobObjectData} objectData={jobObjectData}
type='job' type='job'
@ -402,7 +436,8 @@ const ControlPrinter = () => {
showHyperlink={true} showHyperlink={true}
visibleProperties={{ visibleProperties={{
printers: false, printers: false,
createdAt: false createdAt: false,
finishedAt: false
}} }}
objectData={subJobObjectData} objectData={subJobObjectData}
type='subJob' type='subJob'

View File

@ -142,6 +142,7 @@ const SubJobInfo = () => {
isEditing={isEditing} isEditing={isEditing}
type='subJob' type='subJob'
objectData={objectData} objectData={objectData}
labelWidth='175px'
/> />
)} )}
</ObjectForm> </ObjectForm>

View File

@ -62,14 +62,17 @@ const FileList = ({
const filesToRender = multiple ? files : [files] const filesToRender = multiple ? files : [files]
const renderFileContent = (file) => ( const renderFileContent = (file) => (
<Flex vertical gap='10px'> <Flex vertical gap='10px' style={{ maxWidth: '100%', minWidth: 0 }}>
<Flex justify={card ? 'space-between' : 'start'}> <Flex
<Flex gap={'small'} align='center'> justify={card ? 'space-between' : 'start'}
style={{ maxWidth: '100%', minWidth: 0 }}
>
<Flex gap={'small'} align='center' style={{ minWidth: 0 }}>
<FileIcon <FileIcon
style={{ margin: 0, fontSize: card == true ? '24px' : '16px' }} style={{ margin: 0, fontSize: card == true ? '24px' : '16px' }}
/> />
<Text style={{ marginTop: '1px' }}> <Text style={{ marginTop: '1px', minWidth: 0 }} ellipsis>
{file.name || file.filename || 'Unknown file'} {file.name || file.filename || 'Unknown file'}
</Text> </Text>
<Tag>{file.extension}</Tag> <Tag>{file.extension}</Tag>

View File

@ -90,7 +90,6 @@ const NotesPanel = ({ _id, type }) => {
) )
const handleReloadData = useCallback(async () => { const handleReloadData = useCallback(async () => {
console.log('GOT RELOAD DATA')
setNotes(await generateNotes(_id)) setNotes(await generateNotes(_id))
}, [_id, generateNotes]) }, [_id, generateNotes])
@ -100,7 +99,6 @@ const NotesPanel = ({ _id, type }) => {
'note', 'note',
(noteData) => { (noteData) => {
if (noteData.parent._id == _id) { if (noteData.parent._id == _id) {
console.log('Note note added to parent:', _id)
handleReloadData() handleReloadData()
} }
} }
@ -115,11 +113,11 @@ const NotesPanel = ({ _id, type }) => {
}, [_id, subscribeToObjectTypeUpdates, connected, handleReloadData]) }, [_id, subscribeToObjectTypeUpdates, connected, handleReloadData])
useEffect(() => { useEffect(() => {
if (token != null && !initialized) { if (connected == true && token != null && !initialized) {
handleReloadData() handleReloadData()
setInitialized(true) setInitialized(true)
} }
}, [token, handleReloadData, initialized]) }, [token, handleReloadData, initialized, connected])
const actionItems = { const actionItems = {
items: [ items: [

View File

@ -4,6 +4,7 @@ import { Table, Skeleton, Card, Button, Flex, Form, Typography } from 'antd'
import PlusIcon from '../../Icons/PlusIcon' import PlusIcon from '../../Icons/PlusIcon'
import ObjectProperty from './ObjectProperty' import ObjectProperty from './ObjectProperty'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import BinIcon from '../../Icons/BinIcon'
const { Text } = Typography const { Text } = Typography
const DEFAULT_COLUMN_WIDTHS = { const DEFAULT_COLUMN_WIDTHS = {
@ -135,8 +136,70 @@ const ObjectChildTable = ({
} }
})) }))
return [...propertyColumns, ...additionalColumns] const deleteColumn = isEditing
}, [resolvedProperties, additionalColumns, isEditing]) ? {
title: '',
key: 'delete',
width: 10,
fixed: 'right',
render: (_text, record, index) => {
if (record?.isSkeleton) {
return null
}
return (
<Button
type='text'
danger
size='small'
icon={<BinIcon />}
onClick={(e) => {
e.stopPropagation()
const currentItems = Array.isArray(itemsSource)
? itemsSource
: []
// Use record's unique identifier if available, otherwise use index
let newItems
if (typeof rowKey === 'string' && record[rowKey] != null) {
// Use the unique key to find and remove the item
newItems = currentItems.filter(
(item) => item[rowKey] !== record[rowKey]
)
} else if (typeof rowKey === 'function') {
// If rowKey is a function, find the item by comparing the resolved keys
const recordKey = rowKey(record, index)
newItems = currentItems.filter((item, i) => {
const itemKey = rowKey(item, i)
return itemKey !== recordKey
})
} else {
// Fallback to index-based removal
newItems = currentItems.filter((_, i) => i !== index)
}
if (typeof onChange === 'function') {
onChange(newItems)
}
}}
/>
)
}
}
: null
return [
...propertyColumns,
...additionalColumns,
...(deleteColumn ? [deleteColumn] : [])
]
}, [
resolvedProperties,
additionalColumns,
isEditing,
itemsSource,
onChange,
rowKey
])
const skeletonData = useMemo(() => { const skeletonData = useMemo(() => {
return createSkeletonRows( return createSkeletonRows(
@ -213,7 +276,7 @@ const ObjectChildTable = ({
}, [properties, rollups, objectData]) }, [properties, rollups, objectData])
const rollupColumns = useMemo(() => { const rollupColumns = useMemo(() => {
return properties.map((property, index) => { const propertyColumns = properties.map((property, index) => {
const nextProperty = properties[index + 1] const nextProperty = properties[index + 1]
var nextRollup = null var nextRollup = null
if (nextProperty) { if (nextProperty) {
@ -231,9 +294,14 @@ const ObjectChildTable = ({
return ( return (
<Flex justify={'space-between'}> <Flex justify={'space-between'}>
<Text> <Text>
{record[property.name] !== undefined &&
record[property.name] !== null && (
<>
{property?.prefix} {property?.prefix}
{record[property.name]} {record[property.name]}
{property?.suffix} {property?.suffix}
</>
)}
</Text> </Text>
{rollupLabel && <Text type='secondary'>{rollupLabel}:</Text>} {rollupLabel && <Text type='secondary'>{rollupLabel}:</Text>}
</Flex> </Flex>
@ -241,7 +309,22 @@ const ObjectChildTable = ({
} }
} }
}) })
}, [properties, rollups]) const blankDeleteColumn = isEditing
? {
title: '',
key: 'delete',
width: 40,
fixed: 'right',
render: () => {
return <Flex></Flex>
}
}
: null
return [
...propertyColumns,
...(blankDeleteColumn ? [blankDeleteColumn] : [])
]
}, [properties, rollups, isEditing])
const hasRollups = useMemo( const hasRollups = useMemo(
() => Array.isArray(rollups) && rollups.length > 0, () => Array.isArray(rollups) && rollups.length > 0,
@ -339,7 +422,7 @@ const ObjectChildTable = ({
if (isEditing === true && formListName) { if (isEditing === true && formListName) {
return ( return (
<Form.List name={formListName}> <Form.List name={formListName}>
{(fields, { add }) => { {(fields, { add, remove }) => {
const listDataSource = fields.map((field, index) => ({ const listDataSource = fields.map((field, index) => ({
_field: field, _field: field,
_index: index, _index: index,
@ -373,12 +456,38 @@ const ObjectChildTable = ({
} }
})) }))
const deleteColumn = {
title: '',
key: 'delete',
width: 10,
fixed: 'right',
render: (_text, record) => {
const field = record?._field
const index = record?._index
if (!field || index == null) return null
return (
<Button
type='text'
danger
size='small'
icon={<BinIcon />}
onClick={(e) => {
e.stopPropagation()
if (typeof remove === 'function') {
remove(index)
}
}}
/>
)
}
}
const listTable = ( const listTable = (
<Flex vertical> <Flex vertical>
<div ref={mainTableWrapperRef}> <div ref={mainTableWrapperRef}>
<Table <Table
dataSource={listDataSource} dataSource={listDataSource}
columns={[...listColumns, ...additionalColumns]} columns={[...listColumns, ...additionalColumns, deleteColumn]}
pagination={false} pagination={false}
size={size} size={size}
loading={loading} loading={loading}

View File

@ -53,13 +53,30 @@ const ObjectDisplay = ({ object, objectType }) => {
const model = getModelByName(objectType) const model = getModelByName(objectType)
const Icon = model.icon const Icon = model.icon
return ( return (
<Tag style={{ margin: 0, border: 'none' }}> <Tag
<Flex gap={objectData?.color ? 'small' : '3px'} align='center'> style={{
<Icon /> margin: 0,
<Flex gap={'small'} align='center'> border: 'none',
minWidth: 0,
paddingBottom: '2.5px',
maxWidth: '100%'
}}
className='object-display-tag'
>
<Flex
gap={objectData?.color ? 'small' : '4px'}
align='center'
style={{ minWidth: 0 }}
>
<Icon style={{ marginBottom: '-3px' }} />
<Flex gap={'small'} align='center' style={{ minWidth: 0 }}>
{objectData?.color ? <Badge color={objectData?.color} /> : null} {objectData?.color ? <Badge color={objectData?.color} /> : null}
<div style={{ paddingTop: '1.5px' }}> <div style={{ paddingTop: '1.5px', minWidth: 0 }}>
{objectData?.name ? <Text ellipsis>{objectData.name}</Text> : null} {objectData?.name ? (
<Text ellipsis style={{ lineHeight: '1' }}>
{objectData.name}
</Text>
) : null}
{objectData?._id && !objectData?.name ? ( {objectData?._id && !objectData?.name ? (
<IdDisplay <IdDisplay
id={objectData?._id} id={objectData?._id}

View File

@ -310,11 +310,11 @@ const ObjectForm = forwardRef(
}, []) }, [])
useEffect(() => { useEffect(() => {
if (initialized == false && id && token != null) { if (connected == true && initialized == false && id && token != null) {
setInitialized(true) setInitialized(true)
handleFetchObject() handleFetchObject()
} }
}, [id, initialized, handleFetchObject, token]) }, [id, initialized, handleFetchObject, token, connected])
useEffect(() => { useEffect(() => {
if (id && connected) { if (id && connected) {
@ -391,15 +391,19 @@ const ObjectForm = forwardRef(
try { try {
const value = await form.validateFields() const value = await form.validateFields()
setEditLoading(true) setEditLoading(true)
onStateChangeRef.current({ editLoading: true })
await updateObject(id, type, value)
const currentFormData = {
...value,
...objectData
}
onStateChangeRef.current({ editLoading: true })
await updateObject(id, type, currentFormData)
setIsEditing(false) setIsEditing(false)
isEditingRef.current = false isEditingRef.current = false
onStateChangeRef.current({ isEditing: isEditingRef.current }) onStateChangeRef.current({ isEditing: isEditingRef.current })
setObjectData({ setObjectData({
...objectData, ...objectData,
...value, ...currentFormData,
_isEditing: isEditingRef.current _isEditing: isEditingRef.current
}) })
messageApi.success('Information updated successfully') messageApi.success('Information updated successfully')

View File

@ -17,6 +17,7 @@ const ObjectInfo = ({
required = undefined, required = undefined,
visibleProperties = {}, visibleProperties = {},
objectPropertyProps = {}, objectPropertyProps = {},
labelWidth = '150px',
column = { column = {
xs: 1, xs: 1,
sm: 1, sm: 1,
@ -86,6 +87,7 @@ const ObjectInfo = ({
</Flex> </Flex>
) : null, ) : null,
children: ( children: (
<div style={{ maxWidth: '100%', minWidth: 0, width: '100%' }}>
<ObjectProperty <ObjectProperty
{...item} {...item}
{...objectPropertyProps} {...objectPropertyProps}
@ -93,6 +95,7 @@ const ObjectInfo = ({
objectData={combinedObjectData} objectData={combinedObjectData}
showSince={true} showSince={true}
/> />
</div>
), ),
span: item?.span || undefined span: item?.span || undefined
} }
@ -105,6 +108,8 @@ const ObjectInfo = ({
bordered={true} bordered={true}
{...rest} {...rest}
items={descriptionItems} items={descriptionItems}
styles={{ label: { width: labelWidth } }}
className='object-info-descriptions'
/> />
</Spin> </Spin>
) )
@ -124,7 +129,8 @@ ObjectInfo.propTypes = {
objectData: PropTypes.object, objectData: PropTypes.object,
required: PropTypes.bool, required: PropTypes.bool,
visibleProperties: PropTypes.object, visibleProperties: PropTypes.object,
objectPropertyProps: PropTypes.object objectPropertyProps: PropTypes.object,
labelWidth: PropTypes.string
} }
export default ObjectInfo export default ObjectInfo

View File

@ -45,6 +45,7 @@ import DataTree from './DataTree'
import FileList from './FileList' import FileList from './FileList'
import ObjectChildTable from './ObjectChildTable' import ObjectChildTable from './ObjectChildTable'
import MiscId from './MiscId' import MiscId from './MiscId'
import { round } from '../utils/Utils'
const { Text } = Typography const { Text } = Typography
@ -265,7 +266,7 @@ const ObjectProperty = ({
} else { } else {
var roundedValue = value var roundedValue = value
if (roundNumber != false && typeof value === 'number') { if (roundNumber != false && typeof value === 'number') {
roundedValue = value.toFixed(roundNumber) roundedValue = round(value, roundNumber)
} }
return ( return (

View File

@ -35,7 +35,8 @@ const ObjectSelect = ({
disabled = false, disabled = false,
...rest ...rest
}) => { }) => {
const { fetchObjectsByProperty, fetchObject } = useContext(ApiServerContext) const { fetchObjectsByProperty, fetchObject, connected } =
useContext(ApiServerContext)
const { token } = useContext(AuthContext) const { token } = useContext(AuthContext)
// --- State --- // --- State ---
const [treeData, setTreeData] = useState([]) const [treeData, setTreeData] = useState([])
@ -373,11 +374,22 @@ const ObjectSelect = ({
setInitialized(true) setInitialized(true)
return return
} }
if (!initialized && token != null && type != 'unknown') { if (
!initialized &&
token != null &&
type != 'unknown' &&
type != undefined &&
connected == true
) {
handleFetchObjectsProperties() handleFetchObjectsProperties()
setInitialized(true) setInitialized(true)
} }
if (value == null || type == 'unknown') { if (
value == null ||
type == 'unknown' ||
type == undefined ||
connected == false
) {
setTreeSelectValue(null) setTreeSelectValue(null)
setInitialLoading(false) setInitialLoading(false)
setInitialized(true) setInitialized(true)
@ -392,7 +404,8 @@ const ObjectSelect = ({
initialized, initialized,
token, token,
fetchFullObjectIfNeeded, fetchFullObjectIfNeeded,
type type,
connected
]) ])
const prevValuesRef = useRef({ type, masterFilter }) const prevValuesRef = useRef({ type, masterFilter })
@ -443,7 +456,7 @@ const ObjectSelect = ({
const placeholder = useMemo( const placeholder = useMemo(
() => () =>
type == 'unknown' type == 'unknown' || type == undefined
? 'n/a' ? 'n/a'
: `Select a ${getModelByName(type).label.toLowerCase()}...`, : `Select a ${getModelByName(type).label.toLowerCase()}...`,
[type] [type]
@ -486,7 +499,7 @@ const ObjectSelect = ({
{...rest} {...rest}
value={treeSelectValue} value={treeSelectValue}
onChange={onTreeSelectChange} onChange={onTreeSelectChange}
disabled={disabled || type == 'unknown'} disabled={disabled || type == 'unknown' || type == undefined}
/> />
) )
} }

View File

@ -458,11 +458,16 @@ const ObjectTable = forwardRef(
})) }))
useEffect(() => { useEffect(() => {
if (token != null && !pages.includes(initialPage) && !initialized) { if (
connected == true &&
token != null &&
!pages.includes(initialPage) &&
!initialized
) {
loadInitialPage() loadInitialPage()
setInitialized(true) setInitialized(true)
} }
}, [token, loadInitialPage, initialPage, pages, initialized]) }, [token, loadInitialPage, initialPage, pages, initialized, connected])
// Watch for changes in type and masterFilter, reset component state when they change // Watch for changes in type and masterFilter, reset component state when they change
useEffect(() => { useEffect(() => {

View File

@ -1,22 +1,21 @@
import { useContext, useState, useEffect } from 'react' import { useContext, useState, useEffect } from 'react'
import { Typography, Spin, Flex, Space, Slider, Descriptions, Tag } from 'antd' import { Typography, Spin, Flex, Space, Slider, Descriptions, Tag } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import { PrintServerContext } from '../context/PrintServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import merge from 'lodash/merge'
const { Text } = Typography const { Text } = Typography
const PrinterMiscPanel = ({ const PrinterMiscPanel = ({ id, showControls = true }) => {
printerId, const { subscribeToObjectEvent, connected, sendObjectAction } =
showControls = true, useContext(ApiServerContext)
shouldUnsubscribe = true
}) => {
const [miscData, setMiscData] = useState({ const [miscData, setMiscData] = useState({
fan: { fan: {
speed: 0, speed: 0,
target: 0 target: 0
}, },
ledBacklight: { lcdBacklight: {
// eslint-disable-line camelcase // eslint-disable-line camelcase
brightness: 0 brightness: 0
}, },
@ -27,112 +26,81 @@ const PrinterMiscPanel = ({
enabled: false, enabled: false,
filamentDetected: false filamentDetected: false
}, },
heaterFan: { coolingFan: {
speed: 0 speed: 0
} }
}) })
const [initialized, setInitialized] = useState(false)
const { printServer } = useContext(PrintServerContext)
const [fanSpeed, setFanSpeed] = useState(0) const [fanSpeed, setFanSpeed] = useState(0)
const [ledBrightness, setLedBrightness] = useState(0) const [lcdBrightness, setLcdBrightness] = useState(0)
const [beeperValue, setBeeperValue] = useState(0) const [beeperValue, setBeeperValue] = useState(0)
useEffect(() => { useEffect(() => {
const params = { if (id && connected == true) {
printerId, console.log('subscribing to misc event')
objects: { const miscEventUnsubscribe = subscribeToObjectEvent(
fan: null, id,
'filament_switch_sensor fsensor': null, 'printer',
'output_pin BEEPER_pin': null, 'misc',
'output_pin LCD_backlight_pin': null, (event) => {
'heater_fan nozzle_cooling_fan': null console.log('misc event', event)
} setMiscData((prev) => {
} const merged = merge({}, prev, event.data)
return merged
const notifyMiscStatusUpdate = (statusUpdate) => {
if (statusUpdate?.fan) {
setMiscData((prevData) => ({
...prevData,
fan: statusUpdate.fan
}))
setFanSpeed(statusUpdate.fan.speed)
}
if (statusUpdate?.['heater_fan nozzle_cooling_fan']) {
setMiscData((prevData) => ({
...prevData,
heaterFan: statusUpdate['heater_fan nozzle_cooling_fan']
}))
}
if (statusUpdate?.['output_pin LCD_backlight_pin']) {
setMiscData((prevData) => ({
...prevData,
ledBacklight: statusUpdate['output_pin LCD_backlight_pin'].value
}))
setLedBrightness(statusUpdate['output_pin LCD_backlight_pin'].value)
}
if (statusUpdate?.['output_pin BEEPER_pin']) {
setMiscData((prevData) => ({
...prevData,
beeper: statusUpdate['output_pin BEEPER_pin']
}))
setBeeperValue(statusUpdate['output_pin BEEPER_pin'].value)
}
if (statusUpdate?.['filament_switch_sensor fsensor']) {
setMiscData((prevData) => ({
...prevData,
filamentSensor: {
enabled: statusUpdate['filament_switch_sensor fsensor'].enabled,
filamentDetected:
statusUpdate['filament_switch_sensor fsensor'].filament_detected
}
}))
}
}
if (!initialized && printServer.connected) {
setInitialized(true)
printServer.on('connect', () => {
printServer.emit('printer.objects.subscribe', params)
printServer.emit('printer.objects.query', params)
}) })
printServer.emit('printer.objects.subscribe', params)
printServer.emit('printer.objects.query', params)
printServer.on('notify_status_update', notifyMiscStatusUpdate)
} }
)
return () => { return () => {
if (printServer.connected && initialized && shouldUnsubscribe) { if (miscEventUnsubscribe) miscEventUnsubscribe()
printServer.off('notify_status_update', notifyMiscStatusUpdate)
printServer.emit('printer.objects.unsubscribe', params)
} }
} }
}, [printServer, initialized, printerId, shouldUnsubscribe]) }, [id, connected, subscribeToObjectEvent])
useEffect(() => {
if (miscData.fan?.speed !== undefined) {
setFanSpeed(miscData.fan.speed)
}
if (miscData.lcdBacklight?.brightness !== undefined) {
setLcdBrightness(miscData.lcdBacklight.brightness)
}
if (miscData.beeper?.value !== undefined) {
setBeeperValue(miscData.beeper.value)
}
}, [
miscData.fan?.speed,
miscData.lcdBacklight?.brightness,
miscData.beeper?.value
])
const handleSetFanSpeed = (value) => { const handleSetFanSpeed = (value) => {
if (printServer) { if (id && connected == true) {
printServer.emit('printer.gcode.script', { sendObjectAction(id, 'printer', {
printerId, type: 'gcodeScript',
data: {
script: `M106 S${Math.round(value * 255)}` script: `M106 S${Math.round(value * 255)}`
}
}) })
} }
} }
const handleSetLedBrightness = (value) => { const handleSetLcdBrightness = (value) => {
if (printServer) { if (id && connected == true) {
printServer.emit('printer.gcode.script', { sendObjectAction(id, 'printer', {
printerId, type: 'gcodeScript',
script: `SET_LED LED=led_backlight BRIGHTNESS=${value}` data: {
script: `SET_LCD LCD=lcd_backlight BRIGHTNESS=${value}`
}
}) })
} }
} }
const handleSetBeeperValue = (value) => { const handleSetBeeperValue = (value) => {
if (printServer) { if (id && connected == true) {
printServer.emit('printer.gcode.script', { sendObjectAction(id, 'printer', {
printerId, type: 'gcodeScript',
data: {
script: `M300 S440 P200 V${Math.round(value * 100)}` script: `M300 S440 P200 V${Math.round(value * 100)}`
}
}) })
} }
} }
@ -156,14 +124,14 @@ const PrinterMiscPanel = ({
tooltip={{ open: false }} tooltip={{ open: false }}
/> />
<Text>LED Backlight: {Math.round(ledBrightness * 100)}%</Text> <Text>LCD Backlight: {Math.round(lcdBrightness * 100)}%</Text>
<Slider <Slider
value={ledBrightness} value={lcdBrightness}
min={0} min={0}
max={1} max={1}
step={0.01} step={0.01}
onChange={setLedBrightness} onChange={setLcdBrightness}
onAfterChange={handleSetLedBrightness} onAfterChange={handleSetLcdBrightness}
tooltip={{ open: false }} tooltip={{ open: false }}
/> />
@ -201,9 +169,9 @@ const PrinterMiscPanel = ({
)} )}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='Nozzle Cooling Fan'> <Descriptions.Item label='Nozzle Cooling Fan'>
{miscData.heaterFan.speed > 0 ? ( {miscData.coolingFan.speed > 0 ? (
<Text> <Text>
Running ({Math.round(miscData.heaterFan.speed * 100)}%) Running ({Math.round(miscData.coolingFan.speed * 100)}%)
</Text> </Text>
) : ( ) : (
<Text>Off</Text> <Text>Off</Text>
@ -222,9 +190,8 @@ const PrinterMiscPanel = ({
} }
PrinterMiscPanel.propTypes = { PrinterMiscPanel.propTypes = {
printerId: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
showControls: PropTypes.bool, showControls: PropTypes.bool
shouldUnsubscribe: PropTypes.bool
} }
export default PrinterMiscPanel export default PrinterMiscPanel

View File

@ -10,7 +10,6 @@ import {
Card, Card,
message // eslint-disable-line message // eslint-disable-line
} from 'antd' } from 'antd'
import { PrintServerContext } from '../context/PrintServerContext'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import LevelBedIcon from '../../Icons/LevelBedIcon' import LevelBedIcon from '../../Icons/LevelBedIcon'
import ArrowLeftIcon from '../../Icons/ArrowLeftIcon' import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
@ -18,14 +17,12 @@ import ArrowRightIcon from '../../Icons/ArrowRightIcon'
import ArrowUpIcon from '../../Icons/ArrowUpIcon' import ArrowUpIcon from '../../Icons/ArrowUpIcon'
import ArrowDownIcon from '../../Icons/ArrowDownIcon' import ArrowDownIcon from '../../Icons/ArrowDownIcon'
import HomeIcon from '../../Icons/HomeIcon' import HomeIcon from '../../Icons/HomeIcon'
import { ApiServerContext } from '../context/ApiServerContext'
const PrinterMovementPanel = ({ printerId }) => { const PrinterMovementPanel = ({ id }) => {
const { connected, sendObjectAction } = useContext(ApiServerContext)
const [posValue, setPosValue] = useState(10) const [posValue, setPosValue] = useState(10)
const [rateValue, setRateValue] = useState(1000) const [rateValue, setRateValue] = useState(1000)
const { printServer } = useContext(PrintServerContext)
//const messageApi = message.useMessage()
const handlePosRadioChange = (e) => { const handlePosRadioChange = (e) => {
const value = e.target.value const value = e.target.value
setPosValue(value) // Update posValue state when radio button changes setPosValue(value) // Update posValue state when radio button changes
@ -40,20 +37,26 @@ const PrinterMovementPanel = ({ printerId }) => {
} }
const handleHomeAxisClick = (axis) => { const handleHomeAxisClick = (axis) => {
if (printServer) { if (id && connected == true) {
printServer.emit('printer.gcode.script', { sendObjectAction(id, 'printer', {
printerId, type: 'homeAxis',
script: `G28 ${axis}` data: {
axis: axis
}
}) })
} }
} }
const handleMoveAxisClick = (axis, minus) => { const handleMoveAxisClick = (axis, minus) => {
const distanceValue = !minus ? posValue * -1 : posValue const distanceValue = !minus ? posValue * -1 : posValue
if (printServer) { if (id && connected == true) {
printServer.emit('printer.gcode.script', { sendObjectAction(id, 'printer', {
printerId, type: 'moveAxis',
script: `_CLIENT_LINEAR_MOVE ${axis}=${distanceValue} F=${rateValue}` data: {
axis: axis,
distance: distanceValue,
rate: rateValue
}
}) })
} }
//sendCommand('moveAxis', { axis, pos, rate }) //sendCommand('moveAxis', { axis, pos, rate })
@ -93,7 +96,7 @@ const PrinterMovementPanel = ({ printerId }) => {
return ( return (
<div style={{ minWidth: 190 }}> <div style={{ minWidth: 190 }}>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'} align='center'>
<Flex horizontal='true' gap='small'> <Flex horizontal='true' gap='small'>
<Card size='small' title='XY'> <Card size='small' title='XY'>
<Flex <Flex
@ -211,23 +214,21 @@ const PrinterMovementPanel = ({ printerId }) => {
min={0.1} min={0.1}
max={100} max={100}
value={posValue} value={posValue}
formatter={(value) => `${value} mm`} suffix='mm'
parser={(value) => value?.replace(' mm', '')}
onChange={handlePosInputChange} onChange={handlePosInputChange}
placeholder='10 mm' placeholder='10 mm'
name='posInput' name='posInput'
style={{ flexGrow: 1 }} style={{ flexGrow: 1, minWidth: '125px' }}
/> />
<InputNumber <InputNumber
min={1} min={1}
max={5000} max={5000}
value={rateValue} value={rateValue}
formatter={(value) => `${value} mm/s`} suffix='mm/s'
parser={(value) => value?.replace(' mm/s', '')}
onChange={handleRateInputChange} onChange={handleRateInputChange}
placeholder='100 mm/s' placeholder='100 mm/s'
name='rateInput' name='rateInput'
style={{ flexGrow: 1 }} style={{ flexGrow: 1, minWidth: '125px' }}
/> />
</Flex> </Flex>
</Flex> </Flex>
@ -237,7 +238,7 @@ const PrinterMovementPanel = ({ printerId }) => {
} }
PrinterMovementPanel.propTypes = { PrinterMovementPanel.propTypes = {
printerId: PropTypes.string.isRequired id: PropTypes.string.isRequired
} }
export default PrinterMovementPanel export default PrinterMovementPanel

View File

@ -7,14 +7,16 @@ import {
Collapse, Collapse,
InputNumber, InputNumber,
Descriptions, Descriptions,
Button Button,
Divider
} from 'antd' } from 'antd'
import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' import { LoadingOutlined, CaretRightOutlined } from '@ant-design/icons'
import { PrintServerContext } from '../context/PrintServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import styled from 'styled-components' import styled from 'styled-components'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import BoolDisplay from './BoolDisplay' import BoolDisplay from './BoolDisplay'
import merge from 'lodash/merge'
import { round } from '../utils/Utils'
const { Text } = Typography const { Text } = Typography
const CustomCollapse = styled(Collapse)` const CustomCollapse = styled(Collapse)`
@ -29,91 +31,151 @@ const CustomCollapse = styled(Collapse)`
` `
const PrinterPositionPanel = ({ const PrinterPositionPanel = ({
printerId, id,
showControls = true, showControls = true,
showMoreInfo = true, showMoreInfo = true
shouldUnsubscribe = true
}) => { }) => {
const { subscribeToObjectEvent, connected, sendObjectAction } =
useContext(ApiServerContext)
const [positionData, setPositionData] = useState({ const [positionData, setPositionData] = useState({
speed_factor: 1.0, // eslint-disable-line speedFactor: 1.0, // eslint-disable-line
speed: 100, speed: 100,
extrude_factor: 1.0, // eslint-disable-line extrudeFactor: 1.0, // eslint-disable-line
absolute_foordinates: true, // eslint-disable-line absoluteCoordinates: true, // eslint-disable-line
absolute_extrude: false, // eslint-disable-line absoluteExtrude: false, // eslint-disable-line
homing_origin: [0.0, 0.0, 0.0, 0.0], // eslint-disable-line homingOrigin: [0.0, 0.0, 0.0, 0.0], // eslint-disable-line
position: [0.0, 0.0, 0.0, 0.0], toolheadPosition: [0.0, 0.0, 0.0, 0.0],
gcode_position: [0.0, 0.0, 0.0, 0.0] // eslint-disable-line gcodePosition: [0.0, 0.0, 0.0, 0.0], // eslint-disable-line
livePosition: [0.0, 0.0, 0.0, 0.0],
maxVelocity: 1000,
maxAcceleration: 1000,
squareCornerVelocity: 100,
minCruiseRatio: 0
}) })
const [initialized, setInitialized] = useState(false) useEffect(() => {
const { printServer } = useContext(PrintServerContext) if (id && connected == true) {
const [speedFactor, setSpeedFactor] = useState(positionData.speed_factor) console.log('subscribing to motion event')
const [extrudeFactor, setExtrudeFactor] = useState( const motionEventUnsubscribe = subscribeToObjectEvent(
positionData.extrude_factor id,
'printer',
'motion',
(event) => {
console.log('motion event', event)
setPositionData((prev) => {
const merged = merge({}, prev, event.data)
return merged
})
}
)
return () => {
if (motionEventUnsubscribe) motionEventUnsubscribe()
}
}
}, [id, connected])
const [speedFactor, setSpeedFactor] = useState(positionData.speedFactor)
const [extrudeFactor, setExtrudeFactor] = useState(positionData.extrudeFactor)
const [maxVelocity, setMaxVelocity] = useState(positionData.maxVelocity)
const [maxAcceleration, setMaxAcceleration] = useState(
positionData.maxAcceleration
)
const [squareCornerVelocity, setSquareCornerVelocity] = useState(
positionData.squareCornerVelocity
)
const [minCruiseRatio, setMinCruiseRatio] = useState(
positionData.minCruiseRatio
) )
useEffect(() => { useEffect(() => {
const params = { if (positionData.speedFactor !== undefined) {
printerId, setSpeedFactor(positionData.speedFactor)
objects: {
toolhead: null,
gcode_move: null // eslint-disable-line
} }
if (positionData.extrudeFactor !== undefined) {
setExtrudeFactor(positionData.extrudeFactor)
} }
if (positionData.maxVelocity !== undefined) {
const notifyPositionStatusUpdate = (statusUpdate) => { setMaxVelocity(positionData.maxVelocity)
if (statusUpdate?.toolhead) {
setPositionData((prevData) => ({
...prevData,
...statusUpdate.toolhead
}))
} }
if (statusUpdate?.gcode_move) { if (positionData.maxAcceleration !== undefined) {
setPositionData((prevData) => ({ setMaxAcceleration(positionData.maxAcceleration)
...prevData,
...statusUpdate.gcode_move
}))
} }
if (positionData.squareCornerVelocity !== undefined) {
setSquareCornerVelocity(positionData.squareCornerVelocity)
} }
if (positionData.minCruiseRatio !== undefined) {
if (!initialized && printServer?.connected) { setMinCruiseRatio(positionData.minCruiseRatio)
setInitialized(true)
printServer.on('connect', () => {
printServer.emit('printer.objects.subscribe', params)
printServer.emit('printer.objects.query', params)
})
printServer.emit('printer.objects.subscribe', params)
printServer.emit('printer.objects.query', params)
printServer.on('notify_status_update', notifyPositionStatusUpdate)
} }
}, [
setSpeedFactor(positionData.speed_factor) positionData.speedFactor,
setExtrudeFactor(positionData.extrude_factor) positionData.extrudeFactor,
positionData.maxVelocity,
return () => { positionData.maxAcceleration,
if (printServer?.connected && initialized && shouldUnsubscribe) { positionData.squareCornerVelocity,
printServer.off('notify_status_update', notifyPositionStatusUpdate) positionData.minCruiseRatio
printServer.emit('printer.objects.unsubscribe', params) ])
}
}
}, [printServer, initialized, printerId, shouldUnsubscribe])
const handleSetSpeedFactor = () => { const handleSetSpeedFactor = () => {
if (printServer) { if (id && connected == true) {
printServer.emit('printer.gcode.script', { sendObjectAction(id, 'printer', {
printerId, type: 'setSpeedFactor',
script: `M220 S${speedFactor * 100}` data: {
speedFactor: speedFactor * 100
}
}) })
} }
} }
const handleSetExtrudeFactor = () => { const handleSetExtrudeFactor = () => {
if (printServer) { if (id && connected == true) {
printServer.emit('printer.gcode.script', { sendObjectAction(id, 'printer', {
printerId, type: 'setExtrudeFactor',
script: `M221 S${extrudeFactor * 100}` data: {
extrudeFactor: extrudeFactor * 100
}
})
}
}
const handleSetMaxVelocity = () => {
if (id && connected == true) {
sendObjectAction(id, 'printer', {
type: 'setMaxVelocity',
data: {
maxVelocity: maxVelocity
}
})
}
}
const handleSetMaxAcceleration = () => {
if (id && connected == true) {
sendObjectAction(id, 'printer', {
type: 'setMaxAcceleration',
data: {
maxAcceleration: maxAcceleration
}
})
}
}
const handleSetSquareCornerVelocity = () => {
if (id && connected == true) {
sendObjectAction(id, 'printer', {
type: 'setSquareCornerVelocity',
data: {
squareCornerVelocity: squareCornerVelocity
}
})
}
}
const handleSetMinCruiseRatio = () => {
if (id && connected == true) {
sendObjectAction(id, 'printer', {
type: 'setMinCruiseRatio',
data: {
minCruiseRatio: minCruiseRatio
}
}) })
} }
} }
@ -124,40 +186,156 @@ const PrinterPositionPanel = ({
label: 'More Position Data', label: 'More Position Data',
children: ( children: (
<> <>
<Divider style={{ margin: '0 0 12px 0' }} />
<Flex vertical gap={'middle'}> <Flex vertical gap={'middle'}>
<Text>GCode Position:</Text> <Text>GCode Position:</Text>
<Descriptions column={1} size='small' bordered> <Descriptions
column={1}
size='small'
bordered
styles={{ label: { width: '40px' } }}
>
<Descriptions.Item label='X'> <Descriptions.Item label='X'>
{positionData.gcode_position[0].toFixed(2)}mm {' '}
{round(positionData.gcodePosition[0], 2)}mm
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='Y'> <Descriptions.Item label='Y'>
{positionData.gcode_position[1].toFixed(2)}mm {round(positionData.gcodePosition[1], 2)}mm
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='Z'> <Descriptions.Item label='Z'>
{positionData.gcode_position[2].toFixed(2)}mm {round(positionData.gcodePosition[2], 2)}mm
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='E'> <Descriptions.Item label='E'>
{positionData.gcode_position[3].toFixed(2)}mm {round(positionData.gcodePosition[3], 2)}mm
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
{showControls && (
<>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Max Velocity:</Text>
<Space.Compact block size='small'>
<InputNumber
value={round(maxVelocity, 2)}
min={1}
max={10000}
step={5}
style={{ width: '125px' }}
suffix='mm/s'
onChange={(value) => setMaxVelocity(value)}
onPressEnter={handleSetMaxVelocity}
size='small'
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetMaxVelocity}
>
Set
</Button>
</Space.Compact>
</Space>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Max Acceleration:</Text>
<Space.Compact block size='small'>
<InputNumber
value={round(maxAcceleration, 2)}
min={1}
max={10000}
step={5}
style={{ width: '125px' }}
suffix='mm/s²'
onChange={(value) => setMaxAcceleration(value)}
onPressEnter={handleSetMaxAcceleration}
size='small'
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetMaxAcceleration}
>
Set
</Button>
</Space.Compact>
</Space>
</Space>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Sqr Corner Vel:</Text>
<Space.Compact block size='small'>
<InputNumber
value={round(squareCornerVelocity, 2) || 0}
min={0.1}
max={1000}
step={0.1}
suffix='mm/s'
style={{ width: '125px' }}
onChange={(value) => setSquareCornerVelocity(value)}
onPressEnter={handleSetSquareCornerVelocity}
size='small'
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetSquareCornerVelocity}
>
Set
</Button>
</Space.Compact>
</Space>
</Space>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Min Cruise Ratio:</Text>
<Space.Compact block size='small'>
<InputNumber
value={round(minCruiseRatio * 100, 2) || 0}
min={0}
max={100}
step={1}
suffix='%'
style={{ width: '125px' }}
onChange={(value) => setMinCruiseRatio(value / 100)}
onPressEnter={handleSetMinCruiseRatio}
size='small'
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetMinCruiseRatio}
>
Set
</Button>
</Space.Compact>
</Space>
</Space>
</Space>
</>
)}
<Text>Homing Origin:</Text> <Text>Homing Origin:</Text>
<Descriptions <Descriptions
column={1} column={1}
size='small' size='small'
bordered bordered
style={{ flexGrow: 1 }} style={{ flexGrow: 1 }}
styles={{ label: { width: '40px' } }}
> >
<Descriptions.Item label='X' span={1}> <Descriptions.Item label='X' span={1}>
{positionData.homing_origin[0].toFixed(2)}mm {round(positionData.homingOrigin[0], 2)}mm
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='Y' span={1}> <Descriptions.Item label='Y' span={1}>
{positionData.homing_origin[1].toFixed(2)}mm {round(positionData.homingOrigin[1], 2)}mm
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='Z' span={1}> <Descriptions.Item label='Z' span={1}>
{positionData.homing_origin[2].toFixed(2)}mm {round(positionData.homingOrigin[2], 2)}mm
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='E'> <Descriptions.Item label='E'>
{positionData.homing_origin[3].toFixed(2)}mm {round(positionData.homingOrigin[3], 2)}mm
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</Flex> </Flex>
@ -171,18 +349,43 @@ const PrinterPositionPanel = ({
{positionData ? ( {positionData ? (
<Flex vertical gap='middle'> <Flex vertical gap='middle'>
<Flex vertical gap={'middle'}> <Flex vertical gap={'middle'}>
<Descriptions column={1} size='small' bordered> <Descriptions
column={1}
size='small'
bordered
styles={{ label: { width: '40px' } }}
>
<Descriptions.Item label='X'> <Descriptions.Item label='X'>
{positionData.position[0].toFixed(2)}mm <Space>
<Text>{round(positionData.livePosition[0], 2)}mm</Text>
<Text type='secondary'>
{round(positionData.toolheadPosition[0], 2)}mm
</Text>
</Space>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='Y'> <Descriptions.Item label='Y'>
{positionData.position[1].toFixed(2)}mm <Space>
<Text>{round(positionData.livePosition[1], 2)}mm</Text>
<Text type='secondary'>
{round(positionData.toolheadPosition[1], 2)}mm
</Text>
</Space>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='Z'> <Descriptions.Item label='Z'>
{positionData.position[2].toFixed(2)}mm <Space>
<Text>{round(positionData.livePosition[2], 2)}mm</Text>
<Text type='secondary'>
{round(positionData.toolheadPosition[2], 2)}mm
</Text>
</Space>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='E'> <Descriptions.Item label='E'>
{positionData.position[3].toFixed(2)}mm <Space>
<Text>{round(positionData.livePosition[3], 2)}mm</Text>
<Text type='secondary'>
{round(positionData.toolheadPosition[3], 2)}mm
</Text>
</Space>
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
{showControls && ( {showControls && (
@ -192,11 +395,12 @@ const PrinterPositionPanel = ({
<Text>Speed Factor:</Text> <Text>Speed Factor:</Text>
<Space.Compact block size='small'> <Space.Compact block size='small'>
<InputNumber <InputNumber
value={speedFactor.toFixed(2)} value={round(speedFactor, 2)}
min={0.1} min={0.1}
max={2} max={2}
step={0.1} step={0.1}
style={{ width: '100px' }} style={{ width: '125px' }}
suffix='%'
onChange={(value) => setSpeedFactor(value)} onChange={(value) => setSpeedFactor(value)}
onPressEnter={handleSetSpeedFactor} onPressEnter={handleSetSpeedFactor}
size='small' size='small'
@ -215,11 +419,12 @@ const PrinterPositionPanel = ({
<Text>Extrude Factor:</Text> <Text>Extrude Factor:</Text>
<Space.Compact block size='small'> <Space.Compact block size='small'>
<InputNumber <InputNumber
value={extrudeFactor.toFixed(2)} value={round(extrudeFactor, 2)}
min={0.1} min={0.1}
max={2} max={2}
step={0.1} step={0.1}
style={{ width: '100px' }} style={{ width: '125px' }}
suffix='%'
onChange={(value) => setExtrudeFactor(value)} onChange={(value) => setExtrudeFactor(value)}
onPressEnter={handleSetExtrudeFactor} onPressEnter={handleSetExtrudeFactor}
size='small' size='small'
@ -236,14 +441,29 @@ const PrinterPositionPanel = ({
</Space> </Space>
</> </>
)} )}
<Descriptions column={1} size='small' bordered> <Descriptions
<Descriptions.Item label='Current Speed'> column={1}
{positionData.speed.toFixed(2)}mm/s size='small'
bordered
styles={{ label: { width: '125px' } }}
>
<Descriptions.Item label='Speed'>
{round(positionData.speed, 2)}mm/s
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label='Absolute Coordinates'> <Descriptions.Item label='Abs Coor'>
{positionData ? ( {positionData ? (
<BoolDisplay <BoolDisplay
value={positionData.absolute_coordinates} value={positionData.absoluteCoordinates}
yesNo={true}
/>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Abs Extrude'>
{positionData ? (
<BoolDisplay
value={positionData.absoluteExtrude}
yesNo={true} yesNo={true}
/> />
) : ( ) : (
@ -259,7 +479,7 @@ const PrinterPositionPanel = ({
size='small' size='small'
items={moreInfoItems} items={moreInfoItems}
expandIcon={({ isActive }) => ( expandIcon={({ isActive }) => (
<CaretLeftOutlined rotate={isActive ? 90 : 0} /> <CaretRightOutlined rotate={isActive ? 90 : 0} />
)} )}
/> />
)} )}
@ -274,10 +494,9 @@ const PrinterPositionPanel = ({
} }
PrinterPositionPanel.propTypes = { PrinterPositionPanel.propTypes = {
printerId: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
showControls: PropTypes.bool, showControls: PropTypes.bool,
showMoreInfo: PropTypes.bool, showMoreInfo: PropTypes.bool
shouldUnsubscribe: PropTypes.bool
} }
export default PrinterPositionPanel export default PrinterPositionPanel

View File

@ -8,7 +8,8 @@ import {
Space, Space,
Collapse, Collapse,
InputNumber, InputNumber,
Button Button,
Divider
} from 'antd' } from 'antd'
import { LoadingOutlined, CaretRightOutlined } from '@ant-design/icons' import { LoadingOutlined, CaretRightOutlined } from '@ant-design/icons'
import styled from 'styled-components' import styled from 'styled-components'
@ -113,6 +114,7 @@ const PrinterTemperaturePanel = ({
label: 'More Temperature Data', label: 'More Temperature Data',
children: ( children: (
<> <>
<Divider style={{ margin: '0 0 12px 0' }} />
<Flex vertical gap={0}> <Flex vertical gap={0}>
<Text> <Text>
Hot End Power:{' '} Hot End Power:{' '}

View File

@ -49,7 +49,7 @@ const StateDisplay = ({
strokeColor={ strokeColor={
orangeProgressTypes.includes(currentState.type) ? 'orange' : '' orangeProgressTypes.includes(currentState.type) ? 'orange' : ''
} }
style={{ width: '150px', marginBottom: '2px' }} style={{ maxWidth: '200px', marginBottom: '2px' }}
/> />
) : null} ) : null}

View File

@ -307,8 +307,11 @@ const AuthProvider = ({ children }) => {
setExpiresAt(null) setExpiresAt(null)
setUserProfile(null) setUserProfile(null)
clearAuthCookies() clearAuthCookies()
setAuthenticated(false)
if (showSessionExpiredModal == false) {
setShowUnauthorizedModal(true) setShowUnauthorizedModal(true)
} }
}
const refreshToken = useCallback(async () => { const refreshToken = useCallback(async () => {
try { try {
@ -489,7 +492,6 @@ const AuthProvider = ({ children }) => {
authCode == null authCode == null
) { ) {
setInitialized(true) setInitialized(true)
console.log('Showing unauth', token, authCode)
setShowUnauthorizedModal(true) setShowUnauthorizedModal(true)
setAuthenticated(false) setAuthenticated(false)
} }

View File

@ -384,7 +384,7 @@ const SpotlightProvider = ({ children }) => {
const getModeDescription = (mode) => { const getModeDescription = (mode) => {
switch (mode) { switch (mode) {
case ':': case ':':
return 'ID lookup' return 'ID/Reference lookup'
case '?': case '?':
return 'Filter' return 'Filter'
case '^': case '^':

View File

@ -30,5 +30,9 @@ export function timeStringToMinutes(timeString) {
return Math.floor(totalMinutes) return Math.floor(totalMinutes)
} }
export function round(num, decimals) {
return Math.round(num * 10 ** decimals) / 10 ** decimals
}
// Re-export the functions for backward compatibility // Re-export the functions for backward compatibility
export {} export {}

View File

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

View File

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

View File

@ -26,6 +26,8 @@ import { DocumentSize } from './models/DocumentSize.js'
import { DocumentTemplate } from './models/DocumentTemplate.js' import { DocumentTemplate } from './models/DocumentTemplate.js'
import { DocumentPrinter } from './models/DocumentPrinter.js' import { DocumentPrinter } from './models/DocumentPrinter.js'
import { DocumentJob } from './models/DocumentJob.js' import { DocumentJob } from './models/DocumentJob.js'
import { TaxRate } from './models/TaxRate.js'
import { TaxRecord } from './models/TaxRecord.js'
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon' import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [ export const objectModels = [
@ -56,7 +58,9 @@ export const objectModels = [
DocumentSize, DocumentSize,
DocumentTemplate, DocumentTemplate,
DocumentPrinter, DocumentPrinter,
DocumentJob DocumentJob,
TaxRate,
TaxRecord
] ]
// Re-export individual models for direct access // Re-export individual models for direct access
@ -88,7 +92,9 @@ export {
DocumentSize, DocumentSize,
DocumentTemplate, DocumentTemplate,
DocumentPrinter, DocumentPrinter,
DocumentJob DocumentJob,
TaxRate,
TaxRecord
} }
export function getModelByName(name, ignoreCase = false) { export function getModelByName(name, ignoreCase = false) {

View File

@ -41,8 +41,9 @@ export const Job = {
'_id', '_id',
'gcodeFile', 'gcodeFile',
'gcodeFile._id', 'gcodeFile._id',
'state',
'quantity', 'quantity',
'state',
'createdAt' 'createdAt'
], ],
filters: ['state', '_id', 'gcodeFile._id', 'quantity'], filters: ['state', '_id', 'gcodeFile._id', 'quantity'],

View File

@ -4,7 +4,6 @@ import PartIcon from '../../components/Icons/PartIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import CheckIcon from '../../components/Icons/CheckIcon' import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon' import XMarkIcon from '../../components/Icons/XMarkIcon'
export const Part = { export const Part = {
name: 'part', name: 'part',
label: 'Part', label: 'Part',
@ -132,28 +131,47 @@ export const Part = {
step: 0.01 step: 0.01
}, },
{ {
name: 'priceMode', name: 'costWithTax',
label: 'Price Mode', label: 'Cost with Tax',
columnWidth: 150,
required: true, required: true,
type: 'priceMode', readOnly: true,
disabled: (objectData) => { type: 'number',
return objectData.globalPricing == true prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
if (objectData?.costTaxRate?.rateType == 'percentage') {
return (
(
objectData?.cost *
(1 + objectData?.costTaxRate?.rate / 100)
).toFixed(2) || undefined
)
} else if (objectData?.costTaxRate?.rateType == 'amount') {
return (
(objectData?.cost + objectData?.costTaxRate?.rate).toFixed(2) ||
undefined
)
} else {
return objectData?.cost || undefined
}
} }
}, },
{ {
name: 'margin', name: 'costTaxRate',
label: 'Margin', label: 'Cost Tax Rate',
required: true, required: true,
type: 'number', type: 'object',
disabled: (objectData) => { objectType: 'taxRate'
return (
objectData.globalPricing == true || objectData.priceMode == 'amount'
)
}, },
suffix: '%', {
min: 0, name: 'costTaxRate._id',
max: 100, label: 'Cost Tax Rate ID',
step: 0.01 readOnly: true,
type: 'id',
showHyperlink: true,
objectType: 'taxRate'
}, },
{ {
name: 'price', name: 'price',
@ -163,15 +181,87 @@ export const Part = {
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.1, step: 0.1,
roundNumber: 2, readOnly: (objectData) => {
return objectData?.priceMode == 'margin'
},
value: (objectData) => { value: (objectData) => {
if (objectData?.priceMode == 'margin') { if (
return objectData?.cost * (1 + objectData?.margin / 100) || undefined objectData?.priceMode == 'margin' &&
objectData?.margin !== undefined &&
objectData?.margin !== null
) {
return (
(objectData?.cost * (1 + objectData?.margin / 100)).toFixed(2) ||
undefined
)
} else { } else {
return objectData?.price || undefined return objectData?.price || undefined
} }
} }
}, },
{
name: 'priceWithTax',
label: 'Price with Tax',
columnWidth: 150,
required: true,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
if (objectData?.priceTaxRate?.rateType == 'percentage') {
return (
(
objectData?.price *
(1 + objectData?.priceTaxRate?.rate / 100)
).toFixed(2) || undefined
)
} else if (objectData?.priceTaxRate?.rateType == 'amount') {
return (
(objectData?.price + objectData?.priceTaxRate?.rate).toFixed(2) ||
undefined
)
} else {
return objectData?.price
}
}
},
{
name: 'priceMode',
label: 'Price Mode',
required: true,
type: 'priceMode'
},
{
name: 'margin',
label: 'Margin',
required: true,
type: 'number',
disabled: (objectData) => {
return objectData.priceMode == 'amount'
},
suffix: '%',
min: 0,
max: 100,
step: 0.01
},
{
name: 'priceTaxRate',
label: 'Price Tax Rate',
required: true,
type: 'object',
objectType: 'taxRate'
},
{
name: 'priceTaxRate._id',
label: 'Price Tax Rate ID',
readOnly: true,
type: 'id',
showHyperlink: true,
objectType: 'taxRate'
},
{ {
name: 'file', name: 'file',
label: 'File', label: 'File',

View File

@ -59,21 +59,6 @@ export const PurchaseOrder = {
showHyperlink: true, showHyperlink: true,
objectType: 'vendor' objectType: 'vendor'
}, },
{
name: 'courierService',
label: 'Courier Service',
required: true,
type: 'object',
objectType: 'courierService'
},
{
name: 'courierService._id',
label: 'Courier Service ID',
readOnly: true,
type: 'id',
showHyperlink: true,
objectType: 'courierService'
},
{ {
name: 'items', name: 'items',
label: 'Order Items', label: 'Order Items',
@ -108,29 +93,97 @@ export const PurchaseOrder = {
return objectData?.item?._id return objectData?.item?._id
} }
}, },
{ {
name: 'quantity', name: 'itemCost',
label: 'Quantity', label: 'Item Cost',
type: 'number',
required: true
},
{
name: 'price',
label: 'Price',
type: 'number', type: 'number',
required: true, required: true,
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.01, step: 0.01,
columnWidth: 150,
value: (objectData) => {
if (objectData?.item) {
return objectData?.item?.cost || undefined
} else {
return undefined
}
}
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
required: true,
columnWidth: 150
},
{
name: 'totalCost',
label: 'Total Cost',
type: 'number',
required: true,
prefix: '£',
min: 0,
step: 0.01,
columnWidth: 150,
value: (objectData) => { value: (objectData) => {
if (objectData?.price == undefined) {
console.log('PurchaseOrder.js', objectData)
return ( return (
(objectData?.item?.cost || 0) * (objectData?.quantity || 0) || (objectData?.itemCost || 0) * (objectData?.quantity || 0) ||
undefined undefined
) )
}
},
{
name: 'taxRate',
label: 'Tax Rate',
type: 'object',
objectType: 'taxRate',
value: (objectData) => {
if (objectData?.item) {
console.log(objectData?.item)
return objectData?.item?.costTaxRate || undefined
} else { } else {
return objectData?.part?.price || undefined return undefined
}
}
},
{
name: 'taxRate._id',
label: 'Tax Rate ID',
type: 'id',
showHyperlink: true,
objectType: 'taxRate',
value: (objectData) => {
return objectData?.taxRate?._id || undefined
}
},
{
name: 'totalCostWithTax',
label: 'Total Cost With Tax',
type: 'number',
required: true,
readOnly: true,
prefix: '£',
min: 0,
step: 0.01,
columnWidth: 175,
value: (objectData) => {
if (objectData?.taxRate?.rateType == 'percentage') {
return (
(
(objectData?.totalCost || 0) *
(1 + objectData?.taxRate?.rate / 100)
).toFixed(2) || undefined
)
} else if (objectData?.taxRate?.rateType == 'amount') {
return (
(
(objectData?.totalCost || 0) + objectData?.taxRate?.rate
).toFixed(2) || undefined
)
} else {
return objectData?.totalCost || undefined
} }
} }
} }
@ -149,13 +202,27 @@ export const PurchaseOrder = {
} }
}, },
{ {
name: 'totalPrice', name: 'totalCost',
label: 'Total', label: 'Total',
type: 'number', type: 'number',
prefix: '£', prefix: '£',
property: 'price', property: 'totalCost',
value: (objectData) => { value: (objectData) => {
return objectData?.items?.reduce((acc, item) => acc + item.price, 0) return objectData?.items
?.reduce((acc, item) => acc + (item.totalCost || 0), 0)
.toFixed(2)
}
},
{
name: 'totalCostWithTax',
label: 'Total',
type: 'number',
prefix: '£',
property: 'totalCostWithTax',
value: (objectData) => {
return objectData?.items
?.reduce((acc, item) => acc + (item.totalCostWithTax || 0), 0)
.toFixed(2)
} }
} }
] ]

View File

@ -68,6 +68,36 @@ export const SubJob = {
readOnly: true, readOnly: true,
columnWidth: 175 columnWidth: 175
}, },
{
name: 'moonrakerJobId',
label: 'Moonraker Job ID',
type: 'miscId',
columnWidth: 140,
showCopy: true
},
{
name: 'startedAt',
label: 'Started At',
type: 'dateTime',
readOnly: true
},
{
name: 'createdPartStock',
label: 'Created Part Stock',
type: 'bool',
readOnly: true
},
{
name: 'finishedAt',
label: 'Finished At',
type: 'dateTime',
readOnly: true
},
{ {
name: 'job', name: 'job',
label: 'Job', label: 'Job',
@ -83,27 +113,6 @@ export const SubJob = {
objectType: 'job' objectType: 'job'
}, },
{
name: 'startedAt',
label: 'Started At',
type: 'dateTime',
readOnly: true
},
{
name: 'moonrakerJobId',
label: 'Moonraker Job ID',
type: 'miscId',
columnWidth: 140,
showCopy: true
},
{
name: 'finishedAt',
label: 'Finished At',
type: 'dateTime',
readOnly: true
},
{ {
name: 'printer', name: 'printer',
label: 'Printer', label: 'Printer',

View File

@ -0,0 +1,181 @@
import TaxRateIcon from '../../components/Icons/TaxRateIcon'
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 TaxRate = {
name: 'taxRate',
label: 'Tax Rate',
prefix: 'TXR',
icon: TaxRateIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/taxrates/info?taxRateId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/taxrates/info?taxRateId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/taxrates/info?taxRateId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/taxrates/info?taxRateId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/taxrates/info?taxRateId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/management/taxrates/info?taxRateId=${_id}&action=delete`
}
],
columns: [
'name',
'_id',
'rate',
'rateType',
'active',
'country',
'createdAt'
],
filters: ['name', '_id', 'rate', 'rateType', 'active', 'country'],
sorters: [
'name',
'rate',
'rateType',
'active',
'country',
'createdAt',
'_id'
],
group: ['country', 'rateType'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'taxRate',
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',
required: true,
type: 'bool',
default: true
},
{
name: 'effectiveFrom',
label: 'Effective From',
type: 'date',
readOnly: false,
required: false
},
{
name: 'rate',
label: 'Rate',
required: true,
type: 'number',
min: 0,
step: 0.01,
prefix: (objectData) => {
return objectData?.rateType == 'amount' ? '£' : undefined
},
suffix: (objectData) => {
return objectData?.rateType == 'percentage' ? '%' : undefined
}
},
{
name: 'effectiveTo',
label: 'Effective To',
type: 'date',
readOnly: false,
required: false
},
{
name: 'rateType',
label: 'Rate Type',
required: true,
type: 'select',
options: [
{ label: 'Percentage', value: 'percentage' },
{ label: 'Amount', value: 'amount' }
]
},
{
name: 'country',
label: 'Country',
type: 'country',
readOnly: false,
required: false
},
{
name: 'description',
label: 'Description',
type: 'text',
readOnly: false,
required: false
}
]
}

View File

@ -0,0 +1,174 @@
import TaxRecordIcon from '../../components/Icons/TaxRecordIcon'
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 TaxRecord = {
name: 'taxRecord',
label: 'Tax Record',
prefix: 'TXR',
icon: TaxRecordIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/taxrecords/info?taxRecordId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/taxrecords/info?taxRecordId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/taxrecords/info?taxRecordId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/taxrecords/info?taxRecordId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/taxrecords/info?taxRecordId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/management/taxrecords/info?taxRecordId=${_id}&action=delete`
}
],
columns: [
'_id',
'taxRate',
'transactionType',
'transaction',
'amount',
'taxAmount',
'transactionDate',
'createdAt'
],
filters: ['taxRate', 'transactionType', 'transaction', 'transactionDate'],
sorters: ['transactionDate', 'amount', 'taxAmount', 'createdAt', '_id'],
group: ['transactionType'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'taxRecord',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'taxRate',
label: 'Tax Rate',
required: true,
type: 'object',
objectType: 'taxRate'
},
{
name: 'taxRate._id',
label: 'Tax Rate ID',
readOnly: true,
type: 'id',
showHyperlink: true,
objectType: 'taxRate'
},
{
name: 'transactionType',
label: 'Transaction Type',
required: true,
type: 'select',
options: [
{ label: 'Purchase Order', value: 'purchaseOrder' },
{ label: 'Sales Order', value: 'salesOrder' },
{ label: 'Other', value: 'other' }
]
},
{
name: 'transaction',
label: 'Transaction',
required: true,
type: 'object',
objectType: (objectData) => {
return objectData?.transactionType || 'purchaseOrder'
}
},
{
name: 'transaction._id',
label: 'Transaction ID',
readOnly: true,
type: 'id',
showHyperlink: true,
objectType: (objectData) => {
return objectData?.transactionType || 'purchaseOrder'
}
},
{
name: 'amount',
label: 'Amount',
required: true,
type: 'currency',
min: 0,
step: 0.01
},
{
name: 'taxAmount',
label: 'Tax Amount',
required: true,
type: 'currency',
min: 0,
step: 0.01
},
{
name: 'transactionDate',
label: 'Transaction Date',
required: true,
type: 'date',
default: () => new Date()
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
}
]
}

View File

@ -33,6 +33,10 @@ import DocumentJobInfo from '../components/Dashboard/Management/DocumentJobs/Doc
import DocumentTemplateDesign from '../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx' import DocumentTemplateDesign from '../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx'
import Files from '../components/Dashboard/Management/Files.jsx' import Files from '../components/Dashboard/Management/Files.jsx'
import FileInfo from '../components/Dashboard/Management/Files/FileInfo.jsx' import FileInfo from '../components/Dashboard/Management/Files/FileInfo.jsx'
import TaxRates from '../components/Dashboard/Management/TaxRates.jsx'
import TaxRateInfo from '../components/Dashboard/Management/TaxRates/TaxRateInfo.jsx'
import TaxRecords from '../components/Dashboard/Management/TaxRecords.jsx'
import TaxRecordInfo from '../components/Dashboard/Management/TaxRecords/TaxRecordInfo.jsx'
const ManagementRoutes = [ const ManagementRoutes = [
<Route key='filaments' path='management/filaments' element={<Filaments />} />, <Route key='filaments' path='management/filaments' element={<Filaments />} />,
@ -147,7 +151,23 @@ const ManagementRoutes = [
/>, />,
<Route key='users' path='management/users' element={<Users />} />, <Route key='users' path='management/users' element={<Users />} />,
<Route key='settings' path='management/settings' element={<Settings />} />, <Route key='settings' path='management/settings' element={<Settings />} />,
<Route key='auditlogs' path='management/auditlogs' element={<AuditLogs />} /> <Route key='auditlogs' path='management/auditlogs' element={<AuditLogs />} />,
<Route key='taxrates' path='management/taxrates' element={<TaxRates />} />,
<Route
key='taxrates-info'
path='management/taxrates/info'
element={<TaxRateInfo />}
/>,
<Route
key='taxrecords'
path='management/taxrecords'
element={<TaxRecords />}
/>,
<Route
key='taxrecords-info'
path='management/taxrecords/info'
element={<TaxRecordInfo />}
/>
] ]
export default ManagementRoutes export default ManagementRoutes