This commit is contained in:
seb
2026-11-01 04:33:15 +01:00
parent 7fe9f90f2c
commit 8a416ce384
4 changed files with 231 additions and 1080 deletions

1081
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,7 +41,7 @@
"pdfconvert-wasm": "^0.3.0",
"react": "^19.3.1",
"react-automat": "file:../automat",
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor#v20.0.0",
"react-dom": "^19.3.1",
"react-router-dom": "^7.18.4",
"react-window": "^2.3.4",
@@ -62,6 +62,7 @@
},
"allowScripts": {
"canvas@3.2.3": true,
"react-canvas-label-editor@20.0.0": true,
"github:sebseb7/react-canvas-label-editor#7f0e31b66ff22da011e872b201017398b14e9dd9": true
}
}

View File

@@ -48,5 +48,23 @@ export const labeltemplate = {
},
// #endregion
/**
* POST /api/labeltemplate/testprint (or /api/labeltemplate/:id/testprint)
* @param {number|object} idOrBody
* @param {object} [maybeBody]
* → 200 { success: true, printerjob?: PrinterJob }
*/
// #region POST /api/labeltemplate/testprint — testprint
testprint(idOrBody, maybeBody) {
if (typeof idOrBody === 'object' && idOrBody !== null) {
return http('/api/labeltemplate/testprint', { method: 'POST', body: idOrBody });
}
return http(`/api/labeltemplate/${encodeURIComponent(idOrBody)}/testprint`, {
method: 'POST',
body: maybeBody || {},
});
},
// #endregion
// thumb is returned inline as a base64 string in list/get responses
};

View File

