This commit is contained in:
seb
2026-09-19 06:37:42 +02:00
parent 02e9a555d6
commit abe078ff05
14 changed files with 1197 additions and 560 deletions

View File

@@ -31,8 +31,19 @@ export class Attachment extends PureComponent {
if (onClick && attachment) {
onClick(attachment);
} else if (attachment?.file instanceof Blob || attachment?.file instanceof File) {
const url = URL.createObjectURL(attachment.file);
window.open(url, '_blank');
if (attachment.file.size > 0) {
const url = URL.createObjectURL(attachment.file);
window.open(url, '_blank');
} else {
const docId = attachment.manifestId;
const fileId = attachment.fileId ?? attachment.id;
if (docId != null && fileId != null) {
window.open(
`/api/delivery/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`,
'_blank',
);
}
}
}
};
@@ -49,7 +60,8 @@ export class Attachment extends PureComponent {
const name = attachment?.name || 'Dokument';
const size = attachment?.size;
const isPdf = attachment?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf');
const isText = attachment?.type?.startsWith('text/') || name.toLowerCase().endsWith('.txt');
const isCsv = attachment?.type === 'text/csv' || name.toLowerCase().endsWith('.csv');
const isText = attachment?.type?.startsWith('text/') || name.toLowerCase().endsWith('.txt') || isCsv;
return (
<Box
@@ -125,7 +137,7 @@ export class Attachment extends PureComponent {
letterSpacing: '0.05em',
}}
>
TXT
{isCsv ? 'CSV' : 'TXT'}
</Typography>
</Box>
<Typography

View File

@@ -134,7 +134,7 @@ class ManifestHeader extends PureComponent {
) : null}
{onPdfUpload ? (
<FileDropzone
accept="application/pdf,.pdf"
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
disabled={saving || pdfUploading}
onFileDrop={onPdfUpload}
sx={{
@@ -150,6 +150,7 @@ class ManifestHeader extends PureComponent {
startIcon={pdfUploading ? <CircularProgress size={16} color="inherit" /> : <UploadFileIcon />}
disabled={saving || pdfUploading}
onClick={this.handleUploadClick}
title="PDF, TXT oder CSV hochladen"
sx={{
whiteSpace: 'nowrap',
height: 40,
@@ -164,8 +165,8 @@ class ManifestHeader extends PureComponent {
{pdfUploading
? 'Wird analysiert…'
: isDragging
? 'PDF hier ablegen'
: 'PDF hochladen'}
? 'Datei hier ablegen'
: 'Datei hochladen'}
</Button>
)}
</FileDropzone>
@@ -182,7 +183,7 @@ class ManifestHeader extends PureComponent {
<input
ref={this.fileInputRef}
type="file"
accept="application/pdf,.pdf"
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
style={{ display: 'none' }}
onChange={this.handleFileChange}
/>

View File

@@ -15,6 +15,7 @@ import CloseIcon from '@mui/icons-material/Close';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
@@ -97,6 +98,9 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
const { streaming = false, messages = [], result = null, error = null } = data ?? {};
const hasResult = Boolean(result);
const isTextDoc =
filename?.toLowerCase().endsWith('.txt') || filename?.toLowerCase().endsWith('.csv');
return (
<Dialog
open={open}
@@ -121,12 +125,18 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
<ErrorOutlinedIcon color="error" />
) : streaming ? (
<CircularProgress size={22} />
) : isTextDoc ? (
<TextSnippetOutlinedIcon color="action" />
) : (
<PictureAsPdfIcon color="action" />
)}
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle1" component="div" fontWeight={600}>
{streaming ? 'PDF wird analysiert…' : error ? 'PDF-Analyse fehlgeschlagen' : 'PDF-Analyse abgeschlossen'}
{streaming
? (isTextDoc ? 'Dokument wird analysiert…' : 'PDF wird analysiert…')
: error
? 'Analyse fehlgeschlagen'
: (isTextDoc ? 'Dokument-Analyse abgeschlossen' : 'PDF-Analyse abgeschlossen')}
</Typography>
{filename ? (
<Typography variant="caption" color="text.secondary">

View File

@@ -1,6 +1,7 @@
import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
@@ -12,10 +13,15 @@ import Chip from '@mui/material/Chip';
import CloseIcon from '@mui/icons-material/Close';
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
import { enqueueSnackbar } from 'notistack';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { delivery as deliveryApi } from '../../api';
export class TextEditorDialog extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -26,6 +32,7 @@ export class TextEditorDialog extends PureComponent {
this.handleChange = this.handleChange.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleCopy = this.handleCopy.bind(this);
this.handleDownload = this.handleDownload.bind(this);
}
componentDidMount() {
@@ -34,12 +41,16 @@ export class TextEditorDialog extends PureComponent {
componentDidUpdate(prevProps) {
if (this.props.attachment !== prevProps.attachment || (this.props.open && !prevProps.open)) {
this.loadContent();
if (this.props.open) {
this.loadContent();
} else {
this.setState({ content: '', initialContent: '', loading: false });
}
}
}
async loadContent() {
const { attachment, open } = this.props;
const { attachment, open, docId, onContentLoad } = this.props;
if (!open || !attachment) return;
if (typeof attachment.text === 'string') {
@@ -51,7 +62,10 @@ export class TextEditorDialog extends PureComponent {
return;
}
if (attachment.file instanceof Blob || attachment.file instanceof File) {
if (
(attachment.file instanceof Blob || attachment.file instanceof File) &&
attachment.file.size > 0
) {
this.setState({ loading: true });
try {
const text = await attachment.file.text();
@@ -60,11 +74,44 @@ export class TextEditorDialog extends PureComponent {
initialContent: text,
loading: false,
});
return;
} catch (err) {
console.error('Fehler beim Lesen der Textdatei:', err);
this.setState({ loading: false });
}
}
// Attachment has no text and empty file — load from API (GET /delivery/{id}/file/{fileId})
const effectiveDocId = docId ?? attachment.manifestId;
const fileId = attachment.fileId ?? attachment.id;
if (effectiveDocId != null && fileId != null) {
this.setState({ loading: true });
try {
const getFile =
this.context?.delivery?.getFile?.bind(this.context.delivery) ||
deliveryApi.getFile;
const blob = await getFile(effectiveDocId, fileId);
const text = await blob.text();
this.setState({
content: text,
initialContent: text,
loading: false,
});
onContentLoad?.(attachment, text, blob);
return;
} catch (err) {
console.error('Fehler beim Laden der Textdatei:', err);
enqueueSnackbar('Fehler beim Laden der Datei', { variant: 'error' });
this.setState({ loading: false });
return;
}
}
this.setState({
content: '',
initialContent: '',
loading: false,
});
}
handleChange(e) {
@@ -88,6 +135,22 @@ export class TextEditorDialog extends PureComponent {
}
}
handleDownload() {
const { attachment } = this.props;
const { content } = this.state;
if (!content) return;
const filename = attachment?.name || 'Dokument.txt';
const blob = new Blob([content], { type: 'text/plain;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
render() {
const { open, onClose, attachment, readOnly = false } = this.props;
if (!open) return null;
@@ -152,8 +215,24 @@ export class TextEditorDialog extends PureComponent {
display: 'flex',
flexDirection: 'column',
bgcolor: '#fafafa',
position: 'relative',
}}
>
{loading && (
<Box
sx={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'rgba(250, 250, 250, 0.7)',
zIndex: 2,
}}
>
<CircularProgress size={36} />
</Box>
)}
<TextField
multiline
fullWidth
@@ -216,15 +295,26 @@ export class TextEditorDialog extends PureComponent {
justifyContent: 'space-between',
}}
>
<Button
size="small"
variant="outlined"
startIcon={<ContentCopyIcon />}
onClick={this.handleCopy}
disabled={loading || !content}
>
Kopieren
</Button>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
size="small"
variant="outlined"
startIcon={<ContentCopyIcon />}
onClick={this.handleCopy}
disabled={loading || !content}
>
Kopieren
</Button>
<Button
size="small"
variant="outlined"
startIcon={<DownloadOutlinedIcon />}
onClick={this.handleDownload}
disabled={loading || !content}
>
Download
</Button>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button size="small" onClick={onClose}>
{readOnly ? 'Schließen' : 'Abbrechen'}