u
This commit is contained in:
@@ -25,8 +25,10 @@ import WarehouseIcon from '@mui/icons-material/Warehouse';
|
||||
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
|
||||
import TrendingDownIcon from '@mui/icons-material/TrendingDown';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { stock } from '../../api';
|
||||
import { stock, article as articleApi } from '../../api';
|
||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||
import { formatStock } from '../../pages/artikel/searchFormat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import artikelAutomat from '../../automats/artikelAutomat';
|
||||
@@ -41,9 +43,13 @@ const REASON_PRESETS = [
|
||||
];
|
||||
|
||||
export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
static contextType = StorageManagerContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initialWhId = this.resolveInitialWarehouseId(props);
|
||||
const isTrackStock =
|
||||
props.article?.track_stock !== false && props.article?.track_stock !== 0;
|
||||
this.state = {
|
||||
mode: 'diff', // 'diff' | 'target'
|
||||
warehouseId: initialWhId,
|
||||
@@ -53,6 +59,8 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
submitting: false,
|
||||
error: null,
|
||||
warehouses: warehouseAutomat.state.warehouses || [],
|
||||
trackStock: isTrackStock,
|
||||
initialTrackStock: isTrackStock,
|
||||
};
|
||||
|
||||
this.handleWarehouseChange = this.handleWarehouseChange.bind(this);
|
||||
@@ -60,6 +68,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
this.handleDiffChange = this.handleDiffChange.bind(this);
|
||||
this.handleTargetChange = this.handleTargetChange.bind(this);
|
||||
this.handleReasonChange = this.handleReasonChange.bind(this);
|
||||
this.handleTrackStockToggle = this.handleTrackStockToggle.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
@@ -82,6 +91,8 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
if (this.props.open && !prevProps.open) {
|
||||
void warehouseAutomat.actions.load().catch(() => {});
|
||||
const initialWhId = this.resolveInitialWarehouseId(this.props);
|
||||
const isTrackStock =
|
||||
this.props.article?.track_stock !== false && this.props.article?.track_stock !== 0;
|
||||
this.setState({
|
||||
mode: 'diff',
|
||||
warehouseId: initialWhId,
|
||||
@@ -91,6 +102,8 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
submitting: false,
|
||||
error: null,
|
||||
warehouses: warehouseAutomat.state.warehouses || [],
|
||||
trackStock: isTrackStock,
|
||||
initialTrackStock: isTrackStock,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -134,8 +147,15 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
this.setState({ reason: event.target.value });
|
||||
}
|
||||
|
||||
handleTrackStockToggle(event) {
|
||||
this.setState({ trackStock: event.target.checked, error: null });
|
||||
}
|
||||
|
||||
calculateAdjustment() {
|
||||
const { mode, warehouseId, diffValue, targetValue } = this.state;
|
||||
const { mode, warehouseId, diffValue, targetValue, trackStock } = this.state;
|
||||
if (!trackStock) {
|
||||
return { valid: false, amount: 0, newStock: 0 };
|
||||
}
|
||||
const currentStock = this.getCurrentStock(warehouseId);
|
||||
|
||||
if (mode === 'diff') {
|
||||
@@ -167,43 +187,77 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
|
||||
async handleSubmit(event) {
|
||||
event?.preventDefault();
|
||||
const { article, onClose, onAdjustSuccess } = this.props;
|
||||
const { warehouseId, reason, submitting } = this.state;
|
||||
const { article, onClose, onAdjustSuccess, onArticleUpdated } = this.props;
|
||||
const { warehouseId, reason, submitting, trackStock, initialTrackStock } = this.state;
|
||||
|
||||
if (submitting) return;
|
||||
|
||||
if (!warehouseId) {
|
||||
this.setState({ error: 'Bitte wählen Sie ein Lager aus.' });
|
||||
const trackStockChanged = trackStock !== initialTrackStock;
|
||||
const { valid, amount } = this.calculateAdjustment();
|
||||
const hasAdjustment = trackStock && valid && amount !== 0;
|
||||
|
||||
if (!trackStockChanged && !hasAdjustment) {
|
||||
if (trackStock && (!valid || amount === 0)) {
|
||||
this.setState({ error: 'Bitte geben Sie eine gültige Bestandsänderung ein (ungleich 0).' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { valid, amount } = this.calculateAdjustment();
|
||||
if (!valid || amount === 0) {
|
||||
this.setState({ error: 'Bitte geben Sie eine gültige Bestandsänderung ein (ungleich 0).' });
|
||||
if (hasAdjustment && !warehouseId) {
|
||||
this.setState({ error: 'Bitte wählen Sie ein Lager aus.' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ submitting: true, error: null });
|
||||
try {
|
||||
const payload = {
|
||||
sku: article.sku,
|
||||
warehouseId: Number(warehouseId),
|
||||
amount,
|
||||
text: reason.trim() || undefined,
|
||||
};
|
||||
let updatedArticle = null;
|
||||
|
||||
const result = await stock.adjust(payload);
|
||||
// 1. Update track_stock if changed
|
||||
if (trackStockChanged) {
|
||||
const store = this.context;
|
||||
const setFn = store?.article?.setTrackStock ?? articleApi.setTrackStock;
|
||||
const trackRes = await setFn(article.sku, { track_stock: trackStock });
|
||||
updatedArticle = trackRes?.article || null;
|
||||
}
|
||||
|
||||
enqueueSnackbar(
|
||||
`Bestand für „${article.name || article.sku}“ erfolgreich angepasst (${amount > 0 ? `+${amount}` : amount}).`,
|
||||
{ variant: 'success' }
|
||||
);
|
||||
// 2. Adjust stock if an adjustment was entered and stock is tracked
|
||||
let adjustResult = null;
|
||||
if (hasAdjustment) {
|
||||
const payload = {
|
||||
sku: article.sku,
|
||||
warehouseId: Number(warehouseId),
|
||||
amount,
|
||||
text: reason.trim() || undefined,
|
||||
};
|
||||
adjustResult = await stock.adjust(payload);
|
||||
}
|
||||
|
||||
// Reload article to update UI
|
||||
// User feedback
|
||||
if (trackStockChanged && hasAdjustment) {
|
||||
enqueueSnackbar(
|
||||
`Bestandsführung ${trackStock ? 'aktiviert' : 'deaktiviert'} und Bestand angepasst.`,
|
||||
{ variant: 'success' }
|
||||
);
|
||||
} else if (trackStockChanged) {
|
||||
enqueueSnackbar(
|
||||
`Bestandsführung für „${article.name || article.sku}“ ${trackStock ? 'aktiviert' : 'deaktiviert'}.`,
|
||||
{ variant: 'success' }
|
||||
);
|
||||
} else if (hasAdjustment) {
|
||||
enqueueSnackbar(
|
||||
`Bestand für „${article.name || article.sku}“ erfolgreich angepasst (${amount > 0 ? `+${amount}` : amount}).`,
|
||||
{ variant: 'success' }
|
||||
);
|
||||
}
|
||||
|
||||
// Reload article in automat cache
|
||||
void artikelAutomat.actions.reloadArticle(article.sku).catch(() => {});
|
||||
|
||||
if (onAdjustSuccess) {
|
||||
onAdjustSuccess(result);
|
||||
if (updatedArticle) {
|
||||
onArticleUpdated?.(updatedArticle);
|
||||
}
|
||||
if (adjustResult && onAdjustSuccess) {
|
||||
onAdjustSuccess(adjustResult);
|
||||
}
|
||||
if (onClose) {
|
||||
onClose();
|
||||
@@ -229,10 +283,16 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
submitting,
|
||||
error,
|
||||
warehouses,
|
||||
trackStock,
|
||||
initialTrackStock,
|
||||
} = this.state;
|
||||
|
||||
const trackStockChanged = trackStock !== initialTrackStock;
|
||||
const currentStock = this.getCurrentStock(warehouseId);
|
||||
const { valid, amount, newStock, isZeroChange } = this.calculateAdjustment();
|
||||
const hasAdjustment = trackStock && valid && amount !== 0;
|
||||
const canSubmit =
|
||||
!submitting && (trackStockChanged || (hasAdjustment && Boolean(warehouseId)));
|
||||
|
||||
const selectedWarehouse = warehouses.find((w) => Number(w.id) === Number(warehouseId));
|
||||
const unit =
|
||||
@@ -247,6 +307,13 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
aria-labelledby="stock-adjust-dialog-title"
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
minHeight: 560,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
id="stock-adjust-dialog-title"
|
||||
@@ -273,8 +340,8 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers sx={{ pt: 2 }}>
|
||||
<Stack spacing={2.5}>
|
||||
<DialogContent dividers sx={{ pt: 2, display: 'flex', flexDirection: 'column' }}>
|
||||
<Stack spacing={2.5} sx={{ flex: 1 }}>
|
||||
{/* Article overview */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -292,12 +359,52 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Track Stock Toggle Card */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ pr: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
Bestand verfolgen
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{trackStock
|
||||
? 'Lagerbestand wird für diesen Artikel geführt und bei Buchungen aktualisiert.'
|
||||
: 'Keine Bestandsführung. Artikel hat unbegrenzte Verfügbarkeit.'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Switch
|
||||
checked={Boolean(trackStock)}
|
||||
onChange={this.handleTrackStockToggle}
|
||||
disabled={submitting}
|
||||
color="primary"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{warehouses.length === 0 ? (
|
||||
<Alert severity="warning">
|
||||
Es sind keine Lager vorhanden. Bitte legen Sie zuerst unter „Artikel → Lager“ ein Lager an.
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2.5,
|
||||
opacity: trackStock ? 1 : 0.45,
|
||||
pointerEvents: trackStock ? 'auto' : 'none',
|
||||
transition: 'opacity 0.2s ease',
|
||||
userSelect: trackStock ? 'auto' : 'none',
|
||||
}}
|
||||
>
|
||||
{/* Warehouse selection & Current stock display */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -314,7 +421,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
value={warehouseId ?? ''}
|
||||
label="Lager *"
|
||||
onChange={this.handleWarehouseChange}
|
||||
disabled={submitting}
|
||||
disabled={submitting || !trackStock}
|
||||
startAdornment={
|
||||
<WarehouseIcon sx={{ mr: 1, color: 'text.secondary', fontSize: 20 }} />
|
||||
}
|
||||
@@ -358,7 +465,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
onChange={this.handleModeChange}
|
||||
size="small"
|
||||
fullWidth
|
||||
disabled={submitting}
|
||||
disabled={submitting || !trackStock}
|
||||
>
|
||||
<ToggleButton value="diff" sx={{ py: 0.75, fontWeight: 500 }}>
|
||||
<TrendingUpIcon sx={{ fontSize: 18, mr: 0.75 }} />
|
||||
@@ -380,7 +487,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
type="number"
|
||||
value={diffValue}
|
||||
onChange={this.handleDiffChange}
|
||||
disabled={submitting}
|
||||
disabled={submitting || !trackStock}
|
||||
placeholder="z.B. +5 oder -2"
|
||||
slotProps={{
|
||||
input: {
|
||||
@@ -409,7 +516,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
type="number"
|
||||
value={targetValue}
|
||||
onChange={this.handleTargetChange}
|
||||
disabled={submitting}
|
||||
disabled={submitting || !trackStock}
|
||||
placeholder="z.B. 10"
|
||||
helperText={
|
||||
isZeroChange
|
||||
@@ -459,7 +566,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
label="Grund / Buchungstext"
|
||||
value={reason}
|
||||
onChange={this.handleReasonChange}
|
||||
disabled={submitting}
|
||||
disabled={submitting || !trackStock}
|
||||
placeholder="z.B. Inventur, Bruch, Schwund"
|
||||
helperText="Wird im Journal / Ledger als Transaktionstext gespeichert"
|
||||
/>
|
||||
@@ -470,14 +577,14 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
label={preset}
|
||||
size="small"
|
||||
clickable
|
||||
disabled={submitting}
|
||||
disabled={submitting || !trackStock}
|
||||
onClick={() => this.setState({ reason: preset })}
|
||||
sx={{ fontSize: '0.75rem' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
@@ -497,7 +604,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={this.handleSubmit}
|
||||
disabled={submitting || !valid || amount === 0 || !warehouseId}
|
||||
disabled={!canSubmit}
|
||||
startIcon={
|
||||
submitting ? (
|
||||
<CircularProgress size={16} color="inherit" />
|
||||
@@ -507,7 +614,11 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
}
|
||||
sx={{ minWidth: 150, fontWeight: 600 }}
|
||||
>
|
||||
{submitting ? 'Wird gebucht...' : 'Bestand anpassen'}
|
||||
{submitting
|
||||
? 'Wird gespeichert...'
|
||||
: hasAdjustment
|
||||
? 'Bestand anpassen'
|
||||
: 'Speichern'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -159,7 +159,7 @@ class ArticleTileTab extends PureComponent {
|
||||
case ARTICLE_TAB.seo:
|
||||
return <ArticleSeoTab article={articleData} />;
|
||||
case ARTICLE_TAB.label:
|
||||
return <ArticleLabelTab />;
|
||||
return <ArticleLabelTab article={articleData} />;
|
||||
case ARTICLE_TAB.attributes:
|
||||
return <ArticleAttributesTab />;
|
||||
case ARTICLE_TAB.compound:
|
||||
|
||||
@@ -3,6 +3,7 @@ import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import WarehouseIcon from '@mui/icons-material/Warehouse';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { PanelTile } from 'dashgui/panels';
|
||||
import { formatStock } from '../../../pages/artikel/searchFormat';
|
||||
import ArticleStockAdjustDialog from '../ArticleStockAdjustDialog';
|
||||
@@ -30,6 +31,9 @@ export default class ArticleViewStock extends PureComponent {
|
||||
render() {
|
||||
const { article: articleData } = this.props;
|
||||
const { adjustOpen } = this.state;
|
||||
const isTrackStock =
|
||||
articleData?.track_stock !== false && articleData?.track_stock !== 0;
|
||||
|
||||
const rawWarehouses = Array.isArray(articleData?.warehouses)
|
||||
? articleData.warehouses
|
||||
: [];
|
||||
@@ -63,149 +67,196 @@ export default class ArticleViewStock extends PureComponent {
|
||||
onClick: this.handleOpenAdjust,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Top section: Headline and Total Stock */}
|
||||
<Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.6,
|
||||
fontWeight: 600,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
Bestand
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
gap: 0.75,
|
||||
my: 0.5,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Top section: Headline and Total Stock */}
|
||||
<Box>
|
||||
<Typography
|
||||
component="div"
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontSize: '2.4rem',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: '-0.02em',
|
||||
color: totalStock > 0 ? 'text.primary' : 'text.secondary',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.6,
|
||||
fontWeight: 600,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{formatStock(totalStock)}
|
||||
Bestand
|
||||
</Typography>
|
||||
{unit ? (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{unit}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Bottom section: Per-warehouse breakdown */}
|
||||
{hasWarehouses ? (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 1,
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
pr: 0.5,
|
||||
'&::-webkit-scrollbar': { width: 4 },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.15)',
|
||||
borderRadius: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{warehouses.map((wh) => (
|
||||
{isTrackStock ? (
|
||||
<Tooltip title="Klicken, um Bestand anzupassen" arrow placement="top-start">
|
||||
<Box
|
||||
key={wh.id}
|
||||
onClick={this.handleOpenAdjust}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
py: 0.35,
|
||||
px: 0.75,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'baseline',
|
||||
gap: 0.75,
|
||||
my: 0.5,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.55)',
|
||||
px: 0.5,
|
||||
mx: -0.5,
|
||||
transition: 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="div"
|
||||
sx={{
|
||||
fontSize: '2.4rem',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: '-0.02em',
|
||||
color: totalStock > 0 ? 'text.primary' : 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{formatStock(totalStock)}
|
||||
</Typography>
|
||||
{unit ? (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{unit}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Box
|
||||
onClick={this.handleOpenAdjust}
|
||||
sx={{
|
||||
my: 0.75,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 1,
|
||||
px: 0.5,
|
||||
mx: -0.5,
|
||||
transition: 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="div"
|
||||
sx={{
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.2,
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
Keine Erfassung
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'block', mt: 0.25 }}
|
||||
>
|
||||
Bestand wird nicht überwacht
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Bottom section: Per-warehouse breakdown */}
|
||||
{isTrackStock && hasWarehouses ? (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 1,
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
pr: 0.5,
|
||||
'&::-webkit-scrollbar': { width: 4 },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.15)',
|
||||
borderRadius: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{warehouses.map((wh) => (
|
||||
<Box
|
||||
key={wh.id}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minWidth: 0,
|
||||
mr: 1,
|
||||
justifyContent: 'space-between',
|
||||
py: 0.35,
|
||||
px: 0.75,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.55)',
|
||||
}}
|
||||
>
|
||||
<WarehouseIcon
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 15,
|
||||
color: 'text.secondary',
|
||||
mr: 0.5,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
noWrap
|
||||
title={wh.name}
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: '0.82rem',
|
||||
color: 'text.primary',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minWidth: 0,
|
||||
mr: 1,
|
||||
}}
|
||||
>
|
||||
{wh.name}
|
||||
<WarehouseIcon
|
||||
sx={{
|
||||
fontSize: 15,
|
||||
color: 'text.secondary',
|
||||
mr: 0.5,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
noWrap
|
||||
title={wh.name}
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: '0.82rem',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
{wh.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontSize: '0.82rem',
|
||||
flexShrink: 0,
|
||||
color:
|
||||
Number(wh.stock) > 0
|
||||
? 'text.primary'
|
||||
: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{formatStock(wh.stock)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontSize: '0.82rem',
|
||||
flexShrink: 0,
|
||||
color:
|
||||
Number(wh.stock) > 0
|
||||
? 'text.primary'
|
||||
: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{formatStock(wh.stock)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
</PanelTile>
|
||||
) : null}
|
||||
</Box>
|
||||
</PanelTile>
|
||||
|
||||
<ArticleStockAdjustDialog
|
||||
open={adjustOpen}
|
||||
onClose={this.handleCloseAdjust}
|
||||
article={articleData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<ArticleStockAdjustDialog
|
||||
open={adjustOpen}
|
||||
onClose={this.handleCloseAdjust}
|
||||
article={articleData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user