@@ -9,27 +9,69 @@ import DialogTitle from '@mui/material/DialogTitle';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import InputAdornment from '@mui/material/InputAdornment';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import TextField from '@mui/material/TextField';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import AddIcon from '@mui/icons-material/Add';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import EditIcon from '@mui/icons-material/Edit';
import LabelIcon from '@mui/icons-material/Label';
import PrintIcon from '@mui/icons-material/Print';
import BackendError from '../../components/BackendError';
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
import { setLastArticleSku } from '../../automats/navigationAutomat';
import { labeltemplate } from '../../api';
import companyAutomat from '../../automats/companyAutomat';
import { labeltemplate, printer } from '../../api';
import { enqueueSnackbar } from 'notistack';
import {
BLUE_BG,
SURFACE_BASE,
} from '../../colors';
function filterZplPrinters(agentprinters, fallbackPrinters = []) {
const filteredAgent = (Array.isArray(agentprinters) ? agentprinters : []).filter(
(p) => p.enabled !== false && (!p.type || p.type.toLowerCase() === 'zpl'),
);
if (filteredAgent.length > 0) return filteredAgent;
return (Array.isArray(fallbackPrinters) ? fallbackPrinters : []).filter(
(p) => !p.type || p.type.toLowerCase() === 'zpl',
);
}
function getPrinterLabel(p) {
let label;
if (p.model && p.serial) {
label = `${p.model} (${p.serial})`;
} else if (p.model) {
label = p.model;
} else if (p.serial) {
label = `Drucker (${p.serial})`;
} else if (p.desc && p.address) {
label = `${p.desc} (${p.address})`;
} else if (p.address) {
label = p.address;
} else if (p.desc) {
label = p.desc;
} else {
label = `Drucker #${p.id}`;
}
if (p.online === false) {
return `${label} (offline)`;
}
return label;
}
const CanvasEditor = lazy(async () => {
await import('react-canvas-label-editor/style.css');
const mod = await import('react-canvas-label-editor');
@@ -65,11 +107,15 @@ class ArtikelVorlagenPage extends PureComponent {
const initialDpi = DEFAULT_DPI;
const initialWidthClass = '2';
const initialHeightMm = DEFAULT_HEIGHT_MM;
const initialPrinters = filterZplPrinters(companyAutomat.state.agentprinters);
this.state = {
status: 'loading',
error: null,
templates: [],
printers: initialPrinters,
printerMenuAnchor: null,
printing: false,
// Editor dialog state
editorOpen: false,
editorLoading: false,
@@ -88,6 +134,7 @@ class ArtikelVorlagenPage extends PureComponent {
};
this._cancelled = false;
this._currentEditId = null;
this._legacyPrinters = [];
this.handleRetry = this.handleRetry.bind(this);
this.handleOpenNew = this.handleOpenNew.bind(this);
this.handleOpenEdit = this.handleOpenEdit.bind(this);
@@ -97,16 +144,40 @@ class ArtikelVorlagenPage extends PureComponent {
this.handleDpiChange = this.handleDpiChange.bind(this);
this.handleHeightMmChange = this.handleHeightMmChange.bind(this);
this.handleHeightMmBlur = this.handleHeightMmBlur.bind(this);
this.handlePrintClick = this.handlePrintClick.bind(this);
this.handleClosePrinterMenu = this.handleClosePrinterMenu.bind(this);
this.handleSelectPrinterForPrint = this.handleSelectPrinterForPrint.bind(this);
}
componentDidMount() {
setLastArticleSku(null);
this._cancelled = false;
void this.loadTemplates();
this.unsubPrinters = companyAutomat.subscribe(this, (state) => ({
printers: filterZplPrinters(state.agentprinters, this._legacyPrinters),
}));
void companyAutomat.actions.loadAgentPrinters().catch(() => {});
void this.loadFallbackPrinters();
}
componentWillUnmount() {
this._cancelled = true;
this.unsubPrinters?.();
}
async loadFallbackPrinters() {
try {
const resPrinters = await printer.list();
this._legacyPrinters = (resPrinters.printers || []).filter(
(p) => !p.type || p.type.toLowerCase() === 'zpl',
);
if (this._cancelled) return;
this.setState({
printers: filterZplPrinters(companyAutomat.state.agentprinters, this._legacyPrinters),
});
} catch {
// ignore
}
}
async loadTemplates() {
@@ -246,9 +317,67 @@ class ArtikelVorlagenPage extends PureComponent {
}
handleEditorClose() {
if (this.state.saving) return;
if (this.state.saving || this.state.printing) return;
this._currentEditId = null;
this.setState({ editorOpen: false, editorLoading: false, editorTemplate: null });
this.setState({
editorOpen: false,
editorLoading: false,
editorTemplate: null,
printerMenuAnchor: null,
});
}
handlePrintClick(event) {
const { printers } = this.state;
if (!printers || printers.length === 0) {
enqueueSnackbar('Kein ZPL-Drucker gefunden', { variant: 'warning' });
return;
}
if (printers.length === 1) {
void this.executeTestPrint(printers[0]);
return;
}
this.setState({ printerMenuAnchor: event.currentTarget });
}
handleClosePrinterMenu() {
this.setState({ printerMenuAnchor: null });
}
handleSelectPrinterForPrint(targetPrinter) {
this.setState({ printerMenuAnchor: null });
void this.executeTestPrint(targetPrinter);
}
async executeTestPrint(targetPrinter) {
if (!targetPrinter?.id) return;
const {
editorTemplate, editorObjects, editorWidth, editorHeight, editorDpi, editorName,
} = this.state;
this.setState({ printing: true });
try {
const payload = {
printerId: targetPrinter.id,
agentprinterId: targetPrinter.printeragentId || targetPrinter.id,
templateId: editorTemplate?.id,
name: editorName,
width: editorWidth,
height: editorHeight,
dpi: editorDpi,
objects: editorObjects,
};
await labeltemplate.testprint(payload);
enqueueSnackbar(`Testdruck an ${getPrinterLabel(targetPrinter)} gesendet.`, {
variant: 'success',
});
} catch (err) {
notifyError(err);
} finally {
if (!this._cancelled) {
this.setState({ printing: false });
}
}
}
async handleEditorSave() {
@@ -320,7 +449,7 @@ class ArtikelVorlagenPage extends PureComponent {
const {
editorOpen, editorLoading, editorTemplate, editorName, editorObjects,
editorHeight, editorWidth, editorDpi, editorWidthClass, editorHeightMm,
editorClipboard, saving,
editorClipboard, saving, printing, printers, printerMenuAnchor,
} = this.state;
const minHeightPx = getMinHeightPx(editorDpi);
@@ -438,18 +567,66 @@ class ArtikelVorlagenPage extends PureComponent {
</>
)}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={this.handleEditorClose} disabled={saving}>
<DialogActions sx={{ px: 3, pb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Tooltip
title={
printers.length === 0
? 'Kein ZPL-Drucker verfügbar'
: printers.length === 1
? `Drucken auf ${getPrinterLabel(printers[0])}`
: 'Testdruck an Drucker senden'
}
>
<span>
<Button
variant="outlined"
color="primary"
startIcon={printing ? <CircularProgress size={18} /> : <PrintIcon />}
endIcon={printers.length > 1 ? <ArrowDropDownIcon /> : null}
onClick={this.handlePrintClick}
disabled={printing || saving || editorLoading || printers.length === 0}
>
{printing ? 'Druckt…' : 'Testdruck'}
</Button>
</span>
</Tooltip>
{printers.length > 1 && (
<Menu
anchorEl={printerMenuAnchor}
open={Boolean(printerMenuAnchor)}
onClose={this.handleClosePrinterMenu}
>
{printers.map((p) => (
<MenuItem
key={p.id}
onClick={() => this.handleSelectPrinterForPrint(p)}
>
<ListItemIcon>
<PrintIcon fontSize="small" />
</ListItemIcon>
<ListItemText
primary={getPrinterLabel(p)}
secondary={p.serial ? `Seriennummer: ${p.serial}` : undefined}
/>
</MenuItem>
))}
</Menu>
)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button onClick={this.handleEditorClose} disabled={saving || printing}>
Abbrechen
</Button>
<Button
variant="contained"
onClick={this.handleEditorSave}
disabled={saving || editorLoading || !editorName.trim()}
disabled={saving || printing || editorLoading || !editorName.trim()}
>
{saving ? <CircularProgress size={18} sx={{ mr: 1 }} /> : null}
{saving ? 'Speichert…' : 'Speichern'}
</Button>
</Box>
</DialogActions>
</AppDialog>
);