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

@@ -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>