This commit is contained in:
seb
2026-10-16 13:10:58 +02:00
parent 7bc1da4de6
commit d851c6e1e9
10 changed files with 326 additions and 445 deletions

View File

@@ -199,9 +199,9 @@ export default class ChatDrawer extends PureComponent {
activeTurns: state.activeTurns || [],
};
if (state.pendingInput != null) {
patch.inputText = state.pendingInput;
patch.stagedAttachments = [];
patch.stagedReferences = [];
patch.inputText = this.state.inputText
? `${this.state.inputText}\n\n${state.pendingInput}`
: state.pendingInput;
queueMicrotask(() => {
if (chatAutomat.state.pendingInput != null) {
chatAutomat.setState({ pendingInput: null });

View File

@@ -5,12 +5,12 @@ import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import SyncIcon from '@mui/icons-material/Sync';
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles';
import SkeleHashImage from '../settings/SkeleHashImage';
import { generateTextThumbnail } from '../../utils/textThumbnail';
import { notifyErrorMessage } from '../../utils/notify';
import chatAutomat from '../../automats/chatAutomat';
import {
SCRIM_ACTION,
SCRIM_ATTACHMENT,
@@ -61,18 +61,40 @@ export class Attachment extends PureComponent {
handleRescan = (e) => {
e?.stopPropagation?.();
if (this.props.hasArticles) {
notifyErrorMessage(
'Neu analysieren nicht möglich, solange Artikel vorhanden sind. Bitte entfernen Sie zuerst alle Zeilen.',
);
return;
}
const { attachment, onRescan } = this.props;
if (attachment && onRescan) {
onRescan(attachment);
}
};
handleMentionInChat = async (e) => {
e?.stopPropagation?.();
const { attachment, onMentionInChat } = this.props;
if (!attachment) return;
if (onMentionInChat) {
onMentionInChat(attachment);
return;
}
let textContent = attachment.text || '';
if (!textContent && (attachment.file instanceof Blob || attachment.file instanceof File)) {
try {
textContent = await attachment.file.text();
} catch (err) {
console.error('Fehler beim Lesen des Texts:', err);
}
}
const trimmed = (textContent || '').trim();
if (!trimmed) return;
const title = attachment.name ? `Text aus „${attachment.name}“:` : 'Text:';
const quote = `${title}\n\`\`\`\n${trimmed}\n\`\`\``;
chatAutomat.actions.insertPrompt(quote);
};
handleDelete = (e) => {
e?.stopPropagation?.();
const { attachment, onDelete } = this.props;
@@ -237,51 +259,51 @@ export class Attachment extends PureComponent {
}}
onClick={(e) => e.stopPropagation()}
>
{kind === 'delivery' && Boolean(onRescan) && (isPdf || isText) ? (
<Tooltip
title={
hasArticles
? 'Neu analysieren nicht möglich, solange Artikel vorhanden sind'
: 'Neu analysieren'
}
{kind === 'delivery' && Boolean(onRescan) && isPdf ? (
<IconButton
size="small"
disabled={disabled}
onClick={this.handleRescan}
aria-label="Zu Text"
sx={{
bgcolor: SCRIM_ACTION,
color: WHITE,
p: 0.5,
backdropFilter: 'blur(2px)',
'&:hover': {
bgcolor: SCRIM_HEAVY,
},
}}
>
<span
style={{ display: 'inline-block' }}
onClick={
hasArticles
? (e) => {
e.stopPropagation();
notifyErrorMessage(
'Neu analysieren nicht möglich, solange Artikel vorhanden sind. Bitte entfernen Sie zuerst alle Zeilen.',
);
}
: undefined
}
>
<IconButton
size="small"
disabled={disabled || hasArticles}
onClick={this.handleRescan}
sx={{
bgcolor: SCRIM_ACTION,
color: hasArticles ? VEIL_DISABLED : WHITE,
p: 0.5,
backdropFilter: 'blur(2px)',
cursor: hasArticles ? 'not-allowed' : 'pointer',
'&:hover': {
bgcolor: hasArticles ? SCRIM_ACTION : SCRIM_HEAVY,
},
}}
>
<SyncIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<TextSnippetOutlinedIcon sx={{ fontSize: 16 }} />
</IconButton>
) : null}
{isText ? (
<IconButton
size="small"
disabled={disabled}
onClick={this.handleMentionInChat}
aria-label="Im Chat erwähnen"
sx={{
bgcolor: SCRIM_ACTION,
color: WHITE,
p: 0.5,
backdropFilter: 'blur(2px)',
'&:hover': {
bgcolor: SCRIM_HEAVY,
},
}}
>
<ChatBubbleOutlineRoundedIcon sx={{ fontSize: 16 }} />
</IconButton>
) : null}
<IconButton
size="small"
disabled={disabled}
onClick={this.handleDelete}
aria-label="Löschen"
sx={{
bgcolor: SCRIM_ACTION,
color: WHITE,

View File

@@ -6,7 +6,7 @@ import Attachment from './Attachment';
export class AttachmentList extends PureComponent {
render() {
const { attachments, onRescan, onDelete, onClick, disabled, hasArticles, kind, docId } = this.props;
const { attachments, onRescan, onDelete, onClick, onMentionInChat, disabled, hasArticles, kind, docId } = this.props;
if (!Array.isArray(attachments) || attachments.length === 0) {
return null;
}
@@ -31,6 +31,7 @@ export class AttachmentList extends PureComponent {
onRescan={onRescan}
onDelete={onDelete}
onClick={onClick}
onMentionInChat={onMentionInChat}
disabled={disabled}
hasArticles={hasArticles}
/>

View File

@@ -40,11 +40,14 @@ class ManifestFormLine extends PureComponent {
cachedArticle: sku ? getArticle(sku) : null,
loadingArticle: false,
fallbackSuppliers: [],
editingField: null,
};
this.handleRemove = this.handleRemove.bind(this);
this.handleMoveUp = this.handleMoveUp.bind(this);
this.handleMoveDown = this.handleMoveDown.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleStartEdit = this.handleStartEdit.bind(this);
this.handleCreateArticle = this.handleCreateArticle.bind(this);
this.handleSearchArticle = this.handleSearchArticle.bind(this);
this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this);
@@ -134,7 +137,25 @@ class ManifestFormLine extends PureComponent {
this.props.onMove?.(this.props.line.key, 1);
}
handleStartEdit(columnId) {
const { saving } = this.props;
const isDisabled = saving || (columnId === 'sku' && Boolean(this.props.isOnShipment));
if (isDisabled) return;
if (this.state.editingField !== columnId) {
this.setState({ editingField: columnId });
}
}
handleKeyDown(e) {
if (e.key === 'Enter' || e.key === 'Escape') {
e.target.blur();
}
}
handleBlur(e, columnId) {
if (this.state.editingField === columnId) {
this.setState({ editingField: null });
}
if (columnId === 'sku' && this.props.onSkuBlur) {
this.props.onSkuBlur(this.props.line.key, e.target.value);
}
@@ -229,6 +250,8 @@ class ManifestFormLine extends PureComponent {
renderField(column, colIdx, totalCols) {
const { id, align, type } = column;
const { line, saving, onChange, columns } = this.props;
const isFirstLine = Boolean(this.props.isFirstLine ?? (this.props.index === 0));
const isLastLine = Boolean(this.props.isLastLine);
if (id === 'pos') {
const posVal =
@@ -240,8 +263,10 @@ class ManifestFormLine extends PureComponent {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
minHeight: 38,
height: isFirstLine ? 40 : 41,
minHeight: isFirstLine ? 40 : 41,
maxHeight: isFirstLine ? 40 : 41,
mt: isFirstLine ? 0 : '-1px',
fontWeight: 700,
fontSize: '0.82rem',
color: 'text.secondary',
@@ -297,65 +322,197 @@ class ManifestFormLine extends PureComponent {
const isFirstInput = hasPosCol ? colIdx === 1 : colIdx === 0;
const isLast = colIdx === totalCols - 1;
const topLeftRadius = isFirstInput && isFirstLine ? 4 : 0;
const topRightRadius = isLast && isFirstLine ? 4 : 0;
const bottomRightRadius = isLast && isLastLine ? 4 : 0;
const bottomLeftRadius = isFirstInput && isLastLine ? 4 : 0;
const borderRadius = `${topLeftRadius}px ${topRightRadius}px ${bottomRightRadius}px ${bottomLeftRadius}px`;
const { dirtyFields } = this.props;
const isFieldDirty = Boolean(
dirtyFields && (dirtyFields.__all__ || dirtyFields[id])
);
return (
<TextField
size="small"
fullWidth
type={type}
value={val ?? ''}
error={isError}
disabled={saving || (id === 'sku' && Boolean(this.props.isOnShipment))}
title={id === 'sku' && this.props.isOnShipment ? 'Artikel ist bereits auf einem Lieferschein und kann nicht geändert werden' : undefined}
autoComplete="off"
onChange={(e) => onChange(line.key, id, e.target.value)}
onBlur={(e) => this.handleBlur(e, id)}
slotProps={{
htmlInput: {
inputMode: type === 'number' ? 'decimal' : 'text',
style: { textAlign: align },
autoComplete: 'off',
step: type === 'number' ? 'any' : undefined,
},
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: isFirstInput ? '4px 0 0 4px' : isLast ? '0 4px 4px 0' : 0,
const isEditing = this.state.editingField === id;
const isDisabled = saving || (id === 'sku' && Boolean(this.props.isOnShipment));
const titleText = id === 'sku' && this.props.isOnShipment
? 'Artikel ist bereits auf einem Lieferschein und kann nicht geändert werden'
: undefined;
if (isEditing) {
return (
<TextField
autoFocus
size="small"
type={type}
value={val ?? ''}
error={isError}
disabled={isDisabled}
title={titleText}
autoComplete="off"
onChange={(e) => onChange(line.key, id, e.target.value)}
onBlur={(e) => this.handleBlur(e, id)}
onKeyDown={this.handleKeyDown}
slotProps={{
htmlInput: {
inputMode: type === 'number' ? 'decimal' : 'text',
style: { textAlign: align },
autoComplete: 'off',
step: type === 'number' ? 'any' : undefined,
},
}}
sx={{
position: 'relative',
ml: isFirstInput ? 0 : '-1px',
...(isFieldDirty && {
bgcolor: 'rgba(237, 108, 2, 0.08)',
}),
'&:hover': {
zIndex: 1,
width: isFirstInput ? '100%' : 'calc(100% + 1px)',
mt: isFirstLine ? 0 : '-1px',
height: isFirstLine ? 40 : 41,
zIndex: 3,
'& .MuiOutlinedInput-root': {
height: isFirstLine ? 40 : 41,
borderRadius,
width: '100%',
...(isFieldDirty && {
bgcolor: 'rgba(237, 108, 2, 0.08)',
}),
'&:hover': {
zIndex: 4,
},
'&.Mui-focused': {
zIndex: 5,
},
},
'&.Mui-focused': {
zIndex: 2,
},
},
...(isFieldDirty && {
zIndex: 1,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'warning.main',
borderWidth: 2,
borderWidth: '1px !important',
borderColor: isFieldDirty
? 'warning.main'
: isError
? 'error.main'
: 'rgba(0, 0, 0, 0.23)',
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderWidth: '1px !important',
borderColor: isFieldDirty
? 'warning.main'
: isError
? 'error.main'
: 'primary.main',
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: 'warning.main',
borderWidth: '1px !important',
borderColor: isFieldDirty
? 'warning.main'
: isError
? 'error.main'
: 'rgba(0, 0, 0, 0.87)',
},
'& input[type=number]': {
MozAppearance: 'textfield',
},
'& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button':
{
WebkitAppearance: 'none',
margin: 0,
},
}}
/>
);
}
const hasValue = val != null && String(val).trim() !== '';
return (
<Box
tabIndex={isDisabled ? -1 : 0}
role={isDisabled ? undefined : 'button'}
onClick={() => this.handleStartEdit(id)}
onFocus={() => this.handleStartEdit(id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.handleStartEdit(id);
}
}}
title={titleText}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start',
height: isFirstLine ? 40 : 41,
minHeight: isFirstLine ? 40 : 41,
maxHeight: isFirstLine ? 40 : 41,
boxSizing: 'border-box',
position: 'relative',
ml: isFirstInput ? 0 : '-1px',
width: isFirstInput ? '100%' : 'calc(100% + 1px)',
mt: isFirstLine ? 0 : '-1px',
px: '14px',
py: 0,
borderRadius,
border: '1px solid',
borderColor: isFieldDirty
? 'warning.main'
: isError
? 'error.main'
: 'rgba(0, 0, 0, 0.23)',
cursor: isDisabled ? 'default' : 'text',
touchAction: 'manipulation',
userSelect: 'none',
zIndex: 1,
...(isFieldDirty && {
bgcolor: 'rgba(237, 108, 2, 0.08)',
zIndex: 2,
}),
'& input[type=number]': {
MozAppearance: 'textfield',
},
'& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button':
{
WebkitAppearance: 'none',
margin: 0,
...(isError && !isFieldDirty && {
bgcolor: 'rgba(211, 47, 47, 0.04)',
borderColor: 'error.main',
zIndex: 2,
}),
'&:hover': isDisabled
? undefined
: {
bgcolor: isFieldDirty
? 'rgba(237, 108, 2, 0.14)'
: isError
? 'rgba(211, 47, 47, 0.08)'
: 'action.hover',
borderColor: isFieldDirty
? 'warning.main'
: isError
? 'error.main'
: 'rgba(0, 0, 0, 0.87)',
zIndex: 3,
},
'&:focus-visible': {
outline: '2px solid',
outlineColor: 'primary.main',
outlineOffset: -2,
zIndex: 4,
},
}}
/>
>
<Typography
variant="body2"
sx={{
textAlign: align,
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: '0.875rem',
lineHeight: '20px',
fontWeight: id === 'sku' ? 600 : 400,
color: isError
? 'error.main'
: hasValue
? 'text.primary'
: 'text.disabled',
fontStyle: 'normal',
}}
>
{hasValue ? String(val) : '—'}
</Typography>
</Box>
);
}
@@ -979,7 +1136,11 @@ class ManifestFormLine extends PureComponent {
sx={{
p: 0,
width: col.width,
verticalAlign: 'middle',
minWidth: col.width,
height: 40,
verticalAlign: 'top',
position: 'relative',
overflow: 'visible',
}}
>
{this.renderField(col, colIdx, columns.length)}

View File

@@ -243,6 +243,7 @@ class ManifestHeader extends PureComponent {
onPdfDelete,
onPdfClick,
onAttachmentClick,
onMentionInChat,
onDateChange,
onNumberChange,
onExternalNumberChange,
@@ -456,6 +457,7 @@ class ManifestHeader extends PureComponent {
onRescan={onPdfRescan}
onDelete={onPdfDelete}
onClick={onAttachmentClick ?? onPdfClick}
onMentionInChat={onMentionInChat}
disabled={saving || pdfUploading}
hasArticles={hasArticles}
/>