This commit is contained in:
seb
2026-10-31 21:45:02 +01:00
parent f6eb31b70d
commit 2d1a7364f2
5 changed files with 343 additions and 277 deletions

View File

@@ -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: '<Tooltip> 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',
{

View File

@@ -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",

View File

@@ -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,
});
},

View File

@@ -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 (
<Box sx={{ p: 3, maxWidth: 900, mx: 'auto' }}>
@@ -528,18 +529,13 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
<Typography variant="h6" fontWeight={600} gutterBottom>
Drucker-Agenten
</Typography>
<Typography variant="body2" color="text.secondary">
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.
</Typography>
</Box>
{/* Add new agent row */}
<Box sx={{ display: 'flex', gap: 1.5, mb: 3, alignItems: 'center' }}>
<TextField
size="small"
placeholder="Neuer Drucker-Agent Name (z.B. Bar, Küche)"
placeholder="Agent Name (z.B. Bar, Küche)"
value={newAgentName}
onChange={(e) => 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 (
<Box
key={agent.id}
@@ -727,7 +719,6 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
{/* Agent version */}
{agent.agentversion ? (
<Tooltip title="Agent Software-Version">
<Chip
label={`v${agent.agentversion}`}
size="small"
@@ -735,7 +726,6 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
color="info"
sx={{ fontFamily: 'monospace', fontWeight: 600, fontSize: '0.75rem' }}
/>
</Tooltip>
) : null}
</Box>
@@ -808,18 +798,15 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
borderColor: 'warning.light',
}}
>
<Tooltip title="QR-Code zum Koppeln mit dem Drucker-Agenten">
<Box sx={{ display: 'inline-flex' }}>
<AgentQrCode value={qrPayload} size={110} />
</Box>
</Tooltip>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 220 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 500 }}>
Kopplungs-PIN:
</Typography>
<Tooltip title="PIN in die Zwischenablage kopieren">
<Chip
icon={<ContentCopyIcon sx={{ fontSize: 14 }} />}
label={agent.pairingpin || '—'}
@@ -840,7 +827,6 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
px: 0.5,
}}
/>
</Tooltip>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
@@ -853,7 +839,6 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
variant="outlined"
sx={{ fontFamily: 'monospace', fontSize: '0.8rem' }}
/>
<Tooltip title="QR-Daten (JSON) kopieren">
<IconButton
size="small"
onClick={() => {
@@ -864,7 +849,6 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
>
<ContentCopyIcon sx={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</Box>
<Typography variant="caption" color="text.secondary">
@@ -874,170 +858,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
</Box>
);
})()
) : (
/* Discovered printers section */
<Box sx={{ p: 1.5, borderRadius: 1.5, bgcolor: 'action.hover' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" fontWeight={600} color="text.secondary">
Erkannte Drucker ({printersForAgent.length})
</Typography>
</Box>
{printersForAgent.length === 0 ? (
<Typography
variant="body2"
color="text.secondary"
sx={{ fontStyle: 'italic', py: 0.5 }}
>
Keine Drucker erkannt. Schließen Sie einen Drucker per USB an den Agenten an.
</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{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 (
<Box
key={p.id}
sx={{
p: 1.25,
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'background.paper',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1.5,
flexWrap: 'wrap',
opacity: pEnabled ? 1 : 0.65,
transition: 'opacity 0.2s ease',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<PrintOutlinedIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
<Typography variant="body2" fontWeight={600}>
{p.model || 'Drucker'}
</Typography>
{p.type ? (
<Chip
label={p.type.toUpperCase()}
size="small"
color="primary"
variant="outlined"
sx={{ fontWeight: 600, fontSize: '0.7rem', height: 20 }}
/>
) : null}
{p.serial ? (
<Typography
variant="caption"
color="text.secondary"
sx={{ fontFamily: 'monospace' }}
>
({p.serial})
</Typography>
) : null}
{renderPrinterDiagnostics(p, live)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Test print button */}
<Tooltip title="Test-Etikett an Drucker senden (ZPL)">
<span>
<Button
size="small"
variant="outlined"
color="primary"
startIcon={<PrintOutlinedIcon sx={{ fontSize: 15 }} />}
disabled={testingPrinterId === p.id}
onClick={() => void this.handleTestPrint(p)}
sx={{
textTransform: 'none',
fontSize: '0.75rem',
py: 0.25,
px: 1,
minHeight: 26,
fontWeight: 600,
}}
>
{testingPrinterId === p.id ? 'Druckt…' : 'Testdruck'}
</Button>
</span>
</Tooltip>
{/* Enabled toggle switch */}
<FormControlLabel
control={
<Switch
size="small"
checked={pEnabled}
onChange={(e) =>
void this.handleTogglePrinterEnabled(p, e.target.checked)
}
/>
}
label={
<Typography
variant="caption"
color={pEnabled ? 'text.primary' : 'text.disabled'}
>
{pEnabled ? 'Aktiv' : 'Inaktiv'}
</Typography>
}
sx={{ m: 0 }}
/>
{/* Reorder buttons */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title="Nach oben verschieben">
<span>
<IconButton
size="small"
disabled={pIdx === 0}
onClick={() => void this.handleMovePrinter(p, 'up')}
sx={{ p: 0.25 }}
>
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Nach unten verschieben">
<span>
<IconButton
size="small"
disabled={pIdx === printersForAgent.length - 1}
onClick={() => void this.handleMovePrinter(p, 'down')}
sx={{ p: 0.25 }}
>
<ArrowDownwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
</Box>
{/* Delete printer button */}
<Tooltip title="Drucker entfernen">
<IconButton
size="small"
color="error"
onClick={() => void this.handleDeletePrinter(p)}
sx={{ p: 0.5 }}
>
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
</IconButton>
</Tooltip>
</Box>
</Box>
);
})}
</Box>
)}
</Box>
)}
{/* Agent sort order controls */}
<Box
@@ -1052,7 +873,6 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
<Typography variant="body2" color="text.secondary">
Reihenfolge:
</Typography>
<Tooltip title="Nach oben verschieben">
<span>
<IconButton
size="small"
@@ -1063,8 +883,6 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Nach unten verschieben">
<span>
<IconButton
size="small"
@@ -1075,7 +893,6 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
<ArrowDownwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<TextField
type="number"
size="small"
@@ -1128,6 +945,187 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
})}
</Box>
)}
{/* Separate Printers Section */}
<Divider sx={{ my: 4 }} />
<Box sx={{ mb: 2 }}>
<Typography variant="h6" fontWeight={600} gutterBottom>
Drucker
</Typography>
</Box>
{agentprinters.length === 0 ? (
<Box
sx={{
textAlign: 'center',
py: 5,
color: 'text.secondary',
bgcolor: 'action.hover',
borderRadius: 2,
border: '1px dashed',
borderColor: 'divider',
}}
>
<PrintOutlinedIcon sx={{ fontSize: 44, mb: 1, opacity: 0.5 }} />
<Typography variant="body2">
Keine Drucker erkannt. Schließen Sie Drucker per USB an einen gekoppelten Drucker-Agenten an.
</Typography>
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{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 (
<Box
key={p.id}
sx={{
p: 1.5,
borderRadius: 1.5,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'background.paper',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1.5,
flexWrap: 'wrap',
opacity: pEnabled ? 1 : 0.65,
transition: 'opacity 0.2s ease',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<PrintOutlinedIcon sx={{ fontSize: 22, color: 'text.secondary' }} />
<Typography variant="body2" fontWeight={600}>
{p.model || 'Drucker'}
</Typography>
{p.type ? (
<Chip
label={p.type.toUpperCase()}
size="small"
color="primary"
variant="outlined"
sx={{ fontWeight: 600, fontSize: '0.7rem', height: 20 }}
/>
) : null}
{p.serial ? (
<Typography
variant="caption"
color="text.secondary"
sx={{ fontFamily: 'monospace' }}
>
({p.serial})
</Typography>
) : null}
{parentAgent ? (
<Chip
label={`Agent: ${parentAgent.name}`}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem', height: 20 }}
/>
) : null}
{renderPrinterDiagnostics(p, live)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Test print button */}
<span>
<Button
size="small"
variant="outlined"
color="primary"
startIcon={<PrintOutlinedIcon sx={{ fontSize: 15 }} />}
disabled={testingPrinterId === p.id}
onClick={() => void this.handleTestPrint(p)}
sx={{
textTransform: 'none',
fontSize: '0.75rem',
py: 0.25,
px: 1,
minHeight: 26,
fontWeight: 600,
}}
>
{testingPrinterId === p.id ? 'Druckt…' : 'Testdruck'}
</Button>
</span>
{/* Enabled toggle switch */}
<FormControlLabel
control={
<Switch
size="small"
checked={pEnabled}
onChange={(e) =>
void this.handleTogglePrinterEnabled(p, e.target.checked)
}
/>
}
label={
<Typography
variant="caption"
color={pEnabled ? 'text.primary' : 'text.disabled'}
>
{pEnabled ? 'Aktiv' : 'Inaktiv'}
</Typography>
}
sx={{ m: 0 }}
/>
{/* Reorder buttons across all printers */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<span>
<IconButton
size="small"
disabled={pIdx === 0}
onClick={() => void this.handleMovePrinter(p, 'up')}
sx={{ p: 0.25 }}
>
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
<span>
<IconButton
size="small"
disabled={pIdx === agentprinters.length - 1}
onClick={() => void this.handleMovePrinter(p, 'down')}
sx={{ p: 0.25 }}
>
<ArrowDownwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Box>
{/* Delete printer button: disabled when online! */}
<span>
<IconButton
size="small"
color="error"
disabled={isOnline}
onClick={() => void this.handleDeletePrinter(p)}
sx={{ p: 0.5 }}
>
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
</IconButton>
</span>
</Box>
</Box>
);
})}
</Box>
)}
</Box>
);
}

View File

@@ -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';
@@ -207,11 +206,9 @@ class GlobalHistoryRow extends PureComponent {
{/* 4. Meldung / Beschreibung */}
<Box sx={{ gridColumn: 4, gridRow: 2, minWidth: 0 }}>
<Tooltip title={item.text || item.value || ''} placement="top-start" enterDelay={500}>
<Typography variant="body2" sx={ellipsisSx}>
{item.text || item.value || '–'}
</Typography>
</Tooltip>
</Box>
</ListItemButton>
);
@@ -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() {