diff --git a/src/App.css b/src/App.css index af348a5..2c18b4d 100644 --- a/src/App.css +++ b/src/App.css @@ -15,6 +15,10 @@ background-color: transparent !important; } +code { + margin: 0; +} + .App { text-align: center; } @@ -53,3 +57,28 @@ transform: rotate(360deg); } } + +.markdown-display > .ant-space-item *:first-child { + margin-top: 0; +} + +.markdown-display > .ant-space-item *:last-child { + margin-bottom: 0; +} + +.markdown-display > .ant-space-item h1, +.markdown-display > .ant-space-item h2, +.markdown-display > .ant-space-item h3, +.markdown-display > .ant-space-item h4, +.markdown-display > .ant-space-item h5, +.markdown-display > .ant-space-item h6 { + margin-bottom: 0.15em; +} + +.idtext .ant-popover-inner { + padding: 0 !important; +} + +.ant-popover-inner:has(.spotlight-tooltip) { + padding: 0 !important; +} diff --git a/src/App.jsx b/src/App.jsx index 63f8563..43b8132 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -41,7 +41,7 @@ import PartStocks from './components/Dashboard/Inventory/PartStocks.jsx' import StockAudits from './components/Dashboard/Inventory/StockAudits.jsx' import StockAuditInfo from './components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx' -import Dashboard from './components/Dashboard/common/Dashboard' +import Dashboard from './components/Dashboard/Dashboard.jsx' import PrivateRoute from './components/PrivateRoute' import './App.css' import { SocketProvider } from './components/Dashboard/context/SocketContext.js' @@ -59,6 +59,12 @@ import { import AppError from './components/App/AppError' import NoteTypes from './components/Dashboard/Management/NoteTypes.jsx' import NoteTypeInfo from './components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx' +import SessionStorage from './components/Dashboard/Developer/SessionStorage.jsx' +import AuthContextDebug from './components/Dashboard/Developer/AuthContextDebug.jsx' +import SocketContextDebug from './components/Dashboard/Developer/SocketContextDebug.jsx' +import { NotificationProvider } from './components/Dashboard/context/NotificationContext.js' +import Users from './components/Dashboard/Management/Users.jsx' +import UserInfo from './components/Dashboard/Management/Users/UserInfo.jsx' const AppContent = () => { const { themeConfig } = useThemeContext() @@ -67,133 +73,164 @@ const AppContent = () => { - - - - - ( - - )} + + + + + + ( + + )} + /> + } + /> + } />} + > + {/* Production Routes */} + } /> - } - /> - } />} - > - {/* Production Routes */} - } - /> - } /> - } - /> - } - /> - } /> - } /> - } - /> - } - /> - - {/* Inventory Routes */} - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - {/* Management Routes */} - } - /> - } - /> - } /> - } - /> - } /> - } - /> - } /> - } - /> - } - /> - } - /> - } - /> - } /> - } - /> - - } /> - } - /> - - - - + } + /> + } + /> + } /> + } + /> + } + /> + } + /> + + {/* Inventory Routes */} + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + {/* Management Routes */} + } + /> + } + /> + } /> + } + /> + } + /> + } + /> + } /> + } + /> + } + /> + } + /> + } + /> + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + + } + /> + + + + + diff --git a/src/assets/icons/auditlogicon.afdesign b/src/assets/icons/auditlogicon.afdesign index f0e41b5..3c5798d 100644 Binary files a/src/assets/icons/auditlogicon.afdesign and b/src/assets/icons/auditlogicon.afdesign differ diff --git a/src/assets/icons/auditlogicon.min.svg b/src/assets/icons/auditlogicon.min.svg index d604a2a..25975ff 100644 --- a/src/assets/icons/auditlogicon.min.svg +++ b/src/assets/icons/auditlogicon.min.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/icons/auditlogicon.svg b/src/assets/icons/auditlogicon.svg index b2ef6c7..932aee0 100644 --- a/src/assets/icons/auditlogicon.svg +++ b/src/assets/icons/auditlogicon.svg @@ -1,9 +1,12 @@ - - - + + + + + + + - diff --git a/src/assets/icons/developericon.afdesign b/src/assets/icons/developericon.afdesign new file mode 100644 index 0000000..3169269 Binary files /dev/null and b/src/assets/icons/developericon.afdesign differ diff --git a/src/assets/icons/developericon.min.svg b/src/assets/icons/developericon.min.svg new file mode 100644 index 0000000..b32a215 --- /dev/null +++ b/src/assets/icons/developericon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/developericon.svg b/src/assets/icons/developericon.svg new file mode 100644 index 0000000..bd293fe --- /dev/null +++ b/src/assets/icons/developericon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/gridicon.afdesign b/src/assets/icons/gridicon.afdesign new file mode 100644 index 0000000..f5e2635 Binary files /dev/null and b/src/assets/icons/gridicon.afdesign differ diff --git a/src/assets/icons/gridicon.min.svg b/src/assets/icons/gridicon.min.svg new file mode 100644 index 0000000..bae34fd --- /dev/null +++ b/src/assets/icons/gridicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/gridicon.svg b/src/assets/icons/gridicon.svg new file mode 100644 index 0000000..e64bb4f --- /dev/null +++ b/src/assets/icons/gridicon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/listicon.afdesign b/src/assets/icons/listicon.afdesign new file mode 100644 index 0000000..ec500f9 Binary files /dev/null and b/src/assets/icons/listicon.afdesign differ diff --git a/src/assets/icons/listicon.min.svg b/src/assets/icons/listicon.min.svg new file mode 100644 index 0000000..4425f1c --- /dev/null +++ b/src/assets/icons/listicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/listicon.svg b/src/assets/icons/listicon.svg new file mode 100644 index 0000000..344c644 --- /dev/null +++ b/src/assets/icons/listicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/noteicon.afdesign b/src/assets/icons/noteicon.afdesign new file mode 100644 index 0000000..ac70b33 Binary files /dev/null and b/src/assets/icons/noteicon.afdesign differ diff --git a/src/assets/icons/noteicon.min.svg b/src/assets/icons/noteicon.min.svg new file mode 100644 index 0000000..a8e9d4a --- /dev/null +++ b/src/assets/icons/noteicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/noteicon.svg b/src/assets/icons/noteicon.svg new file mode 100644 index 0000000..77e1396 --- /dev/null +++ b/src/assets/icons/noteicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/threedotsicon.afdesign b/src/assets/icons/threedotsicon.afdesign new file mode 100644 index 0000000..1586bef Binary files /dev/null and b/src/assets/icons/threedotsicon.afdesign differ diff --git a/src/assets/icons/threedotsicon.min.svg b/src/assets/icons/threedotsicon.min.svg new file mode 100644 index 0000000..59c3f60 --- /dev/null +++ b/src/assets/icons/threedotsicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/threedotsicon.svg b/src/assets/icons/threedotsicon.svg new file mode 100644 index 0000000..cfbdaee --- /dev/null +++ b/src/assets/icons/threedotsicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/components/Dashboard/common/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx similarity index 64% rename from src/components/Dashboard/common/Dashboard.jsx rename to src/components/Dashboard/Dashboard.jsx index 4dfe03f..cb3404e 100644 --- a/src/components/Dashboard/common/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -1,13 +1,13 @@ // Dashboard.js import React from 'react' -import DashboardLayout from './DashboardLayout' +import Layout from './Layout' import { Outlet } from 'react-router-dom' const Dashboard = () => { return ( - + - + ) } diff --git a/src/components/Dashboard/Developer/AuthContextDebug.jsx b/src/components/Dashboard/Developer/AuthContextDebug.jsx new file mode 100644 index 0000000..2bd3651 --- /dev/null +++ b/src/components/Dashboard/Developer/AuthContextDebug.jsx @@ -0,0 +1,105 @@ +import React, { useContext } from 'react' +import { + Descriptions, + Button, + Typography, + Flex, + Space, + Dropdown, + message +} from 'antd' +import ReloadIcon from '../../Icons/ReloadIcon.jsx' +import { AuthContext } from '../context/AuthContext.js' +import BoolDisplay from '../common/BoolDisplay.jsx' + +const { Text, Paragraph } = Typography + +const AuthContextDebug = () => { + const { authenticated, userProfile, token, loading, loginWithSSO, logout } = + useContext(AuthContext) + const [msgApi, contextHolder] = message.useMessage() + + const handleLogin = () => { + loginWithSSO() + } + + const handleLogout = () => { + logout() + } + + const actionItems = { + items: [ + { + label: 'Log In', + key: 'login', + disabled: authenticated + }, + { + label: 'Log Out', + key: 'logout', + disabled: !authenticated + }, + { + label: 'Reload', + key: 'reload', + icon: + } + ], + onClick: ({ key }) => { + if (key === 'login') handleLogin() + if (key === 'logout') handleLogout() + if (key === 'reload') { + msgApi.info('Reloading Auth State...') + window.location.reload() + } + } + } + + return ( + + {contextHolder} + + + + + + + +
+ + + + + + + + + +
{token || None}
+
+
+ +
+              {userProfile ? (
+                
+                  
+                    {JSON.stringify(
+                      // eslint-disable-next-line
+                      { ...userProfile, access_token: '...' },
+                      null,
+                      2
+                    )}
+                  
+
+ ) : ( + n/a + )} +
+
+
+
+
+ ) +} + +export default AuthContextDebug diff --git a/src/components/Dashboard/Developer/DeveloperSidebar.jsx b/src/components/Dashboard/Developer/DeveloperSidebar.jsx new file mode 100644 index 0000000..1e359f8 --- /dev/null +++ b/src/components/Dashboard/Developer/DeveloperSidebar.jsx @@ -0,0 +1,41 @@ +import React from 'react' +import { useLocation } from 'react-router-dom' +import DashboardSidebar from '../common/DashboardSidebar' + +const items = [ + { + key: 'sessionstorage', + label: 'Session Storage', + path: '/dashboard/developer/sessionstorage' + }, + { + key: 'authcontextdebug', + label: 'Auth Context Debug', + path: '/dashboard/developer/authcontextdebug' + }, + { + key: 'socketcontextdebug', + label: 'Socket Context Debug', + path: '/dashboard/developer/socketcontextdebug' + } +] + +const routeKeyMap = { + '/dashboard/developer/sessionstorage': 'sessionstorage', + '/dashboard/developer/authcontext': 'authcontextdebug', + '/dashboard/developer/socketcontext': 'socketcontextdebug' +} + +const DeveloperSidebar = (props) => { + const location = useLocation() + const selectedKey = (() => { + const match = Object.keys(routeKeyMap).find((path) => + location.pathname.startsWith(path) + ) + return match ? routeKeyMap[match] : 'sessionstorage' + })() + + return +} + +export default DeveloperSidebar diff --git a/src/components/Dashboard/Developer/SessionStorage.jsx b/src/components/Dashboard/Developer/SessionStorage.jsx new file mode 100644 index 0000000..ceab359 --- /dev/null +++ b/src/components/Dashboard/Developer/SessionStorage.jsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react' +import { Descriptions, Button, Typography, Flex, Space, Dropdown } from 'antd' +import ReloadIcon from '../../Icons/ReloadIcon' +import BoolDisplay from '../common/BoolDisplay' + +const { Text } = Typography + +const getSessionStorageItems = () => { + const items = [] + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i) + items.push({ key, value: sessionStorage.getItem(key) }) + } + return items +} + +const SessionStorage = () => { + const [items, setItems] = useState(getSessionStorageItems()) + + const reload = () => { + setItems(getSessionStorageItems()) + } + + const actionItems = { + items: [ + { + label: 'Reload', + key: 'reload', + icon: + } + ], + onClick: ({ key }) => { + if (key === 'reload') reload() + } + } + + return ( + + + + + + + + +
+ + {items.length === 0 ? ( + + Empty + + ) : ( + items.map(({ key, value }) => { + // Try to detect boolean values (true/false or 'true'/'false') + let isBool = false + let boolValue = false + if (typeof value === 'boolean') { + isBool = true + boolValue = value + } else if (value === 'true' || value === 'false') { + isBool = true + boolValue = value === 'true' + } + return ( + + {isBool ? ( + + ) : ( + + {value} + + )} + + ) + }) + )} + +
+
+ ) +} + +export default SessionStorage diff --git a/src/components/Dashboard/Developer/SocketContextDebug.jsx b/src/components/Dashboard/Developer/SocketContextDebug.jsx new file mode 100644 index 0000000..54b0b53 --- /dev/null +++ b/src/components/Dashboard/Developer/SocketContextDebug.jsx @@ -0,0 +1,77 @@ +import React, { useContext } from 'react' +import { + Descriptions, + Button, + Typography, + Flex, + Space, + Dropdown, + message +} from 'antd' +import ReloadIcon from '../../Icons/ReloadIcon.jsx' +import { SocketContext } from '../context/SocketContext.js' +import BoolDisplay from '../common/BoolDisplay.jsx' + +const { Text, Paragraph } = Typography + +const SocketContextDebug = () => { + const { socket, error, connecting } = useContext(SocketContext) + const [msgApi, contextHolder] = message.useMessage() + + const actionItems = { + items: [ + { + label: 'Reload', + key: 'reload', + icon: + } + ], + onClick: ({ key }) => { + if (key === 'reload') { + msgApi.info('Reloading Page...') + window.location.reload() + } + } + } + + // Helper to display socket info safely + const getSocketInfo = () => { + if (!socket) return 'n/a' + // Only show safe properties + const { id, connected, disconnected, nsp } = socket + return JSON.stringify({ id, connected, disconnected, nsp }, null, 2) + } + + return ( + + {contextHolder} + + + + + + + +
+ + + + + + + + + {error ? {error} : n/a} + + + +
{getSocketInfo()}
+
+
+
+
+
+ ) +} + +export default SocketContextDebug diff --git a/src/components/Dashboard/Inventory/FilamentStocks.jsx b/src/components/Dashboard/Inventory/FilamentStocks.jsx index 276bd16..5be217f 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks.jsx @@ -30,6 +30,9 @@ import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import DashboardTable from '../common/DashboardTable' +import ListIcon from '../../Icons/ListIcon' +import GridIcon from '../../Icons/GridIcon' +import useViewMode from '../hooks/useViewMode' import config from '../../../config' @@ -46,6 +49,8 @@ const FilamentStocks = () => { const { authenticated } = useContext(AuthContext) + const [viewMode, setViewMode] = useViewMode('FilamentStocks') + const getFilterDropdown = ({ setSelectedKeys, selectedKeys, @@ -85,12 +90,11 @@ const FilamentStocks = () => { // Column definitions const columns = [ { - title: '', - dataIndex: '', + title: , key: 'icon', width: 40, fixed: 'left', - render: () => + render: () => }, { title: 'Filament Name', @@ -112,7 +116,7 @@ const FilamentStocks = () => { clearFilters, propertyName: 'filament name' }), - render: (filament) => {filament.name} + render: (filament) => {filament?.name} }, { title: 'ID', @@ -136,7 +140,7 @@ const FilamentStocks = () => { width: 140, sorter: true, render: (currentNetWeight) => ( - {currentNetWeight.toFixed(2) + 'g'} + {currentNetWeight?.toFixed(2) + 'g'} ) }, { @@ -146,7 +150,7 @@ const FilamentStocks = () => { width: 140, sorter: true, render: (startingNetWeight) => ( - {startingNetWeight.toFixed(2) + 'g'} + {startingNetWeight?.toFixed(2) + 'g'} ) }, { @@ -183,7 +187,7 @@ const FilamentStocks = () => { key: 'actions', fixed: 'right', width: 150, - render: (text, record) => { + render: (record) => { return ( + + + + - View + + + + + {error ? ( + +

{error || 'FilamentStock not found'}

+ +
+ ) : ( +
+ + + updateCollapseState('info', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse no-t-padding-collapse' > - {/* Read-only fields */} - - {filamentStockData.id ? ( - - ) : ( - 'n/a' - )} - - - - - - - - - - - - - - - {filamentStockData.filament ? ( - - - - - ) : ( - 'n/a' - )} - - - - {filamentStockData.filament ? ( - - ) : ( - 'n/a' - )} - - - {filamentStockData.currentGrossWeight ? ( - - - {filamentStockData.currentNetWeight.toFixed(2) + 'g'} - - - {filamentStockData.currentGrossWeight.toFixed(2) + 'g'} - - - ) : ( - 'n/a' - )} - - - {filamentStockData.startingGrossWeight ? ( - - - - {filamentStockData.startingNetWeight.toFixed(2) + 'g'} + + + + Filament Stock Information + + + } + key='1' + > + + } + spinning={fetchLoading} + > + + {/* Read-only fields */} + + {filamentStockData?.id ? ( + + ) : ( + n/a + )} - - {filamentStockData.startingGrossWeight.toFixed(2) + - 'g'} + + {filamentStockData?.createdAt ? ( + + ) : ( + n/a + )} + + + + {filamentStockData ? ( + + ) : ( + n/a + )} + + + + {filamentStockData?.updatedAt ? ( + + ) : ( + n/a + )} + + + + {filamentStockData?.filament ? ( + + + + + ) : ( + n/a + )} + + + + {filamentStockData?.filament ? ( + + ) : ( + n/a + )} + + + {filamentStockData?.currentGrossWeight ? ( + + + {filamentStockData.currentNetWeight.toFixed(2) + + 'g'} + + + {filamentStockData.currentGrossWeight.toFixed( + 2 + ) + 'g'} + + + ) : ( + n/a + )} + + + {filamentStockData?.startingGrossWeight ? ( + + + + {filamentStockData.startingNetWeight.toFixed( + 2 + ) + 'g'} + + + {filamentStockData.startingGrossWeight.toFixed( + 2 + ) + 'g'} + + + + ) : ( + n/a + )} - - ) : ( - 'n/a' - )} - - - - - + + + + - updateCollapseState('events', keys.length > 0)} - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' - > - - Filament Stock Events - - } - key='2' - > - - - + + updateCollapseState('events', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Filament Stock Events + + + } + key='2' + > + } spinning={fetchLoading}> + + + + + + + updateCollapseState('notes', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Notes + + + } + key='notes' + > + + + + + + + + updateCollapseState('auditLogs', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Audit Logs + + + } + key='auditLogs' + > + + + + +
+ )} - + ) } diff --git a/src/components/Dashboard/Inventory/InventorySidebar.jsx b/src/components/Dashboard/Inventory/InventorySidebar.jsx new file mode 100644 index 0000000..bcf0688 --- /dev/null +++ b/src/components/Dashboard/Inventory/InventorySidebar.jsx @@ -0,0 +1,73 @@ +import React from 'react' +import { useLocation } from 'react-router-dom' +import DashboardSidebar from '../common/DashboardSidebar' +import { DashboardOutlined } from '@ant-design/icons' +import FilamentStockIcon from '../../Icons/FilamentStockIcon' +import PartStockIcon from '../../Icons/PartStockIcon' +import ProductStockIcon from '../../Icons/ProductStockIcon' +import StockEventIcon from '../../Icons/StockEventIcon' +import StockAuditIcon from '../../Icons/StockAuditIcon' + +const items = [ + { + key: 'overview', + label: 'Overview', + icon: , + path: '/dashboard/inventory/overview' + }, + { type: 'divider' }, + { + key: 'filamentstocks', + label: 'Filament Stocks', + icon: , + path: '/dashboard/inventory/filamentstocks' + }, + { + key: 'partstocks', + label: 'Part Stocks', + icon: , + path: '/dashboard/inventory/partstocks' + }, + { + key: 'productstocks', + label: 'Product Stocks', + icon: , + path: '/dashboard/inventory/productstocks' + }, + { type: 'divider' }, + { + key: 'stockevents', + label: 'Stock Events', + icon: , + path: '/dashboard/inventory/stockevents' + }, + { + key: 'stockaudits', + label: 'Stock Audits', + icon: , + path: '/dashboard/inventory/stockaudits' + } +] + +const routeKeyMap = { + '/dashboard/inventory/overview': 'overview', + '/dashboard/inventory/filamentstocks': 'filamentstocks', + '/dashboard/inventory/partstocks': 'partstocks', + '/dashboard/inventory/productstocks': 'productstocks', + '/dashboard/inventory/stockevents': 'stockevents', + '/dashboard/inventory/stockaudits': 'stockaudits' +} + +const InventorySidebar = (props) => { + const location = useLocation() + const selectedKey = (() => { + const match = Object.keys(routeKeyMap).find((path) => + location.pathname.startsWith(path) + ) + return match ? routeKeyMap[match] : 'filaments' + })() + + return +} + +export default InventorySidebar diff --git a/src/components/Dashboard/Inventory/StockEvents.jsx b/src/components/Dashboard/Inventory/StockEvents.jsx index 36605d7..3431206 100644 --- a/src/components/Dashboard/Inventory/StockEvents.jsx +++ b/src/components/Dashboard/Inventory/StockEvents.jsx @@ -9,7 +9,6 @@ import { Typography, Input } from 'antd' -import { AuditOutlined } from '@ant-design/icons' import { AuthContext } from '../context/AuthContext' import { SocketContext } from '../context/SocketContext' @@ -17,14 +16,17 @@ import IdText from '../common/IdText' import TimeDisplay from '../common/TimeDisplay' import ReloadIcon from '../../Icons/ReloadIcon' import PlusMinusIcon from '../../Icons/PlusMinusIcon' -import SubJobIcon from '../../Icons/SubJobIcon' -import PlayCircleIcon from '../../Icons/PlayCircleIcon' import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import DashboardTable from '../common/DashboardTable' +import GridIcon from '../../Icons/GridIcon' +import ListIcon from '../../Icons/ListIcon' +import useViewMode from '../hooks/useViewMode' import config from '../../../config' +import { getTypeMeta } from '../utils/Utils' +import StockEventIcon from '../../Icons/StockEventIcon' const { Text } = Typography @@ -32,26 +34,16 @@ const StockEvents = () => { const { socket } = useContext(SocketContext) const [initialized, setInitialized] = useState(false) const tableRef = useRef() + const [viewMode, setViewMode] = useViewMode('StockEvents') // Column definitions for visibility const columns = [ { - title: '', + title: , key: 'icon', width: 40, fixed: 'left', - render: (record) => { - switch (record.type.toLowerCase()) { - case 'subjob': - return - case 'audit': - return - case 'initial': - return - default: - return null - } - } + render: () => }, { title: 'Type', @@ -60,6 +52,9 @@ const StockEvents = () => { width: 200, fixed: 'left', sorter: true, + render: (type) => { + return {getTypeMeta(type?.toLowerCase()).title} + }, filterDropdown: ({ setSelectedKeys, selectedKeys, @@ -90,7 +85,7 @@ const StockEvents = () => { width: 100, sorter: true, render: (value, record) => { - const formattedValue = value.toFixed(2) + record.unit + const formattedValue = value?.toFixed(2) + record?.unit return ( {value > 0 ? '+' + formattedValue : formattedValue} @@ -122,7 +117,7 @@ const StockEvents = () => { width: 170 * 2, render: (record) => { const ids = ( - + {record.job ? ( { showHyperlink={true} /> ) : null} - + ) if (!record.stockAudit && !record.job && !record.subJob) { return 'n/a' @@ -307,6 +302,14 @@ const StockEvents = () => { + + + + - View + + + + {isEditing ? ( + <> + + + ) : ( +
+ + + updateCollapseState('info', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse no-t-padding-collapse' > - - {filamentData.id ? ( - - ) : ( - 'n/a' - )} - - - - - - - - {isEditing ? ( - - - - ) : ( - filamentData.name || 'n/a' - )} - - - - - - - - - - {isEditing ? ( - - - - ) : filamentData.vendor.name ? ( - {filamentData.vendor.name} - ) : ( - n/a - )} - - - - - - - - - - {isEditing ? ( - - - - ) : ( - filamentData.type || 'n/a' - )} - - - - - - {isEditing ? ( - - - - ) : filamentData.cost ? ( - `£${filamentData.cost}/kg` - ) : ( - 'n/a' - )} - - - - - - {isEditing ? ( - { - return '#' + color.toHex() + + + + Filament Information + + + } + key='1' + > + + } + spinning={fetchLoading} + > + - - - ) : ( - - )} - - + + {filamentData?._id ? ( + + ) : ( + n/a + )} + + + {filamentData?.createdAt ? ( + + ) : ( + n/a + )} + - - - {isEditing ? ( - - - - ) : filamentData.diameter ? ( - `${filamentData.diameter}mm` - ) : ( - 'n/a' - )} - - + + {isEditing ? ( + + + + ) : filamentData?.name ? ( + {filamentData.name} + ) : ( + n/a + )} + - - - {isEditing ? ( - - - - ) : filamentData.density ? ( - `${filamentData.density}g/cm³` - ) : ( - 'n/a' - )} - - + + {filamentData?.updatedAt ? ( + + ) : ( + n/a + )} + - - - {isEditing ? ( - - - - ) : filamentData.url ? ( - - {filamentData.url} - - ) : ( - 'n/a' - )} - - + + {isEditing ? ( + + + + ) : filamentData?.vendor?.name ? ( + {filamentData.vendor.name} + ) : ( + n/a + )} + - - - {isEditing ? ( - - - - ) : ( - filamentData.barcode || 'n/a' - )} - - - - - - + + {filamentData?.vendor?.id ? ( + + ) : ( + n/a + )} + - updateCollapseState('details', keys.length > 0)} - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' - > - - Additional Details - - } - key='2' - > - {/* Add any additional details sections here */} - - + + {isEditing ? ( + + + + ) : filamentData?.type ? ( + {filamentData.type} + ) : ( + n/a + )} + + + + {isEditing ? ( + + + + ) : filamentData?.cost ? ( + {`£${filamentData.cost}/kg`} + ) : ( + n/a + )} + + + + + {isEditing ? ( + { + return '#' + color.toHex() + }} + > + + + ) : filamentData?.color ? ( + + ) : ( + n/a + )} + + + + + {isEditing ? ( + + + + ) : filamentData?.diameter ? ( + {`${filamentData.diameter}mm`} + ) : ( + n/a + )} + + + + {isEditing ? ( + + + + ) : filamentData?.density ? ( + {`${filamentData.density}g/cm³`} + ) : ( + n/a + )} + + + + {isEditing ? ( + + + + ) : filamentData?.url ? ( + + {filamentData.url} + + ) : ( + n/a + )} + + + + {isEditing ? ( + + + + ) : filamentData?.barcode ? ( + {filamentData.barcode} + ) : ( + n/a + )} + + + + + + + + + updateCollapseState('notes', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Notes + + + } + key='notes' + > + + + + + + + + updateCollapseState('auditLogs', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Audit Logs + + + } + key='auditLogs' + > + + + + +
+ )} - - setIsDeleteModalOpen(false)} - confirmLoading={loading} - > -

