diff --git a/package-lock.json b/package-lock.json index 005bd84..9244c2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "antd-style": "^3.7.1", "axios": "^1.9.0", "country-list": "^2.3.0", + "dayjs": "^1.11.13", "dotenv": "^16.5.0", "eslint": "^8.57.0", "eslint-config-prettier": "^10.1.5", diff --git a/package.json b/package.json index f7b0ca1..5f94f7c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "antd-style": "^3.7.1", "axios": "^1.9.0", "country-list": "^2.3.0", + "dayjs": "^1.11.13", "dotenv": "^16.5.0", "eslint": "^8.57.0", "eslint-config-prettier": "^10.1.5", diff --git a/src/App.css b/src/App.css index 8c52b6d..c35bfdb 100644 --- a/src/App.css +++ b/src/App.css @@ -110,7 +110,7 @@ code { margin-bottom: 0.15em; } -.idtext .ant-popover-inner { +.iddisplay .ant-popover-inner { padding: 0 !important; } diff --git a/src/assets/icons/eyeicon.afdesign b/src/assets/icons/eyeicon.afdesign new file mode 100644 index 0000000..ee04223 Binary files /dev/null and b/src/assets/icons/eyeicon.afdesign differ diff --git a/src/assets/icons/eyeicon.min.svg b/src/assets/icons/eyeicon.min.svg new file mode 100644 index 0000000..2d7c988 --- /dev/null +++ b/src/assets/icons/eyeicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/eyeicon.svg b/src/assets/icons/eyeicon.svg new file mode 100644 index 0000000..bc9d2f6 --- /dev/null +++ b/src/assets/icons/eyeicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/eyeslashicon.afdesign b/src/assets/icons/eyeslashicon.afdesign new file mode 100644 index 0000000..ddb19eb Binary files /dev/null and b/src/assets/icons/eyeslashicon.afdesign differ diff --git a/src/assets/icons/eyeslashicon.min.svg b/src/assets/icons/eyeslashicon.min.svg new file mode 100644 index 0000000..790ae6c --- /dev/null +++ b/src/assets/icons/eyeslashicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/eyeslashicon.svg b/src/assets/icons/eyeslashicon.svg new file mode 100644 index 0000000..f59e1ec --- /dev/null +++ b/src/assets/icons/eyeslashicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/linkicon.afdesign b/src/assets/icons/linkicon.afdesign new file mode 100644 index 0000000..a1dc0ec Binary files /dev/null and b/src/assets/icons/linkicon.afdesign differ diff --git a/src/assets/icons/linkicon.min.svg b/src/assets/icons/linkicon.min.svg new file mode 100644 index 0000000..737619a --- /dev/null +++ b/src/assets/icons/linkicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/linkicon.svg b/src/assets/icons/linkicon.svg new file mode 100644 index 0000000..ce2b1a1 --- /dev/null +++ b/src/assets/icons/linkicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/newmailicon.afdesign b/src/assets/icons/newmailicon.afdesign new file mode 100644 index 0000000..8863f1d Binary files /dev/null and b/src/assets/icons/newmailicon.afdesign differ diff --git a/src/assets/icons/newmailicon.min.svg b/src/assets/icons/newmailicon.min.svg new file mode 100644 index 0000000..7d8a94c --- /dev/null +++ b/src/assets/icons/newmailicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/newmailicon.svg b/src/assets/icons/newmailicon.svg new file mode 100644 index 0000000..d570f57 --- /dev/null +++ b/src/assets/icons/newmailicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/App/AppParticles.jsx b/src/components/App/AppParticles.jsx index a6acb57..5aa190e 100644 --- a/src/components/App/AppParticles.jsx +++ b/src/components/App/AppParticles.jsx @@ -29,9 +29,7 @@ const AppParticles = () => { }) }, []) - const particlesLoaded = useCallback(() => { - console.log('Particles Loaded!') - }, []) + const particlesLoaded = useCallback(() => {}, []) const options = useMemo( () => ({ diff --git a/src/components/Dashboard/Inventory/FilamentStocks.jsx b/src/components/Dashboard/Inventory/FilamentStocks.jsx index c9275a6..de83cc2 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks.jsx @@ -19,7 +19,7 @@ import { AuthContext } from '../context/AuthContext' import { PrintServerContext } from '../context/PrintServerContext' import NewFilamentStock from './FilamentStocks/NewFilamentStock' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import FilamentStockIcon from '../../Icons/FilamentStockIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import PlusIcon from '../../Icons/PlusIcon' @@ -124,7 +124,7 @@ const FilamentStocks = () => { key: 'id', width: 180, render: (text) => ( - + ) }, { @@ -216,7 +216,6 @@ const FilamentStocks = () => { if (printServer && !initialized) { setInitialized(true) printServer.on('notify_filamentstock_update', (updateData) => { - console.log('Received filament stock update:', updateData) if (tableRef.current) { tableRef.current.updateData(updateData._id, updateData) } @@ -225,7 +224,6 @@ const FilamentStocks = () => { return () => { if (printServer && initialized) { - console.log('Deregistering filament stock update listener') printServer.off('notify_filamentstock_update') } } diff --git a/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx b/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx index 2f59597..b2767ed 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx @@ -18,7 +18,7 @@ import { Checkbox } from 'antd' import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' -import IdText from '../../common/IdText' +import IdDisplay from '../../common/IdDisplay' import { PrintServerContext } from '../../context/PrintServerContext' import FilamentStockState from '../../common/FilamentStockState' import StockEventTable from '../../common/StockEventTable' @@ -78,7 +78,6 @@ const FilamentStockInfo = () => { if (printServer && !initialized && filamentStockId) { setInitialized(true) printServer.on('notify_filamentstock_update', (statusUpdate) => { - console.log('GOT FILAMENT STOCK UPDATE', statusUpdate) setFilamentStockData((prevData) => { if (statusUpdate?._id === filamentStockId) { return { @@ -92,7 +91,6 @@ const FilamentStockInfo = () => { } return () => { if (printServer && initialized) { - console.log('Deregistering filament stock update listener') printServer.off('notify_filamentstock_update') } } @@ -260,7 +258,7 @@ const FilamentStockInfo = () => { {/* Read-only fields */} {filamentStockData?.id ? ( - @@ -316,7 +314,7 @@ const FilamentStockInfo = () => { {filamentStockData?.filament ? ( - { dataIndex: '_id', key: 'id', width: 180, - render: (text) => + render: (text) => ( + + ) }, { title: 'State', diff --git a/src/components/Dashboard/Inventory/StockAudits.jsx b/src/components/Dashboard/Inventory/StockAudits.jsx index 28f0272..8dcc5e1 100644 --- a/src/components/Dashboard/Inventory/StockAudits.jsx +++ b/src/components/Dashboard/Inventory/StockAudits.jsx @@ -5,7 +5,7 @@ import { Button, Flex, Space, message, Dropdown, Typography } from 'antd' import { AuthContext } from '../context/AuthContext' import { PrintServerContext } from '../context/PrintServerContext' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import StockAuditIcon from '../../Icons/StockAuditIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import PlusIcon from '../../Icons/PlusIcon' @@ -30,7 +30,6 @@ const StockAudits = () => { if (printServer && !initialized) { setInitialized(true) printServer.on('notify_stockaudit_update', (updateData) => { - console.log('Received stock audit update:', updateData) if (tableRef.current) { tableRef.current.updateData(updateData._id, updateData) } @@ -39,7 +38,6 @@ const StockAudits = () => { return () => { if (printServer && initialized) { - console.log('Deregistering stock audit update listener') printServer.off('notify_stockaudit_update') } } @@ -76,7 +74,9 @@ const StockAudits = () => { dataIndex: '_id', key: 'id', width: 180, - render: (text) => + render: (text) => ( + + ) }, { title: 'Status', diff --git a/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx b/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx index eada020..68ddf2f 100644 --- a/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx +++ b/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx @@ -18,7 +18,7 @@ import { } from '@ant-design/icons' import { AuthContext } from '../../context/AuthContext' -import IdText from '../../common/IdText' +import IdDisplay from '../../common/IdDisplay' import TimeDisplay from '../../common/TimeDisplay' import config from '../../../../config' @@ -105,7 +105,7 @@ const StockAuditInfo = () => { key: 'id', width: 180, render: (text) => ( - + ) }, { @@ -180,7 +180,11 @@ const StockAuditInfo = () => { Stock Audit Details - + {getStatusTag(stockAudit.status)} diff --git a/src/components/Dashboard/Inventory/StockEvents.jsx b/src/components/Dashboard/Inventory/StockEvents.jsx index ce14d3a..931499a 100644 --- a/src/components/Dashboard/Inventory/StockEvents.jsx +++ b/src/components/Dashboard/Inventory/StockEvents.jsx @@ -12,7 +12,7 @@ import { import { AuthContext } from '../context/AuthContext' import { PrintServerContext } from '../context/PrintServerContext' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import TimeDisplay from '../common/TimeDisplay' import ReloadIcon from '../../Icons/ReloadIcon' import PlusMinusIcon from '../../Icons/PlusMinusIcon' @@ -75,7 +75,7 @@ const StockEvents = () => { dataIndex: '_id', width: 170, render: (id) => { - return + return } }, { @@ -100,7 +100,7 @@ const StockEvents = () => { render: (record) => { if (record.filamentStock?._id) { return ( - { const ids = ( {record.job ? ( - { /> ) : null} {record.subJob?.number ? ( - ) : null} {record.stockAudit ? ( - { if (printServer && !initialized) { setInitialized(true) printServer.on('notify_stockevent_update', (updateData) => { - console.log('Received stock event update:', updateData) if (tableRef.current) { tableRef.current.updateData(updateData._id, updateData) } @@ -237,7 +236,6 @@ const StockEvents = () => { return () => { if (printServer && initialized) { - console.log('Deregistering stock event update listener') printServer.off('notify_stockevent_update') } } diff --git a/src/components/Dashboard/Management/AuditLogs.jsx b/src/components/Dashboard/Management/AuditLogs.jsx index 96a92f2..22d628d 100644 --- a/src/components/Dashboard/Management/AuditLogs.jsx +++ b/src/components/Dashboard/Management/AuditLogs.jsx @@ -13,7 +13,7 @@ import { } from 'antd' import { AuthContext } from '../context/AuthContext' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import TimeDisplay from '../common/TimeDisplay' @@ -66,7 +66,7 @@ const formatValue = (value, propertyName) => { if (isObjectId(value)) { return ( - { key: 'id', fixed: 'left', width: 180, - render: (text) => , + render: (text) => ( + + ), filterDropdown: ({ setSelectedKeys, selectedKeys, @@ -147,7 +149,7 @@ const AuditLogs = () => { key: 'owner', width: 180, render: (record) => ( - { key: 'target', width: 180, render: (record) => ( - { dataIndex: '_id', key: 'id', width: 180, - render: (text) => , + render: (text) => ( + + ), filterDropdown: ({ setSelectedKeys, selectedKeys, diff --git a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx index adf0696..2dd4171 100644 --- a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx +++ b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx @@ -1,62 +1,30 @@ -import React, { useState, useEffect, useContext, useCallback } from 'react' +import React from 'react' import { useLocation } from 'react-router-dom' -import { - Descriptions, - Spin, - Space, - Button, - message, - Badge, - Typography, - Flex, - Form, - Input, - InputNumber, - ColorPicker, - Select, - Dropdown, - Popover, - Checkbox, - Card, - Tag -} from 'antd' +import { Space, Button, Flex, Dropdown, Card } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' -import IdText from '../../common/IdText' import ReloadIcon from '../../../Icons/ReloadIcon' -import EditIcon from '../../../Icons/EditIcon.jsx' -import XMarkIcon from '../../../Icons/XMarkIcon.jsx' -import CheckIcon from '../../../Icons/CheckIcon.jsx' -import TimeDisplay from '../../common/TimeDisplay.jsx' -import VendorSelect from '../../common/VendorSelect' import useCollapseState from '../../hooks/useCollapseState' import AuditLogTable from '../../common/AuditLogTable' import DashboardNotes from '../../common/DashboardNotes' 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 LockIcon from '../../../Icons/LockIcon.jsx' -import { ApiServerContext } from '../../context/ApiServerContext' +import EditObjectForm from '../../common/EditObjectForm' +import EditButtons from '../../common/EditButtons' +import LockIndicator from './LockIndicator' const log = loglevel.getLogger('FilamentInfo') log.setLevel(config.logLevel) -const { Link, Text } = Typography - const FilamentInfo = () => { - const [filamentData, setFilamentData] = useState(null) - const [fetchLoading, setFetchLoading] = useState(true) - const [editLoading, setEditLoading] = useState(false) - const [lockUser, setLockUser] = useState(null) - const [initialized, setInitialized] = useState(false) const location = useLocation() - const [messageApi, contextHolder] = message.useMessage() const filamentId = new URLSearchParams(location.search).get('filamentId') - const [isEditing, setIsEditing] = useState(false) - const [form] = Form.useForm() const [collapseState, updateCollapseState] = useCollapseState( 'FilamentInfo', { @@ -66,600 +34,217 @@ const FilamentInfo = () => { auditLogs: true } ) - const { - apiServer, - fetchObjectInfo, - updateObjectInfo, - lockObject, - unlockObject, - onLockEvent, - onUpdateEvent, - fetchObjectLock, - showError - } = useContext(ApiServerContext) - - // Define the event handler function - const lockEventHandler = useCallback((lockEvent) => { - if (lockEvent.locked === true) { - setLockUser(lockEvent.user) - } else { - setLockUser(null) - } - }, []) - - // Cleanup effect for component unmount - useEffect(() => { - return () => { - if (filamentId) { - // Ensure any remaining locks are released when component unmounts - unlockObject(filamentId, 'filament') - } - } - }, [filamentId, unlockObject]) - - useEffect(() => { - if (filamentData) { - form.setFieldsValue({ - name: filamentData.name || '', - brand: filamentData.brand || '', - type: filamentData.type || '', - cost: filamentData.cost || null, - color: filamentData.color || '#000000', - diameter: filamentData.diameter || null, - density: filamentData.density || null, - url: filamentData.url || '', - barcode: filamentData.barcode || '', - emptySpoolWeight: filamentData.emptySpoolWeight || '' - }) - } - }, [filamentData, form]) - - const fetchFilamentInfo = useCallback(async () => { - try { - setFetchLoading(true) - const data = await fetchObjectInfo(filamentId, 'filament') - const lockEvent = await fetchObjectLock(filamentId, 'filament') - setLockUser(lockEvent?.user || null) - setFilamentData(data) - form.setFieldsValue(data) - setFetchLoading(false) - } catch (err) { - messageApi.error('Failed to fetch filament info') - // Show error modal with retry functionality - showError( - `Failed to fetch filament information. Message: ${err.message}. Code: ${err.code}`, - fetchFilamentInfo - ) - } - }, [ - fetchObjectInfo, - fetchObjectLock, - filamentId, - form, - messageApi, - showError - ]) - - const updateFilamentInfo = async () => { - const values = form.getFieldsValue() - const updateValue = { - name: values.name, - vendor: values.vendor, - type: values.type, - cost: values.cost, - color: values.color, - diameter: values.diameter, - density: values.density, - url: values.url, - barcode: values.barcode, - emptySpoolWeight: values.emptySpoolWeight - } - await updateObjectInfo(filamentId, 'filament', updateValue) - } - - // Define the update event handler function - const updateEventHandler = useCallback( - (updateEvent) => { - log.debug('Update event received for filament:', updateEvent) - // Refresh the filament data when an update is received - fetchFilamentInfo() - }, - [fetchFilamentInfo] - ) - - useEffect(() => { - if (initialized == false && filamentId && apiServer?.connected === true) { - setInitialized(true) - fetchFilamentInfo() - } - }, [filamentId, apiServer?.connected, initialized, fetchFilamentInfo]) - - useEffect(() => { - if (filamentId) { - const cleanup = onLockEvent(filamentId, lockEventHandler) - return cleanup - } - }, [filamentId, onLockEvent, lockEventHandler]) - - useEffect(() => { - if (filamentId) { - const cleanup = onUpdateEvent(filamentId, updateEventHandler) - return cleanup - } - }, [filamentId, onUpdateEvent, updateEventHandler]) - - const startEditing = () => { - updateCollapseState('info', true) - setIsEditing(true) - lockObject(filamentId, 'filament') - } - - const cancelEditing = () => { - // Reset form values to original data - if (filamentData) { - form.setFieldsValue({ - name: filamentData.name || '', - brand: filamentData.brand || '', - type: filamentData.type || '', - cost: filamentData.cost || null, - color: filamentData.color || '#000000', - diameter: filamentData.diameter || null, - density: filamentData.density || null, - url: filamentData.url || '', - barcode: filamentData.barcode || '', - emptySpoolWeight: filamentData.emptySpoolWeight || '' - }) - } - setIsEditing(false) - unlockObject(filamentId, 'filament') - } - - const handleUpdateFilamentInfo = async () => { - try { - const values = await form.validateFields() - setEditLoading(true) - - await updateFilamentInfo() - - // Update the local state with the new values - setFilamentData({ ...filamentData, ...values }) - setIsEditing(false) - messageApi.success('Filament information updated successfully') - } catch (err) { - if (err.errorFields) { - // This is a form validation error - return - } - console.error('Failed to update filament information:', err) - messageApi.error('Failed to update filament information') - // Show error modal with retry functionality - showError( - `Failed to update filament information. Message: ${err.message}. Code: ${err.code}`, - () => handleUpdateFilamentInfo() - ) - } finally { - fetchFilamentInfo() - setEditLoading(false) - } - } - - const actionItems = { - items: [ - { - label: 'Reload Filament', - key: 'reload', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchFilamentInfo() - } - } - } - - const getViewDropdownItems = () => { - const sections = [ - { key: 'info', label: 'Filament Information' }, - { key: 'notes', label: 'Notes' }, - { key: 'auditLogs', label: 'Audit Logs' } - ] - - return ( - - - {sections.map((section) => ( - { - updateCollapseState(section.key, e.target.checked) - }} - > - {section.label} - - ))} - - - ) - } return ( - <> - {contextHolder} - - - - - - - - - - + + {({ + loading, + isEditing, + startEditing, + cancelEditing, + handleUpdate, + formValid, + objectData, + editLoading, + lock, + fetchObject + }) => ( + + + + + + } + ], + onClick: ({ key }) => { + if (key === 'reload') { + fetchObject() + } + } + }} + > + + + + + - {lockUser && ( - - } - style={{ margin: 0 }} - color={'orange'} - /> - - - )} - - - {isEditing ? ( - <> - - - - - - - - {isEditing ? ( - <> - - - ) : ( -
- {contextHolder} - - updateCollapseState('info', keys.length > 0)} - expandIcon={({ isActive }) => ( - + + + + } + ], + onClick: ({ key }) => { + if (key === 'reload') { + fetchObject() + } + } + }} + > + + + - )} - className='no-h-padding-collapse no-t-padding-collapse' - > - - - - Note Type Information - - - } - key='1' + + + + + + + + +
+ + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + key='info' > - }> -
- - - - - - - - - - {isEditing ? ( - - - - ) : ( - noteTypeData?.name - )} - - - - - - - {isEditing ? ( - - { - if (color != null) { - return '#' + color.toHex() - } - return null - }} - > - - - { - setColorEnabled(e.target.checked) - if (!e.target.checked) { - form.setFieldValue('color', null) - } else if (e.target.checked) { - form.setFieldValue('color', '#000000') - } - }} - /> - - ) : noteTypeData?.color ? ( - - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : noteTypeData ? ( - - ) : ( - n/a - )} - - -
-
- - - - - updateCollapseState('auditLogs', keys.length > 0) - } - expandIcon={({ isActive }) => ( - } + isEditing={isEditing} + type='notetype' + items={[ + { + name: 'id', + label: 'ID', + value: objectData?._id, + type: 'id', + objectType: 'notetype', + showCopy: true + }, + { + name: 'createdAt', + label: 'Created At', + value: objectData?.createdAt, + type: 'dateTime', + readOnly: true + }, + { + name: 'name', + label: 'Name', + value: objectData?.name, + required: true, + type: 'text' + }, + { + name: 'updatedAt', + label: 'Updated At', + value: objectData?.updatedAt, + type: 'dateTime', + readOnly: true + }, + { + name: 'color', + label: 'Color', + value: objectData?.color, + type: 'color' + }, + { + name: 'active', + label: 'Active', + value: objectData?.active, + type: 'bool' + } + ]} /> - )} - className='no-h-padding-collapse' - > - - - - Audit Logs - -
+ + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) } - key='2' + key='auditLogs' > - - - -
+ + +
+
)} - + ) } diff --git a/src/components/Dashboard/Management/Parts.jsx b/src/components/Dashboard/Management/Parts.jsx index 8d58126..969cd7d 100644 --- a/src/components/Dashboard/Management/Parts.jsx +++ b/src/components/Dashboard/Management/Parts.jsx @@ -17,7 +17,7 @@ import { import { DownloadOutlined } from '@ant-design/icons' import { AuthContext } from '../context/AuthContext' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import DashboardTable from '../common/DashboardTable' import NewProduct from './Products/NewProduct' import PartIcon from '../../Icons/PartIcon' @@ -81,7 +81,7 @@ const Parts = () => { dataIndex: '_id', key: 'id', width: 180, - render: (text) => + render: (text) => }, { title: 'Product Name', @@ -109,7 +109,7 @@ const Parts = () => { key: 'productId', width: 180, render: (record) => ( - { setError(null) } catch (err) { setError('Failed to fetch part details') - console.log(err) + logger.debug(err) messageApi.error('Failed to fetch part details') } finally { setFetchLoading(false) @@ -176,7 +179,7 @@ const PartInfo = () => { } } catch (err) { setError('Failed to fetch part content') - console.log(err) + logger.debug(err) messageApi.error('Failed to fetch part content') } finally { setFetchLoading(false) @@ -398,7 +401,7 @@ const PartInfo = () => { > {partData?.id ? ( - + ) : ( n/a )} @@ -459,7 +462,7 @@ const PartInfo = () => { {partData?.product?._id ? ( - { key: 'id', fixed: 'left', width: 180, - render: (text) => , + render: (text) => , filterDropdown: ({ setSelectedKeys, selectedKeys, diff --git a/src/components/Dashboard/Management/Products/ProductInfo.jsx b/src/components/Dashboard/Management/Products/ProductInfo.jsx index a127def..d43d4af 100644 --- a/src/components/Dashboard/Management/Products/ProductInfo.jsx +++ b/src/components/Dashboard/Management/Products/ProductInfo.jsx @@ -1,56 +1,25 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' import { useLocation } from 'react-router-dom' -import axios from 'axios' -import { - Descriptions, - Spin, - Space, - Button, - message, - Typography, - Flex, - Form, - Input, - Tag, - Checkbox, - InputNumber, - Collapse, - Dropdown, - Popover, - Card -} from 'antd' -import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' -import IdText from '../../common/IdText.jsx' -import VendorSelect from '../../common/VendorSelect.jsx' -import PartsTable from '../../common/PartsTable.jsx' -import useCollapseState from '../../hooks/useCollapseState' -import PlusIcon from '../../../Icons/PlusIcon' +import { Space, Button, Flex, Dropdown, Card } from 'antd' import ReloadIcon from '../../../Icons/ReloadIcon' -import EditIcon from '../../../Icons/EditIcon.jsx' -import XMarkIcon from '../../../Icons/XMarkIcon.jsx' -import CheckIcon from '../../../Icons/CheckIcon.jsx' -import TimeDisplay from '../../common/TimeDisplay.jsx' +import useCollapseState from '../../hooks/useCollapseState' import AuditLogTable from '../../common/AuditLogTable' import DashboardNotes from '../../common/DashboardNotes' - -import config from '../../../../config.js' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ViewButton from '../../common/ViewButton' +import EditObjectForm from '../../common/EditObjectForm' +import EditButtons from '../../common/EditButtons' +import LockIndicator from '../Filaments/LockIndicator' +import PartsTable from '../../common/PartsTable' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ProductIcon from '../../../Icons/ProductIcon.jsx' -import NoteIcon from '../../../Icons/NoteIcon.jsx' - -const { Title, Text } = Typography const ProductInfo = () => { - const [productData, setProductData] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) const location = useLocation() - const [messageApi, contextHolder] = message.useMessage() const productId = new URLSearchParams(location.search).get('productId') - const [isEditing, setIsEditing] = useState(false) - const [fetchLoading, setFetchLoading] = useState(true) - const [marginOrPrice, setMarginOrPrice] = useState(false) const [collapseState, updateCollapseState] = useCollapseState('ProductInfo', { info: true, parts: true, @@ -58,600 +27,209 @@ const ProductInfo = () => { auditLogs: true }) - const [productForm] = Form.useForm() - const [productFormValues, setProductFormValues] = useState({}) - - const handleTagClose = (removedTag) => { - const newTags = productData.tags.filter((tag) => tag !== removedTag) - setProductData((prev) => ({ ...prev, tags: newTags })) - } - - const handleTagAdd = () => { - const input = productForm.getFieldValue('newTag') - if (input) { - const newTag = input.trim() - if (newTag && !productData.tags.includes(newTag)) { - setProductData((prev) => ({ ...prev, tags: [...prev.tags, newTag] })) - productForm.setFieldValue('newTag', '') - } - } - } - - useEffect(() => { - setMarginOrPrice(productFormValues.marginOrPrice) - }, [productFormValues]) - - useEffect(() => { - async function fetchData() { - console.log('hello') - await fetchProductDetails() - } - if (productId) { - fetchData() - } - }, [productId]) - - useEffect(() => { - if (productData) { - productForm.setFieldsValue({ - name: productData.name || '', - vendor: productData.vendor || null, - version: productData.version || '', - tags: productData.tags || [], - price: productData.price || null, - margin: productData.margin || null, - marginOrPrice: productData.marginOrPrice || false - }) - setProductFormValues(productData) - setMarginOrPrice(productData.marginOrPrice) - } - }, [productData, productForm]) - - const fetchProductDetails = async () => { - try { - setFetchLoading(true) - const response = await axios.get( - `${config.backendUrl}/products/${productId}`, - { - headers: { - Accept: 'application/json' - }, - withCredentials: true - } - ) - setProductData(response.data) - setError(null) - } catch (err) { - setError('Failed to fetch product details') - console.log(err) - messageApi.error('Failed to fetch product details') - } finally { - setFetchLoading(false) - } - } - - const startEditing = () => { - updateCollapseState('info', true) - setIsEditing(true) - } - - const cancelEditing = () => { - // Reset form values to original data - if (productData) { - productForm.setFieldsValue({ - name: productData.name || '', - vendor: productData.vendor || { id: null, name: '' }, - version: productData.version || '', - tags: productData.tags || [], - cost: productData.cost || null, - price: productData.price || null, - margin: productData.margin || null, - marginOrPrice: productData.marginOrPrice || null - }) - setMarginOrPrice(productData.marginOrPrice) - } - setIsEditing(false) - } - - const updateInfo = async () => { - try { - const values = await productForm.validateFields() - setLoading(true) - - await axios.put(`${config.backendUrl}/products/${productId}`, values, { - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true - }) - - setProductData({ - ...productData, - ...values - }) - setIsEditing(false) - messageApi.success('Product information updated successfully') - } catch (err) { - if (err.errorFields) { - return - } - console.error('Failed to update product information:', err) - messageApi.error('Failed to update product information') - } finally { - await fetchProductDetails() - setLoading(false) - } - } - - const actionItems = { - items: [ - { - label: 'Reload Product', - key: 'reload', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchProductDetails() - } - } - } - - const getViewDropdownItems = () => { - const sections = [ - { key: 'info', label: 'Product Information' }, - { key: 'parts', label: 'Product Parts' }, - { key: 'notes', label: 'Notes' }, - { key: 'auditLogs', label: 'Audit Logs' } - ] - - return ( - - - {sections.map((section) => ( - { - updateCollapseState(section.key, e.target.checked) - }} - > - {section.label} - - ))} - - - ) - } - - if (error) { - return ( - -

{error || 'Product not found'}

- -
- ) - } - return ( - <> - {contextHolder} - - - - - - - - - - - - {isEditing ? ( - <> - + + - - - ) : (
- - updateCollapseState('info', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + key='info' > - - - - Product Information - - - } - key='1' - > -
- setProductFormValues((prevValues) => ({ - ...prevValues, - ...changedValues - })) + - } - spinning={fetchLoading} - > - - - {productData?.id ? ( - - ) : ( - n/a - )} - + ]} + /> + - - {productData?.createdAt ? ( - - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : productData?.name ? ( - {productData.name} - ) : ( - n/a - )} - - - - {productData?.updatedAt ? ( - - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : productData?.vendor?.name ? ( - {productData.vendor.name} - ) : ( - n/a - )} - - - - {productData?.vendor?.id ? ( - - ) : ( - n/a - )} - - - - {isEditing ? ( - - {marginOrPrice == false ? ( - - - - ) : ( - - - - )} - - Price - - - ) : productData?.margin && marginOrPrice == false ? ( - {productData.margin + '%'} - ) : productData?.price && marginOrPrice == true ? ( - {'£' + productData.price} - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : productData?.version ? ( - {productData.version} - ) : ( - n/a - )} - - - - {isEditing ? ( - - - {productData?.tags?.map((tag) => ( - handleTagClose(tag)} - style={{ marginBottom: 12 }} - > - {tag} - - ))} - - - - - -
- )} -
- +
+ )} + ) } diff --git a/src/components/Dashboard/Management/Users.jsx b/src/components/Dashboard/Management/Users.jsx index b148e46..5c83815 100644 --- a/src/components/Dashboard/Management/Users.jsx +++ b/src/components/Dashboard/Management/Users.jsx @@ -12,7 +12,7 @@ import { } from 'antd' import { ExportOutlined } from '@ant-design/icons' import { AuthContext } from '../context/AuthContext' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import TimeDisplay from '../common/TimeDisplay' import DashboardTable from '../common/DashboardTable' import PersonIcon from '../../Icons/PersonIcon' @@ -248,7 +248,7 @@ const Users = () => { dataIndex: '_id', key: 'id', width: 180, - render: (text) => , + render: (text) => , filterDropdown: ({ setSelectedKeys, selectedKeys, diff --git a/src/components/Dashboard/Management/Users/UserInfo.jsx b/src/components/Dashboard/Management/Users/UserInfo.jsx index f35daa4..d903a9f 100644 --- a/src/components/Dashboard/Management/Users/UserInfo.jsx +++ b/src/components/Dashboard/Management/Users/UserInfo.jsx @@ -1,509 +1,207 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' import { useLocation } from 'react-router-dom' -import axios from 'axios' -import { - Descriptions, - Spin, - Space, - Button, - message, - Typography, - Flex, - Form, - Input, - Collapse, - Dropdown, - Popover, - Card, - Checkbox -} from 'antd' -import { - LoadingOutlined, - ExportOutlined, - CaretLeftOutlined -} from '@ant-design/icons' -import IdText from '../../common/IdText' -import TimeDisplay from '../../common/TimeDisplay' +import { Space, Button, Flex, Dropdown, Card } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import ReloadIcon from '../../../Icons/ReloadIcon' -import EditIcon from '../../../Icons/EditIcon.jsx' -import XMarkIcon from '../../../Icons/XMarkIcon.jsx' -import CheckIcon from '../../../Icons/CheckIcon.jsx' import useCollapseState from '../../hooks/useCollapseState' import AuditLogTable from '../../common/AuditLogTable' import DashboardNotes from '../../common/DashboardNotes' +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 config from '../../../../config.js' - -const { Title, Link, Text } = Typography +import EditObjectForm from '../../common/EditObjectForm' +import EditButtons from '../../common/EditButtons' +import LockIndicator from '../Filaments/LockIndicator' const UserInfo = () => { - const [userData, setUserData] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) const location = useLocation() - const [messageApi, contextHolder] = message.useMessage() const userId = new URLSearchParams(location.search).get('userId') - const [isEditing, setIsEditing] = useState(false) - const [form] = Form.useForm() - const [fetchLoading, setFetchLoading] = useState(true) const [collapseState, updateCollapseState] = useCollapseState('UserInfo', { info: true, notes: true, auditLogs: true }) - useEffect(() => { - if (userId) { - fetchUserDetails() - } - }, [userId]) - - useEffect(() => { - if (userData) { - form.setFieldsValue({ - username: userData.username || '', - name: userData.name || '', - firstName: userData.firstName || '', - lastName: userData.lastName || '', - email: userData.email || '' - }) - } - }, [userData, form]) - - const fetchUserDetails = async () => { - try { - setFetchLoading(true) - const response = await axios.get(`${config.backendUrl}/users/${userId}`, { - headers: { - Accept: 'application/json' - }, - withCredentials: true - }) - setUserData(response.data) - setError(null) - } catch (err) { - setError('Failed to fetch user details') - messageApi.error('Failed to fetch user details') - } finally { - setFetchLoading(false) - } - } - - const startEditing = () => { - updateCollapseState('info', true) - setIsEditing(true) - } - - const cancelEditing = () => { - // Reset form values to original data - if (userData) { - form.setFieldsValue({ - username: userData.username || '', - name: userData.name || '', - firstName: userData.firstName || '', - lastName: userData.lastName || '', - email: userData.email || '' - }) - } - setIsEditing(false) - } - - const updateInfo = async () => { - try { - const values = await form.validateFields() - setLoading(true) - - await axios.put(`${config.backendUrl}/users/${userId}`, values, { - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true - }) - - setUserData({ ...userData, ...values }) - setIsEditing(false) - messageApi.success('User information updated successfully') - } catch (err) { - if (err.errorFields) { - return - } - console.error('Failed to update user information:', err) - messageApi.error('Failed to update user information') - } finally { - fetchUserDetails() - setLoading(false) - } - } - - const actionItems = { - items: [ - { - label: 'Reload User', - key: 'reload', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchUserDetails() - } - } - } - - const getViewDropdownItems = () => { - const sections = [ - { key: 'info', label: 'User Information' }, - { key: 'notes', label: 'Notes' }, - { key: 'auditLogs', label: 'Audit Logs' } - ] - - return ( - - - {sections.map((section) => ( - { - updateCollapseState(section.key, e.target.checked) - }} - > - {section.label} - - ))} - - - ) - } - - if (error) { - return ( - -

{error || 'User not found'}

- -
- ) - } - return ( - <> - {contextHolder} - - - - - - - - - - - - {isEditing ? ( - <> - + + - - - ) : (
- - updateCollapseState('info', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + key='info' > - - - - User Information - - - } - key='1' - > -
- } - spinning={fetchLoading} - > - - - {userData?._id ? ( - - ) : ( - n/a - )} - + } + isEditing={isEditing} + type='user' + items={[ + { + name: '_id', + label: 'ID', + value: objectData?._id, + type: 'id', + objectType: 'user', + showCopy: true + }, + { + name: 'createdAt', + label: 'Created At', + value: objectData?.createdAt, + type: 'dateTime', + readOnly: true + }, + { + name: 'name', + label: 'Name', + value: objectData?.name, + required: true, + type: 'text' + }, + { + name: 'updatedAt', + label: 'Updated At', + value: objectData?.updatedAt, + type: 'dateTime', + readOnly: true + }, - - {userData?.createdAt ? ( - - ) : ( - n/a - )} - + { + name: 'firstName', + label: 'First Name', + value: objectData?.firstName, + type: 'text' + }, + { + name: 'username', + label: 'Username', + value: objectData?.username, + required: true, + type: 'text' + }, + { + name: 'lastName', + label: 'Last Name', + value: objectData?.lastName, + type: 'text' + }, + { + name: 'email', + label: 'Email', + value: objectData?.email, + type: 'email' + } + ]} + /> + - - {isEditing ? ( - - - - ) : userData?.name ? ( - {userData.name} - ) : ( - n/a - )} - - - - {userData?.updatedAt ? ( - - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : userData?.username ? ( - {userData.username} - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : userData?.firstName ? ( - {userData.firstName} - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : userData?.email ? ( - - {userData.email + ' '} - - - ) : ( - n/a - )} - - - {isEditing ? ( - - - - ) : userData?.lastName ? ( - {userData.lastName} - ) : ( - n/a - )} - - - -
- - - - - updateCollapseState('notes', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + key='notes' > - - - - Notes - - - } - key='notes' - > - - - - - + + + + - - updateCollapseState('auditLogs', keys.length > 0) + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + key='auditLogs' > - - - - Audit Logs - - - } - key='auditLogs' - > - - - + +
- )} -
- +
+ )} + ) } diff --git a/src/components/Dashboard/Management/Vendors.jsx b/src/components/Dashboard/Management/Vendors.jsx index 2c4fa61..4c99876 100644 --- a/src/components/Dashboard/Management/Vendors.jsx +++ b/src/components/Dashboard/Management/Vendors.jsx @@ -14,7 +14,7 @@ import { } from 'antd' import { ExportOutlined } from '@ant-design/icons' import { AuthContext } from '../context/AuthContext' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import NewVendor from './Vendors/NewVendor' import CountryDisplay from '../common/CountryDisplay' import TimeDisplay from '../common/TimeDisplay' @@ -155,7 +155,7 @@ const Vendors = () => { dataIndex: '_id', key: 'id', width: 180, - render: (text) => , + render: (text) => , filterDropdown: ({ setSelectedKeys, selectedKeys, diff --git a/src/components/Dashboard/Management/Vendors/VendorInfo.jsx b/src/components/Dashboard/Management/Vendors/VendorInfo.jsx index 9868486..104ccb1 100644 --- a/src/components/Dashboard/Management/Vendors/VendorInfo.jsx +++ b/src/components/Dashboard/Management/Vendors/VendorInfo.jsx @@ -1,539 +1,216 @@ -import React, { useState, useEffect, useCallback } from 'react' +import React from 'react' import { useLocation } from 'react-router-dom' -import axios from 'axios' -import { - Descriptions, - Spin, - Space, - Button, - message, - Typography, - Flex, - Form, - Input, - Collapse, - Dropdown, - Popover, - Card, - Checkbox -} from 'antd' -import { - LoadingOutlined, - ExportOutlined, - CaretLeftOutlined -} from '@ant-design/icons' -import IdText from '../../common/IdText' -import CountrySelect from '../../common/CountrySelect' -import CountryDisplay from '../../common/CountryDisplay' -import TimeDisplay from '../../common/TimeDisplay' +import { Space, Button, Flex, Dropdown, Card } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config' import ReloadIcon from '../../../Icons/ReloadIcon' -import EditIcon from '../../../Icons/EditIcon.jsx' -import XMarkIcon from '../../../Icons/XMarkIcon.jsx' -import CheckIcon from '../../../Icons/CheckIcon.jsx' import useCollapseState from '../../hooks/useCollapseState' import AuditLogTable from '../../common/AuditLogTable' import DashboardNotes from '../../common/DashboardNotes' +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 EditObjectForm from '../../common/EditObjectForm' +import EditButtons from '../../common/EditButtons' +import LockIndicator from '../Filaments/LockIndicator' -import config from '../../../../config.js' - -const { Title, Link, Text } = Typography +const log = loglevel.getLogger('VendorInfo') +log.setLevel(config.logLevel) const VendorInfo = () => { - const [vendorData, setVendorData] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) const location = useLocation() - const [messageApi, contextHolder] = message.useMessage() const vendorId = new URLSearchParams(location.search).get('vendorId') - const [isEditing, setIsEditing] = useState(false) - const [form] = Form.useForm() - const [fetchLoading, setFetchLoading] = useState(true) const [collapseState, updateCollapseState] = useCollapseState('VendorInfo', { info: true, notes: true, auditLogs: true }) - useEffect(() => { - if (vendorId) { - fetchVendorDetails() - } - }, [vendorId, fetchVendorDetails]) - - useEffect(() => { - if (vendorData) { - form.setFieldsValue({ - name: vendorData.name || '', - website: vendorData.website || '', - contact: vendorData.contact || '', - country: vendorData.country || '', - phone: vendorData.phone || '', - email: vendorData.email || '' - }) - } - }, [vendorData, form]) - - const fetchVendorDetails = useCallback(async () => { - try { - setFetchLoading(true) - const response = await axios.get( - `${config.backendUrl}/vendors/${vendorId}`, - { - headers: { - Accept: 'application/json' - }, - withCredentials: true - } - ) - setVendorData(response.data) - setError(null) - } catch (err) { - setError('Failed to fetch vendor details') - messageApi.error('Failed to fetch vendor details') - } finally { - setFetchLoading(false) - } - }, [messageApi, vendorId]) - - const startEditing = () => { - updateCollapseState('info', true) - setIsEditing(true) - } - - const cancelEditing = () => { - // Reset form values to original data - if (vendorData) { - form.setFieldsValue({ - name: vendorData.name || '', - website: vendorData.website || '', - contact: vendorData.contact || '', - country: vendorData.country || '', - phone: vendorData.phone || '', - email: vendorData.email || '' - }) - } - setIsEditing(false) - } - - const updateInfo = async () => { - try { - const values = await form.validateFields() - setLoading(true) - - await axios.put(`${config.backendUrl}/vendors/${vendorId}`, values, { - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true - }) - - setVendorData({ ...vendorData, ...values }) - setIsEditing(false) - messageApi.success('Vendor information updated successfully') - } catch (err) { - if (err.errorFields) { - return - } - messageApi.error('Failed to update vendor information') - } finally { - fetchVendorDetails() - setLoading(false) - } - } - - const actionItems = { - items: [ - { - label: 'Reload Vendor', - key: 'reload', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchVendorDetails() - } - } - } - - const getViewDropdownItems = () => { - const sections = [ - { key: 'info', label: 'Vendor Information' }, - { key: 'notes', label: 'Notes' }, - { key: 'auditLogs', label: 'Audit Logs' } - ] - - return ( - - - {sections.map((section) => ( - { - updateCollapseState(section.key, e.target.checked) - }} - > - {section.label} - - ))} - - - ) - } - - if (error) { - return ( - -

{error || 'Vendor not found'}

- -
- ) - } - return ( - <> - {contextHolder} - - - - - - - - - - - - {isEditing ? ( - <> - + + - - - ) : (
- - updateCollapseState('info', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + key='info' > - - - - Vendor Information - - - } - key='1' - > -
- } - spinning={fetchLoading} - > - - - {vendorData?._id ? ( - - ) : ( - n/a - )} - - - {vendorData?.createdAt ? ( - - ) : ( - n/a - )} - + } + isEditing={isEditing} + items={[ + { + name: 'id', + label: 'ID', + value: objectData?._id, + type: 'id', + objectType: 'vendor', + showCopy: true + }, + { + name: 'createdAt', + label: 'Created At', + value: objectData?.createdAt, + type: 'dateTime', + readOnly: true + }, + { + name: 'name', + label: 'Name', + value: objectData?.name, + required: true, + type: 'text' + }, + { + name: 'updatedAt', + label: 'Updated At', + value: objectData?.updatedAt, + type: 'dateTime', + readOnly: true + }, + { + name: 'website', + label: 'Website', + value: objectData?.website, + type: 'url' + }, + { + name: 'country', + label: 'Country', + value: objectData?.country, + type: 'country' + }, + { + name: 'contact', + label: 'Contact', + value: objectData?.contact, + type: 'text' + }, + { + name: 'phone', + label: 'Phone', + value: objectData?.phone, + type: 'text' + }, + { + name: 'email', + label: 'Email', + value: objectData?.email, + type: 'email' + } + ]} + /> + - - {isEditing ? ( - - - - ) : vendorData?.name ? ( - {vendorData.name} - ) : ( - n/a - )} - - - - {vendorData?.updatedAt ? ( - - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : vendorData?.website ? ( - - {new URL(vendorData.website).hostname + ' '} - - - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : vendorData?.country ? ( - - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : vendorData?.contact ? ( - {vendorData.contact} - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : vendorData?.phone ? ( - {vendorData.phone} - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : vendorData?.email ? ( - - {vendorData.email + ' '} - - - ) : ( - n/a - )} - - - -
- - - - - updateCollapseState('notes', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + key='notes' > - - - - Notes - - - } - key='notes' - > - - - - - + + + + - - updateCollapseState('auditLogs', keys.length > 0) + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + key='auditLogs' > - - - - Audit Logs - - - } - key='auditLogs' - > - - - + +
- )} -
- +
+ )} + ) } diff --git a/src/components/Dashboard/Production/GCodeFiles.jsx b/src/components/Dashboard/Production/GCodeFiles.jsx index c6287c9..090a542 100644 --- a/src/components/Dashboard/Production/GCodeFiles.jsx +++ b/src/components/Dashboard/Production/GCodeFiles.jsx @@ -21,7 +21,7 @@ import { DownloadOutlined } from '@ant-design/icons' import { AuthContext } from '../context/AuthContext' import NewGCodeFile from './GCodeFiles/NewGCodeFile' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import GCodeFileIcon from '../../Icons/GCodeFileIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import useColumnVisibility from '../hooks/useColumnVisibility' @@ -120,7 +120,9 @@ const GCodeFiles = () => { dataIndex: '_id', key: 'id', width: 180, - render: (text) => + render: (text) => ( + + ) }, { title: 'Filament', diff --git a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx index 9af6e66..508c171 100644 --- a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx +++ b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx @@ -1,596 +1,295 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' import { useLocation } from 'react-router-dom' -import axios from 'axios' -import { - Descriptions, - Spin, - Space, - Button, - message, - Badge, - Form, - Typography, - Flex, - Input, - Card, - Collapse, - Dropdown, - Popover, - Checkbox -} from 'antd' -import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' -import IdText from '../../common/IdText.jsx' -import { capitalizeFirstLetter } from '../../utils/Utils.js' -import FilamentSelect from '../../common/FilamentSelect' -import useCollapseState from '../../hooks/useCollapseState' -import FilamentIcon from '../../../Icons/FilamentIcon' -import TimeDisplay from '../../common/TimeDisplay.jsx' +import { Space, Button, Flex, Dropdown, Card, Typography } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import ReloadIcon from '../../../Icons/ReloadIcon' -import EditIcon from '../../../Icons/EditIcon.jsx' -import XMarkIcon from '../../../Icons/XMarkIcon.jsx' -import CheckIcon from '../../../Icons/CheckIcon.jsx' - -import config from '../../../../config.js' -import AuditLogTable from '../../common/AuditLogTable.jsx' -import DashboardNotes from '../../common/DashboardNotes.jsx' -import BinIcon from '../../../Icons/BinIcon.jsx' +import useCollapseState from '../../hooks/useCollapseState' +import AuditLogTable from '../../common/AuditLogTable' +import DashboardNotes from '../../common/DashboardNotes' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ViewButton from '../../common/ViewButton' +import EditObjectForm from '../../common/EditObjectForm' +import EditButtons from '../../common/EditButtons' +import LockIndicator from '../../Management/Filaments/LockIndicator' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' -import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx' -const { Title, Text } = Typography +const { Text } = Typography const GCodeFileInfo = () => { - const [gcodeFileData, setGCodeFileData] = useState(null) - const [editLoading, setLoading] = useState(false) - const [error, setError] = useState(null) const location = useLocation() - const [messageApi, contextHolder] = message.useMessage() const gcodeFileId = new URLSearchParams(location.search).get('gcodeFileId') - const [isEditing, setIsEditing] = useState(false) - const [form] = Form.useForm() - const [fetchLoading, setFetchLoading] = useState(true) const [collapseState, updateCollapseState] = useCollapseState( 'GCodeFileInfo', { info: true, - preview: true + preview: true, + notes: true, + auditLogs: true } ) - useEffect(() => { - if (gcodeFileId) { - fetchGCodeFileDetails() - } - }, [gcodeFileId]) - - useEffect(() => { - if (gcodeFileData) { - form.setFieldsValue({ - name: gcodeFileData.name || '', - filament: gcodeFileData.filament || { id: null, name: '' } - }) - } - }, [gcodeFileData, form]) - - const fetchGCodeFileDetails = async () => { - try { - setFetchLoading(true) - const response = await axios.get( - `${config.backendUrl}/gcodefiles/${gcodeFileId}`, - { - headers: { - Accept: 'application/json' - }, - withCredentials: true - } - ) - setGCodeFileData(response.data) - setError(null) - } catch (err) { - setError('Failed to fetch GCodeFile details') - messageApi.error('Failed to fetch GCodeFile details') - } finally { - setFetchLoading(false) - } - } - - const startEditing = () => { - setIsEditing(true) - updateCollapseState('info', true) - } - - const cancelEditing = () => { - form.setFieldsValue({ - name: gcodeFileData?.name || '', - filament: gcodeFileData?.filament || { id: null, name: '' } - }) - setIsEditing(false) - } - - const updateGCodeFileInfo = async () => { - try { - const values = await form.validateFields() - setLoading(true) - - await axios.put( - `${config.backendUrl}/gcodefiles/${gcodeFileId}`, - values, - { - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true - } - ) - - setGCodeFileData({ ...gcodeFileData, ...values }) - setIsEditing(false) - messageApi.success('GCode File information updated successfully') - } catch (err) { - if (err.errorFields) { - return - } - console.error('Failed to update gcode file information:', err) - messageApi.error('Failed to update gcode file information') - } finally { - fetchGCodeFileDetails() - setLoading(false) - } - } - - const actionItems = { - items: [ - { - label: 'Edit GCode File', - key: 'edit', - icon: - }, - { - label: 'Delete GCode File', - key: 'delete', - icon: , - danger: true - }, - { type: 'divider' }, - { - label: 'Reload GCode File', - key: 'reload', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchGCodeFileDetails() - } - } - } - - const getViewDropdownItems = () => { - const sections = [ - { key: 'info', label: 'GCode File Information' }, - { key: 'preview', label: 'GCode File Preview' }, - { key: 'notes', label: 'Notes' }, - { key: 'auditLogs', label: 'Audit Logs' } - ] - - return ( - - - {sections.map((section) => ( - { - updateCollapseState(section.key, e.target.checked) - }} - > - {section.label} - - ))} - - - ) - } - return ( - <> - {contextHolder} - - - - - - - - - - - - {isEditing ? ( - <> - + + - - - ) : (
- - updateCollapseState('info', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + key='info' > - - - - GCode File Information - - - } - key='info' - > -
- } - > - - - {gcodeFileData?._id ? ( - - ) : ( - n/a - )} - - - {gcodeFileData?.createdAt ? ( - - ) : ( - n/a - )} - + } + isEditing={isEditing} + items={[ + { + name: '_id', + label: 'ID', + type: 'id', + objectType: 'gcodefile', + value: objectData?._id, + showCopy: true + }, + { + name: 'createdAt', + label: 'Created At', + type: 'dateTime', + value: objectData?.createdAt, + readOnly: true + }, + { + name: 'name', + label: 'Name', + type: 'text', + value: objectData?.name, + required: true + }, + { + name: 'updatedAt', + label: 'Updated At', + type: 'dateTime', + value: objectData?.updatedAt, + readOnly: true + }, + { + name: 'filament', + label: 'Filament', + type: 'object', + value: objectData?.filament, + objectType: 'filament', + required: true + }, + { + name: 'cost', + label: 'Cost', + type: 'currency', + value: objectData?.cost, + readOnly: true + }, + { + name: [ + 'gcodeFileInfo', + 'estimatedPrintingTimeNormalMode' + ], + label: 'Est Print Time', + value: + objectData?.gcodeFileInfo + ?.estimatedPrintingTimeNormalMode, + type: 'text', + readOnly: true + }, + { + name: ['gcodeFileInfo', 'sparseInfillDensity'], + label: 'Infill Density', + value: objectData?.gcodeFileInfo?.sparseInfillDensity, + type: 'number', + readOnly: true + }, + { + name: ['gcodeFileInfo', 'sparseInfillPattern'], + label: 'Infill Pattern', + value: objectData?.gcodeFileInfo?.sparseInfillPattern, + type: 'text', + readOnly: true + }, + { + name: ['gcodeFileInfo', 'filamentUsedMm'], + label: 'Filament Used (mm)', + value: objectData?.gcodeFileInfo?.filamentUsedMm, + type: 'mm', + readOnly: true + }, + { + name: ['gcodeFileInfo', 'filamentUsedG'], + label: 'Filament Used (g)', + value: objectData?.gcodeFileInfo?.filamentUsedG, + type: 'weight', + readOnly: true + }, + { + name: ['gcodeFileInfo', 'nozzleTemperature'], + label: 'Hotend Temperature', + value: objectData?.gcodeFileInfo?.nozzleTemperature, + type: 'number', + readOnly: true + }, + { + name: ['gcodeFileInfo', 'hotPlateTemp'], + label: 'Bed Temperature', + value: objectData?.gcodeFileInfo?.hotPlateTemp, + type: 'number', + readOnly: true + }, + { + name: ['gcodeFileInfo', 'filamentSettingsId'], + label: 'Filament Profile', + value: objectData?.gcodeFileInfo?.filamentSettingsId, + type: 'text', + readOnly: true + }, + { + name: ['gcodeFileInfo', 'printSettingsId'], + label: 'Print Profile', + value: objectData?.gcodeFileInfo?.printSettingsId, + type: 'text', + readOnly: true + } + ]} + objectData={objectData} + type='gcodefile' + /> + - - {isEditing ? ( - - - - ) : gcodeFileData?.name ? ( - {gcodeFileData.name} - ) : ( - n/a - )} - - - - {gcodeFileData?.updatedAt ? ( - - ) : ( - n/a - )} - - - {isEditing ? ( - - - - ) : gcodeFileData?.filament ? ( - - - - - ) : ( - n/a - )} - - - {gcodeFileData?.filament ? ( - - ) : ( - n/a - )} - - - {gcodeFileData?.gcodeFileInfo - ?.estimatedPrintingTimeNormalMode ? ( - - { - gcodeFileData.gcodeFileInfo - .estimatedPrintingTimeNormalMode - } - - ) : ( - n/a - )} - - - {gcodeFileData?.cost ? ( - {'£' + gcodeFileData.cost.toFixed(2)} - ) : ( - n/a - )} - - - {gcodeFileData?.gcodeFileInfo?.sparseInfillDensity ? ( - - {gcodeFileData.gcodeFileInfo.sparseInfillDensity} - - ) : ( - n/a - )} - - - {gcodeFileData?.gcodeFileInfo?.sparseInfillPattern ? ( - - {capitalizeFirstLetter( - gcodeFileData.gcodeFileInfo.sparseInfillPattern - )} - - ) : ( - n/a - )} - - - {gcodeFileData?.gcodeFileInfo?.filamentUsedMm ? ( - - {gcodeFileData.gcodeFileInfo.filamentUsedMm}mm - - ) : ( - n/a - )} - - - {gcodeFileData?.gcodeFileInfo?.filamentUsedG ? ( - - {gcodeFileData.gcodeFileInfo.filamentUsedG}g - - ) : ( - n/a - )} - - - {gcodeFileData?.gcodeFileInfo?.nozzleTemperature ? ( - - {gcodeFileData.gcodeFileInfo.nozzleTemperature}° - - ) : ( - n/a - )} - - - {gcodeFileData?.gcodeFileInfo?.hotPlateTemp ? ( - - {gcodeFileData.gcodeFileInfo.hotPlateTemp}° - - ) : ( - n/a - )} - - - {gcodeFileData?.gcodeFileInfo?.filamentSettingsId ? ( - - {gcodeFileData.gcodeFileInfo.filamentSettingsId.replaceAll( - '"', - '' - )} - - ) : ( - n/a - )} - - - {gcodeFileData?.gcodeFileInfo?.printSettingsId ? ( - - {gcodeFileData.gcodeFileInfo.printSettingsId.replaceAll( - '"', - '' - )} - - ) : ( - n/a - )} - - - -
- - - - - updateCollapseState('preview', keys.length > 0) + } + active={collapseState.preview} + onToggle={(expanded) => + updateCollapseState('preview', expanded) } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + key='preview' > - - - - GCode File Preview - - - } - key='preview' - > - }> - - {gcodeFileData?.gcodeFileInfo?.thumbnail ? ( - GCodeFile - ) : ( - n/a - )} - - - - + + {objectData?.gcodeFileInfo?.thumbnail ? ( + GCodeFile + ) : ( + n/a + )} + + - - updateCollapseState('notes', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + key='notes' > - - - - Notes - - - } - key='notes' - > - - - - - + + + + - - updateCollapseState('auditLogs', keys.length > 0) + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + key='auditLogs' > - - - - Audit Log - - - } - key='auditLogs' - > - - - + +
- )} -
- +
+ )} + ) } diff --git a/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx b/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx index 5474888..9c0ab19 100644 --- a/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx +++ b/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx @@ -218,7 +218,6 @@ const NewGCodeFile = ({ onOk, reset }) => { newGCodeFileFormValues?.gcodeFileInfo?.filamentUsedG if (filamentCost && gcodeFilamentUsed) { const cost = (filamentCost / 1000) * gcodeFilamentUsed - console.log('Setting cost') setNewGCodeFileFormValues((prev) => ({ ...prev, cost: cost.toFixed(2) })) newGCodeFileForm.setFieldValue('cost', cost.toFixed(2)) } @@ -304,8 +303,6 @@ const NewGCodeFile = ({ onOk, reset }) => { gcodeFileInfo: parsedConfig }) - console.log(parsedConfig) - // Update filter settings if filament info is available if (parsedConfig.filament_type && parsedConfig.filament_diameter) { setFilamentSelectFilter({ @@ -525,7 +522,6 @@ const NewGCodeFile = ({ onOk, reset }) => { onClick={() => { setCurrentStep(currentStep + 1) setNextEnabled(false) - console.log(newGCodeFileFormValues) }} > Next diff --git a/src/components/Dashboard/Production/Jobs.jsx b/src/components/Dashboard/Production/Jobs.jsx index e8ca155..5254c3e 100644 --- a/src/components/Dashboard/Production/Jobs.jsx +++ b/src/components/Dashboard/Production/Jobs.jsx @@ -22,7 +22,7 @@ import NewJob from './Jobs/NewJob.jsx' import JobState from '../common/JobState.jsx' import SubJobCounter from '../common/SubJobCounter.jsx' import TimeDisplay from '../common/TimeDisplay.jsx' -import IdText from '../common/IdText.jsx' +import IdDisplay from '../common/IdDisplay.jsx' import useColumnVisibility from '../hooks/useColumnVisibility.js' import JobIcon from '../../Icons/JobIcon.jsx' import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx' @@ -126,7 +126,7 @@ const Jobs = () => { dataIndex: 'id', key: 'id', width: 180, - render: (text) => , + render: (text) => , filterDropdown: ({ setSelectedKeys, selectedKeys, diff --git a/src/components/Dashboard/Production/Jobs/JobInfo.jsx b/src/components/Dashboard/Production/Jobs/JobInfo.jsx index 0aed559..1234921 100644 --- a/src/components/Dashboard/Production/Jobs/JobInfo.jsx +++ b/src/components/Dashboard/Production/Jobs/JobInfo.jsx @@ -1,48 +1,26 @@ -import React, { useState, useEffect, useContext } from 'react' +import React from 'react' import { useLocation } from 'react-router-dom' -import axios from 'axios' -import { - Descriptions, - Spin, - Space, - Button, - message, - Progress, - Typography, - Collapse, - Flex, - Dropdown, - Popover, - Checkbox, - Card -} from 'antd' -import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' -import TimeDisplay from '../../common/TimeDisplay' -import JobState from '../../common/JobState' -import IdText from '../../common/IdText' -import SubJobsTree from '../../common/SubJobsTree' -import { PrintServerContext } from '../../context/PrintServerContext' -import GCodeFileIcon from '../../../Icons/GCodeFileIcon' -import ReloadIcon from '../../../Icons/ReloadIcon' +import { Space, Button, Flex, Dropdown, Card } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' -import config from '../../../../config' import AuditLogTable from '../../common/AuditLogTable' import DashboardNotes from '../../common/DashboardNotes' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ViewButton from '../../common/ViewButton' +import EditObjectForm from '../../common/EditObjectForm' +import EditButtons from '../../common/EditButtons' +import LockIndicator from '../../Management/Filaments/LockIndicator' +import SubJobsTree from '../../common/SubJobsTree' import InfoCircleIcon from '../../../Icons/InfoCircleIcon' import JobIcon from '../../../Icons/JobIcon' import AuditLogIcon from '../../../Icons/AuditLogIcon' import NoteIcon from '../../../Icons/NoteIcon' - -const { Title, Text } = Typography +import GCodeFileIcon from '../../../Icons/GCodeFileIcon' const JobInfo = () => { - const [jobData, setJobData] = useState(null) - const [fetchLoading, setFetchLoading] = useState(true) - const [error, setError] = useState(null) const location = useLocation() - const [messageApi] = message.useMessage() const jobId = new URLSearchParams(location.search).get('jobId') - const { printServer } = useContext(PrintServerContext) const [collapseState, updateCollapseState] = useCollapseState('JobInfo', { info: true, subJobs: true, @@ -50,352 +28,205 @@ const JobInfo = () => { auditLogs: true }) - useEffect(() => { - if (jobId) { - fetchJobDetails() - } - }, [jobId]) - - useEffect(() => { - if (printServer && jobId) { - printServer.on('notify_job_update', (updateData) => { - if (updateData._id === jobId) { - setJobData((prevData) => { - if (!prevData) return prevData - return { - ...prevData, - state: updateData.state, - ...updateData - } - }) - } - }) - } - - return () => { - if (printServer) { - printServer.off('notify_job_update') - } - } - }, [printServer, jobId]) - - const fetchJobDetails = async () => { - try { - setFetchLoading(true) - const response = await axios.get(`${config.backendUrl}/jobs/${jobId}`, { - headers: { - Accept: 'application/json' - }, - withCredentials: true // Important for including cookies - }) - setJobData(response.data) - setError(null) - } catch (err) { - setError('Failed to fetch print job details') - messageApi.error('Failed to fetch print job details') - } finally { - setFetchLoading(false) - } - } - - const getViewDropdownItems = () => { - const sections = [ - { key: 'info', label: 'Job Information' }, - { key: 'subJobs', label: 'Sub Jobs' }, - { key: 'notes', label: 'Notes' }, - { key: 'auditLogs', label: 'Audit Logs' } - ] - - return ( - - - {sections.map((section) => ( - { - updateCollapseState(section.key, e.target.checked) - }} - > - {section.label} - - ))} - - - ) - } - - const actionItems = { - items: [ - { - label: 'Reload Job', - key: 'reload', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'edit') { - // TODO: Implement edit functionality - messageApi.info('Edit functionality coming soon') - } else if (key === 'reload') { - fetchJobDetails() - } - } - } - return ( - <> - - - - - - - - - - - + + {({ + loading, + isEditing, + startEditing, + cancelEditing, + handleUpdate, + formValid, + objectData, + editLoading, + lock, + fetchObject + }) => ( + + + + + + } + ], + onClick: ({ key }) => { + if (key === 'reload') { + fetchObject() + } + } + }} + > + + + + + + + + + + - {error ? ( - -

