Implemented about page.
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

This commit is contained in:
Tom Butcher 2026-06-21 13:19:09 +01:00
parent 8901cdbc98
commit 4363f08f50
11 changed files with 426 additions and 65 deletions

6
Jenkinsfile vendored
View File

@ -20,7 +20,7 @@ def deploy() {
stage('Build (Ubuntu)') { stage('Build (Ubuntu)') {
nodejs(nodeJSInstallationName: 'Node23') { nodejs(nodeJSInstallationName: 'Node23') {
sh 'NODE_ENV=production pnpm build:cloudflare' sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production pnpm build:cloudflare"
} }
} }
@ -70,9 +70,9 @@ def buildOnLabel(label, buildCommand) {
stage("Build (${label})") { stage("Build (${label})") {
nodejs(nodeJSInstallationName: 'Node23') { nodejs(nodeJSInstallationName: 'Node23') {
if (isUnix()) { if (isUnix()) {
sh "NODE_ENV=production ${buildCommand}" sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production ${buildCommand}"
} else { } else {
bat "set NODE_ENV=production && ${buildCommand}" bat "set VITE_BUILD_NUMBER=${env.BUILD_NUMBER} && set NODE_ENV=production && ${buildCommand}"
} }
} }
} }

View File

@ -596,3 +596,9 @@ body {
.ant-table-wrapper .ant-table-filter-column { .ant-table-wrapper .ant-table-filter-column {
align-items: center; align-items: center;
} }
span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
width: 50px;
min-width: 0;
height: 20px;
}

0
public/appupdate.js Normal file
View File

View File

@ -206,6 +206,10 @@ export function getWindow() {
return win return win
} }
export function getElectronVersion() {
return process.versions.electron
}
export function setupMainWindowIPC() { export function setupMainWindowIPC() {
// IPC handler to get window state // IPC handler to get window state
ipcMain.handle('window-state', () => { ipcMain.handle('window-state', () => {
@ -267,6 +271,8 @@ export function setupMainWindowIPC() {
applyApplicationMenu() applyApplicationMenu()
return true return true
}) })
ipcMain.handle('electron-version', () => getElectronVersion())
} }
export function setupMainWindowAppEvents(app) { export function setupMainWindowAppEvents(app) {

View File

@ -0,0 +1,180 @@
import { useContext, useEffect, useState } from 'react'
import {
Flex,
Typography,
Button,
Dropdown,
Skeleton,
Tag,
Divider
} from 'antd'
import useCollapseState from '../hooks/useCollapseState'
import InfoCollapse from '../common/InfoCollapse'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import DeveloperIcon from '../../Icons/DeveloperIcon'
import { version as appVersion } from '../../../../package.json'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import { ElectronContext } from '../context/ElectronContext'
import { useMediaQuery } from 'react-responsive'
const { Title, Text, Link } = Typography
const About = () => {
const [collapseState, updateCollapseState] = useCollapseState('About', {
updater: true
})
const { token } = useContext(AuthContext)
const actions = [
{
label: 'Check for Updates',
icon: <ReloadIcon />,
onClick: () => {
console.log('Check for Updates')
}
}
]
const buildNumber = import.meta.env.VITE_BUILD_NUMBER
? 'b' + import.meta.env.VITE_BUILD_NUMBER
: 'dev'
const developmentMode = import.meta.env.MODE === 'development'
const { fetchApiServerVersion, fetchWsServerVersion } =
useContext(ApiServerContext)
const { isElectron, getElectronVersion } = useContext(ElectronContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
useEffect(() => {
if (token) {
fetchApiServerVersion().then((version) => {
setApiServerVersion(version)
})
fetchWsServerVersion().then((version) => {
setWsServerVersion(version)
})
}
}, [fetchApiServerVersion, fetchWsServerVersion, token])
useEffect(() => {
if (!isElectron) return
getElectronVersion()
.then((version) => {
setElectronVersion(version || 'unknown')
})
.catch(() => {
setElectronVersion('unknown')
})
}, [getElectronVersion, isElectron])
const [apiServerVersion, setApiServerVersion] = useState(null)
const [wsServerVersion, setWsServerVersion] = useState(null)
const [electronVersion, setElectronVersion] = useState(null)
const apiServerVersionText = apiServerVersion ? (
<Text>
{`v${apiServerVersion.version}-${apiServerVersion.buildNumber}`}
</Text>
) : (
<Skeleton.Input active size='small' className='text-skeleton' />
)
const wsServerVersionText = wsServerVersion ? (
<Text>{`v${wsServerVersion.version}-${wsServerVersion.buildNumber}`}</Text>
) : (
<Skeleton.Input active size='small' className='text-skeleton' />
)
const electronVersionText = electronVersion ? (
<Text>{`v${electronVersion}`}</Text>
) : (
<Skeleton.Input active size='small' className='text-skeleton' />
)
return (
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
<Flex vertical gap='large'>
<Flex vertical gap='middle' align='start'>
<Dropdown menu={{ items: actions }}>
<Button>Actions</Button>
</Dropdown>
</Flex>
<Flex vertical gap='large'>
<InfoCollapse
title='About Farm Control'
icon={<InfoCircleIcon />}
canCollapse={false}
active={collapseState.purchaseOrderStats}
onToggle={(isActive) =>
updateCollapseState('purchaseOrderStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='purchaseOrderStats'
>
<Flex gap='large'>
<img
src={'/logo512.png'}
alt='Farm Control Logo'
style={{ width: '200px', height: '200px' }}
/>
<Flex vertical gap='small' justify='center'>
<Flex gap='middle' align='center'>
<Title level={2} style={{ margin: 0 }}>
Farm Control
</Title>
{developmentMode && !isMobile && (
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
>
Development
</Tag>
)}
</Flex>
<Text type='secondary'>
3D Printer ERP and Control Software.
</Text>
<Flex style={{ columnGap: '15px', rowGap: '8px' }} wrap='wrap'>
<Text type='secondary'>
User Interface:{' '}
<Text>
v{appVersion}-{buildNumber}
</Text>
</Text>
{isElectron && (
<Text type='secondary'>
Electron: {electronVersionText}
</Text>
)}
<Text type='secondary'>REST API: {apiServerVersionText}</Text>
<Text type='secondary'>
Web Socket: {wsServerVersionText}
</Text>
</Flex>
<Flex gap='middle' align='center'>
{developmentMode && isMobile && (
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
>
Development
</Tag>
)}
<Link href='https://ci.tombutcher.work/job/farmcontrol'>
Jenkins
</Link>
<Divider type='vertical' style={{ margin: 0 }} />
<Link href='https://github.com/farmcontrol'>GitHub</Link>
</Flex>
</Flex>
</Flex>
</InfoCollapse>
</Flex>
</Flex>
</div>
)
}
export default About

View File

@ -17,7 +17,8 @@ const breadcrumbNameMap = {
info: 'Info', info: 'Info',
design: 'Design', design: 'Design',
control: 'Control', control: 'Control',
preview: 'Preview' preview: 'Preview',
about: 'About'
} }
const mainSections = ['production', 'inventory', 'management', 'developer'] const mainSections = ['production', 'inventory', 'management', 'developer']

View File

@ -744,7 +744,7 @@ const ApiServerProvider = ({ children }) => {
[offLockEvent] [offLockEvent]
) )
const showError = (error, callback = null) => { const showError = useCallback((error, callback = null) => {
const code = error.response.data.code || 'UNKNOWN' const code = error.response.data.code || 'UNKNOWN'
if (code == 'UNAUTHORIZED') { if (code == 'UNAUTHORIZED') {
setUnauthenticated() setUnauthenticated()
@ -757,7 +757,7 @@ const ApiServerProvider = ({ children }) => {
setErrorModalContent(content) setErrorModalContent(content)
setRetryCallback(() => callback) setRetryCallback(() => callback)
setShowErrorModal(true) setShowErrorModal(true)
} }, [setUnauthenticated])
const handleRetry = () => { const handleRetry = () => {
setShowErrorModal(false) setShowErrorModal(false)
@ -1678,35 +1678,78 @@ const ApiServerProvider = ({ children }) => {
return response.data return response.data
}, []) }, [])
const fetchAppUpdateBranches = useCallback(async () => { const fetchApiServerVersion = useCallback(async () => {
try { try {
const response = await axios.get(`${config.backendUrl}/appupdate/branches`, { const response = await axios.get(`${config.backendUrl}/server/version`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return Array.isArray(response.data?.branches) ? response.data.branches : []
} catch (err) {
console.error(err)
showError(err, () => {
fetchAppUpdateBranches()
})
return []
}
}, [token])
const fetchAppUpdateCurrent = useCallback(
async (branch) => {
try {
const response = await axios.get(`${config.backendUrl}/appupdate/current`, {
params: { branch },
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
} }
}) })
return response.data return response.data
} catch (err) {
console.error(err)
showError(err, () => {
fetchApiServerVersion()
})
return null
}
}, [token, showError])
const fetchWsServerVersion = useCallback(async () => {
try {
if (socketRef.current && socketRef.current.connected) {
return await new Promise((resolve) => {
socketRef.current.emit('getServerVersion', {}, resolve)
})
}
return null
} catch (err) {
console.error(err)
showError(err, () => {
fetchWsServerVersion()
})
return null
}
}, [showError])
const fetchAppUpdateBranches = useCallback(async () => {
try {
const response = await axios.get(
`${config.backendUrl}/appupdate/branches`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
return Array.isArray(response.data?.branches)
? response.data.branches
: []
} catch (err) {
console.error(err)
showError(err, () => {
fetchAppUpdateBranches()
})
return []
}
}, [token, showError])
const fetchAppUpdateCurrent = useCallback(
async (branch) => {
try {
const response = await axios.get(
`${config.backendUrl}/appupdate/current`,
{
params: { branch },
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
return response.data
} catch (err) { } catch (err) {
console.error(err) console.error(err)
showError(err, () => { showError(err, () => {
@ -1715,7 +1758,7 @@ const ApiServerProvider = ({ children }) => {
return null return null
} }
}, },
[token] [token, showError]
) )
const flushFile = async (id) => { const flushFile = async (id) => {
@ -1816,7 +1859,9 @@ const ApiServerProvider = ({ children }) => {
completeAppLaunchSession, completeAppLaunchSession,
getAppLaunchSession, getAppLaunchSession,
fetchAppUpdateBranches, fetchAppUpdateBranches,
fetchAppUpdateCurrent fetchAppUpdateCurrent,
fetchWsServerVersion,
fetchApiServerVersion
}} }}
> >
{contextHolder} {contextHolder}

View File

@ -152,6 +152,11 @@ const ElectronProvider = ({ children }) => {
[electronAvailable] [electronAvailable]
) )
const getElectronVersion = useCallback(async () => {
if (!electronAvailable || !ipcRenderer) return null
return await ipcRenderer.invoke('electron-version')
}, [electronAvailable])
return ( return (
<ElectronContext.Provider <ElectronContext.Provider
value={{ value={{
@ -168,7 +173,8 @@ const ElectronProvider = ({ children }) => {
getToken, getToken,
setToken, setToken,
resizeSpotlightWindow, resizeSpotlightWindow,
setSidebarViewMenu setSidebarViewMenu,
getElectronVersion
}} }}
> >
{children} {children}

View File

@ -48,6 +48,7 @@ import CourierServiceIcon from './CourierServiceIcon'
import TaxRateIcon from './TaxRateIcon' import TaxRateIcon from './TaxRateIcon'
import TaxRecordIcon from './TaxRecordIcon' import TaxRecordIcon from './TaxRecordIcon'
import AppPasswordIcon from './AppPasswordIcon' import AppPasswordIcon from './AppPasswordIcon'
import InfoCircleIcon from './InfoCircleIcon'
const toEmoji = (emoji) => <span aria-hidden>{emoji}</span> const toEmoji = (emoji) => <span aria-hidden>{emoji}</span>
@ -102,6 +103,7 @@ const sidebarIconMap = {
taxRate: <TaxRateIcon />, taxRate: <TaxRateIcon />,
taxRecord: <TaxRecordIcon />, taxRecord: <TaxRecordIcon />,
appPassword: <AppPasswordIcon />, appPassword: <AppPasswordIcon />,
infoCircle: <InfoCircleIcon />,
sessionStorage: toEmoji('🗃️'), sessionStorage: toEmoji('🗃️'),
authDebug: toEmoji('🔐'), authDebug: toEmoji('🔐'),
apiDebug: toEmoji('🌐') apiDebug: toEmoji('🌐')

View File

@ -168,6 +168,12 @@ const managementSidebarItems = [
label: 'Developer', label: 'Developer',
path: '/dashboard/developer/sessionstorage', path: '/dashboard/developer/sessionstorage',
devOnly: true devOnly: true
},
{
key: 'about',
iconKey: 'infoCircle',
label: 'About',
path: '/dashboard/management/about'
} }
] ]

View File

@ -1,55 +1,155 @@
import { lazy } from 'react' import { lazy } from 'react'
import { Route } from 'react-router-dom' import { Route } from 'react-router-dom'
const Filaments = lazy(() => import('../components/Dashboard/Management/Filaments')) const Filaments = lazy(
const FilamentInfo = lazy(() => import('../components/Dashboard/Management/Filaments/FilamentInfo.jsx')) () => import('../components/Dashboard/Management/Filaments')
const FilamentSkus = lazy(() => import('../components/Dashboard/Management/FilamentSkus.jsx')) )
const FilamentSkuInfo = lazy(() => import('../components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx')) const FilamentInfo = lazy(
() => import('../components/Dashboard/Management/Filaments/FilamentInfo.jsx')
)
const FilamentSkus = lazy(
() => import('../components/Dashboard/Management/FilamentSkus.jsx')
)
const FilamentSkuInfo = lazy(
() =>
import('../components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx')
)
const Parts = lazy(() => import('../components/Dashboard/Management/Parts.jsx')) const Parts = lazy(() => import('../components/Dashboard/Management/Parts.jsx'))
const PartInfo = lazy(() => import('../components/Dashboard/Management/Parts/PartInfo.jsx')) const PartInfo = lazy(
const PartSkus = lazy(() => import('../components/Dashboard/Management/PartSkus.jsx')) () => import('../components/Dashboard/Management/Parts/PartInfo.jsx')
const PartSkuInfo = lazy(() => import('../components/Dashboard/Management/PartSkus/PartSkuInfo.jsx')) )
const Products = lazy(() => import('../components/Dashboard/Management/Products.jsx')) const PartSkus = lazy(
const ProductInfo = lazy(() => import('../components/Dashboard/Management/Products/ProductInfo.jsx')) () => import('../components/Dashboard/Management/PartSkus.jsx')
const ProductCategories = lazy(() => import('../components/Dashboard/Management/ProductCategories.jsx')) )
const ProductCategoryInfo = lazy(() => import('../components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx')) const PartSkuInfo = lazy(
const ProductSkus = lazy(() => import('../components/Dashboard/Management/ProductSkus.jsx')) () => import('../components/Dashboard/Management/PartSkus/PartSkuInfo.jsx')
const ProductSkuInfo = lazy(() => import('../components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx')) )
const Products = lazy(
() => import('../components/Dashboard/Management/Products.jsx')
)
const ProductInfo = lazy(
() => import('../components/Dashboard/Management/Products/ProductInfo.jsx')
)
const ProductCategories = lazy(
() => import('../components/Dashboard/Management/ProductCategories.jsx')
)
const ProductCategoryInfo = lazy(
() =>
import('../components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx')
)
const ProductSkus = lazy(
() => import('../components/Dashboard/Management/ProductSkus.jsx')
)
const ProductSkuInfo = lazy(
() =>
import('../components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx')
)
const Vendors = lazy(() => import('../components/Dashboard/Management/Vendors')) const Vendors = lazy(() => import('../components/Dashboard/Management/Vendors'))
const VendorInfo = lazy(() => import('../components/Dashboard/Management/Vendors/VendorInfo')) const VendorInfo = lazy(
const Materials = lazy(() => import('../components/Dashboard/Management/Materials')) () => import('../components/Dashboard/Management/Vendors/VendorInfo')
const MaterialInfo = lazy(() => import('../components/Dashboard/Management/Materials/MaterialInfo.jsx')) )
const Couriers = lazy(() => import('../components/Dashboard/Management/Couriers')) const Materials = lazy(
const CourierInfo = lazy(() => import('../components/Dashboard/Management/Couriers/CourierInfo.jsx')) () => import('../components/Dashboard/Management/Materials')
const CourierServices = lazy(() => import('../components/Dashboard/Management/CourierServices')) )
const CourierServiceInfo = lazy(() => import('../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx')) const MaterialInfo = lazy(
const Settings = lazy(() => import('../components/Dashboard/Management/Settings')) () => import('../components/Dashboard/Management/Materials/MaterialInfo.jsx')
const AppUpdate = lazy(() => import('../components/Dashboard/Management/AppUpdate')) )
const AuditLogs = lazy(() => import('../components/Dashboard/Management/AuditLogs.jsx')) const Couriers = lazy(
const NoteTypes = lazy(() => import('../components/Dashboard/Management/NoteTypes.jsx')) () => import('../components/Dashboard/Management/Couriers')
const NoteTypeInfo = lazy(() => import('../components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx')) )
const NoteInfo = lazy(() => import('../components/Dashboard/Management/Notes/NoteInfo.jsx')) const CourierInfo = lazy(
() => import('../components/Dashboard/Management/Couriers/CourierInfo.jsx')
)
const CourierServices = lazy(
() => import('../components/Dashboard/Management/CourierServices')
)
const CourierServiceInfo = lazy(
() =>
import('../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx')
)
const Settings = lazy(
() => import('../components/Dashboard/Management/Settings')
)
const AppUpdate = lazy(
() => import('../components/Dashboard/Management/AppUpdate')
)
const AuditLogs = lazy(
() => import('../components/Dashboard/Management/AuditLogs.jsx')
)
const NoteTypes = lazy(
() => import('../components/Dashboard/Management/NoteTypes.jsx')
)
const NoteTypeInfo = lazy(
() => import('../components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx')
)
const NoteInfo = lazy(
() => import('../components/Dashboard/Management/Notes/NoteInfo.jsx')
)
const Users = lazy(() => import('../components/Dashboard/Management/Users.jsx')) const Users = lazy(() => import('../components/Dashboard/Management/Users.jsx'))
const UserInfo = lazy(() => import('../components/Dashboard/Management/Users/UserInfo.jsx')) const UserInfo = lazy(
const AppPasswords = lazy(() => import('../components/Dashboard/Management/AppPasswords.jsx')) () => import('../components/Dashboard/Management/Users/UserInfo.jsx')
const AppPasswordInfo = lazy(() => import('../components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx')) )
const AppPasswords = lazy(
() => import('../components/Dashboard/Management/AppPasswords.jsx')
)
const AppPasswordInfo = lazy(
() =>
import('../components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx')
)
const Hosts = lazy(() => import('../components/Dashboard/Management/Hosts.jsx')) const Hosts = lazy(() => import('../components/Dashboard/Management/Hosts.jsx'))
const HostInfo = lazy(() => import('../components/Dashboard/Management/Hosts/HostInfo.jsx')) const HostInfo = lazy(
const DocumentSizes = lazy(() => import('../components/Dashboard/Management/DocumentSizes.jsx')) () => import('../components/Dashboard/Management/Hosts/HostInfo.jsx')
const DocumentSizeInfo = lazy(() => import('../components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx')) )
const DocumentTemplates = lazy(() => import('../components/Dashboard/Management/DocumentTemplates.jsx')) const DocumentSizes = lazy(
const DocumentTemplateInfo = lazy(() => import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx')) () => import('../components/Dashboard/Management/DocumentSizes.jsx')
const DocumentPrinters = lazy(() => import('../components/Dashboard/Management/DocumentPrinters.jsx')) )
const DocumentPrinterInfo = lazy(() => import('../components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx')) const DocumentSizeInfo = lazy(
const DocumentJobs = lazy(() => import('../components/Dashboard/Management/DocumentJobs.jsx')) () =>
const DocumentJobInfo = lazy(() => import('../components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx')) import('../components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx')
const DocumentTemplateDesign = lazy(() => import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx')) )
const DocumentTemplates = lazy(
() => import('../components/Dashboard/Management/DocumentTemplates.jsx')
)
const DocumentTemplateInfo = lazy(
() =>
import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx')
)
const DocumentPrinters = lazy(
() => import('../components/Dashboard/Management/DocumentPrinters.jsx')
)
const DocumentPrinterInfo = lazy(
() =>
import('../components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx')
)
const DocumentJobs = lazy(
() => import('../components/Dashboard/Management/DocumentJobs.jsx')
)
const DocumentJobInfo = lazy(
() =>
import('../components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx')
)
const DocumentTemplateDesign = lazy(
() =>
import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx')
)
const Files = lazy(() => import('../components/Dashboard/Management/Files.jsx')) const Files = lazy(() => import('../components/Dashboard/Management/Files.jsx'))
const FileInfo = lazy(() => import('../components/Dashboard/Management/Files/FileInfo.jsx')) const FileInfo = lazy(
const TaxRates = lazy(() => import('../components/Dashboard/Management/TaxRates.jsx')) () => import('../components/Dashboard/Management/Files/FileInfo.jsx')
const TaxRateInfo = lazy(() => import('../components/Dashboard/Management/TaxRates/TaxRateInfo.jsx')) )
const TaxRecords = lazy(() => import('../components/Dashboard/Management/TaxRecords.jsx')) const TaxRates = lazy(
const TaxRecordInfo = lazy(() => import('../components/Dashboard/Management/TaxRecords/TaxRecordInfo.jsx')) () => import('../components/Dashboard/Management/TaxRates.jsx')
)
const TaxRateInfo = lazy(
() => import('../components/Dashboard/Management/TaxRates/TaxRateInfo.jsx')
)
const TaxRecords = lazy(
() => import('../components/Dashboard/Management/TaxRecords.jsx')
)
const TaxRecordInfo = lazy(
() =>
import('../components/Dashboard/Management/TaxRecords/TaxRecordInfo.jsx')
)
const About = lazy(() => import('../components/Dashboard/Management/About.jsx'))
const ManagementRoutes = [ const ManagementRoutes = [
<Route key='filaments' path='management/filaments' element={<Filaments />} />, <Route key='filaments' path='management/filaments' element={<Filaments />} />,
@ -58,7 +158,11 @@ const ManagementRoutes = [
path='management/filaments/info' path='management/filaments/info'
element={<FilamentInfo />} element={<FilamentInfo />}
/>, />,
<Route key='filamentskus' path='management/filamentskus' element={<FilamentSkus />} />, <Route
key='filamentskus'
path='management/filamentskus'
element={<FilamentSkus />}
/>,
<Route <Route
key='filamentskus-info' key='filamentskus-info'
path='management/filamentskus/info' path='management/filamentskus/info'
@ -92,7 +196,11 @@ const ManagementRoutes = [
path='management/productcategories/info' path='management/productcategories/info'
element={<ProductCategoryInfo />} element={<ProductCategoryInfo />}
/>, />,
<Route key='productskus' path='management/productskus' element={<ProductSkus />} />, <Route
key='productskus'
path='management/productskus'
element={<ProductSkus />}
/>,
<Route <Route
key='productskus-info' key='productskus-info'
path='management/productskus/info' path='management/productskus/info'
@ -208,6 +316,7 @@ const ManagementRoutes = [
/>, />,
<Route key='settings' path='management/settings' element={<Settings />} />, <Route key='settings' path='management/settings' element={<Settings />} />,
<Route key='appupdate' path='management/appupdate' element={<AppUpdate />} />, <Route key='appupdate' path='management/appupdate' element={<AppUpdate />} />,
<Route key='about' path='management/about' element={<About />} />,
<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' path='management/taxrates' element={<TaxRates />} />,
<Route <Route