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 Dialog from '@mui/material/Dialog';
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';
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 (
);
}
render() {
const { status, error, templates, deletingId } = this.state;
if (status === 'loading') {
return (
);
}
if (status === 'failed') {
return ;
}
return (
<>
}
onClick={this.handleOpenNew}
>
Neue Vorlage
{templates.length === 0 ? (
Noch keine Vorlagen angelegt.
) : (
{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;