{error || 'Print job not found'}

- -
- ) : (
- - - updateCollapseState('info', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + key='info' > - - - - Job Information - - - } - key='info' - > - }> - - - {jobData?._id ? ( - - ) : ( - n/a - )} - - - {jobData?.state ? ( - - ) : ( - n/a - )} - - - {jobData?.gcodeFile ? ( - - - - {jobData.gcodeFile.name || 'Not specified'} - - - ) : ( - n/a - )} - - - {jobData?.gcodeFile?._id ? ( - - ) : ( - n/a - )} - - - {jobData?.quantity ? ( - {jobData.quantity} - ) : ( - n/a - )} - - - {jobData?.createdAt ? ( - - ) : ( - n/a - )} - - - {jobData?.startedAt ? ( - - ) : ( - n/a - )} - - {jobData?.state?.type === 'printing' && ( - - - - )} - - {jobData?.printers ? ( - - {jobData.printers.length} printers assigned - - ) : ( - n/a - )} - - - - - + } + isEditing={isEditing} + type='job' + items={[ + { + name: '_id', + label: 'ID', + value: objectData?._id, + type: 'id', + objectType: 'job', + showCopy: true + }, + { + name: 'state', + label: 'Status', + value: objectData, + type: 'state', + objectType: 'job', + showStatus: true, + showProgress: true, + showId: false, + showQuantity: false, + readOnly: true + }, + { + name: 'gcodeFile', + label: 'GCode File', + value: objectData?.gcodeFile, + type: 'object', + objectType: 'gcodeFile', + readOnly: true + }, + { + name: 'gcodeFileId', + label: 'GCode File ID', + value: objectData?.gcodeFile?._id, + type: 'id', + objectType: 'gcodefile', + showHyperlink: true + }, + { + name: 'quantity', + label: 'Quantity', + value: objectData?.quantity, + type: 'number', + readOnly: true + }, + { + name: 'createdAt', + label: 'Created At', + value: objectData?.createdAt, + type: 'dateTime', + readOnly: true + }, + { + name: 'startedAt', + label: 'Started At', + value: objectData?.startedAt, + type: 'dateTime', + readOnly: true + }, + { + name: 'assignedPrinters', + label: 'Assigned Printers', + value: objectData?.printers?.length, + type: 'number', + readOnly: true + } + ]} + /> + - - updateCollapseState('subJobs', keys.length > 0) + } + active={collapseState.subJobs} + onToggle={(expanded) => + updateCollapseState('subJobs', expanded) } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + key='subJobs' > - - - - Sub Job Information - - - } - key='2' - > - - - + + - - updateCollapseState('notes', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + key='notes' > - - - - Notes - - - } - key='notes' - > - - - - - + + + + - - updateCollapseState('auditLogs', keys.length > 0) + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' + key='auditLogs' > - - - - Audit Logs - - - } - key='auditLogs' - > - - - + +
- )} -
- +
+ )} + ) } diff --git a/src/components/Dashboard/Production/Printers.jsx b/src/components/Dashboard/Production/Printers.jsx index fa011ee..674197a 100644 --- a/src/components/Dashboard/Production/Printers.jsx +++ b/src/components/Dashboard/Production/Printers.jsx @@ -18,7 +18,7 @@ import { import { AuthContext } from '../context/AuthContext' import PrinterState from '../common/PrinterState' import NewPrinter from './Printers/NewPrinter' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import PrinterIcon from '../../Icons/PrinterIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import ControlIcon from '../../Icons/ControlIcon' @@ -79,7 +79,7 @@ const Printers = () => { dataIndex: '_id', key: 'id', width: 180, - render: (text) => + render: (text) => }, { title: 'State', @@ -89,7 +89,7 @@ const Printers = () => { return ( ) diff --git a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx index 8f8c689..ac82778 100644 --- a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx +++ b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx @@ -31,7 +31,7 @@ import PrinterMiscPanel from '../../common/PrinterMiscPanel' import PrinterState from '../../common/PrinterState' import { AuthContext } from '../../context/AuthContext' import PrinterSubJobsTree from '../../common/PrinterJobsTree' -import IdText from '../../common/IdText' +import IdDisplay from '../../common/IdDisplay' import FilamentIcon from '../../../Icons/FilamentIcon' import FilamentStockIcon from '../../../Icons/FilamentStockIcon' @@ -179,7 +179,6 @@ const ControlPrinter = () => { } return () => { if (printServer && initialized) { - console.log('Deregistering') printServer.off('notify_printer_update') printServer.off('notify_filamentstock_update') } @@ -187,7 +186,6 @@ const ControlPrinter = () => { }, [printServer, initialized, printerId]) function handleEmergencyStop() { - console.log('Emergency stop button clicked') printServer.emit('printer.emergency_stop', { printerId }) } @@ -438,7 +436,7 @@ const ControlPrinter = () => { ) : ( @@ -548,7 +546,7 @@ const ControlPrinter = () => { {printerData?._id ? ( - { {printerData?.currentJob?.gcodeFile ? ( - { {printerData?.currentJob?.id ? ( - { {printerData?.currentSubJob?.id ? ( - { {printerData?.currentFilamentStock?._id ? ( - { {printerData?.currentFilamentStock?.filament ? ( - { - const [printerData, setPrinterData] = useState(null) - const [fetchLoading, setFetchLoading] = useState(true) - const [editLoading, setEditLoading] = useState(false) - const [error, setError] = useState(null) const location = useLocation() const printerId = new URLSearchParams(location.search).get('printerId') - const [messageApi, contextHolder] = message.useMessage() - const [isEditing, setIsEditing] = useState(false) - const [form] = Form.useForm() const [collapseState, updateCollapseState] = useCollapseState('PrinterInfo', { info: true, jobs: true, + notes: true, auditLogs: true }) - useEffect(() => { - if (printerId) { - fetchPrinterDetails() - } - }, [printerId]) - - useEffect(() => { - if (printerData) { - form.setFieldsValue({ - name: printerData.name || '', - vendor: printerData.vendor || { id: null, name: '' }, - moonraker: { - host: printerData.moonraker?.host || '', - port: printerData.moonraker?.port || null, - protocol: printerData.moonraker?.protocol || 'ws', - apiKey: printerData.moonraker?.apiKey || '' - }, - tags: printerData.tags || [] - }) - } - }, [printerData, form]) - - const fetchPrinterDetails = async () => { - try { - setFetchLoading(true) - const response = await axios.get( - `${config.backendUrl}/printers/${printerId}`, - { - headers: { - Accept: 'application/json' - }, - withCredentials: true - } - ) - setPrinterData(response.data) - setError(null) - } catch (err) { - setError('Failed to fetch printer details') - messageApi.error('Failed to fetch printer details') - } finally { - setFetchLoading(false) - } - } - - const startEditing = () => { - updateCollapseState('info', true) - setIsEditing(true) - } - - const cancelEditing = () => { - // Reset form values to original data - if (printerData) { - form.setFieldsValue({ - name: printerData.name || '', - vendor: printerData.vendor || { id: null, name: '' }, - moonraker: { - host: printerData.moonraker?.host || '', - port: printerData.moonraker?.port || null, - protocol: printerData.moonraker?.protocol || 'ws', - apiKey: printerData.moonraker?.apiKey || '' - }, - tags: printerData.tags || [] - }) - } - setIsEditing(false) - } - - const updatePrinterInfo = async () => { - try { - const values = await form.validateFields() - setEditLoading(true) - - await axios.put( - `${config.backendUrl}/printers/${printerId}`, - { - name: values.name, - vendor: values.vendor, - moonraker: { - host: values.moonraker.host, - port: values.moonraker.port, - protocol: values.moonraker.protocol, - apiKey: values.moonraker.apiKey - }, - tags: values.tags - }, - { - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true - } - ) - - // Update the local state with the new values - setPrinterData({ ...printerData, ...values }) - setIsEditing(false) - messageApi.success('Printer information updated successfully') - } catch (err) { - if (err.errorFields) { - // This is a form validation error - return - } - console.error('Failed to update printer information:', err) - messageApi.error('Failed to update printer information') - } finally { - setEditLoading(false) - } - } - - const handleTagClose = (removedTag) => { - const newTags = printerData.tags.filter((tag) => tag !== removedTag) - setPrinterData((prev) => ({ ...prev, tags: newTags })) - } - - const handleTagAdd = () => { - const input = form.getFieldValue('newTag') - if (input) { - const newTag = input.trim() - if (newTag && !printerData.tags.includes(newTag)) { - setPrinterData((prev) => ({ ...prev, tags: [...prev.tags, newTag] })) - form.setFieldValue('newTag', '') - } - } - } - - const actionItems = { - items: [ - { - label: 'Reload Printer', - key: 'reload', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchPrinterDetails() - } - } - } - - const getViewDropdownItems = () => { - const sections = [ - { key: 'info', label: 'Printer Information' }, - { key: 'jobs', label: 'Printer Jobs' }, - { key: 'notes', label: 'Notes' }, - { key: 'auditLogs', label: 'Audit Logs' } - ] - - return ( - - - {sections.map((section) => ( - { - updateCollapseState(section.key, e.target.checked) - }} - > - {section.label} - - ))} - - - ) - } - return ( - <> - {contextHolder} - - - - - - - - - - - - {isEditing ? ( - <> - + + - - - ) : (
- - updateCollapseState('info', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + key='info' > - - - - Printer Information - - - } - key='info' - > -
- } - > - - {/* Read-only fields */} - - {printerData?._id ? ( - - ) : ( - n/a - )} - - - {printerData?.connectedAt ? ( - - ) : ( - n/a - )} - + } + isEditing={isEditing} + type='printer' + items={[ + { + name: '_id', + label: 'ID', + value: objectData?._id, + type: 'id', + objectType: 'printer', + showCopy: true + }, + { + name: 'connectedAt', + label: 'Connected At', + value: objectData?.connectedAt, + type: 'dateTime', + readOnly: true + }, + { + name: 'name', + label: 'Name', + value: objectData?.name, + required: true, + type: 'text' + }, + { + name: 'state', + label: 'Status', + value: objectData, + type: 'state', + objectType: 'printer', + showName: false, + readOnly: true + }, + { + name: 'vendor', + label: 'Vendor', + value: objectData?.vendor, + type: 'object', + objectType: 'vendor', + required: true + }, + { + name: ['moonraker', 'host'], + label: 'Host', + value: objectData?.moonraker?.host, + type: 'text', + required: true + }, + { + name: 'vendorId', + label: 'Vendor ID', + value: objectData?.vendor?.id, + type: 'id', + objectType: 'vendor', + showHyperlink: true, + readOnly: true + }, - {/* Editable fields */} - - {isEditing ? ( - - - - ) : printerData?.name ? ( - {printerData.name} - ) : ( - n/a - )} - + { + name: ['moonraker', 'port'], + label: 'Port', + value: objectData?.moonraker?.port, + type: 'number', + required: true + }, + { + name: ['moonraker', 'apiKey'], + label: 'API Key', + value: objectData?.moonraker?.apiKey, + type: 'secret', + reveal: true, + required: false + }, + { + name: ['moonraker', 'protocol'], + label: 'Protocol', + value: objectData?.moonraker?.protocol, + type: 'wsprotocol', + required: true + }, - - {isEditing ? ( - - - - ) : printerData?.moonraker?.host ? ( - {printerData.moonraker.host} - ) : ( - n/a - )} - + { + name: 'tags', + label: 'Tags', + value: objectData?.tags, + type: 'tags', + required: false + }, + { + name: 'firmware', + label: 'Firmware Version', + value: objectData?.firmware, + type: 'text', + required: false, + readOnly: true + } + ]} + /> + - - {isEditing ? ( - - - - ) : printerData?.vendor?.name ? ( - - - {printerData?.vendor?.name || 'n/a'} - - ) : ( - n/a - )} - - - - {printerData?.vendor ? ( - - ) : ( - n/a - )} - - - - {isEditing ? ( - - - - ) : printerData?.moonraker?.port ? ( - {printerData.moonraker.port} - ) : ( - n/a - )} - - - - {isEditing ? ( - - - -
- )} -
- +
+ )} + ) } diff --git a/src/components/Dashboard/Production/ProductionOverview.jsx b/src/components/Dashboard/Production/ProductionOverview.jsx index 4c32241..807d243 100644 --- a/src/components/Dashboard/Production/ProductionOverview.jsx +++ b/src/components/Dashboard/Production/ProductionOverview.jsx @@ -61,7 +61,6 @@ const ProductionOverview = () => { await fetchPrinterStats() await fetchJobstats() await fetchChartData() - console.log(stats) }, []) const fetchPrinterStats = async () => { @@ -74,7 +73,6 @@ const ProductionOverview = () => { withCredentials: true }) const printStats = response.data - console.log(printStats) setStats((prev) => ({ ...prev, printers: printStats })) setError(null) } catch (err) { diff --git a/src/components/Dashboard/common/AuditLogTable.jsx b/src/components/Dashboard/common/AuditLogTable.jsx index a486286..d8f670e 100644 --- a/src/components/Dashboard/common/AuditLogTable.jsx +++ b/src/components/Dashboard/common/AuditLogTable.jsx @@ -1,7 +1,7 @@ import React, { forwardRef, useState } from 'react' import { Typography, Space, Descriptions, Badge, Table } from 'antd' import PropTypes from 'prop-types' -import IdText from './IdText' +import IdDisplay from './IdDisplay' import { AuditOutlined, LoadingOutlined } from '@ant-design/icons' import TimeDisplay from '../common/TimeDisplay' import BoolDisplay from './BoolDisplay' @@ -51,7 +51,7 @@ const formatValue = (value, propertyName) => { if (isObjectId(value)) { return ( - , + render: (text) => ( + + ), sorter: (a, b) => a._id.localeCompare(b._id) } ] @@ -110,7 +112,7 @@ const AuditLogTable = forwardRef( key: 'owner', width: 180, render: (record) => ( - ( - { + // Determine initial enabled state based on value + const [colorEnabled, setColorEnabled] = useState(!!value) + + useEffect(() => { + setColorEnabled(!!value) + }, [value]) + + const handleCheckboxChange = (e) => { + const checked = e.target.checked + setColorEnabled(checked) + if (!checked) { + onChange(null) + } else if (checked && !value) { + onChange('#000000') + } + } + + const handleColorChange = (color) => { + onChange('#' + color.toHex()) + } + + return ( + + + {!required && ( + + )} + + ) +} + +ColorSelector.propTypes = { + value: PropTypes.string, + onChange: PropTypes.func.isRequired, + disabled: PropTypes.bool, + required: PropTypes.bool +} + +export default ColorSelector diff --git a/src/components/Dashboard/common/CopyButton.jsx b/src/components/Dashboard/common/CopyButton.jsx new file mode 100644 index 0000000..d8e745f --- /dev/null +++ b/src/components/Dashboard/common/CopyButton.jsx @@ -0,0 +1,73 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Button, Tooltip, message } from 'antd' +import CopyIcon from '../../Icons/CopyIcon' + +const CopyButton = ({ + text, + style = {}, + iconStyle = {}, + tooltip = 'Copy', + size = 'small' +}) => { + const [messageApi, contextHolder] = message.useMessage() + + const doCopy = (copyText) => { + if (navigator && navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard + .writeText(copyText) + .then(() => { + messageApi.success('Copied to clipboard') + }) + .catch(() => { + messageApi.error('Failed to copy') + }) + } else if ( + document.queryCommandSupported && + document.queryCommandSupported('copy') + ) { + // Legacy fallback + const textarea = document.createElement('textarea') + textarea.value = copyText + textarea.setAttribute('readonly', '') + textarea.style.position = 'absolute' + textarea.style.left = '-9999px' + document.body.appendChild(textarea) + textarea.select() + try { + document.execCommand('copy') + messageApi.success('Copied to clipboard') + } catch (err) { + messageApi.error('Failed to copy') + } + document.body.removeChild(textarea) + } else { + messageApi.error('Copy not supported in this browser') + } + } + + return ( + <> + {contextHolder} + + - + danger + /> + ) : ( { dataIndex: '_id', key: 'id', width: 180, - render: (text) => + render: (text) => ( + + ) } ] diff --git a/src/components/Dashboard/common/PrinterMovementPanel.jsx b/src/components/Dashboard/common/PrinterMovementPanel.jsx index 0e35f5f..1b41daa 100644 --- a/src/components/Dashboard/common/PrinterMovementPanel.jsx +++ b/src/components/Dashboard/common/PrinterMovementPanel.jsx @@ -41,7 +41,7 @@ const PrinterMovementPanel = ({ printerId }) => { const handleHomeAxisClick = (axis) => { if (printServer) { - console.log('Homeing Axis:', axis) + logger.debug('Homeing Axis:', axis) printServer.emit('printer.gcode.script', { printerId, script: `G28 ${axis}` @@ -52,7 +52,7 @@ const PrinterMovementPanel = ({ printerId }) => { const handleMoveAxisClick = (axis, minus) => { const distanceValue = !minus ? posValue * -1 : posValue if (printServer) { - console.log('Moving Axis:', axis, distanceValue) + logger.debug('Moving Axis:', axis, distanceValue) printServer.emit('printer.gcode.script', { printerId, script: `_CLIENT_LINEAR_MOVE ${axis}=${distanceValue} F=${rateValue}` diff --git a/src/components/Dashboard/common/PrinterState.jsx b/src/components/Dashboard/common/PrinterState.jsx index 6bbc0b0..2e4ae50 100644 --- a/src/components/Dashboard/common/PrinterState.jsx +++ b/src/components/Dashboard/common/PrinterState.jsx @@ -12,7 +12,7 @@ const PrinterState = ({ printer, showProgress = true, showStatus = true, - showPrinterName = true, + showName = true, showControls = true }) => { const { printServer } = useContext(PrintServerContext) @@ -43,7 +43,7 @@ const PrinterState = ({ return ( - {showPrinterName && {printer.name}} + {showName && {printer.name}} {showStatus && ( @@ -122,7 +122,7 @@ PrinterState.propTypes = { }), showProgress: PropTypes.bool, showStatus: PropTypes.bool, - showPrinterName: PropTypes.bool, + showName: PropTypes.bool, showControls: PropTypes.bool } diff --git a/src/components/Dashboard/common/PrinterTemperaturePanel.jsx b/src/components/Dashboard/common/PrinterTemperaturePanel.jsx index 0924a1c..a60191d 100644 --- a/src/components/Dashboard/common/PrinterTemperaturePanel.jsx +++ b/src/components/Dashboard/common/PrinterTemperaturePanel.jsx @@ -97,14 +97,12 @@ const PrinterTemperaturePanel = ({ } } if (printServer?.connected == true) { - console.log('Printer Temperature Panel is subscribing...') printServer.emit('printer.objects.subscribe', params) printServer.emit('printer.objects.query', params) printServer.on('notify_status_update', notifyTemperatureStatusUpdate) } return () => { if (printServer && shouldUnsubscribe == true) { - console.log('Printer Temperature Panel is unsubscribing...') printServer.off('notify_status_update', notifyTemperatureStatusUpdate) printServer.emit('printer.objects.unsubscribe', params) } @@ -113,7 +111,6 @@ const PrinterTemperaturePanel = ({ const handleSetTemperatureClick = (target, value) => { if (printServer) { - console.log('printer.gcode.script', target, value) printServer.emit('printer.gcode.script', { printerId, script: `SET_HEATER_TEMPERATURE HEATER=${target} TARGET=${value}` diff --git a/src/components/Dashboard/common/SecretDisplay.jsx b/src/components/Dashboard/common/SecretDisplay.jsx new file mode 100644 index 0000000..ac61b99 --- /dev/null +++ b/src/components/Dashboard/common/SecretDisplay.jsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import { Typography, Tooltip, Button } from 'antd' +import CopyButton from './CopyButton' +import EyeIcon from '../../Icons/EyeIcon' +import EyeSlashIcon from '../../Icons/EyeSlashIcon' + +const { Text } = Typography + +const SecretDisplay = ({ value, reveal = false }) => { + const [visible, setVisible] = useState(false) + + if (!value) { + return n/a + } + + const masked = '•'.repeat(Math.max(8, value.length)) + + return ( + + {reveal && visible ? value : masked} + {reveal && ( + + + + ) +} + +ViewButton.propTypes = { + loading: PropTypes.bool, + sections: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + }) + ), + collapseState: PropTypes.object, + updateCollapseState: PropTypes.func +} + +export default ViewButton diff --git a/src/components/Dashboard/context/ApiServerContext.js b/src/components/Dashboard/context/ApiServerContext.js index 519373b..cbcc39d 100644 --- a/src/components/Dashboard/context/ApiServerContext.js +++ b/src/components/Dashboard/context/ApiServerContext.js @@ -10,13 +10,14 @@ import io from 'socket.io-client' import { message, notification, Modal, Space, Button } from 'antd' import PropTypes from 'prop-types' import { AuthContext } from './AuthContext' -import config from '../../../config' -import loglevel from 'loglevel' + import axios from 'axios' import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon' import ReloadIcon from '../../Icons/ReloadIcon' -const log = loglevel.getLogger('Api Server') -log.setLevel(config.logLevel) +import config from '../../../config' +import loglevel from 'loglevel' +const logger = loglevel.getLogger('ApiServerContext') +logger.setLevel(config.logLevel) const ApiServerContext = createContext() @@ -34,7 +35,7 @@ const ApiServerProvider = ({ children }) => { useEffect(() => { if (token) { - log.debug('Token is available, connecting to api server...') + logger.debug('Token is available, connecting to api server...') const newSocket = io(config.apiServerUrl, { reconnectionAttempts: 3, @@ -45,18 +46,18 @@ const ApiServerProvider = ({ children }) => { setConnecting(true) newSocket.on('connect', () => { - log.debug('Api Server connected') + logger.debug('Api Server connected') setConnecting(false) setError(null) }) newSocket.on('disconnect', () => { - log.debug('Api Server disconnected') + logger.debug('Api Server disconnected') setError('Api Server disconnected') }) newSocket.on('connect_error', (err) => { - log.error('Api Server connection error:', err) + logger.error('Api Server connection error:', err) messageApi.error('Api Server connection error: ' + err.message) setError('Api Server connection error') }) @@ -69,7 +70,7 @@ const ApiServerProvider = ({ children }) => { }) newSocket.on('error', (err) => { - log.error('Api Server error:', err) + logger.error('Api Server error:', err) setError('Api Server error') }) @@ -78,37 +79,37 @@ const ApiServerProvider = ({ children }) => { // Clean up function return () => { if (socketRef.current) { - log.debug('Cleaning up api server connection...') + logger.debug('Cleaning up api server connection...') socketRef.current.disconnect() socketRef.current = null } } } else if (!token && socketRef.current) { - log.debug('Token not available, disconnecting api server...') + logger.debug('Token not available, disconnecting api server...') socketRef.current.disconnect() socketRef.current = null } }, [token, messageApi]) const lockObject = (id, type) => { - log.debug('Locking ' + id) + logger.debug('Locking ' + id) if (socketRef.current && socketRef.current.connected) { socketRef.current.emit('lock', { _id: id, type: type }) - log.debug('Sent lock command for object:', id) + logger.debug('Sent lock command for object:', id) } } const unlockObject = (id, type) => { - log.debug('Unlocking ' + id) + logger.debug('Unlocking ' + id) if (socketRef.current && socketRef.current.connected == true) { socketRef.current.emit('unlock', { _id: id, type: type }) - log.debug('Sent unlock command for object:', id) + logger.debug('Sent unlock command for object:', id) } } const fetchObjectLock = async (id, type) => { if (socketRef.current && socketRef.current.connected == true) { - log.debug('Fetching lock status for ' + id) + logger.debug('Fetching lock status for ' + id) return new Promise((resolve) => { socketRef.current.emit( 'getLock', @@ -117,11 +118,11 @@ const ApiServerProvider = ({ children }) => { type: type }, (lockEvent) => { - log.debug('Received lock event for object:', id, lockEvent) + logger.debug('Received lock event for object:', id, lockEvent) resolve(lockEvent) } ) - log.debug('Sent fetch lock command for object:', id) + logger.debug('Sent fetch lock command for object:', id) }) } } @@ -130,7 +131,7 @@ const ApiServerProvider = ({ children }) => { if (socketRef.current && socketRef.current.connected == true) { const eventHandler = (data) => { if (data._id === id && data?.user !== userProfile._id) { - log.debug( + logger.debug( 'Lock update received for object:', id, 'locked:', @@ -141,7 +142,7 @@ const ApiServerProvider = ({ children }) => { } socketRef.current.on('notify_lock_update', eventHandler) - log.debug('Registered lock event listener for object:', id) + logger.debug('Registered lock event listener for object:', id) // Return cleanup function return () => offLockEvent(id, eventHandler) @@ -151,7 +152,7 @@ const ApiServerProvider = ({ children }) => { const offLockEvent = (id, eventHandler) => { if (socketRef.current && socketRef.current.connected == true) { socketRef.current.off('notify_lock_update', eventHandler) - log.debug('Removed lock event listener for object:', id) + logger.debug('Removed lock event listener for object:', id) } } @@ -159,7 +160,7 @@ const ApiServerProvider = ({ children }) => { if (socketRef.current && socketRef.current.connected == true) { const eventHandler = (data) => { if (data._id === id && data?.user !== userProfile._id) { - log.debug( + logger.debug( 'Update event received for object:', id, 'updatedAt:', @@ -170,7 +171,7 @@ const ApiServerProvider = ({ children }) => { } socketRef.current.on('notify_object_update', eventHandler) - log.debug('Registered update event listener for object:', id) + logger.debug('Registered update event listener for object:', id) // Return cleanup function return () => offUpdateEvent(id, eventHandler) @@ -180,7 +181,7 @@ const ApiServerProvider = ({ children }) => { const offUpdateEvent = (id, eventHandler) => { if (socketRef.current && socketRef.current.connected == true) { socketRef.current.off('notify_update', eventHandler) - log.debug('Removed update event listener for object:', id) + logger.debug('Removed update event listener for object:', id) } } @@ -203,7 +204,7 @@ const ApiServerProvider = ({ children }) => { const fetchObjectInfo = async (id, type) => { const fetchUrl = `${config.backendUrl}/${type}s/${id}` setFetchLoading(true) - log.debug('Fetching from ' + fetchUrl) + logger.debug('Fetching from ' + fetchUrl) try { const response = await axios.get(fetchUrl, { headers: { @@ -213,7 +214,7 @@ const ApiServerProvider = ({ children }) => { }) return response.data } catch (err) { - log.error('Failed to fetch object information:', err) + logger.error('Failed to fetch object information:', err) // Don't automatically show error - let the component handle it throw err } finally { @@ -224,7 +225,7 @@ const ApiServerProvider = ({ children }) => { // Update filament information const updateObjectInfo = async (id, type, value) => { const updateUrl = `${config.backendUrl}/${type}s/${id}` - log.debug('Updating info for ' + id) + logger.debug('Updating info for ' + id) try { const response = await axios.put(updateUrl, value, { headers: { @@ -232,7 +233,7 @@ const ApiServerProvider = ({ children }) => { }, withCredentials: true }) - log.debug('Filament updated successfully') + logger.debug('Filament updated successfully') if (socketRef.current && socketRef.current.connected == true) { await socketRef.current.emit('update', { _id: id, @@ -242,7 +243,7 @@ const ApiServerProvider = ({ children }) => { } return response.data } catch (err) { - log.error('Failed to update filament information:', err) + logger.error('Failed to update filament information:', err) // Don't automatically show error - let the component handle it throw err } diff --git a/src/components/Dashboard/context/AuthContext.js b/src/components/Dashboard/context/AuthContext.js index b9bf589..41f4f6f 100644 --- a/src/components/Dashboard/context/AuthContext.js +++ b/src/components/Dashboard/context/AuthContext.js @@ -7,6 +7,9 @@ import ExclamationOctogonIcon from '../../Icons/ExclamationOctagonIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import config from '../../../config' import AppError from '../../App/AppError' +import loglevel from 'loglevel' +const logger = loglevel.getLogger('ApiServerContext') +logger.setLevel(config.logLevel) const AuthContext = createContext() @@ -50,7 +53,7 @@ const AuthProvider = ({ children }) => { }) if (response.status === 200 && response.data) { - console.log('User is authenticated!') + logger.debug('User is authenticated!') setAuthenticated(true) setToken(response.data.access_token) setExpiresAt(response.data.expires_at) @@ -60,7 +63,7 @@ const AuthProvider = ({ children }) => { setAuthError('Failed to authenticate user.') } } catch (error) { - console.log('Auth check failed', error) + logger.debug('Auth check failed', error) if (error.response?.status === 401) { setShowUnauthorizedModal(true) } else { diff --git a/src/components/Dashboard/context/SpotlightContext.js b/src/components/Dashboard/context/SpotlightContext.js index d53090b..ebd5843 100644 --- a/src/components/Dashboard/context/SpotlightContext.js +++ b/src/components/Dashboard/context/SpotlightContext.js @@ -16,7 +16,7 @@ import PropTypes from 'prop-types' import { useNavigate } from 'react-router-dom' import PrinterState from '../common/PrinterState' import JobState from '../common/JobState' -import IdText from '../common/IdText' +import IdDisplay from '../common/IdDisplay' import config from '../../../config' import { getTypeMeta, getPrefixMeta } from '../utils/Utils' @@ -243,7 +243,6 @@ const SpotlightProvider = ({ children }) => { if (!value || value.trim() === '') { // Only clear the prefix if the input is completely empty if (value === '') { - console.log('Clearing prefix') setInputPrefix(null) } if (formRef.current) { @@ -278,7 +277,6 @@ const SpotlightProvider = ({ children }) => { const handleKeyDown = (e) => { // If backspace is pressed and there's a prefix but the input is empty if (e.key === 'Backspace' && inputPrefix && query === '') { - console.log('Clearing prefix on backspace') // Clear the prefix setInputPrefix(null) // Prevent the default backspace behavior in this case @@ -462,7 +460,6 @@ const SpotlightProvider = ({ children }) => { // Add more inference as needed } const meta = getTypeMeta(type) - console.log('meta', inputPrefix?.type) const Icon = meta.icon // Determine shortcut text @@ -489,7 +486,7 @@ const SpotlightProvider = ({ children }) => { {meta.type == 'printer' ? ( @@ -520,7 +517,7 @@ const SpotlightProvider = ({ children }) => { /> ) : null} - !item.isSkeleton) })) const relevantPages = filteredPages.slice(-2) - console.log('Pages after scroll down:', { + logger.debug('Pages after scroll down:', { current: currentLoadedPageNumber, next: nextPage, keeping: relevantPages.map((p) => p.pageNum) @@ -78,7 +82,7 @@ export const useTableScroll = ({ items: page.items.filter((item) => !item.isSkeleton) })) - console.log('Pages after scroll up:', { + logger.debug('Pages after scroll up:', { current: currentLoadedPageNumber, prev: prevPage, keeping: relevantPages.map((p) => p.pageNum) diff --git a/src/components/Dashboard/utils/Utils.js b/src/components/Dashboard/utils/Utils.js index bb9fe67..c3a21bc 100644 --- a/src/components/Dashboard/utils/Utils.js +++ b/src/components/Dashboard/utils/Utils.js @@ -55,14 +55,32 @@ export const TYPE_META = [ title: 'Printer', prefix: 'PRN', icon: PrinterIcon, - url: (id) => `/dashboard/production/printers/info?printerId=${id}` + url: (id) => `/dashboard/production/printers/info?printerId=${id}`, + properties: { + name: 'text' + } }, { type: 'filament', title: 'Filament', prefix: 'FIL', icon: FilamentIcon, - url: (id) => `/dashboard/management/filaments/info?filamentId=${id}` + url: (id) => `/dashboard/management/filaments/info?filamentId=${id}`, + properties: { + id: 'id', + createdAt: 'dateTime', + name: 'text', + updatedAt: 'dateTime', + vendor: 'object', // objectType: vendor + vendorId: 'id', // objectType: vendor + type: 'material', + cost: 'currency', + color: 'color', + diameter: 'mm', + density: 'density', + url: 'text', + barcode: 'text' + } }, { type: 'spool', @@ -104,7 +122,18 @@ export const TYPE_META = [ title: 'Vendor', prefix: 'VEN', icon: VendorIcon, - url: (id) => `/dashboard/management/vendors/info?vendorId=${id}` + url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`, + properties: { + id: 'id', // objectType: vendor + createdAt: 'dateTime', + name: 'text', + updatedAt: 'dateTime', + website: 'url', + country: 'country', + contact: 'text', + phone: 'text', + email: 'email' + } }, { type: 'subjob', diff --git a/src/components/Icons/EmailDisplay.jsx b/src/components/Icons/EmailDisplay.jsx new file mode 100644 index 0000000..2d4a950 --- /dev/null +++ b/src/components/Icons/EmailDisplay.jsx @@ -0,0 +1,55 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Typography, Flex, Button, Tooltip } from 'antd' +import NewMailIcon from './NewMailIcon' +// import CopyIcon from './CopyIcon' +import CopyButton from '../Dashboard/common/CopyButton' + +const { Text, Link } = Typography + +const EmailDisplay = ({ email, showCopy = true, showLink = false }) => { + if (!email) return n/a + + return ( + <> + + {showLink ? ( + + {email} + + ) : ( + <> + {email} + +