329 lines
10 KiB
JavaScript
329 lines
10 KiB
JavaScript
import { PureComponent } from 'react';
|
|
import Box from '@mui/material/Box';
|
|
import IconButton from '@mui/material/IconButton';
|
|
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 DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
|
import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles';
|
|
import { generateTextThumbnail } from '../../utils/textThumbnail';
|
|
import { notifyErrorMessage } from '../../utils/notify';
|
|
import {
|
|
SCRIM_ACTION,
|
|
SCRIM_ATTACHMENT,
|
|
SCRIM_HEAVY,
|
|
SURFACE_BASE,
|
|
SURFACE_FAINT,
|
|
SURFACE_RAISED,
|
|
SURFACE_SUBTLE,
|
|
VEIL_DISABLED,
|
|
VEIL_TEXT,
|
|
WHITE,
|
|
} from '../../colors';
|
|
|
|
export class Attachment extends PureComponent {
|
|
state = {
|
|
generatedThumbnail: null,
|
|
};
|
|
|
|
componentDidMount() {
|
|
this.ensureThumbnail();
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (this.props.attachment !== prevProps.attachment) {
|
|
this.ensureThumbnail();
|
|
}
|
|
}
|
|
|
|
async ensureThumbnail() {
|
|
const { attachment } = this.props;
|
|
if (!attachment || attachment.thumbnail) return;
|
|
const isCsv = attachment.type === 'text/csv' || attachment.name?.toLowerCase().endsWith('.csv');
|
|
const isText = attachment.type?.startsWith('text/') || attachment.name?.toLowerCase().endsWith('.txt') || isCsv;
|
|
if (!isText) return;
|
|
|
|
const source = attachment.text || attachment.file;
|
|
if (!source) return;
|
|
|
|
try {
|
|
const thumb = await generateTextThumbnail(source);
|
|
if (thumb && this.props.attachment === attachment) {
|
|
this.setState({ generatedThumbnail: thumb });
|
|
}
|
|
} catch (err) {
|
|
console.error('Fehler beim Generieren des Text-Thumbnails:', err);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
handleDelete = (e) => {
|
|
e?.stopPropagation?.();
|
|
const { attachment, onDelete } = this.props;
|
|
if (attachment && onDelete) {
|
|
onDelete(attachment);
|
|
}
|
|
};
|
|
|
|
handleTileClick = () => {
|
|
const { attachment, onClick } = this.props;
|
|
if (onClick && attachment) {
|
|
onClick(attachment);
|
|
} else if (attachment?.file instanceof Blob || attachment?.file instanceof File) {
|
|
if (attachment.file.size > 0) {
|
|
const url = URL.createObjectURL(attachment.file);
|
|
window.open(url, '_blank');
|
|
} else {
|
|
const docId = this.props.docId ?? attachment.manifestId;
|
|
const fileId = attachment.fileId ?? attachment.id;
|
|
const kind = this.props.kind || 'delivery';
|
|
const prefix = kind === 'delivery' ? 'delivery' : kind === 'quote' ? 'quote' : 'order';
|
|
if (docId != null && fileId != null) {
|
|
window.open(
|
|
`/api/${prefix}/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`,
|
|
'_blank',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
formatFileSize(bytes) {
|
|
if (bytes == null || Number.isNaN(bytes)) return '';
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
render() {
|
|
const { attachment, disabled, sx, hasArticles, kind, onRescan } = this.props;
|
|
const thumbnail = attachment?.thumbnail || this.state.generatedThumbnail;
|
|
const name = attachment?.name || 'Dokument';
|
|
const size = attachment?.size;
|
|
const isPdf = attachment?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf');
|
|
const isCsv = attachment?.type === 'text/csv' || name.toLowerCase().endsWith('.csv');
|
|
const isText = attachment?.type?.startsWith('text/') || name.toLowerCase().endsWith('.txt') || isCsv;
|
|
|
|
return (
|
|
<Box
|
|
component="div"
|
|
role="button"
|
|
tabIndex={disabled ? -1 : 0}
|
|
onClick={disabled ? undefined : this.handleTileClick}
|
|
sx={{
|
|
...tileBaseSx,
|
|
width: TILE_SIZE,
|
|
height: TILE_SIZE,
|
|
p: 0,
|
|
gap: 0,
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
cursor: disabled ? 'default' : 'pointer',
|
|
bgcolor: isText ? SURFACE_FAINT : SURFACE_BASE,
|
|
border: '1px solid',
|
|
borderColor: 'divider',
|
|
...(disabled ? { opacity: 0.6, pointerEvents: 'none' } : {}),
|
|
...sx,
|
|
}}
|
|
>
|
|
{/* Thumbnail, Text preview, or Fallback Icon */}
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
bgcolor: isText ? SURFACE_SUBTLE : SURFACE_RAISED,
|
|
}}
|
|
>
|
|
{thumbnail ? (
|
|
<Box
|
|
component="img"
|
|
src={thumbnail}
|
|
alt={name}
|
|
sx={{
|
|
width: '100%',
|
|
height: '100%',
|
|
objectFit: 'contain',
|
|
display: 'block',
|
|
}}
|
|
/>
|
|
) : isText ? (
|
|
<Box
|
|
sx={{
|
|
width: '100%',
|
|
height: '100%',
|
|
p: 1.25,
|
|
bgcolor: SURFACE_SUBTLE,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'flex-start',
|
|
overflow: 'hidden',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
|
|
<TextSnippetOutlinedIcon sx={{ fontSize: 18, color: 'primary.main' }} />
|
|
<Typography
|
|
component="span"
|
|
sx={{
|
|
fontSize: '0.62rem',
|
|
fontWeight: 700,
|
|
color: 'primary.dark',
|
|
bgcolor: 'action.selected',
|
|
px: 0.5,
|
|
py: 0.1,
|
|
borderRadius: 0.5,
|
|
letterSpacing: '0.05em',
|
|
}}
|
|
>
|
|
{isCsv ? 'CSV' : 'TXT'}
|
|
</Typography>
|
|
</Box>
|
|
<Typography
|
|
component="pre"
|
|
sx={{
|
|
m: 0,
|
|
fontFamily: 'monospace',
|
|
fontSize: '0.45rem',
|
|
lineHeight: 1.2,
|
|
color: 'text.secondary',
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-all',
|
|
overflow: 'hidden',
|
|
opacity: 0.8,
|
|
userSelect: 'none',
|
|
}}
|
|
>
|
|
{attachment?.text ? attachment.text.slice(0, 300) : 'Textdatei'}
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<PictureAsPdfIcon sx={{ fontSize: 48, color: 'error.main' }} />
|
|
)}
|
|
</Box>
|
|
|
|
{/* Action buttons: Rescan (for PDF) and Delete */}
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 4,
|
|
right: 4,
|
|
display: 'flex',
|
|
gap: 0.5,
|
|
zIndex: 2,
|
|
}}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{kind === 'delivery' && Boolean(onRescan) && (isPdf || isText) ? (
|
|
<Tooltip
|
|
title={
|
|
hasArticles
|
|
? 'Neu analysieren nicht möglich, solange Artikel vorhanden sind'
|
|
: 'Neu analysieren'
|
|
}
|
|
>
|
|
<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>
|
|
) : null}
|
|
<IconButton
|
|
size="small"
|
|
disabled={disabled}
|
|
onClick={this.handleDelete}
|
|
sx={{
|
|
bgcolor: SCRIM_ACTION,
|
|
color: WHITE,
|
|
p: 0.5,
|
|
backdropFilter: 'blur(2px)',
|
|
'&:hover': { bgcolor: 'error.main' },
|
|
}}
|
|
>
|
|
<DeleteOutlineOutlinedIcon sx={{ fontSize: 16 }} />
|
|
</IconButton>
|
|
</Box>
|
|
|
|
{/* Bottom file metadata overlay */}
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
px: 1,
|
|
py: 0.5,
|
|
background: SCRIM_ATTACHMENT,
|
|
textAlign: 'left',
|
|
pointerEvents: 'none',
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
noWrap
|
|
sx={{ color: WHITE, display: 'block', fontSize: '0.72rem', lineHeight: 1.2 }}
|
|
>
|
|
{name}
|
|
</Typography>
|
|
{size ? (
|
|
<Typography
|
|
variant="caption"
|
|
noWrap
|
|
sx={{ color: VEIL_TEXT, fontSize: '0.65rem' }}
|
|
>
|
|
{this.formatFileSize(size)}
|
|
</Typography>
|
|
) : null}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default Attachment;
|