This commit is contained in:
seb
2026-09-28 06:49:46 +02:00
parent 0daf9fef3d
commit c4f8ebb41b
15 changed files with 2659 additions and 324 deletions

View File

@@ -1,12 +1,256 @@
import { Component } from 'react';
import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import CircularProgress from '@mui/material/CircularProgress';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import MenuItem from '@mui/material/MenuItem';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import PrintIcon from '@mui/icons-material/Print';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { enqueueSnackbar } from 'notistack';
import { labeltemplate, printer, article } from '../../../api';
import { notifyError } from '../../../utils/notify';
import BackendError from '../../BackendError';
export default class ArticleLabelTab extends PureComponent {
constructor(props) {
super(props);
this.state = {
templates: null,
printers: null,
selectedTemplateId: '',
selectedPrinterId: '',
quantity: 1,
loading: true,
error: null,
};
}
componentDidMount() {
this.loadData();
}
async loadData() {
this.setState({ loading: true, error: null });
try {
const [resTemplates, resPrinters] = await Promise.all([
labeltemplate.list(),
printer.list(),
]);
const templates = resTemplates.labeltemplates || [];
const zplPrinters = (resPrinters.printers || []).filter((p) => p.type === 'zpl');
this.setState({
templates,
printers: zplPrinters,
selectedTemplateId: templates.length > 0 ? templates[0].id : '',
selectedPrinterId: zplPrinters.length > 0 ? zplPrinters[0].id : '',
loading: false,
});
} catch (error) {
this.setState({ error, loading: false });
}
}
handlePrint = async () => {
const { selectedTemplateId, selectedPrinterId, quantity } = this.state;
const { article: articleData } = this.props;
if (!selectedTemplateId || !selectedPrinterId || quantity < 1) {
enqueueSnackbar('Bitte Vorlage, Drucker und Menge auswählen', { variant: 'warning' });
return;
}
try {
await article.printLabel(articleData.sku, {
templateId: selectedTemplateId,
printerId: selectedPrinterId,
quantity,
});
enqueueSnackbar('Druckauftrag erfolgreich gesendet', { variant: 'success' });
} catch (err) {
notifyError(err);
}
};
export default class ArticleLabelTab extends Component {
render() {
const {
templates,
printers,
selectedTemplateId,
selectedPrinterId,
quantity,
loading,
error,
} = this.state;
if (loading) {
return (
<Box sx={{ p: 4, display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Box sx={{ p: 2 }}>
<BackendError error={error} onRetry={() => this.loadData()} />
</Box>
);
}
if (templates.length === 0) {
return (
<Box sx={{ p: 3 }}>
<Typography color="text.secondary">
Es sind keine ZPL-Etikettenvorlagen vorhanden.
</Typography>
</Box>
);
}
if (printers.length === 0) {
return (
<Box sx={{ p: 3 }}>
<Typography color="text.secondary">
Es sind keine ZPL-Drucker konfiguriert.
</Typography>
</Box>
);
}
return (
<Typography variant="body2" color="text.secondary">
Label
</Typography>
<Box sx={{ p: 3, display: 'flex', flexDirection: 'column', gap: 4 }}>
<Box>
<Grid container spacing={2}>
{templates.map((t) => {
const isSelected = t.id === selectedTemplateId;
return (
<Grid item xs={12} sm={6} md={4} key={t.id}>
<Paper
variant="outlined"
onClick={() => this.setState({ selectedTemplateId: t.id })}
sx={{
p: 2,
cursor: 'pointer',
border: '2px solid',
borderColor: isSelected ? 'primary.main' : 'divider',
bgcolor: isSelected ? 'action.selected' : 'background.paper',
boxShadow: isSelected ? 3 : 0,
transition: 'all 0.2s',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
position: 'relative',
gap: 2,
'&:hover': {
borderColor: 'primary.main',
boxShadow: isSelected ? 3 : 1,
},
}}
>
{isSelected && (
<CheckCircleIcon
color="primary"
sx={{
position: 'absolute',
top: 8,
right: 8,
zIndex: 1,
}}
/>
)}
<Box
sx={{
width: '100%',
height: 120,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'background.default',
borderRadius: 1,
overflow: 'hidden',
}}
>
{t.thumb ? (
<img
src={`data:image/png;base64,${t.thumb}`}
alt={t.name}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
}}
/>
) : (
<Typography variant="caption" color="text.disabled">
Keine Vorschau
</Typography>
)}
</Box>
<Box sx={{ width: '100%', textAlign: 'center' }}>
<Typography variant="subtitle2" noWrap title={t.name}>
{t.name}
</Typography>
<Typography variant="caption" color="text.secondary">
{t.width}x{t.height} ({t.dpi} dpi)
</Typography>
</Box>
</Paper>
</Grid>
);
})}
</Grid>
</Box>
<Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'flex-start' }}>
<TextField
select
label="Drucker"
value={selectedPrinterId}
onChange={(e) => this.setState({ selectedPrinterId: e.target.value })}
sx={{ minWidth: 200 }}
size="small"
>
{printers.map((p) => (
<MenuItem key={p.id} value={p.id}>
{p.desc ? `${p.desc} (${p.address})` : p.address}
</MenuItem>
))}
</TextField>
<TextField
type="number"
label="Anzahl"
value={quantity}
onChange={(e) => {
const val = parseInt(e.target.value, 10);
this.setState({ quantity: isNaN(val) ? 1 : Math.max(1, val) });
}}
inputProps={{ min: 1, max: 9999 }}
sx={{ width: 100 }}
size="small"
/>
<Button
variant="contained"
size="large"
startIcon={<PrintIcon />}
onClick={this.handlePrint}
disabled={!selectedTemplateId || !selectedPrinterId || quantity < 1}
sx={{ height: 40 }}
>
Etiketten drucken
</Button>
</Box>
</Box>
</Box>
);
}
}