Are you sure you want to delete this filament?

-
- + ) } diff --git a/src/components/Dashboard/Management/ManagementSidebar.jsx b/src/components/Dashboard/Management/ManagementSidebar.jsx new file mode 100644 index 0000000..e25a2ef --- /dev/null +++ b/src/components/Dashboard/Management/ManagementSidebar.jsx @@ -0,0 +1,109 @@ +import React from 'react' +import { useLocation } from 'react-router-dom' +import DashboardSidebar from '../common/DashboardSidebar' +import FilamentIcon from '../../Icons/FilamentIcon' +import PartIcon from '../../Icons/PartIcon' +import ProductIcon from '../../Icons/ProductIcon' +import VendorIcon from '../../Icons/VendorIcon' +import MaterialIcon from '../../Icons/MaterialIcon' +import NoteTypeIcon from '../../Icons/NoteTypeIcon' +import SettingsIcon from '../../Icons/SettingsIcon' +import AuditLogIcon from '../../Icons/AuditLogIcon' +import DeveloperIcon from '../../Icons/DeveloperIcon' +import PersonIcon from '../../Icons/PersonIcon' + +const items = [ + { + key: 'filaments', + icon: , + label: 'Filaments', + path: '/dashboard/management/filaments' + }, + { + key: 'parts', + icon: , + label: 'Parts', + path: '/dashboard/management/parts' + }, + { + key: 'products', + icon: , + label: 'Products', + path: '/dashboard/management/products' + }, + { + key: 'vendors', + icon: , + label: 'Vendors', + path: '/dashboard/management/vendors' + }, + { + key: 'materials', + icon: , + label: 'Materials', + path: '/dashboard/management/materials' + }, + { type: 'divider' }, + { + key: 'notetypes', + icon: , + label: 'Note Types', + path: '/dashboard/management/notetypes' + }, + { + key: 'users', + icon: , + label: 'Users', + path: '/dashboard/management/users' + }, + { + key: 'settings', + icon: , + label: 'Settings', + path: '/dashboard/management/settings' + }, + { + key: 'auditlogs', + icon: , + label: 'Audit Logs', + path: '/dashboard/management/auditlogs' + } +] + +if (process.env.NODE_ENV === 'development') { + items.push( + { type: 'divider' }, + { + key: 'developer', + icon: , + label: 'Developer', + path: '/dashboard/developer/sessionstorage' + } + ) +} + +const routeKeyMap = { + '/dashboard/management/filaments': 'filaments', + '/dashboard/management/parts': 'parts', + '/dashboard/management/users': 'users', + '/dashboard/management/products': 'products', + '/dashboard/management/vendors': 'vendors', + '/dashboard/management/materials': 'materials', + '/dashboard/management/notetypes': 'notetypes', + '/dashboard/management/settings': 'settings', + '/dashboard/management/auditlogs': 'auditlogs' +} + +const ManagementSidebar = (props) => { + const location = useLocation() + const selectedKey = (() => { + const match = Object.keys(routeKeyMap).find((path) => + location.pathname.startsWith(path) + ) + return match ? routeKeyMap[match] : 'filaments' + })() + + return +} + +export default ManagementSidebar diff --git a/src/components/Dashboard/Management/NoteTypes.jsx b/src/components/Dashboard/Management/NoteTypes.jsx index 055a797..50e6bcc 100644 --- a/src/components/Dashboard/Management/NoteTypes.jsx +++ b/src/components/Dashboard/Management/NoteTypes.jsx @@ -11,7 +11,7 @@ import { Popover, Input, Badge, - Tag + Typography } from 'antd' import { AuthContext } from '../context/AuthContext' import IdText from '../common/IdText' @@ -24,9 +24,15 @@ import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import useColumnVisibility from '../hooks/useColumnVisibility' +import GridIcon from '../../Icons/GridIcon' +import ListIcon from '../../Icons/ListIcon' +import useViewMode from '../hooks/useViewMode' import config from '../../../config' import NoteTypeIcon from '../../Icons/NoteTypeIcon' +import BoolDisplay from '../common/BoolDisplay' + +const { Text } = Typography const NoteTypes = () => { const [messageApi, contextHolder] = message.useMessage() @@ -34,6 +40,7 @@ const NoteTypes = () => { const [newNoteTypeOpen, setNewNoteTypeOpen] = useState(false) const tableRef = useRef() const { authenticated } = useContext(AuthContext) + const [viewMode, setViewMode] = useViewMode('NoteTypes') const getFilterDropdown = ({ setSelectedKeys, @@ -114,8 +121,7 @@ const NoteTypes = () => { const columns = [ { - title: '', - dataIndex: '', + title: , key: 'icon', width: 40, fixed: 'left', @@ -172,18 +178,15 @@ const NoteTypes = () => { dataIndex: 'color', key: 'color', width: 120, - render: (color) => + render: (color) => + color ? : n/a }, { title: 'Active', - dataIndex: 'isActive', - key: 'isActive', + dataIndex: 'active', + key: 'active', width: 100, - render: (isActive) => ( - - {isActive ? 'Yes' : 'No'} - - ), + render: (active) => , sorter: true }, { @@ -221,7 +224,7 @@ const NoteTypes = () => { key: 'actions', fixed: 'right', width: 150, - render: (text, record) => { + render: (record) => { return ( + + + + + + - - - {partData.id ? ( - - ) : ( - 'n/a' - )} - - - - + + + + + {isEditing ? ( + <> + + + ) : ( +
+ + + updateCollapseState('info', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse no-t-padding-collapse' + > + + + + Part Information + + + } + key='1' > - - - - {stlLoadError} - - -
- ) : ( - partFileObjectId && ( - + setPartFormValues((prevValues) => ({ + ...prevValues, + ...changedValues + })) + } + initialValues={{ + name: partData?.name || '', + version: partData?.version || '', + tags: partData?.tags || [] }} - > - ) - )} - - - + > + } + spinning={fetchLoading} + > + + + {partData?.id ? ( + + ) : ( + n/a + )} + + + {partData?.createdAt ? ( + + ) : ( + n/a + )} + + + + {isEditing ? ( + + + + ) : partData?.name ? ( + {partData.name} + ) : ( + n/a + )} + + + + {partData?.updatedAt ? ( + + ) : ( + n/a + )} + + + + {partData?.product?.name ? ( + {partData.product.name} + ) : ( + n/a + )} + + + {partData?.product?._id ? ( + + ) : ( + n/a + )} + + + {isEditing && useGlobalPricing == false ? ( + + {marginOrPrice == false ? ( + + + + ) : ( + + + + )} + + Price + + + ) : partData?.margin && + marginOrPrice == false && + partData?.useGlobalPricing == false ? ( + {partData.margin + '%'} + ) : partData?.price && + marginOrPrice == true && + partData?.useGlobalPricing == false ? ( + {'£' + partData.price} + ) : ( + n/a + )} + + + {isEditing ? ( + + + + ) : partData ? ( + + ) : ( + n/a + )} + + + {partData?.version ? ( + {partData.version} + ) : ( + n/a + )} + + + {partData?.tags && partData.tags.length > 0 ? ( + partData.tags.map((tag, index) => ( + {tag} + )) + ) : ( + n/a + )} + + + + + + + + + updateCollapseState('preview', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Part Preview + + + } + key='2' + > + + {stlLoadError ? ( +
+ + + + {stlLoadError} + + +
+ ) : ( + partFileObjectId && ( + + ) + )} +
+
+
+ + + updateCollapseState('notes', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Notes + + + } + key='notes' + > + + + + + + + + updateCollapseState('auditLogs', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Audit Logs + + + } + key='auditLogs' + > + + + + + + )} - + ) } diff --git a/src/components/Dashboard/Management/Products.jsx b/src/components/Dashboard/Management/Products.jsx index 170f79e..7459d50 100644 --- a/src/components/Dashboard/Management/Products.jsx +++ b/src/components/Dashboard/Management/Products.jsx @@ -28,6 +28,9 @@ import ReloadIcon from '../../Icons/ReloadIcon' import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import useColumnVisibility from '../hooks/useColumnVisibility' +import GridIcon from '../../Icons/GridIcon' +import ListIcon from '../../Icons/ListIcon' +import useViewMode from '../hooks/useViewMode' import config from '../../../config' @@ -37,6 +40,7 @@ const Products = () => { const [newProductOpen, setNewProductOpen] = useState(false) const tableRef = useRef() const { authenticated } = useContext(AuthContext) + const [viewMode, setViewMode] = useViewMode('Products') const getProductActionItems = (id) => { return { @@ -63,12 +67,11 @@ const Products = () => { // Column definitions const columns = [ { - title: '', - dataIndex: '', - key: '', + title: , + key: 'icon', width: 40, fixed: 'left', - render: () => + render: () => }, { title: 'Name', @@ -114,7 +117,7 @@ const Products = () => { propertyName: 'ID' }), onFilter: (value, record) => - record._id.toLowerCase().includes(value.toLowerCase()), + record?._id.toLowerCase().includes(value.toLowerCase()), sorter: true }, { @@ -212,7 +215,7 @@ const Products = () => { key: 'actions', fixed: 'right', width: 150, - render: (text, record) => { + render: (record) => { return ( + + + + - - - {productData.id ? ( - - ) : ( - 'n/a' - )} - + + + + + {isEditing ? ( + <> + + + ) : ( +
+ + + updateCollapseState('info', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse no-t-padding-collapse' + > + + + + Product Information + + + } + key='1' + > +
+ setProductFormValues((prevValues) => ({ + ...prevValues, + ...changedValues + })) + } + initialValues={{ + name: productData?.name || '', + vendor: productData?.vendor || { id: null, name: '' }, + version: productData?.version || '', + tags: productData?.tags || [] + }} + > + } + 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/Settings.jsx b/src/components/Dashboard/Management/Settings.jsx index 00f5b79..faaa9c0 100644 --- a/src/components/Dashboard/Management/Settings.jsx +++ b/src/components/Dashboard/Management/Settings.jsx @@ -1,6 +1,6 @@ import React from 'react' import { Select, Typography, Descriptions, Collapse, Flex } from 'antd' -import { CaretRightOutlined } from '@ant-design/icons' +import { CaretLeftOutlined } from '@ant-design/icons' import { useThemeContext } from '../context/ThemeContext' import useCollapseState from '../hooks/useCollapseState' @@ -53,13 +53,13 @@ const Settings = () => { updateCollapseState('appearance', keys.length > 0) } expandIcon={({ isActive }) => ( - diff --git a/src/components/Dashboard/Management/Users.jsx b/src/components/Dashboard/Management/Users.jsx new file mode 100644 index 0000000..b148e46 --- /dev/null +++ b/src/components/Dashboard/Management/Users.jsx @@ -0,0 +1,381 @@ +import React, { useContext, useRef } from 'react' +import { useNavigate } from 'react-router-dom' +import { + Button, + Flex, + Space, + Dropdown, + Typography, + Checkbox, + Popover, + Input +} from 'antd' +import { ExportOutlined } from '@ant-design/icons' +import { AuthContext } from '../context/AuthContext' +import IdText from '../common/IdText' +import TimeDisplay from '../common/TimeDisplay' +import DashboardTable from '../common/DashboardTable' +import PersonIcon from '../../Icons/PersonIcon' +import ReloadIcon from '../../Icons/ReloadIcon' +import XMarkIcon from '../../Icons/XMarkIcon' +import CheckIcon from '../../Icons/CheckIcon' +import useColumnVisibility from '../hooks/useColumnVisibility' +import InfoCircleIcon from '../../Icons/InfoCircleIcon' +import GridIcon from '../../Icons/GridIcon' +import ListIcon from '../../Icons/ListIcon' +import useViewMode from '../hooks/useViewMode' + +import config from '../../../config' + +const { Link, Text } = Typography + +const Users = () => { + const navigate = useNavigate() + const tableRef = useRef() + const { authenticated } = useContext(AuthContext) + const [viewMode, setViewMode] = useViewMode('Users') + + const getFilterDropdown = ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + propertyName + }) => { + return ( +
+ + + setSelectedKeys(e.target.value ? [e.target.value] : []) + } + onPressEnter={() => confirm()} + style={{ width: 200, display: 'block' }} + /> +
+ ) + } + + const getViewDropdownItems = () => { + const columnItems = columns + .filter((col) => col.key && col.title !== '') + .map((col) => ( + { + updateColumnVisibility(col.key, e.target.checked) + }} + > + {col.title} + + )) + + return ( + + + {columnItems} + + + ) + } + + const getUserActionItems = (id) => { + return { + items: [ + { + label: 'Info', + key: 'info', + icon: + } + ], + onClick: ({ key }) => { + if (key === 'info') { + navigate(`/dashboard/management/users/info?userId=${id}`) + } + } + } + } + + const columns = [ + { + title: , + key: 'icon', + width: 40, + fixed: 'left', + render: () => + }, + + { + title: 'Name', + dataIndex: 'name', + key: 'name', + width: 200, + render: (text) => (text ? text : 'n/a'), + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters + }) => + getFilterDropdown({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + propertyName: 'name' + }), + onFilter: (value, record) => + record.name?.toLowerCase().includes(value.toLowerCase()), + sorter: true + }, + { + title: 'Username', + dataIndex: 'username', + key: 'username', + width: 150, + fixed: 'left', + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters + }) => + getFilterDropdown({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + propertyName: 'username' + }), + onFilter: (value, record) => + record.username.toLowerCase().includes(value.toLowerCase()), + sorter: true + }, + { + title: 'First Name', + dataIndex: 'firstName', + key: 'firstName', + width: 150, + render: (text) => (text ? text : 'n/a'), + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters + }) => + getFilterDropdown({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + propertyName: 'first name' + }), + onFilter: (value, record) => + record.firstName?.toLowerCase().includes(value.toLowerCase()), + sorter: true + }, + { + title: 'Last Name', + dataIndex: 'lastName', + key: 'lastName', + width: 150, + render: (text) => (text ? text : 'n/a'), + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters + }) => + getFilterDropdown({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + propertyName: 'last name' + }), + onFilter: (value, record) => + record.lastName?.toLowerCase().includes(value.toLowerCase()), + sorter: true + }, + { + title: 'Email', + dataIndex: 'email', + key: 'email', + width: 250, + render: (email) => + email ? ( + + {email} + + ) : ( + n/a + ), + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters + }) => + getFilterDropdown({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + propertyName: 'email' + }), + onFilter: (value, record) => + record.email?.toLowerCase().includes(value.toLowerCase()), + sorter: true + }, + { + title: 'ID', + dataIndex: '_id', + key: 'id', + width: 180, + render: (text) => , + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters + }) => + getFilterDropdown({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + propertyName: 'ID' + }), + onFilter: (value, record) => + record._id.toLowerCase().includes(value.toLowerCase()), + sorter: true + }, + { + title: 'Created At', + dataIndex: 'createdAt', + key: 'createdAt', + width: 180, + render: (createdAt) => { + if (createdAt) { + return + } else { + return 'n/a' + } + }, + sorter: true, + defaultSortOrder: 'descend' + }, + { + title: 'Updated At', + dataIndex: 'updatedAt', + key: 'updatedAt', + width: 180, + render: (updatedAt) => { + if (updatedAt) { + return + } else { + return 'n/a' + } + }, + sorter: true, + defaultSortOrder: 'descend' + }, + { + title: 'Actions', + key: 'actions', + fixed: 'right', + width: 150, + render: (record) => { + return ( + + + + + ) + } + } + ] + + const [columnVisibility, updateColumnVisibility] = useColumnVisibility( + 'Users', + columns + ) + + const visibleColumns = columns.filter( + (col) => !col.key || columnVisibility[col.key] + ) + + const actionItems = { + items: [ + { + label: 'Reload List', + key: 'reloadList', + icon: + } + ], + onClick: ({ key }) => { + if (key === 'reloadList') { + tableRef.current?.reload() + } + } + } + + return ( + + + + + + + + + + + + + + {currentStep < steps.length - 1 ? ( + + ) : ( + + )} + + +
+ ) +} + +NewUser.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool +} + +export default NewUser diff --git a/src/components/Dashboard/Management/Users/UserInfo.jsx b/src/components/Dashboard/Management/Users/UserInfo.jsx new file mode 100644 index 0000000..f35daa4 --- /dev/null +++ b/src/components/Dashboard/Management/Users/UserInfo.jsx @@ -0,0 +1,510 @@ +import React, { useState, useEffect } 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 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 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 + +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' + > + + + + User Information + + + } + key='1' + > +
+ } + spinning={fetchLoading} + > + + + {userData?._id ? ( + + ) : ( + n/a + )} + + + + {userData?.createdAt ? ( + + ) : ( + n/a + )} + + + + {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' + > + + + + Notes + + + } + key='notes' + > + + + + + + + + updateCollapseState('auditLogs', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Audit Logs + + + } + key='auditLogs' + > + + + + +
+ )} +
+ + ) +} + +export default UserInfo diff --git a/src/components/Dashboard/Management/Vendors.jsx b/src/components/Dashboard/Management/Vendors.jsx index 5aa49dc..2c4fa61 100644 --- a/src/components/Dashboard/Management/Vendors.jsx +++ b/src/components/Dashboard/Management/Vendors.jsx @@ -26,6 +26,9 @@ import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import InfoCircleIcon from '../../Icons/InfoCircleIcon' +import GridIcon from '../../Icons/GridIcon' +import ListIcon from '../../Icons/ListIcon' +import useViewMode from '../hooks/useViewMode' import config from '../../../config' @@ -37,6 +40,7 @@ const Vendors = () => { const [newVendorOpen, setNewVendorOpen] = useState(false) const tableRef = useRef() const { authenticated } = useContext(AuthContext) + const [viewMode, setViewMode] = useViewMode('Vendors') const getFilterDropdown = ({ setSelectedKeys, @@ -117,9 +121,8 @@ const Vendors = () => { const columns = [ { - title: '', - dataIndex: '', - key: '', + title: , + key: 'icon', width: 40, fixed: 'left', render: () => @@ -282,7 +285,7 @@ const Vendors = () => { key: 'actions', fixed: 'right', width: 150, - render: (text, record) => { + render: (record) => { return ( + + + + + + + + + {isEditing ? ( + <> + + + ) : ( +
+ + + updateCollapseState('info', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse no-t-padding-collapse' > - - - - - + + + Vendor Information + + + } + key='1' + > + + } + spinning={fetchLoading} + > + + + {vendorData?._id ? ( + + ) : ( + n/a + )} + + + {vendorData?.createdAt ? ( + + ) : ( + n/a + )} + + + + {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' + > + + + + Notes + + + } + key='notes' + > + + + + + + + + updateCollapseState('auditLogs', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Audit Logs + + + } + key='auditLogs' + > + - - - - {isEditing ? ( - - - - ) : ( - vendorData.name - )} - - - - - - - - {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' - )} - - - - - + + + +
+ )}
- + ) } diff --git a/src/components/Dashboard/Production/GCodeFiles.jsx b/src/components/Dashboard/Production/GCodeFiles.jsx index 2f60a6a..c6287c9 100644 --- a/src/components/Dashboard/Production/GCodeFiles.jsx +++ b/src/components/Dashboard/Production/GCodeFiles.jsx @@ -31,6 +31,9 @@ import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import TimeDisplay from '../common/TimeDisplay' import DashboardTable from '../common/DashboardTable' +import ListIcon from '../../Icons/ListIcon' +import GridIcon from '../../Icons/GridIcon' +import useViewMode from '../hooks/useViewMode' import config from '../../../config' @@ -42,6 +45,7 @@ const GCodeFiles = () => { const [newGCodeFileOpen, setNewGCodeFileOpen] = useState(false) const [showDeleted, setShowDeleted] = useState(false) const tableRef = useRef() + const [viewMode, setViewMode] = useViewMode('GCodeFiles') const getFilterDropdown = ({ setSelectedKeys, @@ -82,12 +86,11 @@ const GCodeFiles = () => { // Column definitions const columns = [ { - title: '', - dataIndex: '', - key: '', + title: , + key: 'icon', width: 40, fixed: 'left', - render: () => + render: () => }, { title: 'Name', @@ -121,12 +124,14 @@ const GCodeFiles = () => { }, { title: 'Filament', - dataIndex: ['filament', 'name'], key: 'filament', width: 200, - render: (text, record) => { + render: (record) => { return ( - + ) }, filterDropdown: ({ @@ -151,17 +156,16 @@ const GCodeFiles = () => { key: 'cost', width: 120, render: (cost) => { - return '£' + cost.toFixed(2) + return '£' + cost?.toFixed(2) }, sorter: true }, { title: 'Print Time', key: 'estimatedPrintingTimeNormalMode', - dataIndex: ['gcodeFileInfo', 'estimatedPrintingTimeNormalMode'], width: 140, - render: (text, record) => { - return `${record.gcodeFileInfo.estimatedPrintingTimeNormalMode}` + render: (record) => { + return `${record?.gcodeFileInfo?.estimatedPrintingTimeNormalMode}` }, sorter: true }, @@ -198,7 +202,7 @@ const GCodeFiles = () => { key: 'actions', fixed: 'right', width: 150, - render: (text, record) => { + render: (record) => { return ( - - - - - - + + + + + + + + + + + - - ) + 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() + } + } } - if (fetchLoading) { + 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} - - updateCollapseState('info', keys.length > 0)} - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' - > - - - GCode File Information - - - {isEditing ? ( - <> - + + + + + + + {isEditing ? ( + <> + + + ) : ( +
+ + + updateCollapseState('info', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse no-t-padding-collapse' + > + + + + GCode File Information + + + } + key='info' + > +
+ } + > + + + {gcodeFileData?._id ? ( + + ) : ( + n/a + )} + + + {gcodeFileData?.createdAt ? ( + + ) : ( + n/a + )} + + + + {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) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + GCode File Preview + + + } + key='preview' + > + }> + + {gcodeFileData?.gcodeFileInfo?.thumbnail ? ( + GCodeFile + ) : ( + n/a + )} + + + + + + + updateCollapseState('notes', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Notes + + + } + key='notes' + > + + + + + + + + updateCollapseState('auditLogs', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse' + > + + + + Audit Log + + + } + key='auditLogs' + > + + + + +
+ )}
-
+ ) } diff --git a/src/components/Dashboard/Production/Jobs.jsx b/src/components/Dashboard/Production/Jobs.jsx index 4de127f..be54fc8 100644 --- a/src/components/Dashboard/Production/Jobs.jsx +++ b/src/components/Dashboard/Production/Jobs.jsx @@ -37,6 +37,9 @@ import PauseCircleIcon from '../../Icons/PauseCircleIcon.jsx' import XMarkCircleIcon from '../../Icons/XMarkCircleIcon.jsx' import QuestionCircleIcon from '../../Icons/QuestionCircleIcon.jsx' import DashboardTable from '../common/DashboardTable' +import ListIcon from '../../Icons/ListIcon.jsx' +import GridIcon from '../../Icons/GridIcon.jsx' +import useViewMode from '../hooks/useViewMode.js' import config from '../../../config.js' @@ -49,6 +52,7 @@ const Jobs = () => { const navigate = useNavigate() const [newJobOpen, setNewJobOpen] = useState(false) const tableRef = useRef() + const [viewMode, setViewMode] = useViewMode('Jobs') const getFilterDropdown = ({ setSelectedKeys, @@ -89,20 +93,18 @@ const Jobs = () => { // Column definitions const columns = [ { - title: '', - dataIndex: '', - key: '', + title: , + key: 'icon', width: 40, fixed: 'left', render: () => }, { title: 'GCode File Name', - dataIndex: 'gcodeFile', key: 'gcodeFileName', width: 200, fixed: 'left', - render: (gcodeFile) => {gcodeFile.name}, + render: (record) => {record?.gcodeFile?.name}, filterDropdown: ({ setSelectedKeys, selectedKeys, @@ -162,7 +164,7 @@ const Jobs = () => { propertyName: 'state' }), onFilter: (value, record) => - record.state.type.toLowerCase().includes(value.toLowerCase()) + record?.state?.type?.toLowerCase().includes(value.toLowerCase()) }, { title: , @@ -226,13 +228,13 @@ const Jobs = () => { }, { title: 'Actions', - key: 'operation', + key: 'actions', fixed: 'right', width: 150, render: (record) => { return ( - {record.state.type === 'draft' ? ( + {record?.state?.type === 'draft' ? ( - - - - - + + + + + + + + + + + @@ -284,24 +289,35 @@ const Printers = () => { <> {contextHolder} - - - - - - - - + + + + + + + + + + + - + + + {sections.map((section) => ( + { + updateCollapseState(section.key, e.target.checked) + }} + > + {section.label} + + ))} + + ) } return ( -
+ <> {contextHolder} - - updateCollapseState('info', keys.length > 0)} - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' - > - - - Printer Information - - - {isEditing ? ( - <> - + + - View + + + + {isEditing ? ( + <> + + + ) : ( +
+ + + updateCollapseState('info', keys.length > 0) + } + expandIcon={({ isActive }) => ( + + )} + className='no-h-padding-collapse no-t-padding-collapse' > - {/* Read-only fields */} - - - - - - - - {/* Editable fields */} - - {isEditing ? ( - + + + Printer Information + + + } + key='info' + > + + } > - - - ) : ( - printerData.name || 'n/a' - )} - - - - {isEditing ? ( - - - - ) : ( - printerData.moonraker?.host || 'n/a' - )} - - - - {isEditing ? ( - - - - ) : ( - - - {printerData?.vendor?.name || 'n/a'} - - )} - - - - {printerData?.vendor ? ( - - ) : ( - 'n/a' - )} - - - - {isEditing ? ( - - - - ) : ( - printerData.moonraker.port - )} - - - - {isEditing ? ( - - - -
+ )}
-
+ ) } diff --git a/src/components/Dashboard/Production/ProductionOverview.jsx b/src/components/Dashboard/Production/ProductionOverview.jsx index 8cd9f91..4c32241 100644 --- a/src/components/Dashboard/Production/ProductionOverview.jsx +++ b/src/components/Dashboard/Production/ProductionOverview.jsx @@ -12,7 +12,7 @@ import { Segmented, Card } from 'antd' -import { LoadingOutlined, CaretRightOutlined } from '@ant-design/icons' +import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' import { Line } from '@ant-design/charts' import axios from 'axios' import PrinterIcon from '../../Icons/PrinterIcon' @@ -151,11 +151,11 @@ const ProductionOverview = () => { updateCollapseState('overview', keys.length > 0)} expandIcon={({ isActive }) => ( - @@ -275,13 +275,13 @@ const ProductionOverview = () => { updateCollapseState('printerStats', keys.length > 0) } expandIcon={({ isActive }) => ( - @@ -357,13 +357,13 @@ const ProductionOverview = () => { updateCollapseState('jobStats', keys.length > 0) } expandIcon={({ isActive }) => ( - diff --git a/src/components/Dashboard/Production/ProductionSidebar.jsx b/src/components/Dashboard/Production/ProductionSidebar.jsx new file mode 100644 index 0000000..65b3d50 --- /dev/null +++ b/src/components/Dashboard/Production/ProductionSidebar.jsx @@ -0,0 +1,56 @@ +import React from 'react' +import { useLocation } from 'react-router-dom' +import DashboardSidebar from '../common/DashboardSidebar' +import ProductionIcon from '../../Icons/ProductionIcon' +import PrinterIcon from '../../Icons/PrinterIcon' +import JobIcon from '../../Icons/JobIcon' +import GCodeFileIcon from '../../Icons/GCodeFileIcon' + +const items = [ + { + key: 'overview', + icon: , + label: 'Overview', + path: '/dashboard/production/overview' + }, + { type: 'divider' }, + { + key: 'printers', + icon: , + label: 'Printers', + path: '/dashboard/production/printers' + }, + { + key: 'jobs', + icon: , + label: 'Jobs', + path: '/dashboard/production/jobs' + }, + { + key: 'gcodefiles', + icon: , + label: 'GCode Files', + path: '/dashboard/production/gcodefiles' + } +] + +const routeKeyMap = { + '/dashboard/production/overview': 'overview', + '/dashboard/production/printers': 'printers', + '/dashboard/production/jobs': 'jobs', + '/dashboard/production/gcodefiles': 'gcodefiles' +} + +const ProductionSidebar = (props) => { + const location = useLocation() + const selectedKey = (() => { + const match = Object.keys(routeKeyMap).find((path) => + location.pathname.startsWith(path) + ) + return match ? routeKeyMap[match] : 'overview' + })() + + return +} + +export default ProductionSidebar diff --git a/src/components/Dashboard/common/AuditLogTable.jsx b/src/components/Dashboard/common/AuditLogTable.jsx index e77a32e..5a22f06 100644 --- a/src/components/Dashboard/common/AuditLogTable.jsx +++ b/src/components/Dashboard/common/AuditLogTable.jsx @@ -1,9 +1,10 @@ import React, { forwardRef, useState } from 'react' -import { Typography, Space, Descriptions, Badge, Tag, Table } from 'antd' +import { Typography, Space, Descriptions, Badge, Table } from 'antd' import PropTypes from 'prop-types' import IdText from './IdText' import { AuditOutlined, LoadingOutlined } from '@ant-design/icons' import TimeDisplay from '../common/TimeDisplay' +import BoolDisplay from './BoolDisplay' const { Text } = Typography @@ -19,7 +20,11 @@ const isObjectId = (value) => { const formatValue = (value, propertyName) => { if (value === null || value === undefined || value === '') { - return n/a + return ( +
+ n/a +
+ ) } // Handle colors specifically @@ -42,11 +47,7 @@ const formatValue = (value, propertyName) => { } if (typeof value === 'boolean' || value === true || value === false) { - return ( - - {value ? 'Yes' : 'No'} - - ) + return } if (isObjectId(value)) { diff --git a/src/components/Dashboard/common/BoolDisplay.jsx b/src/components/Dashboard/common/BoolDisplay.jsx new file mode 100644 index 0000000..3653503 --- /dev/null +++ b/src/components/Dashboard/common/BoolDisplay.jsx @@ -0,0 +1,41 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Space, Tag } from 'antd' +import CheckIcon from '../../Icons/CheckIcon' +import XMarkIcon from '../../Icons/XMarkIcon' + +const BoolDisplay = ({ + value, + yesNo, + showIcon = true, + showText = true, + showColor = true +}) => { + var falseText = 'False' + var trueText = 'True' + if (yesNo) { + falseText = 'No' + trueText = 'Yes' + } + return ( + + : : undefined} + > + {showText ? (value === true ? trueText : falseText) : null} + + + ) +} + +BoolDisplay.propTypes = { + value: PropTypes.bool.isRequired, + yesNo: PropTypes.bool, + showIcon: PropTypes.bool, + showText: PropTypes.bool, + showColor: PropTypes.bool +} + +export default BoolDisplay diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx index 175e036..01db842 100644 --- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx +++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx @@ -9,6 +9,7 @@ const breadcrumbNameMap = { '/dashboard/production': 'Production', '/dashboard/inventory': 'Inventory', '/dashboard/management': 'Management', + '/dashboard/developer': 'Developer', '/dashboard/production/overview': 'Overview', '/dashboard/production/printers': 'Printers', '/dashboard/production/printers/control': 'Control', @@ -29,6 +30,8 @@ const breadcrumbNameMap = { '/dashboard/management/materials/info': 'Info', '/dashboard/management/notetypes': 'Note Types', '/dashboard/management/notetypes/info': 'Info', + '/dashboard/management/users': 'Users', + '/dashboard/management/users/info': 'Info', '/dashboard/management/settings': 'Settings', '/dashboard/management/auditlogs': 'Audit Logs', '/dashboard/inventory/filamentstocks': 'Filament Stocks', @@ -40,7 +43,10 @@ const breadcrumbNameMap = { '/dashboard/inventory/stockevents': 'Stock Events', '/dashboard/inventory/stockevents/info': 'Info', '/dashboard/inventory/stockaudits': 'Stock Audits', - '/dashboard/inventory/stockaudits/info': 'Info' + '/dashboard/inventory/stockaudits/info': 'Info', + '/dashboard/developer/sessionstorage': 'Session Storage', + '/dashboard/developer/authcontextdebug': 'Auth Context Debug', + '/dashboard/developer/socketcontextdebug': 'Socket Context Debug' } const DashboardBreadcrumb = () => { diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx index 889f234..19acece 100644 --- a/src/components/Dashboard/common/DashboardNavigation.jsx +++ b/src/components/Dashboard/common/DashboardNavigation.jsx @@ -9,7 +9,8 @@ import { Button, Tooltip, Typography, - Divider + Divider, + Badge } from 'antd' import { LogoutOutlined, @@ -20,6 +21,7 @@ import { import { AuthContext } from '../context/AuthContext' import { SocketContext } from '../context/SocketContext' import { SpotlightContext } from '../context/SpotlightContext' +import { NotificationContext } from '../context/NotificationContext' import { useNavigate, useLocation } from 'react-router-dom' import { Header } from 'antd/es/layout/layout' import { useMediaQuery } from 'react-responsive' @@ -33,12 +35,15 @@ import CloudIcon from '../../Icons/CloudIcon' import BellIcon from '../../Icons/BellIcon' import SearchIcon from '../../Icons/SearchIcon' import SettingsIcon from '../../Icons/SettingsIcon' +import DeveloperIcon from '../../Icons/DeveloperIcon' const { Text } = Typography const DashboardNavigation = () => { const { logout, userProfile } = useContext(AuthContext) const { showSpotlight } = useContext(SpotlightContext) + const { toggleNotificationCenter, unreadCount } = + useContext(NotificationContext) const { socket } = useContext(SocketContext) const [socketState, setSocketState] = useState('disconnected') const navigate = useNavigate() @@ -168,12 +173,14 @@ const DashboardNavigation = () => { onClick={() => showSpotlight()} > - + + + {socketState === 'connected' ? ( @@ -206,10 +213,15 @@ const DashboardNavigation = () => { {process.env.NODE_ENV === 'development' && ( - - - Dev - + + } + onClick={() => { + navigate('/dashboard/developer/sessionstorage') + }} + /> )} diff --git a/src/components/Dashboard/common/DashboardNotes.jsx b/src/components/Dashboard/common/DashboardNotes.jsx index e879e23..f160ce4 100644 --- a/src/components/Dashboard/common/DashboardNotes.jsx +++ b/src/components/Dashboard/common/DashboardNotes.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useCallback, useContext, useEffect, useState } from 'react' import PropTypes from 'prop-types' import { Card, @@ -9,31 +9,464 @@ import { Modal, Form, Input, - Select, - Switch + Switch, + Spin, + Alert, + message, + Divider, + Tag, + Dropdown } from 'antd' +import { CaretLeftFilled, LoadingOutlined } from '@ant-design/icons' import PlusIcon from '../../Icons/PlusIcon' +import BinIcon from '../../Icons/BinIcon' +import PersonIcon from '../../Icons/PersonIcon' import TimeDisplay from './TimeDisplay' import MarkdownDisplay from './MarkdownDisplay' +import axios from 'axios' +import config from '../../../config' +import { AuthContext } from '../context/AuthContext' +import InfoCircleIcon from '../../Icons/InfoCircleIcon' +import NoteTypeSelect from './NoteTypeSelect' +import IdText from './IdText' +import ReloadIcon from '../../Icons/ReloadIcon' +import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon' -const { Text } = Typography +const { Text, Title } = Typography const { TextArea } = Input -const DashboardNotes = ({ notes = [], onNewNote }) => { - const [isModalOpen, setIsModalOpen] = useState(false) - const [showMarkdown, setShowMarkdown] = useState(false) - const [form] = Form.useForm() +const NoteItem = ({ + note, + expandedNotes, + setExpandedNotes, + fetchData, + onNewNote, + onDeleteNote, + userProfile, + onChildNoteAdded +}) => { + const [childNotes, setChildNotes] = useState({}) + const [loadingChildNotes, setLoadingChildNotes] = useState(null) - const handleNewNote = () => { - setIsModalOpen(true) + const isExpanded = expandedNotes[note._id] + const hasChildNotes = childNotes[note._id] && childNotes[note._id].length > 0 + const isThisNoteLoading = loadingChildNotes === note._id + + let transformValue = 'rotate(0deg)' + if (isExpanded) { + transformValue = 'rotate(-90deg)' } + const handleNoteExpand = async (noteId) => { + const newExpandedState = !expandedNotes[noteId] + + setExpandedNotes((prev) => ({ + ...prev, + [noteId]: newExpandedState + })) + + if (newExpandedState && !childNotes[noteId]) { + setLoadingChildNotes(noteId) + try { + const childNotesData = await fetchData(noteId) + setChildNotes((prev) => ({ + ...prev, + [noteId]: childNotesData + })) + } catch (error) { + console.error('Error fetching child notes:', error) + } finally { + setLoadingChildNotes(null) + } + } + } + + const handleNewChildNote = () => { + if (onNewNote) { + onNewNote(note._id) + } + } + + const handleDeleteNote = () => { + if (onDeleteNote) { + onDeleteNote(note._id) + } + } + + // Reload child notes when a new child note is added + const reloadChildNotes = async () => { + // Always fetch child notes when this function is called + // This ensures child notes are loaded even if the parent wasn't expanded before + setLoadingChildNotes(note._id) + try { + const childNotesData = await fetchData(note._id) + setChildNotes((prev) => ({ + ...prev, + [note._id]: childNotesData + })) + } catch (error) { + console.error('Error fetching child notes:', error) + } finally { + setLoadingChildNotes(null) + } + } + + // Listen for child note additions + useEffect(() => { + if (onChildNoteAdded) { + onChildNoteAdded(note._id, reloadChildNotes) + } + }, [note._id, onChildNoteAdded]) + + // Check if the current user can delete this note + const canDeleteNote = userProfile && userProfile._id === note.user._id + + const dropdownItems = [ + { + key: 'new', + icon: , + label: 'New Note', + onClick: handleNewChildNote + } + ] + + // Only add delete option if user owns the note + if (canDeleteNote) { + dropdownItems.push({ + key: 'delete', + label: 'Delete Note', + icon: , + onClick: handleDeleteNote, + danger: true + }) + } + + return ( + + + + + + {note.user.name}: + +
+ +
+
+ + + + + + + Type: + + {note.noteType.name} + + + + User ID: + + + + Created At: + + + + + + + + -