u
This commit is contained in:
1081
package-lock.json
generated
1081
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,7 @@
|
|||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"react": "^19.3.1",
|
"react": "^19.3.1",
|
||||||
"react-automat": "file:../automat",
|
"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-dom": "^19.3.1",
|
||||||
"react-router-dom": "^7.18.4",
|
"react-router-dom": "^7.18.4",
|
||||||
"react-window": "^2.3.4",
|
"react-window": "^2.3.4",
|
||||||
@@ -62,6 +62,7 @@
|
|||||||
},
|
},
|
||||||
"allowScripts": {
|
"allowScripts": {
|
||||||
"canvas@3.2.3": true,
|
"canvas@3.2.3": true,
|
||||||
|
"react-canvas-label-editor@20.0.0": true,
|
||||||
"github:sebseb7/react-canvas-label-editor#7f0e31b66ff22da011e872b201017398b14e9dd9": true
|
"github:sebseb7/react-canvas-label-editor#7f0e31b66ff22da011e872b201017398b14e9dd9": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,5 +48,23 @@ export const labeltemplate = {
|
|||||||
},
|
},
|
||||||
// #endregion
|
// #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
|
// thumb is returned inline as a base64 string in list/get responses
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,27 +9,69 @@ import DialogTitle from '@mui/material/DialogTitle';
|
|||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import InputAdornment from '@mui/material/InputAdornment';
|
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 TextField from '@mui/material/TextField';
|
||||||
import ToggleButton from '@mui/material/ToggleButton';
|
import ToggleButton from '@mui/material/ToggleButton';
|
||||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import LabelIcon from '@mui/icons-material/Label';
|
import LabelIcon from '@mui/icons-material/Label';
|
||||||
|
import PrintIcon from '@mui/icons-material/Print';
|
||||||
import BackendError from '../../components/BackendError';
|
import BackendError from '../../components/BackendError';
|
||||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { setLastArticleSku } from '../../automats/navigationAutomat';
|
import { setLastArticleSku } from '../../automats/navigationAutomat';
|
||||||
import { labeltemplate } from '../../api';
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
|
import { labeltemplate, printer } from '../../api';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import {
|
import {
|
||||||
BLUE_BG,
|
BLUE_BG,
|
||||||
SURFACE_BASE,
|
SURFACE_BASE,
|
||||||
} from '../../colors';
|
} 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 () => {
|
const CanvasEditor = lazy(async () => {
|
||||||
await import('react-canvas-label-editor/style.css');
|
await import('react-canvas-label-editor/style.css');
|
||||||
const mod = await import('react-canvas-label-editor');
|
const mod = await import('react-canvas-label-editor');
|
||||||
@@ -65,11 +107,15 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
const initialDpi = DEFAULT_DPI;
|
const initialDpi = DEFAULT_DPI;
|
||||||
const initialWidthClass = '2';
|
const initialWidthClass = '2';
|
||||||
const initialHeightMm = DEFAULT_HEIGHT_MM;
|
const initialHeightMm = DEFAULT_HEIGHT_MM;
|
||||||
|
const initialPrinters = filterZplPrinters(companyAutomat.state.agentprinters);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
status: 'loading',
|
status: 'loading',
|
||||||
error: null,
|
error: null,
|
||||||
templates: [],
|
templates: [],
|
||||||
|
printers: initialPrinters,
|
||||||
|
printerMenuAnchor: null,
|
||||||
|
printing: false,
|
||||||
// Editor dialog state
|
// Editor dialog state
|
||||||
editorOpen: false,
|
editorOpen: false,
|
||||||
editorLoading: false,
|
editorLoading: false,
|
||||||
@@ -88,6 +134,7 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
};
|
};
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
this._currentEditId = null;
|
this._currentEditId = null;
|
||||||
|
this._legacyPrinters = [];
|
||||||
this.handleRetry = this.handleRetry.bind(this);
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
this.handleOpenNew = this.handleOpenNew.bind(this);
|
this.handleOpenNew = this.handleOpenNew.bind(this);
|
||||||
this.handleOpenEdit = this.handleOpenEdit.bind(this);
|
this.handleOpenEdit = this.handleOpenEdit.bind(this);
|
||||||
@@ -97,16 +144,40 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
this.handleDpiChange = this.handleDpiChange.bind(this);
|
this.handleDpiChange = this.handleDpiChange.bind(this);
|
||||||
this.handleHeightMmChange = this.handleHeightMmChange.bind(this);
|
this.handleHeightMmChange = this.handleHeightMmChange.bind(this);
|
||||||
this.handleHeightMmBlur = this.handleHeightMmBlur.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() {
|
componentDidMount() {
|
||||||
setLastArticleSku(null);
|
setLastArticleSku(null);
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
void this.loadTemplates();
|
void this.loadTemplates();
|
||||||
|
this.unsubPrinters = companyAutomat.subscribe(this, (state) => ({
|
||||||
|
printers: filterZplPrinters(state.agentprinters, this._legacyPrinters),
|
||||||
|
}));
|
||||||
|
void companyAutomat.actions.loadAgentPrinters().catch(() => {});
|
||||||
|
void this.loadFallbackPrinters();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._cancelled = true;
|
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() {
|
async loadTemplates() {
|
||||||
@@ -246,9 +317,67 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleEditorClose() {
|
handleEditorClose() {
|
||||||
if (this.state.saving) return;
|
if (this.state.saving || this.state.printing) return;
|
||||||
this._currentEditId = null;
|
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() {
|
async handleEditorSave() {
|
||||||
@@ -320,7 +449,7 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
const {
|
const {
|
||||||
editorOpen, editorLoading, editorTemplate, editorName, editorObjects,
|
editorOpen, editorLoading, editorTemplate, editorName, editorObjects,
|
||||||
editorHeight, editorWidth, editorDpi, editorWidthClass, editorHeightMm,
|
editorHeight, editorWidth, editorDpi, editorWidthClass, editorHeightMm,
|
||||||
editorClipboard, saving,
|
editorClipboard, saving, printing, printers, printerMenuAnchor,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const minHeightPx = getMinHeightPx(editorDpi);
|
const minHeightPx = getMinHeightPx(editorDpi);
|
||||||
@@ -438,18 +567,66 @@ class ArtikelVorlagenPage extends PureComponent {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
<DialogActions sx={{ px: 3, pb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Button onClick={this.handleEditorClose} disabled={saving}>
|
<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
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={this.handleEditorSave}
|
onClick={this.handleEditorSave}
|
||||||
disabled={saving || editorLoading || !editorName.trim()}
|
disabled={saving || printing || editorLoading || !editorName.trim()}
|
||||||
>
|
>
|
||||||
{saving ? <CircularProgress size={18} sx={{ mr: 1 }} /> : null}
|
{saving ? <CircularProgress size={18} sx={{ mr: 1 }} /> : null}
|
||||||
{saving ? 'Speichert…' : 'Speichern'}
|
{saving ? 'Speichert…' : 'Speichern'}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Box>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</AppDialog>
|
</AppDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user