import { PureComponent, lazy, Suspense } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import AppDialog from '../../components/AppDialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; 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 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 DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import EditIcon from '@mui/icons-material/Edit'; import LabelIcon from '@mui/icons-material/Label'; 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 { enqueueSnackbar } from 'notistack'; import { BLUE_BG, SURFACE_BASE, } from '../../colors'; const CanvasEditor = lazy(async () => { await import('react-canvas-label-editor/style.css'); const mod = await import('react-canvas-label-editor'); return { default: mod.CanvasEditor }; }); const MIN_HEIGHT_MM = 10; const DEFAULT_HEIGHT_MM = 25; const DEFAULT_DPI = 203; function getCanvasWidth(widthClass, dpi) { if (widthClass === '4') { return dpi === 300 ? 1248 : 832; } return dpi === 300 ? 640 : 448; } function getMinHeightPx(dpi) { return Math.max(80, Math.round((MIN_HEIGHT_MM * dpi) / 25.4)); } function mmToPx(mm, dpi) { return Math.round((Number(mm) * dpi) / 25.4); } function pxToMm(px, dpi) { return Number(((px / dpi) * 25.4).toFixed(1)); } class ArtikelVorlagenPage extends PureComponent { constructor(props) { super(props); const initialDpi = DEFAULT_DPI; const initialWidthClass = '2'; const initialHeightMm = DEFAULT_HEIGHT_MM; this.state = { status: 'loading', error: null, templates: [], // Editor dialog state editorOpen: false, editorLoading: false, editorTemplate: null, // null = new, object = edit editorName: '', editorObjects: [], editorDpi: initialDpi, editorWidthClass: initialWidthClass, editorWidth: getCanvasWidth(initialWidthClass, initialDpi), editorHeightMm: initialHeightMm, editorHeight: mmToPx(initialHeightMm, initialDpi), editorClipboard: null, saving: false, // Delete busy deletingId: null, }; this._cancelled = false; this._currentEditId = null; this.handleRetry = this.handleRetry.bind(this); this.handleOpenNew = this.handleOpenNew.bind(this); this.handleOpenEdit = this.handleOpenEdit.bind(this); this.handleEditorClose = this.handleEditorClose.bind(this); this.handleEditorSave = this.handleEditorSave.bind(this); this.handleWidthClassChange = this.handleWidthClassChange.bind(this); this.handleDpiChange = this.handleDpiChange.bind(this); this.handleHeightMmChange = this.handleHeightMmChange.bind(this); this.handleHeightMmBlur = this.handleHeightMmBlur.bind(this); } componentDidMount() { setLastArticleSku(null); this._cancelled = false; void this.loadTemplates(); } componentWillUnmount() { this._cancelled = true; } async loadTemplates() { this.setState({ status: 'loading', error: null }); try { const { labeltemplates } = await labeltemplate.list(); if (this._cancelled) return; this.setState({ status: 'ready', templates: labeltemplates || [] }); } catch (err) { if (this._cancelled) return; this.setState({ status: 'failed', error: backendErrorFrom(err) }); } } handleRetry() { void this.loadTemplates(); } handleOpenNew() { this._currentEditId = null; const dpi = DEFAULT_DPI; const widthClass = '2'; const width = getCanvasWidth(widthClass, dpi); const heightMm = DEFAULT_HEIGHT_MM; const height = mmToPx(heightMm, dpi); this.setState({ editorOpen: true, editorLoading: false, editorTemplate: null, editorName: 'Neue Vorlage', editorObjects: [], editorDpi: dpi, editorWidthClass: widthClass, editorWidth: width, editorHeightMm: heightMm, editorHeight: height, }); } async handleOpenEdit(tmpl) { const fetchId = tmpl.id; this._currentEditId = fetchId; const initialDpi = tmpl.dpi === 300 ? 300 : 203; const initialWidthClass = (tmpl.width || 0) >= 800 ? '4' : '2'; const initialWidth = tmpl.width || getCanvasWidth(initialWidthClass, initialDpi); const initialHeight = tmpl.height || mmToPx(DEFAULT_HEIGHT_MM, initialDpi); const initialHeightMm = pxToMm(initialHeight, initialDpi); this.setState({ editorOpen: true, editorLoading: true, editorTemplate: tmpl, editorName: tmpl.name, editorObjects: [], editorDpi: initialDpi, editorWidthClass: initialWidthClass, editorWidth: initialWidth, editorHeightMm: initialHeightMm, editorHeight: initialHeight, }); try { const { labeltemplate: full } = await labeltemplate.get(fetchId); if (this._cancelled || this._currentEditId !== fetchId || !this.state.editorOpen) return; const content = full?.content || {}; const dpi = full?.dpi === 300 ? 300 : 203; const widthClass = (full?.width || 0) >= 800 ? '4' : '2'; const width = full?.width || getCanvasWidth(widthClass, dpi); const height = full?.height || mmToPx(DEFAULT_HEIGHT_MM, dpi); const heightMm = pxToMm(height, dpi); this.setState({ editorLoading: false, editorTemplate: full, editorName: full.name ?? tmpl.name, editorObjects: content.objects || [], editorDpi: dpi, editorWidthClass: widthClass, editorWidth: width, editorHeightMm: heightMm, editorHeight: height, }); } catch (err) { if (this._cancelled || this._currentEditId !== fetchId) return; notifyError(err); this.setState({ editorOpen: false, editorLoading: false, editorTemplate: null }); } } handleWidthClassChange(event, nextValue) { if (!nextValue) return; const width = getCanvasWidth(nextValue, this.state.editorDpi); this.setState({ editorWidthClass: nextValue, editorWidth: width, }); } handleDpiChange(event, nextValue) { if (!nextValue) return; const dpi = Number(nextValue); const width = getCanvasWidth(this.state.editorWidthClass, dpi); const mm = parseFloat(this.state.editorHeightMm) || DEFAULT_HEIGHT_MM; const minHeightPx = getMinHeightPx(dpi); const height = Math.max(minHeightPx, mmToPx(mm, dpi)); this.setState({ editorDpi: dpi, editorWidth: width, editorHeight: height, }); } handleHeightMmChange(event) { const val = event.target.value; const dpi = this.state.editorDpi; const num = parseFloat(val); const minHeightPx = getMinHeightPx(dpi); const height = !isNaN(num) && num > 0 ? Math.max(minHeightPx, mmToPx(num, dpi)) : this.state.editorHeight; this.setState({ editorHeightMm: val, editorHeight: height, }); } handleHeightMmBlur() { const dpi = this.state.editorDpi; const num = parseFloat(this.state.editorHeightMm); const safeMm = !isNaN(num) && num >= MIN_HEIGHT_MM ? num : MIN_HEIGHT_MM; const minHeightPx = getMinHeightPx(dpi); this.setState({ editorHeightMm: safeMm, editorHeight: Math.max(minHeightPx, mmToPx(safeMm, dpi)), }); } handleEditorClose() { if (this.state.saving) return; this._currentEditId = null; this.setState({ editorOpen: false, editorLoading: false, editorTemplate: null }); } async handleEditorSave() { const { editorTemplate, editorName, editorObjects, editorHeight, editorWidth, editorDpi, } = this.state; const name = editorName.trim(); if (!name) return; this.setState({ saving: true }); const body = { name, width: editorWidth, height: editorHeight, dpi: editorDpi, type: 'zpl', content: { objects: editorObjects }, }; try { if (editorTemplate) { const { labeltemplate: updated } = await labeltemplate.update(editorTemplate.id, body); if (this._cancelled) return; this.setState((prev) => ({ saving: false, editorOpen: false, templates: prev.templates.map((t) => (t.id === updated.id ? updated : t)), })); enqueueSnackbar(`Vorlage „${updated.name}" gespeichert.`, { variant: 'success' }); } else { const { labeltemplate: created } = await labeltemplate.create(body); if (this._cancelled) return; this.setState((prev) => ({ saving: false, editorOpen: false, templates: [...prev.templates, created], })); enqueueSnackbar(`Vorlage „${created.name}" angelegt.`, { variant: 'success' }); } } catch (err) { if (this._cancelled) return; notifyError(err); this.setState({ saving: false }); } } async handleDelete(tmpl) { if (this.state.deletingId != null) return; if (!window.confirm(`Vorlage „${tmpl.name}" wirklich löschen?`)) return; this.setState({ deletingId: tmpl.id }); try { await labeltemplate.delete(tmpl.id); if (this._cancelled) return; this.setState((prev) => ({ deletingId: null, templates: prev.templates.filter((t) => t.id !== tmpl.id), })); enqueueSnackbar('Vorlage gelöscht.', { variant: 'success' }); } catch (err) { if (this._cancelled) return; notifyError(err); this.setState({ deletingId: null }); } } renderEditor() { const { editorOpen, editorLoading, editorTemplate, editorName, editorObjects, editorHeight, editorWidth, editorDpi, editorWidthClass, editorHeightMm, editorClipboard, saving, } = this.state; const minHeightPx = getMinHeightPx(editorDpi); return ( {editorTemplate ? `„${editorTemplate.name}" bearbeiten` : 'Neue Vorlage'} {editorLoading ? ( Vorlage wird geladen… ) : ( <> this.setState({ editorName: e.target.value })} disabled={saving} sx={{ flex: 1, minWidth: 200 }} /> Breite: 2" 4" Auflösung: 203 DPI 300 DPI mm, }, htmlInput: { min: MIN_HEIGHT_MM, max: 300, step: 1, }, }} sx={{ width: 120 }} /> } > this.setState({ editorObjects: objects })} clipboard={editorClipboard} onCopy={(obj) => this.setState({ editorClipboard: obj })} /> )} ); } render() { const { status, error, templates, deletingId } = this.state; if (status === 'loading') { return ( ); } if (status === 'failed') { return ; } return ( <> {templates.length > 0 && ( {templates.map((tmpl) => ( {/* Thumbnail */} this.handleOpenEdit(tmpl)} sx={{ height: 100, bgcolor: SURFACE_BASE, display: 'flex', alignItems: 'center', justifyContent: 'center', borderBottom: '1px solid', borderColor: 'divider', overflow: 'hidden', cursor: 'pointer', }} > {tmpl.thumb ? ( ) : ( )} {/* Footer */} {tmpl.name} this.handleOpenEdit(tmpl)} disabled={deletingId != null} > void this.handleDelete(tmpl)} disabled={deletingId === tmpl.id} > {deletingId === tmpl.id ? : } {/* Dimensions chip */} {Number(((tmpl.width / (tmpl.dpi || 203)) * 25.4).toFixed(0))} × {Number(((tmpl.height / (tmpl.dpi || 203)) * 25.4).toFixed(0))} mm ({tmpl.width} × {tmpl.height} px) · {tmpl.dpi || 203} DPI ))} )} {this.renderEditor()} ); } } export default ArtikelVorlagenPage;