This commit is contained in:
seb
2026-10-04 08:18:36 +02:00
parent f2fce9097b
commit 4edd417dab
19 changed files with 141 additions and 220 deletions

View File

@@ -3,7 +3,6 @@ import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import CategoryIcon from '@mui/icons-material/Category';
import HistoryIcon from '@mui/icons-material/History';
import LabelIcon from '@mui/icons-material/Label';

View File

@@ -45,7 +45,7 @@ export default class CategoryPickerDialog extends PureComponent {
componentDidMount() {
if (this.props.categories === undefined || this.props.rootCategoryId === undefined) {
this._unsub = categoryAutomat.subscribe((s) => {
this.setState((prev) => ({
this.setState(() => ({
categories: this.props.categories ?? s.categories,
rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId,
}));
@@ -132,7 +132,6 @@ export default class CategoryPickerDialog extends PureComponent {
const dialogTitle = title || (mode === 'single' ? 'Kategorie auswählen' : 'Kategorie zuweisen');
const actionLabel = confirmLabel || (mode === 'single' ? 'Auswählen' : 'Zuweisen');
const selectedNode = categories && selectedId != null ? categories[String(selectedId)] : null;
const isAlreadyAssigned =
mode !== 'single' && currentCategoryIds.some((id) => String(id) === String(selectedId));
const isSelectedDisabled =

View File

@@ -366,22 +366,20 @@ export default class ArticleViewLists extends PureComponent {
const count = Math.max(1, Number(stuecklisteCount) || 1);
const existingIndex = currentItems.findIndex((i) => i.sku === stuecklisteTargetArticle.sku);
let nextItems = [];
if (existingIndex >= 0) {
nextItems = currentItems.map((item, idx) =>
idx === existingIndex ? { ...item, count: item.count + count } : item
);
} else {
nextItems = [
...currentItems,
{
articleId: stuecklisteTargetArticle.id,
sku: stuecklisteTargetArticle.sku,
count,
sort: currentItems.length,
},
];
}
const nextItems =
existingIndex >= 0
? currentItems.map((item, idx) =>
idx === existingIndex ? { ...item, count: item.count + count } : item
)
: [
...currentItems,
{
articleId: stuecklisteTargetArticle.id,
sku: stuecklisteTargetArticle.sku,
count,
sort: currentItems.length,
},
];
this.setState({ busy: true });
try {
@@ -492,25 +490,23 @@ export default class ArticleViewLists extends PureComponent {
const count = Math.max(1, Number(optionCount) || 1);
const existingIndex = currentOptions.findIndex((o) => o.sku === optionTargetArticle.sku);
let nextOptions = [];
if (existingIndex >= 0) {
nextOptions = currentOptions.map((opt, idx) =>
idx === existingIndex
? { ...opt, count: opt.count + count, mandated: Boolean(optionMandated) }
: opt
);
} else {
nextOptions = [
...currentOptions,
{
articleId: optionTargetArticle.id,
sku: optionTargetArticle.sku,
count,
sort: currentOptions.length,
mandated: Boolean(optionMandated),
},
];
}
const nextOptions =
existingIndex >= 0
? currentOptions.map((opt, idx) =>
idx === existingIndex
? { ...opt, count: opt.count + count, mandated: Boolean(optionMandated) }
: opt
)
: [
...currentOptions,
{
articleId: optionTargetArticle.id,
sku: optionTargetArticle.sku,
count,
sort: currentOptions.length,
mandated: Boolean(optionMandated),
},
];
this.setState({ busy: true });
try {
@@ -1363,15 +1359,9 @@ export default class ArticleViewLists extends PureComponent {
busy,
activeCreatorMode,
activeListTab,
selectedAttributeId,
selectedChildValueId,
newChildText,
isCustomChildValue,
stuecklisteTargetArticle,
stuecklisteCount,
optionTargetArticle,
optionCount,
optionMandated,
removeTargetChild,
confirmClearType,
} = this.state;

View File

@@ -184,13 +184,14 @@ export default class ArticleAttributesTab extends PureComponent {
return;
}
let nextValues = [];
if (isExclusive) {
const others = assignedAttributes.filter((a) => a.attributeId !== targetAttr.id);
nextValues = [...others.map((a) => a.attributeValueId), targetValId];
} else {
nextValues = [...assignedAttributes.map((a) => a.attributeValueId), targetValId];
}
const nextValues = isExclusive
? [
...assignedAttributes
.filter((a) => a.attributeId !== targetAttr.id)
.map((a) => a.attributeValueId),
targetValId,
]
: [...assignedAttributes.map((a) => a.attributeValueId), targetValId];
this.setState({ saving: true });
try {
@@ -222,19 +223,13 @@ export default class ArticleAttributesTab extends PureComponent {
return;
}
}
let nextProps = [];
if (isExclusive) {
const existing = assignedProperties.filter((p) => p.propertyId !== propId);
nextProps = [
...existing.map((p) => ({ propertyId: p.propertyId, value: p.value, sort: p.sort })),
{ propertyId: propId, value: val },
];
} else {
nextProps = [
...assignedProperties.map((p) => ({ propertyId: p.propertyId, value: p.value, sort: p.sort })),
{ propertyId: propId, value: val },
];
}
const sourceProps = isExclusive
? assignedProperties.filter((p) => p.propertyId !== propId)
: assignedProperties;
const nextProps = [
...sourceProps.map((p) => ({ propertyId: p.propertyId, value: p.value, sort: p.sort })),
{ propertyId: propId, value: val },
];
this.setState({ saving: true });
try {

View File

@@ -43,7 +43,7 @@ function getMatchingAndAncestorIds(categories, query, rootCategoryId, excludeIds
if (!categories || !query.trim()) return matches;
const q = query.trim().toLowerCase();
for (const [idStr, node] of Object.entries(categories)) {
for (const node of Object.values(categories)) {
if (excludeIds.some((id) => String(id) === String(node.id))) continue;
const label = categoryDisplayName(node.name, node.id, rootCategoryId).toLowerCase();
if (label.includes(q)) {
@@ -580,7 +580,6 @@ export default class CategoryTree extends PureComponent {
addCategoryName,
isSubmitting,
renameDialogOpen,
renameNode,
renameValue,
deleteDialogOpen,
deleteNode,

View File

@@ -2,7 +2,6 @@ import {
ROOT_CATEGORY_NAME,
categorySlug,
getCategoryBreadcrumbs,
getCategoryChildren,
} from '../../utils/categoryUtils';
export { ROOT_CATEGORY_NAME, categorySlug };

View File

@@ -9,7 +9,6 @@ import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import SendRoundedIcon from '@mui/icons-material/SendRounded';
@@ -231,7 +230,6 @@ export default class ChatDrawer extends PureComponent {
const {
conversations,
selectedConversationId,
sseStatus,
loadingConversations,
} = this.state;
@@ -249,21 +247,8 @@ export default class ChatDrawer extends PureComponent {
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25 }}>
<Box
sx={{
width: 32,
height: 32,
borderRadius: 2,
bgcolor: 'primary.main',
color: 'primary.contrastText',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<AutoAwesomeRoundedIcon sx={{ fontSize: 18 }} />
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AutoAwesomeRoundedIcon sx={{ fontSize: 22, color: 'primary.main' }} />
<Typography variant="subtitle1" fontWeight={700}>
KI-Assistent
</Typography>
@@ -275,80 +260,82 @@ export default class ChatDrawer extends PureComponent {
</Box>
{/* Conversation Selector Bar */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FormControl fullWidth size="small">
<InputLabel id="chat-conversation-label">Konversation</InputLabel>
<Select
labelId="chat-conversation-label"
value={selectedConversationId || ''}
label="Konversation"
onChange={this.handleSelectConversation}
disabled={loadingConversations || conversations.length === 0}
sx={{ borderRadius: 2 }}
renderValue={(selectedId) => {
const conv = conversations.find((c) => c.id === selectedId);
if (!conv) return '';
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, overflow: 'hidden' }}>
<Typography variant="body2" fontWeight={600} noWrap>
#{conv.id}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{conv.model || 'Standard'}
</Typography>
</Box>
);
{conversations.length > 0 && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FormControl fullWidth size="small">
<InputLabel id="chat-conversation-label">Konversation</InputLabel>
<Select
labelId="chat-conversation-label"
value={selectedConversationId || ''}
label="Konversation"
onChange={this.handleSelectConversation}
disabled={loadingConversations || conversations.length === 0}
sx={{ borderRadius: 2 }}
renderValue={(selectedId) => {
const conv = conversations.find((c) => c.id === selectedId);
if (!conv) return '';
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, overflow: 'hidden' }}>
<Typography variant="body2" fontWeight={600} noWrap>
#{conv.id}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{conv.model || 'Standard'}
</Typography>
</Box>
);
}}
>
{conversations.map((c) => (
<MenuItem key={c.id} value={c.id}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%', overflow: 'hidden' }}>
<Typography variant="body2" fontWeight={600} noWrap>
#{c.id}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ flexGrow: 1 }}>
{c.model || 'Standard'}
</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
<IconButton
size="small"
color="error"
disabled={!selectedConversationId}
onClick={(e) => this.handleDeleteConversation(e, selectedConversationId)}
aria-label="Konversation löschen"
sx={{
borderRadius: 2,
border: 1,
borderColor: 'divider',
p: 0.9,
flexShrink: 0,
}}
>
{conversations.map((c) => (
<MenuItem key={c.id} value={c.id}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%', overflow: 'hidden' }}>
<Typography variant="body2" fontWeight={600} noWrap>
#{c.id}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ flexGrow: 1 }}>
{c.model || 'Standard'}
</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
<DeleteOutlineRoundedIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="error"
disabled={!selectedConversationId}
onClick={(e) => this.handleDeleteConversation(e, selectedConversationId)}
aria-label="Konversation löschen"
sx={{
borderRadius: 2,
border: 1,
borderColor: 'divider',
p: 0.9,
flexShrink: 0,
}}
>
<DeleteOutlineRoundedIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="primary"
disabled={!selectedConversationId}
onClick={this.handleNewChat}
aria-label="Neuer Chat"
sx={{
borderRadius: 2,
border: 1,
borderColor: 'divider',
p: 0.9,
flexShrink: 0,
}}
>
<AddRoundedIcon fontSize="small" />
</IconButton>
</Box>
<IconButton
size="small"
color="primary"
disabled={!selectedConversationId}
onClick={this.handleNewChat}
aria-label="Neuer Chat"
sx={{
borderRadius: 2,
border: 1,
borderColor: 'divider',
p: 0.9,
flexShrink: 0,
}}
>
<AddRoundedIcon fontSize="small" />
</IconButton>
</Box>
)}
</Box>
);
}
@@ -362,7 +349,6 @@ export default class ChatDrawer extends PureComponent {
messages,
streamingText,
loadingConversations,
loadingMessages,
model,
} = this.state;
@@ -391,20 +377,7 @@ export default class ChatDrawer extends PureComponent {
gap: 2,
}}
>
<Box
sx={{
width: 56,
height: 56,
borderRadius: '50%',
bgcolor: 'primary.light',
color: 'primary.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<AutoAwesomeRoundedIcon sx={{ fontSize: 32 }} />
</Box>
<AutoAwesomeRoundedIcon sx={{ fontSize: 44, color: 'primary.main', opacity: 0.85 }} />
<Typography variant="h6" fontWeight={700}>
Willkommen im Chat
</Typography>

View File

@@ -177,14 +177,17 @@ function MessageRow({ msg, showTime = true }) {
bodyLength = textContent.length;
}
let displayText = '';
if (resObj && ('answer' in resObj || 'content' in resObj) && ('tool_call_id' in resObj) && !('status' in resObj)) {
displayText = resObj.answer || resObj.content || '';
} else {
displayText = statusText
const isToolAnswer =
Boolean(resObj) &&
('answer' in resObj || 'content' in resObj) &&
'tool_call_id' in resObj &&
!('status' in resObj);
const displayText = isToolAnswer
? resObj.answer || resObj.content || ''
: statusText
? `${statusText}${bodyLength != null ? ` · ${bodyLength} byte` : ''}`
: (bodyLength != null ? `${bodyLength} byte` : String(textContent));
}
return (
<Box sx={{ display: 'flex', justifyContent: 'flex-start', py: 0.35, px: 2 }}>

View File

@@ -68,7 +68,7 @@ export class Attachment extends PureComponent {
}
};
handleTileClick = (e) => {
handleTileClick = () => {
const { attachment, onClick } = this.props;
if (onClick && attachment) {
onClick(attachment);

View File

@@ -1,12 +1,9 @@
import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Typography from '@mui/material/Typography';
import { enqueueSnackbar } from 'notistack';
import BackendError from '../BackendError';
import { PageTitleRight } from '../PageLayout';
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import { PanelTileGrid } from 'dashgui/panels';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
import { backendErrorFrom } from '../../utils/backendError';
@@ -435,7 +432,7 @@ class ManifestForm extends PureComponent {
}
render() {
const { kind, mode } = this.props;
const { kind } = this.props;
const cfg = this.cfg;
const {
status,

View File

@@ -85,7 +85,6 @@ class ManifestHeader extends PureComponent {
saving,
pdfUploading,
attachments,
lines,
onPdfUpload,
onPdfRescan,
onPdfDelete,

View File

@@ -24,7 +24,6 @@ class ManifestLines extends PureComponent {
onAddLine,
onRemoveLine,
onRemoveAllLines,
onSave,
columns = LINE_COLUMNS,
supplierArticlesMap = {},
supplierArticlesByLineKey = {},

View File

@@ -275,31 +275,6 @@ function fileToBase64(file) {
});
}
/**
* Convert base64 string to Blob
* @param {string} base64
* @param {string} mimeType
* @returns {Blob}
*/
function base64ToBlob(base64, mimeType) {
const clean = base64.includes(',') ? base64.split(',')[1] : base64;
const byteCharacters = atob(clean);
const byteArrays = [];
const sliceSize = 512;
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: mimeType });
}
/**
* Restore attachments from API response
* @param {Array<{id: number, fileId?: number, manifestId?: number, mimetype: string, name: string, thumbavif?: any, size: number, created?: string, meta?: object}>} apiFiles