diff --git a/eslint.config.js b/eslint.config.js index 1e165f6..6c1c690 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,6 +2,67 @@ import js from '@eslint/js'; import globals from 'globals'; import reactHooks from 'eslint-plugin-react-hooks'; +const banMuiTooltipRule = { + meta: { + type: 'problem', + docs: { + description: 'Ban MUI Tooltip component', + }, + messages: { + badDesign: ' is bad design', + }, + }, + create(context) { + const muiTooltipIdentifiers = new Set(); + + return { + ImportDeclaration(node) { + if (node.source.value === '@mui/material/Tooltip') { + context.report({ + node, + messageId: 'badDesign', + }); + for (const specifier of node.specifiers) { + if (specifier.local?.name) { + muiTooltipIdentifiers.add(specifier.local.name); + } + } + } else if (node.source.value === '@mui/material') { + for (const specifier of node.specifiers) { + if ( + specifier.type === 'ImportSpecifier' && + specifier.imported?.name === 'Tooltip' + ) { + context.report({ + node: specifier, + messageId: 'badDesign', + }); + if (specifier.local?.name) { + muiTooltipIdentifiers.add(specifier.local.name); + } + } + } + } + }, + JSXOpeningElement(node) { + let tagName = ''; + if (node.name.type === 'JSXIdentifier') { + tagName = node.name.name; + } else if (node.name.type === 'JSXMemberExpression') { + tagName = node.name.property.name; + } + + if (muiTooltipIdentifiers.has(tagName) || tagName === 'Tooltip') { + context.report({ + node, + messageId: 'badDesign', + }); + } + }, + }; + }, +}; + export default [ { ignores: [ @@ -32,10 +93,16 @@ export default [ }, plugins: { 'react-hooks': reactHooks, + react: { + rules: { + 'no-mui-tooltip': banMuiTooltipRule, + }, + }, }, rules: { 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', + 'react/no-mui-tooltip': 'error', 'no-unused-vars': [ 'warn', { diff --git a/package.json b/package.json index c9abae8..784333e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dev:pwa:https": "VITE_HTTPS=1 node scripts/dev.mjs", "cert": "./scripts/generate-cert.sh", "build": "vite build", + "lint": "eslint .", "preview": "vite preview", "preview:https": "VITE_HTTPS=1 vite preview", "rig:collect": "node test/collect.mjs", diff --git a/src/api/agentprinter.js b/src/api/agentprinter.js index 97d53b4..1b2b3db 100644 --- a/src/api/agentprinter.js +++ b/src/api/agentprinter.js @@ -41,16 +41,19 @@ export const agentprinter = { /** * PUT /api/agentprinter/order - * Body: { printeragentId: number, agentprinterIds: number[] } + * Body: { printeragentId?: number, agentprinterIds: number[] } * → { agentprinterIds: number[] } */ order({ printeragentId, agentprinterIds }) { + const body = { + agentprinterIds: Array.isArray(agentprinterIds) ? agentprinterIds.map(Number) : [], + }; + if (printeragentId != null) { + body.printeragentId = Number(printeragentId); + } return http('/api/agentprinter/order', { method: 'PUT', - body: { - printeragentId: Number(printeragentId), - agentprinterIds: Array.isArray(agentprinterIds) ? agentprinterIds.map(Number) : [], - }, + body, }); }, diff --git a/src/components/settings/PrinterAgentSettingsPanel.jsx b/src/components/settings/PrinterAgentSettingsPanel.jsx index df5b64b..60041d8 100644 --- a/src/components/settings/PrinterAgentSettingsPanel.jsx +++ b/src/components/settings/PrinterAgentSettingsPanel.jsx @@ -7,7 +7,6 @@ import FormControlLabel from '@mui/material/FormControlLabel'; import IconButton from '@mui/material/IconButton'; import Switch from '@mui/material/Switch'; import TextField from '@mui/material/TextField'; -import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import AddIcon from '@mui/icons-material/Add'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; @@ -259,12 +258,12 @@ export default class PrinterAgentSettingsPanel extends PureComponent { error: state.printeragentsError ? backendErrorFrom(state.printeragentsError) : null, })); if (companyAutomat.state.printeragentsStatus === 'idle') { - void companyAutomat.actions.loadPrinterAgents().catch(() => {}); + void companyAutomat.actions.loadPrinterAgents().catch(() => { }); } if (companyAutomat.state.agentprintersStatus === 'idle') { - void companyAutomat.actions.loadAgentPrinters().catch(() => {}); + void companyAutomat.actions.loadAgentPrinters().catch(() => { }); } - void companyAutomat.actions.loadLivePrinters().catch(() => {}); + void companyAutomat.actions.loadLivePrinters().catch(() => { }); } componentWillUnmount() { @@ -274,7 +273,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { handleRetry() { void companyAutomat.actions.loadPrinterAgents().catch(notifyError); void companyAutomat.actions.loadAgentPrinters().catch(notifyError); - void companyAutomat.actions.loadLivePrinters().catch(() => {}); + void companyAutomat.actions.loadLivePrinters().catch(() => { }); } async handleCreateAgent() { @@ -440,21 +439,17 @@ export default class PrinterAgentSettingsPanel extends PureComponent { async handleMovePrinter(printer, direction) { const { agentprinters } = this.state; - const printersForAgent = agentprinters.filter( - (p) => p.printeragentId === printer.printeragentId, - ); - const currentIndex = printersForAgent.findIndex((p) => p.id === printer.id); + const currentIndex = agentprinters.findIndex((p) => p.id === printer.id); if (currentIndex === -1) return; const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; - if (targetIndex < 0 || targetIndex >= printersForAgent.length) return; + if (targetIndex < 0 || targetIndex >= agentprinters.length) return; - const reordered = [...printersForAgent]; + const reordered = [...agentprinters]; const [moved] = reordered.splice(currentIndex, 1); reordered.splice(targetIndex, 0, moved); try { await companyAutomat.actions.reorderAgentPrinters({ - printeragentId: printer.printeragentId, agentprinterIds: reordered.map((p) => p.id), }); } catch (err) { @@ -463,6 +458,11 @@ export default class PrinterAgentSettingsPanel extends PureComponent { } async handleDeletePrinter(printer) { + const isOnline = Boolean(printer?.online); + if (isOnline) { + enqueueSnackbar('Online-Drucker können nicht entfernt werden.', { variant: 'warning' }); + return; + } const label = printer.model || printer.serial || `#${printer.id}`; if (!window.confirm(`Möchten Sie den Drucker „${label}“ wirklich entfernen?`)) { return; @@ -521,6 +521,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { for (const lp of livePrinters) { if (lp.serial) livePrintersBySerial.set(lp.serial, lp); } + const agentMap = new Map(printeragents.map((a) => [a.id, a])); return ( @@ -528,18 +529,13 @@ export default class PrinterAgentSettingsPanel extends PureComponent { Drucker-Agenten - - Verwalten Sie externe Drucker-Agenten und die daran angeschlossenen Drucker. Nach - Erstellung eines Agenten geben Sie die 6-stellige PIN auf dem Gerät ein oder scannen - Sie den QR-Code, um die Kopplung abzuschließen. - {/* Add new agent row */} this.setState({ newAgentName: e.target.value })} onKeyDown={(e) => { @@ -595,10 +591,6 @@ export default class PrinterAgentSettingsPanel extends PureComponent { const lastseenText = formatDateTime(agent.lastseenAt) ?? 'Nie'; const updatedText = formatDateTime(agent.updatedAt) ?? '—'; - const printersForAgent = agentprinters.filter( - (p) => p.printeragentId === agent.id, - ); - return ( @@ -727,15 +719,13 @@ export default class PrinterAgentSettingsPanel extends PureComponent { {/* Agent version */} {agent.agentversion ? ( - - - + ) : null} @@ -808,39 +798,35 @@ export default class PrinterAgentSettingsPanel extends PureComponent { borderColor: 'warning.light', }} > - - - - - + + + Kopplungs-PIN: - - } - label={agent.pairingpin || '—'} - onClick={() => { - if (agent.pairingpin) { - navigator.clipboard?.writeText(agent.pairingpin); - enqueueSnackbar('PIN in Zwischenablage kopiert.', { variant: 'info' }); - } - }} - clickable - color="warning" - variant="filled" - sx={{ - fontFamily: 'monospace', - fontWeight: 700, - fontSize: '0.9rem', - letterSpacing: '0.1em', - px: 0.5, - }} - /> - + } + label={agent.pairingpin || '—'} + onClick={() => { + if (agent.pairingpin) { + navigator.clipboard?.writeText(agent.pairingpin); + enqueueSnackbar('PIN in Zwischenablage kopiert.', { variant: 'info' }); + } + }} + clickable + color="warning" + variant="filled" + sx={{ + fontFamily: 'monospace', + fontWeight: 700, + fontSize: '0.9rem', + letterSpacing: '0.1em', + px: 0.5, + }} + /> @@ -853,18 +839,16 @@ export default class PrinterAgentSettingsPanel extends PureComponent { variant="outlined" sx={{ fontFamily: 'monospace', fontSize: '0.8rem' }} /> - - { - navigator.clipboard?.writeText(qrPayload); - enqueueSnackbar('QR-Daten kopiert.', { variant: 'info' }); - }} - sx={{ p: 0.5 }} - > - - - + { + navigator.clipboard?.writeText(qrPayload); + enqueueSnackbar('QR-Daten kopiert.', { variant: 'info' }); + }} + sx={{ p: 0.5 }} + > + + @@ -874,170 +858,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { ); })() - ) : ( - /* Discovered printers section */ - - - - Erkannte Drucker ({printersForAgent.length}) - - - {printersForAgent.length === 0 ? ( - - Keine Drucker erkannt. Schließen Sie einen Drucker per USB an den Agenten an. - - ) : ( - - {printersForAgent.map((p, pIdx) => { - const pEnabled = p.enabled !== false; - const live = - (p.serial ? livePrintersBySerial.get(p.serial) : null) ?? - livePrinters.find( - (lp) => lp.agentId === agent.id && lp.serial === p.serial, - ); - - return ( - - - - - {p.model || 'Drucker'} - - {p.type ? ( - - ) : null} - {p.serial ? ( - - ({p.serial}) - - ) : null} - {renderPrinterDiagnostics(p, live)} - - - - {/* Test print button */} - - - - - - - {/* Enabled toggle switch */} - - void this.handleTogglePrinterEnabled(p, e.target.checked) - } - /> - } - label={ - - {pEnabled ? 'Aktiv' : 'Inaktiv'} - - } - sx={{ m: 0 }} - /> - - {/* Reorder buttons */} - - - - void this.handleMovePrinter(p, 'up')} - sx={{ p: 0.25 }} - > - - - - - - - void this.handleMovePrinter(p, 'down')} - sx={{ p: 0.25 }} - > - - - - - - - {/* Delete printer button */} - - void this.handleDeletePrinter(p)} - sx={{ p: 0.5 }} - > - - - - - - ); - })} - - )} - - )} + ) : null} {/* Agent sort order controls */} Reihenfolge: - - - void this.handleMoveAgent(agent, 'up')} - sx={{ p: 0.25 }} - > - - - - - - - void this.handleMoveAgent(agent, 'down')} - sx={{ p: 0.25 }} - > - - - - + + void this.handleMoveAgent(agent, 'up')} + sx={{ p: 0.25 }} + > + + + + + void this.handleMoveAgent(agent, 'down')} + sx={{ p: 0.25 }} + > + + + )} + + {/* Separate Printers Section */} + + + + + Drucker + + + + {agentprinters.length === 0 ? ( + + + + Keine Drucker erkannt. Schließen Sie Drucker per USB an einen gekoppelten Drucker-Agenten an. + + + ) : ( + + {agentprinters.map((p, pIdx) => { + const pEnabled = p.enabled !== false; + const live = + (p.serial ? livePrintersBySerial.get(p.serial) : null) ?? + livePrinters.find( + (lp) => lp.agentId === p.printeragentId && lp.serial === p.serial, + ); + const isOnline = Boolean( + p.online ?? (live ? !live.unreachable && live.status !== 'offline' : false), + ); + const parentAgent = agentMap.get(p.printeragentId); + + return ( + + + + + {p.model || 'Drucker'} + + {p.type ? ( + + ) : null} + {p.serial ? ( + + ({p.serial}) + + ) : null} + {parentAgent ? ( + + ) : null} + {renderPrinterDiagnostics(p, live)} + + + + {/* Test print button */} + + + + + {/* Enabled toggle switch */} + + void this.handleTogglePrinterEnabled(p, e.target.checked) + } + /> + } + label={ + + {pEnabled ? 'Aktiv' : 'Inaktiv'} + + } + sx={{ m: 0 }} + /> + + {/* Reorder buttons across all printers */} + + + void this.handleMovePrinter(p, 'up')} + sx={{ p: 0.25 }} + > + + + + + + void this.handleMovePrinter(p, 'down')} + sx={{ p: 0.25 }} + > + + + + + + {/* Delete printer button: disabled when online! */} + + void this.handleDeletePrinter(p)} + sx={{ p: 0.5 }} + > + + + + + + ); + })} + + )} ); } diff --git a/src/routes/infos/GlobalHistoryPage.jsx b/src/routes/infos/GlobalHistoryPage.jsx index 03c3687..650c475 100644 --- a/src/routes/infos/GlobalHistoryPage.jsx +++ b/src/routes/infos/GlobalHistoryPage.jsx @@ -5,7 +5,6 @@ import CircularProgress from '@mui/material/CircularProgress'; import ListItemButton from '@mui/material/ListItemButton'; import Typography from '@mui/material/Typography'; import Skeleton from '@mui/material/Skeleton'; -import Tooltip from '@mui/material/Tooltip'; import ViewModuleIcon from '@mui/icons-material/ViewModule'; import Chip from '@mui/material/Chip'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; @@ -147,8 +146,8 @@ class GlobalHistoryRow extends PureComponent { onClick={ isArticle ? () => { - void router.navigate(`/artikel/${encodeURIComponent(refStr)}`); - } + void router.navigate(`/artikel/${encodeURIComponent(refStr)}`); + } : undefined } > @@ -207,11 +206,9 @@ class GlobalHistoryRow extends PureComponent { {/* 4. Meldung / Beschreibung */} - - - {item.text || item.value || '–'} - - + + {item.text || item.value || '–'} + ); @@ -445,11 +442,11 @@ export default class GlobalHistoryPage extends PureComponent { } handleReload() { - void globalHistoryAutomat.actions.reload().catch(() => {}); + void globalHistoryAutomat.actions.reload().catch(() => { }); } handleRetry() { - void globalHistoryAutomat.actions.requireChunk(0, 0, { immediate: true }).catch(() => {}); + void globalHistoryAutomat.actions.requireChunk(0, 0, { immediate: true }).catch(() => { }); } render() {