Compare commits
4 Commits
1933ac794d
...
5f7db9adf5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f7db9adf5 | ||
|
|
c0b436f9f2 | ||
|
|
8285936bc9 | ||
|
|
38339b13a2 |
@@ -244,11 +244,11 @@ export const article = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/** POST /api/article → 201 { article } */
|
/** POST /api/article → 201 { article } */
|
||||||
create({ parentId, name, track_stock }) {
|
create({ categoryId, name, track_stock }) {
|
||||||
return http('/api/article', {
|
return http('/api/article', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
parentId: Number(parentId),
|
categoryId: Number(categoryId),
|
||||||
name,
|
name,
|
||||||
...(track_stock !== undefined ? { track_stock: Boolean(track_stock) } : {}),
|
...(track_stock !== undefined ? { track_stock: Boolean(track_stock) } : {}),
|
||||||
},
|
},
|
||||||
@@ -2234,4 +2234,3 @@ export default {
|
|||||||
globalHistory,
|
globalHistory,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ const actions = {
|
|||||||
* @param {{ parentId: number|string, name: string }} param0
|
* @param {{ parentId: number|string, name: string }} param0
|
||||||
*/
|
*/
|
||||||
async createArticle({ parentId, name }) {
|
async createArticle({ parentId, name }) {
|
||||||
const res = await articleApi.create({ parentId, name });
|
const res = await articleApi.create({ categoryId: parentId, name });
|
||||||
const created = res.article;
|
const created = res.article;
|
||||||
if (created) appendArticlesToCategoryViews({ categoryId: parentId, articles: [created] });
|
if (created) appendArticlesToCategoryViews({ categoryId: parentId, articles: [created] });
|
||||||
return created;
|
return created;
|
||||||
@@ -223,7 +223,7 @@ const actions = {
|
|||||||
if (list.length === 0) throw new Error('Keine Namen übergeben.');
|
if (list.length === 0) throw new Error('Keine Namen übergeben.');
|
||||||
const created = [];
|
const created = [];
|
||||||
for (const name of list) {
|
for (const name of list) {
|
||||||
const res = await articleApi.create({ parentId, name });
|
const res = await articleApi.create({ categoryId: parentId, name });
|
||||||
if (res.article) created.push(res.article);
|
if (res.article) created.push(res.article);
|
||||||
}
|
}
|
||||||
appendArticlesToCategoryViews({ categoryId: parentId, articles: created });
|
appendArticlesToCategoryViews({ categoryId: parentId, articles: created });
|
||||||
|
|||||||
@@ -37,12 +37,16 @@ import supplierAutomat from './supplierAutomat';
|
|||||||
* @typedef {Object} SseState
|
* @typedef {Object} SseState
|
||||||
* @property {'disconnected'|'connecting'|'connected'|'error'} status Connection state.
|
* @property {'disconnected'|'connecting'|'connected'|'error'} status Connection state.
|
||||||
* @property {string|null} companyId Company the stream is bound to.
|
* @property {string|null} companyId Company the stream is bound to.
|
||||||
|
* @property {Array<{id: number, type: string, data: string, lastEventId: string, time: number}>} events Recent stream events.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const EVENT_HISTORY_LIMIT = 200;
|
||||||
|
|
||||||
/** @type {SseState} */
|
/** @type {SseState} */
|
||||||
const initialState = {
|
const initialState = {
|
||||||
status: 'disconnected',
|
status: 'disconnected',
|
||||||
companyId: null,
|
companyId: null,
|
||||||
|
events: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Event name prefix for per-article signals. */
|
/** Event name prefix for per-article signals. */
|
||||||
@@ -54,9 +58,11 @@ let eventSource = null;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handlers for statically named events (`category`, `articles`, …).
|
* Handlers for statically named events (`category`, `articles`, …).
|
||||||
* @type {Map<string, Set<(event: MessageEvent) => void>>}
|
* @type {Map<string, Set<{handler: (event: MessageEvent) => void, detach: (() => void)|null}>>}
|
||||||
*/
|
*/
|
||||||
const handlers = new Map();
|
const handlers = new Map();
|
||||||
|
let nextEventId = 0;
|
||||||
|
const seenEvents = new WeakSet();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handlers per watched SKU, so one `article-<sku>` listener is attached while
|
* Handlers per watched SKU, so one `article-<sku>` listener is attached while
|
||||||
@@ -65,6 +71,31 @@ const handlers = new Map();
|
|||||||
*/
|
*/
|
||||||
const articleWatchers = new Map();
|
const articleWatchers = new Map();
|
||||||
|
|
||||||
|
function recordEvent(event) {
|
||||||
|
if (seenEvents.has(event)) return;
|
||||||
|
seenEvents.add(event);
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
id: ++nextEventId,
|
||||||
|
type: event.type,
|
||||||
|
data: typeof event.data === 'string' ? event.data : '',
|
||||||
|
lastEventId: event.lastEventId ?? '',
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
sseAutomat.setState((state) => ({
|
||||||
|
events: [...state.events.slice(-(EVENT_HISTORY_LIMIT - 1)), entry],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTrackedListener(es, type, handler) {
|
||||||
|
const listener = (event) => {
|
||||||
|
recordEvent(event);
|
||||||
|
handler(event);
|
||||||
|
};
|
||||||
|
es.addEventListener(type, listener);
|
||||||
|
return () => es.removeEventListener(type, listener);
|
||||||
|
}
|
||||||
|
|
||||||
function disconnect() {
|
function disconnect() {
|
||||||
if (!eventSource) return;
|
if (!eventSource) return;
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
@@ -80,8 +111,8 @@ function disconnect() {
|
|||||||
*/
|
*/
|
||||||
function attachHandlers(es) {
|
function attachHandlers(es) {
|
||||||
for (const [event, set] of handlers) {
|
for (const [event, set] of handlers) {
|
||||||
for (const handler of set) {
|
for (const entry of set) {
|
||||||
es.addEventListener(event, handler);
|
entry.detach = addTrackedListener(es, event, entry.handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [sku] of articleWatchers) {
|
for (const [sku] of articleWatchers) {
|
||||||
@@ -100,7 +131,8 @@ function attachHandlers(es) {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function attachArticleListener(sku) {
|
function attachArticleListener(sku) {
|
||||||
eventSource?.addEventListener(`${ARTICLE_EVENT_PREFIX}${sku}`, (event) => {
|
if (!eventSource) return;
|
||||||
|
addTrackedListener(eventSource, `${ARTICLE_EVENT_PREFIX}${sku}`, (event) => {
|
||||||
const set = articleWatchers.get(sku);
|
const set = articleWatchers.get(sku);
|
||||||
if (!set || set.size === 0) return;
|
if (!set || set.size === 0) return;
|
||||||
const changed = String(event.type).slice(ARTICLE_EVENT_PREFIX.length);
|
const changed = String(event.type).slice(ARTICLE_EVENT_PREFIX.length);
|
||||||
@@ -121,7 +153,7 @@ function connect() {
|
|||||||
const es = new EventSource(url, { withCredentials: true });
|
const es = new EventSource(url, { withCredentials: true });
|
||||||
|
|
||||||
// Category tree changed: refresh the tree and every open category view.
|
// Category tree changed: refresh the tree and every open category view.
|
||||||
es.addEventListener('category', () => {
|
addTrackedListener(es, 'category', () => {
|
||||||
void categoryAutomat.actions.loadTree().catch((err) => {
|
void categoryAutomat.actions.loadTree().catch((err) => {
|
||||||
console.error('[sse] category tree refresh failed', err);
|
console.error('[sse] category tree refresh failed', err);
|
||||||
});
|
});
|
||||||
@@ -129,44 +161,47 @@ function connect() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Article index changed: refresh the index and open category views.
|
// Article index changed: refresh the index and open category views.
|
||||||
es.addEventListener('articles', () => {
|
addTrackedListener(es, 'articles', () => {
|
||||||
invalidateAllCategoryViews();
|
invalidateAllCategoryViews();
|
||||||
void articleIndexAutomat.actions.reload().catch((err) => {
|
void articleIndexAutomat.actions.reload().catch((err) => {
|
||||||
console.error('[sse] article index refresh failed', err);
|
console.error('[sse] article index refresh failed', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
es.addEventListener('warehouse', () => {
|
addTrackedListener(es, 'warehouse', () => {
|
||||||
void warehouseAutomat.actions.reload().catch((err) => {
|
void warehouseAutomat.actions.reload().catch((err) => {
|
||||||
console.error('[sse] warehouse refresh failed', err);
|
console.error('[sse] warehouse refresh failed', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
es.addEventListener('attributename', () => {
|
addTrackedListener(es, 'attributename', () => {
|
||||||
void attributeAutomat.actions.load().catch((err) => {
|
void attributeAutomat.actions.load().catch((err) => {
|
||||||
console.error('[sse] attributename refresh failed', err);
|
console.error('[sse] attributename refresh failed', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
es.addEventListener('propertyname', () => {
|
addTrackedListener(es, 'propertyname', () => {
|
||||||
void propertyAutomat.actions.load().catch((err) => {
|
void propertyAutomat.actions.load().catch((err) => {
|
||||||
console.error('[sse] propertyname refresh failed', err);
|
console.error('[sse] propertyname refresh failed', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
es.addEventListener('supplier', () => {
|
addTrackedListener(es, 'supplier', () => {
|
||||||
void supplierAutomat.actions.reload().catch((err) => {
|
void supplierAutomat.actions.reload().catch((err) => {
|
||||||
console.error('[sse] supplier refresh failed', err);
|
console.error('[sse] supplier refresh failed', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
es.addEventListener('open', () => {
|
addTrackedListener(es, 'open', () => {
|
||||||
sseAutomat.setState({ status: 'connected' });
|
sseAutomat.setState({ status: 'connected' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addTrackedListener(es, 'message', () => {});
|
||||||
|
|
||||||
// EventSource reconnects on its own; only surface the state.
|
// EventSource reconnects on its own; only surface the state.
|
||||||
es.onerror = () => {
|
es.onerror = () => {
|
||||||
console.error('[sse] event stream error');
|
console.error('[sse] event stream error');
|
||||||
|
recordEvent({ type: 'error', data: '', lastEventId: '' });
|
||||||
sseAutomat.setState({ status: 'error' });
|
sseAutomat.setState({ status: 'error' });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,6 +221,10 @@ const actions = {
|
|||||||
connect();
|
connect();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearEvents() {
|
||||||
|
sseAutomat.setState({ events: [] });
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a handler for a statically named backend event.
|
* Registers a handler for a statically named backend event.
|
||||||
*
|
*
|
||||||
@@ -202,11 +241,15 @@ const actions = {
|
|||||||
set = new Set();
|
set = new Set();
|
||||||
handlers.set(event, set);
|
handlers.set(event, set);
|
||||||
}
|
}
|
||||||
set.add(handler);
|
let entry = Array.from(set).find((candidate) => candidate.handler === handler);
|
||||||
eventSource?.addEventListener(event, handler);
|
if (!entry) {
|
||||||
|
entry = { handler, detach: null };
|
||||||
|
set.add(entry);
|
||||||
|
if (eventSource) entry.detach = addTrackedListener(eventSource, event, handler);
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
eventSource?.removeEventListener(event, handler);
|
entry.detach?.();
|
||||||
set.delete(handler);
|
set.delete(entry);
|
||||||
if (set.size === 0) handlers.delete(event);
|
if (set.size === 0) handlers.delete(event);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Component, PureComponent } from 'react';
|
import { Component, PureComponent } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import InputAdornment from '@mui/material/InputAdornment';
|
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { EditablePanelTile, PanelTile } from 'dashgui/panels';
|
import { EditablePanelTile, PanelTile } from 'dashgui/panels';
|
||||||
@@ -45,12 +44,12 @@ const articlePriceTextSx = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const articlePriceFieldSx = {
|
const articlePriceFieldSx = {
|
||||||
flexShrink: 0,
|
flexShrink: 1,
|
||||||
width: 'auto',
|
width: '100%',
|
||||||
mr: 11,
|
|
||||||
'& .MuiOutlinedInput-root': {
|
'& .MuiOutlinedInput-root': {
|
||||||
alignItems: 'baseline',
|
alignItems: 'baseline',
|
||||||
flexShrink: 0,
|
width: '100%',
|
||||||
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
'& .MuiInputBase-input': {
|
'& .MuiInputBase-input': {
|
||||||
fontSize: '3.5rem',
|
fontSize: '3.5rem',
|
||||||
@@ -58,17 +57,6 @@ const articlePriceFieldSx = {
|
|||||||
height: 'auto',
|
height: 'auto',
|
||||||
...articlePriceTextSx,
|
...articlePriceTextSx,
|
||||||
},
|
},
|
||||||
'& .MuiInputAdornment-root': {
|
|
||||||
marginRight: 0.75,
|
|
||||||
height: 'auto',
|
|
||||||
maxHeight: 'none',
|
|
||||||
alignItems: 'baseline',
|
|
||||||
},
|
|
||||||
'& .MuiInputAdornment-root .MuiTypography-root': {
|
|
||||||
fontSize: '2rem',
|
|
||||||
fontWeight: 700,
|
|
||||||
lineHeight: 1.1,
|
|
||||||
},
|
|
||||||
'& .MuiOutlinedInput-notchedOutline': {
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
borderWidth: '1px !important',
|
borderWidth: '1px !important',
|
||||||
},
|
},
|
||||||
@@ -96,6 +84,7 @@ const priceBodySx = {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 0.75,
|
gap: 0.75,
|
||||||
|
pt: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
function articleHasPackUnit(art) {
|
function articleHasPackUnit(art) {
|
||||||
@@ -198,11 +187,6 @@ class PriceAmountField extends Component {
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
readOnly,
|
readOnly,
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position="start">
|
|
||||||
<Typography component="span">€</Typography>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
htmlInput: {
|
htmlInput: {
|
||||||
inputMode: 'decimal',
|
inputMode: 'decimal',
|
||||||
@@ -217,11 +201,10 @@ class PriceAmountField extends Component {
|
|||||||
|
|
||||||
class PriceDisplay extends Component {
|
class PriceDisplay extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { saved, taxRates, unitOptions, displaySymbol } = this.props;
|
const { saved, unitOptions, displaySymbol } = this.props;
|
||||||
return (
|
return (
|
||||||
<Box sx={priceBodySx}>
|
<Box sx={priceBodySx}>
|
||||||
<PriceAmountField value={saved.gross} readOnly />
|
<PriceAmountField value={saved.gross} readOnly />
|
||||||
<PriceTaxRow rates={taxRates} selected={saved.taxRate} readOnly />
|
|
||||||
<TierPricePills tiers={saved.tiers} />
|
<TierPricePills tiers={saved.tiers} />
|
||||||
<PriceUnitRow
|
<PriceUnitRow
|
||||||
hasUnit={saved.hasUnit}
|
hasUnit={saved.hasUnit}
|
||||||
@@ -245,11 +228,13 @@ class PriceOverlay extends Component {
|
|||||||
unitOptions,
|
unitOptions,
|
||||||
articleUnitSi,
|
articleUnitSi,
|
||||||
onOpenEditor,
|
onOpenEditor,
|
||||||
|
onOpenTiers,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (!editing || draft == null) return null;
|
if (!editing || draft == null) return null;
|
||||||
return (
|
return (
|
||||||
<PriceUnitCorner
|
<PriceUnitCorner
|
||||||
hasUnit={draft.hasUnit}
|
hasUnit={draft.hasUnit}
|
||||||
|
onOpenTiers={onOpenTiers}
|
||||||
onToggleHasUnit={(hasUnit) => {
|
onToggleHasUnit={(hasUnit) => {
|
||||||
if (hasUnit) {
|
if (hasUnit) {
|
||||||
const unitamount = draft.unitamount || '1';
|
const unitamount = draft.unitamount || '1';
|
||||||
@@ -304,61 +289,40 @@ class PriceEdit extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
draft,
|
draft, onChange, inputRef, unitOptions, displaySymbol,
|
||||||
onChange,
|
tiersDialogOpen, unitEditorOpen, onCloseTiers, onCloseUnitEditor,
|
||||||
inputRef,
|
|
||||||
taxRates,
|
|
||||||
unitOptions,
|
|
||||||
displaySymbol,
|
|
||||||
tiersDialogOpen,
|
|
||||||
unitEditorOpen,
|
|
||||||
onOpenTiers,
|
|
||||||
onCloseTiers,
|
|
||||||
onCloseUnitEditor,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const draftGross = parsePriceInput(draft.gross);
|
const draftGross = parsePriceInput(draft.gross);
|
||||||
const draftAmount = parsePriceInput(draft.unitamount);
|
const draftAmount = parsePriceInput(draft.unitamount);
|
||||||
return (
|
return (
|
||||||
<Box sx={priceBodySx}>
|
<Box sx={priceBodySx}>
|
||||||
<PriceAmountField
|
<PriceAmountField
|
||||||
inputRef={inputRef}
|
inputRef={inputRef} value={draft.gross}
|
||||||
value={draft.gross}
|
|
||||||
onChange={(gross) => onChange({ ...draft, gross })}
|
onChange={(gross) => onChange({ ...draft, gross })}
|
||||||
error={draft.gross !== '' && draftGross == null}
|
error={draft.gross !== '' && draftGross == null}
|
||||||
/>
|
/>
|
||||||
<PriceTaxRow
|
|
||||||
rates={taxRates}
|
|
||||||
selected={draft.taxRate}
|
|
||||||
onSelectTax={(taxRate) => onChange({ ...draft, taxRate })}
|
|
||||||
onOpenTiers={onOpenTiers}
|
|
||||||
/>
|
|
||||||
<TierPricePills tiers={draft.tiers} />
|
<TierPricePills tiers={draft.tiers} />
|
||||||
<PriceUnitRow
|
<PriceUnitRow
|
||||||
hasUnit={draft.hasUnit}
|
hasUnit={draft.hasUnit} unitamount={draft.unitamount}
|
||||||
unitamount={draft.unitamount}
|
unitSi={draft.unitSi} units={unitOptions}
|
||||||
unitSi={draft.unitSi}
|
|
||||||
units={unitOptions}
|
|
||||||
displaySymbol={displaySymbol}
|
displaySymbol={displaySymbol}
|
||||||
onChangeAmount={(unitamount) => onChange({ ...draft, unitamount })}
|
onChangeAmount={(unitamount) => onChange({ ...draft, unitamount })}
|
||||||
onChangeSi={(unitSi) => onChange({ ...draft, unitSi })}
|
onChangeSi={(unitSi) => onChange({ ...draft, unitSi })}
|
||||||
amountError={
|
amountError={
|
||||||
draft.hasUnit &&
|
draft.hasUnit && draft.unitamount !== '' &&
|
||||||
draft.unitamount !== '' &&
|
|
||||||
(draftAmount == null || draftAmount <= 0)
|
(draftAmount == null || draftAmount <= 0)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ArticlePriceTiersDialog
|
<ArticlePriceTiersDialog
|
||||||
open={tiersDialogOpen}
|
open={tiersDialogOpen} tiers={draft.tiers ?? []}
|
||||||
tiers={draft.tiers ?? []}
|
onClose={onCloseTiers} onApply={(tiers) => onChange({ ...draft, tiers })}
|
||||||
onClose={onCloseTiers}
|
|
||||||
onApply={(tiers) => onChange({ ...draft, tiers })}
|
|
||||||
/>
|
/>
|
||||||
<ArticleUnitEditorDialog
|
<ArticleUnitEditorDialog
|
||||||
open={unitEditorOpen}
|
open={unitEditorOpen} onClose={onCloseUnitEditor}
|
||||||
onClose={onCloseUnitEditor}
|
onUnitUpserted={this.handleUnitUpserted} onUnitDeleted={this.handleUnitDeleted}
|
||||||
onUnitUpserted={this.handleUnitUpserted}
|
|
||||||
onUnitDeleted={this.handleUnitDeleted}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -524,8 +488,8 @@ export default class ArticleViewPrice extends PureComponent {
|
|||||||
const max = Math.max(...prices);
|
const max = Math.max(...prices);
|
||||||
priceText =
|
priceText =
|
||||||
min === max
|
min === max
|
||||||
? `€ ${formatPriceAmount(min)}`
|
? formatPriceAmount(min)
|
||||||
: `€ ${formatPriceAmount(min)} bis € ${formatPriceAmount(max)}`;
|
: `${formatPriceAmount(min)} bis ${formatPriceAmount(max)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -533,24 +497,19 @@ export default class ArticleViewPrice extends PureComponent {
|
|||||||
wide={1}
|
wide={1}
|
||||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||||
color={BLUE_PANEL}
|
color={BLUE_PANEL}
|
||||||
|
title={
|
||||||
|
<Box component="span" sx={{ fontWeight: 700 }}>
|
||||||
|
€
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
contentAlignItems="flex-start"
|
contentAlignItems="flex-start"
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
|
||||||
<Typography
|
<Box sx={{ width: '100%', mt: 0.5 }}>
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
letterSpacing: 0.6,
|
|
||||||
fontWeight: 600,
|
|
||||||
display: 'block',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Preis auf Variantenebene pflegen
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ my: 0.75 }}>
|
|
||||||
<Typography
|
<Typography
|
||||||
component="div"
|
component="div"
|
||||||
sx={{
|
sx={{
|
||||||
|
width: '100%',
|
||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
@@ -578,6 +537,31 @@ export default class ArticleViewPrice extends PureComponent {
|
|||||||
wide={1}
|
wide={1}
|
||||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||||
color={BLUE_PANEL}
|
color={BLUE_PANEL}
|
||||||
|
titleContent={({ editing, draft, onChange }) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box component="span" sx={{ fontWeight: 700 }}>
|
||||||
|
€
|
||||||
|
</Box>
|
||||||
|
<PriceTaxRow
|
||||||
|
rates={taxRates}
|
||||||
|
selected={editing ? draft?.taxRate : priceValue.taxRate}
|
||||||
|
onSelectTax={
|
||||||
|
editing
|
||||||
|
? (taxRate) => onChange({ ...draft, taxRate })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
readOnly={!editing}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
contentAlignItems="flex-start"
|
contentAlignItems="flex-start"
|
||||||
value={priceValue}
|
value={priceValue}
|
||||||
onSave={this.handleSave}
|
onSave={this.handleSave}
|
||||||
@@ -592,12 +576,12 @@ export default class ArticleViewPrice extends PureComponent {
|
|||||||
unitOptions={unitOptions}
|
unitOptions={unitOptions}
|
||||||
articleUnitSi={articleData.unit?.si}
|
articleUnitSi={articleData.unit?.si}
|
||||||
onOpenEditor={this.openUnitEditor}
|
onOpenEditor={this.openUnitEditor}
|
||||||
|
onOpenTiers={this.openTiersDialog}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
display={
|
display={
|
||||||
<PriceDisplay
|
<PriceDisplay
|
||||||
saved={priceValue}
|
saved={priceValue}
|
||||||
taxRates={taxRates}
|
|
||||||
unitOptions={unitOptions}
|
unitOptions={unitOptions}
|
||||||
displaySymbol={displaySymbol}
|
displaySymbol={displaySymbol}
|
||||||
/>
|
/>
|
||||||
@@ -607,12 +591,10 @@ export default class ArticleViewPrice extends PureComponent {
|
|||||||
draft={draft}
|
draft={draft}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
taxRates={taxRates}
|
|
||||||
unitOptions={unitOptions}
|
unitOptions={unitOptions}
|
||||||
displaySymbol={displaySymbol}
|
displaySymbol={displaySymbol}
|
||||||
tiersDialogOpen={tiersDialogOpen}
|
tiersDialogOpen={tiersDialogOpen}
|
||||||
unitEditorOpen={unitEditorOpen}
|
unitEditorOpen={unitEditorOpen}
|
||||||
onOpenTiers={this.openTiersDialog}
|
|
||||||
onCloseTiers={this.closeTiersDialog}
|
onCloseTiers={this.closeTiersDialog}
|
||||||
onCloseUnitEditor={this.closeUnitEditor}
|
onCloseUnitEditor={this.closeUnitEditor}
|
||||||
onUnitUpserted={this.refreshUnits}
|
onUnitUpserted={this.refreshUnits}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined';
|
|
||||||
import {
|
import {
|
||||||
TINT_CHIP,
|
TINT_CHIP,
|
||||||
TINT_PILL_ACTIVE,
|
TINT_PILL_ACTIVE,
|
||||||
@@ -50,7 +48,7 @@ export class TaxRatePills extends Component {
|
|||||||
|
|
||||||
export class PriceTaxRow extends Component {
|
export class PriceTaxRow extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { rates, selected, onSelectTax, readOnly, onOpenTiers } = this.props;
|
const { rates, selected, onSelectTax, readOnly } = this.props;
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -67,20 +65,6 @@ export class PriceTaxRow extends Component {
|
|||||||
onSelect={onSelectTax}
|
onSelect={onSelectTax}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
|
||||||
type="button"
|
|
||||||
size="small"
|
|
||||||
aria-label="Staffelpreise"
|
|
||||||
disabled={readOnly}
|
|
||||||
onClick={readOnly ? undefined : onOpenTiers}
|
|
||||||
sx={{
|
|
||||||
p: 0.5,
|
|
||||||
visibility: readOnly ? 'hidden' : 'visible',
|
|
||||||
pointerEvents: readOnly ? 'none' : 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LayersOutlinedIcon sx={{ fontSize: 28 }} />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import Select from '@mui/material/Select';
|
import Select from '@mui/material/Select';
|
||||||
|
import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined';
|
||||||
import StraightenOutlinedIcon from '@mui/icons-material/StraightenOutlined';
|
import StraightenOutlinedIcon from '@mui/icons-material/StraightenOutlined';
|
||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
import TextInput from '../../TextInput';
|
import TextInput from '../../TextInput';
|
||||||
@@ -16,12 +17,12 @@ import {
|
|||||||
|
|
||||||
const priceUnitCornerSx = {
|
const priceUnitCornerSx = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 72,
|
bottom: 8,
|
||||||
right: 4,
|
right: 4,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 1.25,
|
gap: 0.25,
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,9 +120,18 @@ export class PriceUnitRow extends Component {
|
|||||||
|
|
||||||
export class PriceUnitCorner extends Component {
|
export class PriceUnitCorner extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { hasUnit, onToggleHasUnit, onOpenEditor } = this.props;
|
const { hasUnit, onToggleHasUnit, onOpenEditor, onOpenTiers } = this.props;
|
||||||
return (
|
return (
|
||||||
<Box sx={priceUnitCornerSx}>
|
<Box sx={priceUnitCornerSx}>
|
||||||
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
aria-label="Staffelpreise"
|
||||||
|
onClick={onOpenTiers}
|
||||||
|
sx={{ p: 0.5 }}
|
||||||
|
>
|
||||||
|
<LayersOutlinedIcon sx={{ fontSize: 28 }} />
|
||||||
|
</IconButton>
|
||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={Boolean(hasUnit)}
|
checked={Boolean(hasUnit)}
|
||||||
|
|||||||
270
src/devtools/SseMonitor.jsx
Normal file
270
src/devtools/SseMonitor.jsx
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import { PureComponent, createRef } from 'react';
|
||||||
|
import { keyframes } from '@emotion/react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Fab from '@mui/material/Fab';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
||||||
|
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||||
|
import SensorsIcon from '@mui/icons-material/Sensors';
|
||||||
|
import sseAutomat from '../automats/sseAutomat';
|
||||||
|
|
||||||
|
const PANEL_WIDTH = 420;
|
||||||
|
const PANEL_HEIGHT = 380;
|
||||||
|
const pulseAnimationA = keyframes`
|
||||||
|
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
|
||||||
|
35% { transform: scale(1.16); box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.55); }
|
||||||
|
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
|
||||||
|
`;
|
||||||
|
const pulseAnimationB = keyframes`
|
||||||
|
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
|
||||||
|
35% { transform: scale(1.15); box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.55); }
|
||||||
|
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
function formatEventData(data) {
|
||||||
|
if (!data) return '';
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(data), null, 2);
|
||||||
|
} catch {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SseMonitor extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
status: sseAutomat.state.status,
|
||||||
|
companyId: sseAutomat.state.companyId,
|
||||||
|
events: sseAutomat.state.events,
|
||||||
|
open: false,
|
||||||
|
position: null,
|
||||||
|
eventPulse: 0,
|
||||||
|
};
|
||||||
|
this.panelRef = createRef();
|
||||||
|
this.handleToggle = this.handleToggle.bind(this);
|
||||||
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
|
this.handlePointerDown = this.handlePointerDown.bind(this);
|
||||||
|
this.handlePointerMove = this.handlePointerMove.bind(this);
|
||||||
|
this.handlePointerUp = this.handlePointerUp.bind(this);
|
||||||
|
this.handleClear = this.handleClear.bind(this);
|
||||||
|
this.handleClose = this.handleClose.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsubscribe = sseAutomat.subscribe(this, (state) => ({
|
||||||
|
status: state.status,
|
||||||
|
companyId: state.companyId,
|
||||||
|
events: state.events,
|
||||||
|
}));
|
||||||
|
window.addEventListener('keydown', this.handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubscribe?.();
|
||||||
|
window.removeEventListener('keydown', this.handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const previousEventId = prevState.events.at(-1)?.id;
|
||||||
|
const currentEventId = this.state.events.at(-1)?.id;
|
||||||
|
if (currentEventId != null && currentEventId !== previousEventId) {
|
||||||
|
this.setState((state) => ({ eventPulse: state.eventPulse + 1 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(event) {
|
||||||
|
if (!event.altKey || !event.shiftKey || event.key?.toLowerCase() !== 's') return;
|
||||||
|
event.preventDefault();
|
||||||
|
this.handleToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggle() {
|
||||||
|
this.setState((state) => ({ open: !state.open }));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
this.setState({ open: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClear() {
|
||||||
|
sseAutomat.actions.clearEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerDown(event) {
|
||||||
|
if (event.target.closest('button')) return;
|
||||||
|
const rect = this.panelRef.current?.getBoundingClientRect();
|
||||||
|
if (!rect) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
|
this.drag = {
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
pointerX: event.clientX,
|
||||||
|
pointerY: event.clientY,
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerMove(event) {
|
||||||
|
if (!this.drag || event.pointerId !== this.drag.pointerId) return;
|
||||||
|
const maxLeft = Math.max(0, window.innerWidth - PANEL_WIDTH);
|
||||||
|
const maxTop = Math.max(0, window.innerHeight - 120);
|
||||||
|
this.setState({
|
||||||
|
position: {
|
||||||
|
left: Math.max(0, Math.min(maxLeft, this.drag.left + event.clientX - this.drag.pointerX)),
|
||||||
|
top: Math.max(0, Math.min(maxTop, this.drag.top + event.clientY - this.drag.pointerY)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerUp(event) {
|
||||||
|
if (this.drag?.pointerId === event.pointerId) this.drag = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEvent(entry) {
|
||||||
|
const data = formatEventData(entry.data);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={entry.id}
|
||||||
|
sx={{ px: 1.25, py: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
||||||
|
>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="baseline">
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>
|
||||||
|
{new Date(entry.time).toLocaleTimeString()}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 700, overflowWrap: 'anywhere' }}>
|
||||||
|
{entry.type}
|
||||||
|
</Typography>
|
||||||
|
{entry.lastEventId ? (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto !important' }}>
|
||||||
|
#{entry.lastEventId}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
{data ? (
|
||||||
|
<Typography
|
||||||
|
component="pre"
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
m: 0,
|
||||||
|
mt: 0.5,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
overflowWrap: 'anywhere',
|
||||||
|
color: 'text.secondary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { open, position, status, companyId, events, eventPulse } = this.state;
|
||||||
|
const statusColor =
|
||||||
|
status === 'connected' ? 'success' : status === 'error' ? 'error' : status === 'connecting' ? 'warning' : 'default';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!open ? (
|
||||||
|
<Fab
|
||||||
|
size="small"
|
||||||
|
color={statusColor}
|
||||||
|
aria-label={`SSE-Monitor umschalten (${status})`}
|
||||||
|
aria-pressed={open}
|
||||||
|
onClick={this.handleToggle}
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: { xs: 27, sm: 171 },
|
||||||
|
bottom: { xs: 132, sm: 142 },
|
||||||
|
zIndex: (theme) => theme.zIndex.tooltip,
|
||||||
|
animation: `${eventPulse % 2 === 0 ? pulseAnimationA : pulseAnimationB} 650ms ease-out`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SensorsIcon fontSize="small" />
|
||||||
|
</Fab>
|
||||||
|
) : (
|
||||||
|
<Paper
|
||||||
|
ref={this.panelRef}
|
||||||
|
component="aside"
|
||||||
|
aria-label="SSE-Monitor"
|
||||||
|
elevation={12}
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
...(position ? { left: position.left, top: position.top } : { right: 16, bottom: 16 }),
|
||||||
|
width: `min(${PANEL_WIDTH}px, calc(100vw - 24px))`,
|
||||||
|
height: `min(${PANEL_HEIGHT}px, calc(100vh - 24px))`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
zIndex: (theme) => theme.zIndex.tooltip,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={1}
|
||||||
|
onPointerDown={this.handlePointerDown}
|
||||||
|
onPointerMove={this.handlePointerMove}
|
||||||
|
onPointerUp={this.handlePointerUp}
|
||||||
|
onPointerCancel={this.handlePointerUp}
|
||||||
|
sx={{
|
||||||
|
px: 1,
|
||||||
|
py: 0.75,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
cursor: 'grab',
|
||||||
|
touchAction: 'none',
|
||||||
|
userSelect: 'none',
|
||||||
|
'&:active': { cursor: 'grabbing' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DragIndicatorIcon fontSize="small" color="action" />
|
||||||
|
<SensorsIcon fontSize="small" color="primary" />
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, flexGrow: 1 }}>
|
||||||
|
SSE-Monitor
|
||||||
|
</Typography>
|
||||||
|
<Chip size="small" color={statusColor} label={status} />
|
||||||
|
<Tooltip title="Ereignisse löschen">
|
||||||
|
<IconButton size="small" aria-label="Ereignisse löschen" onClick={this.handleClear}>
|
||||||
|
<DeleteSweepIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Schließen">
|
||||||
|
<IconButton size="small" aria-label="Schließen" onClick={this.handleClose}>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
<Box sx={{ px: 1.25, py: 0.75, borderBottom: '1px solid', borderColor: 'divider' }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{companyId ? `Company ${companyId}` : 'Keine aktive Company'} · {events.length} Ereignisse
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
||||||
|
{events.length > 0 ? (
|
||||||
|
[...events].reverse().map((entry) => this.renderEvent(entry))
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ p: 2 }}>
|
||||||
|
Noch keine SSE-Ereignisse empfangen.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SseMonitor;
|
||||||
@@ -24,6 +24,9 @@ installCssVars();
|
|||||||
const AutomatInspector = import.meta.env.DEV
|
const AutomatInspector = import.meta.env.DEV
|
||||||
? lazy(() => import('./devtools/AutomatInspector'))
|
? lazy(() => import('./devtools/AutomatInspector'))
|
||||||
: null;
|
: null;
|
||||||
|
const SseMonitor = import.meta.env.DEV
|
||||||
|
? lazy(() => import('./devtools/SseMonitor'))
|
||||||
|
: null;
|
||||||
|
|
||||||
async function clearDevServiceWorker() {
|
async function clearDevServiceWorker() {
|
||||||
if (!import.meta.env.DEV || !('serviceWorker' in navigator)) return;
|
if (!import.meta.env.DEV || !('serviceWorker' in navigator)) return;
|
||||||
@@ -63,6 +66,11 @@ createRoot(document.getElementById('root')).render(
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
{SseMonitor ? (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<SseMonitor />
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
<PwaUpdateDialog />
|
<PwaUpdateDialog />
|
||||||
</AuthBootstrap>
|
</AuthBootstrap>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
|
|||||||
@@ -329,6 +329,10 @@ class ArtikelSearchPage extends Component {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleClearInput = () => {
|
||||||
|
this.handleInputChange({ target: { value: '' } });
|
||||||
|
};
|
||||||
|
|
||||||
fetchResults = () => {
|
fetchResults = () => {
|
||||||
const { target, filter, offset } = queryFromParams(this.props.searchParams);
|
const { target, filter, offset } = queryFromParams(this.props.searchParams);
|
||||||
const { pageSize } = this.state;
|
const { pageSize } = this.state;
|
||||||
@@ -422,9 +426,8 @@ class ArtikelSearchPage extends Component {
|
|||||||
<TextField
|
<TextField
|
||||||
sx={{ maxWidth: 520, flexGrow: 1 }}
|
sx={{ maxWidth: 520, flexGrow: 1 }}
|
||||||
autoFocus
|
autoFocus
|
||||||
type="search"
|
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="Artikel suchen (EAN, Artikelnr., Name, Barcode)…"
|
placeholder="(EAN, SKU, Name)"
|
||||||
value={input}
|
value={input}
|
||||||
onChange={this.handleInputChange}
|
onChange={this.handleInputChange}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -439,13 +442,26 @@ class ArtikelSearchPage extends Component {
|
|||||||
<SearchIcon color="action" />
|
<SearchIcon color="action" />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
|
endAdornment: input ? (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={this.handleClearInput}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
title="Suche löschen"
|
||||||
|
aria-label="Suche löschen"
|
||||||
|
>
|
||||||
|
<ClearIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
) : null,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{facetEntries.length > 0 && (
|
|
||||||
<Button
|
<Button
|
||||||
variant={categoryFilterOpen ? 'contained' : 'outlined'}
|
variant={categoryFilterOpen ? 'contained' : 'outlined'}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
disabled={facetEntries.length === 0}
|
||||||
onClick={this.toggleCategoryFilter}
|
onClick={this.toggleCategoryFilter}
|
||||||
startIcon={<FilterListIcon />}
|
startIcon={<FilterListIcon />}
|
||||||
endIcon={categoryFilterOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
endIcon={categoryFilterOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
@@ -453,7 +469,6 @@ class ArtikelSearchPage extends Component {
|
|||||||
>
|
>
|
||||||
Kategorien
|
Kategorien
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
{categories.map((catId) => {
|
{categories.map((catId) => {
|
||||||
const label = getCategoryBreadcrumbLabel(
|
const label = getCategoryBreadcrumbLabel(
|
||||||
catId,
|
catId,
|
||||||
|
|||||||
@@ -390,7 +390,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
|
|
||||||
// Create article in root category
|
// Create article in root category
|
||||||
const articleRes = await articleApi.create({
|
const articleRes = await articleApi.create({
|
||||||
parentId: rootCategoryId,
|
categoryId: rootCategoryId,
|
||||||
name: categoryName,
|
name: categoryName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Everything is created through the existing dashback API:
|
* Everything is created through the existing dashback API:
|
||||||
* - categories: POST /api/category ({ parentId, name })
|
* - categories: POST /api/category ({ parentId, name })
|
||||||
* - brands: PUT /api/brand ({ name }) → { brand: { id } }
|
* - brands: PUT /api/brand ({ name }) → { brand: { id } }
|
||||||
* - articles: POST /api/article ({ parentId, name }) → { article: { sku } }
|
* - articles: POST /api/article ({ categoryId, name }) → { article: { sku } }
|
||||||
* - price: PUT /api/article/:sku/prices ({ prices: [{ price, tax }] })
|
* - price: PUT /api/article/:sku/prices ({ prices: [{ price, tax }] })
|
||||||
* - brand: PUT /api/article/:sku/brand ({ brandId })
|
* - brand: PUT /api/article/:sku/brand ({ brandId })
|
||||||
* - eans: PUT /api/article/:sku/eans ({ eans })
|
* - eans: PUT /api/article/:sku/eans ({ eans })
|
||||||
@@ -142,7 +142,7 @@ async function importRow(row, { ensureCategory, ensureBrand }) {
|
|||||||
const brandId = row.brand ? await ensureBrand(row.brand) : null;
|
const brandId = row.brand ? await ensureBrand(row.brand) : null;
|
||||||
|
|
||||||
// Create the article under the leaf category.
|
// Create the article under the leaf category.
|
||||||
const createRes = await article.create({ parentId: leafCategoryId, name });
|
const createRes = await article.create({ categoryId: leafCategoryId, name });
|
||||||
requireObject(createRes, 'POST /api/article');
|
requireObject(createRes, 'POST /api/article');
|
||||||
const created = createRes.article;
|
const created = createRes.article;
|
||||||
requireDefined(created, 'article');
|
requireDefined(created, 'article');
|
||||||
|
|||||||
Reference in New Issue
Block a user