Files
paperlessmanager/paperless-frontend/src/pages/MailDetailPage.tsx
T
2026-05-18 20:53:17 +02:00

261 lines
9.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card, Button, Space, Spin, Tag, Typography, Table, message, Empty, Popconfirm
} from 'antd';
import { ArrowLeftOutlined, FileTextOutlined, CloseCircleOutlined, LinkOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import { emailsApi, type EmailItem, type EmailAttachment } from '../api/emails';
import { emailImportApi } from '../api/email-import';
import { getEnv } from '../utils/env';
import MailImportWizard from '../components/MailImportWizard';
const { Title, Text } = Typography;
export default function MailDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [email, setEmail] = useState<EmailItem | null>(null);
const [attachments, setAttachments] = useState<EmailAttachment[]>([]);
const [selected, setSelected] = useState<EmailAttachment | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [previewLoading, setPreviewLoading] = useState(false);
const [wizardOpen, setWizardOpen] = useState(false);
useEffect(() => {
if (!id) return;
const emailId = parseInt(id, 10);
Promise.all([emailsApi.get(emailId), emailsApi.listAttachments(emailId)])
.then(([mail, att]) => {
setEmail(mail);
setAttachments(att);
if (att.length > 0) setSelected(att[0]);
})
.catch(() => message.error('E-Mail nicht gefunden'))
.finally(() => setLoading(false));
}, [id]);
const handleIgnore = async () => {
if (!email) return;
try {
await emailsApi.updateStatus(email.Id, 3);
message.success('E-Mail als ignoriert markiert');
navigate('/mailpostfach');
} catch (err) {
message.error('Fehler beim Markieren der E-Mail');
}
};
useEffect(() => {
if (!selected) {
setPreviewUrl(null);
return;
}
setPreviewLoading(true);
let objectUrl: string | null = null;
emailsApi.getAttachmentContent(selected.Id)
.then((blob) => {
objectUrl = URL.createObjectURL(blob);
setPreviewUrl(objectUrl);
})
.catch(() => message.error('Vorschau konnte nicht geladen werden'))
.finally(() => setPreviewLoading(false));
return () => {
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [selected]);
if (loading) return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
if (!email) return <Text>E-Mail nicht gefunden.</Text>;
const isHtml = /<[a-z][\s\S]*>/i.test(email.Body);
const hasErechnung = attachments.some((a) => a.Erechnung);
const columns: ColumnsType<EmailAttachment> = [
{
title: 'Dateiname',
dataIndex: 'FileName',
key: 'FileName',
ellipsis: true,
},
{
title: 'Typ',
dataIndex: 'ContentType',
key: 'ContentType',
width: 160,
ellipsis: true,
},
{
title: 'eRechnung',
dataIndex: 'Erechnung',
key: 'Erechnung',
width: 100,
align: 'center',
render: (v: boolean) => (v ? <Tag color="green">Ja</Tag> : <Tag>Nein</Tag>),
},
{
title: 'Paperless ID',
dataIndex: 'PaperlessDocumentIds',
key: 'PaperlessDocumentIds',
width: 130,
render: (ids: Record<string, number> | null) => {
if (!ids) return null;
const entries = Object.entries(ids);
if (entries.length === 0) return null;
return (
<Space size={[0, 4]} wrap>
{entries.map(([, id]) => {
const paperlessUrl = getEnv('PAPERLESS_URL');
return (
<a
key={id}
href={paperlessUrl ? `${paperlessUrl}/documents/${id}` : undefined}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Tag color="blue" style={{ cursor: 'pointer' }}>
<LinkOutlined style={{ marginRight: 4 }} />
{id}
</Tag>
</a>
);
})}
</Space>
);
}
},
];
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<Space>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/mailpostfach')}>
Zurück
</Button>
<Title level={3} style={{ margin: 0 }}>{email.Subject}</Title>
{hasErechnung && <Tag color="green">eRechnung</Tag>}
</Space>
<Space>
<Popconfirm
title="E-Mail ignorieren"
description="Möchten Sie diese E-Mail wirklich als ignoriert markieren?"
onConfirm={handleIgnore}
okText="Ja"
cancelText="Nein"
placement="bottomRight"
>
<Button danger icon={<CloseCircleOutlined />}>
Als ignoriert markieren
</Button>
</Popconfirm>
<Button
type="primary"
icon={<FileTextOutlined />}
onClick={async () => {
if (!email) return;
const hide = message.loading('Prüfe Vorschaubilder...', 0);
try {
await emailImportApi.ensurePreviews(email.Id);
// Re-fetch attachments to get updated PageCount
const att = await emailsApi.listAttachments(email.Id);
setAttachments(att);
setWizardOpen(true);
} catch (err) {
message.error('Fehler bei der Vorschau-Prüfung');
} finally {
hide();
}
}}
>
Import-Wizard starten
</Button>
</Space>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 3fr', gap: 16, height: 'calc(100vh - 140px)' }}>
{/* Linke Seite: E-Mail-Inhalt */}
<Card
title="E-Mail"
size="small"
styles={{ body: { overflow: 'auto', height: 'calc(100vh - 200px)', display: 'flex', flexDirection: 'column' } }}
>
<div style={{ marginBottom: 12 }}>
<div><Text type="secondary">Von:</Text> <Text>{email.SenderAddress}</Text></div>
<div><Text type="secondary">An:</Text> <Text>{email.RecipientAddress}</Text></div>
<div><Text type="secondary">Datum:</Text> <Text>{dayjs(email.Date).format('DD.MM.YYYY HH:mm')}</Text></div>
</div>
{isHtml ? (
<iframe
title="E-Mail Body"
srcDoc={email.Body}
sandbox=""
style={{ width: '100%', minHeight: '400px', flex: 1, border: '1px solid #303030', borderRadius: 4, background: '#fff' }}
/>
) : (
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', margin: 0 }}>{email.Body}</pre>
)}
</Card>
{/* Rechte Seite: Anhänge + Vorschau */}
<Card size="small" styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: 'calc(100vh - 200px)' } }}>
<div style={{ flex: '0 0 auto', borderBottom: '1px solid #303030', maxHeight: 240, overflow: 'auto' }}>
<Table<EmailAttachment>
columns={columns}
dataSource={attachments}
rowKey="Id"
size="small"
pagination={false}
rowClassName={(r) => (selected?.Id === r.Id ? 'ant-table-row-selected' : '')}
onRow={(record) => ({
onClick: () => setSelected(record),
style: { cursor: 'pointer' },
})}
locale={{ emptyText: <Empty description="Keine Anhänge" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
/>
</div>
<div style={{ flex: 1, minHeight: 0, background: '#1a1a2e' }}>
{previewLoading ? (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<Spin />
</div>
) : previewUrl && selected ? (
selected.ContentType?.startsWith('image/') ? (
<img src={previewUrl} alt={selected.FileName} style={{ maxWidth: '100%', maxHeight: '100%', display: 'block', margin: '0 auto' }} />
) : (
<iframe
title={selected.FileName}
src={previewUrl}
style={{ width: '100%', height: '100%', border: 'none' }}
/>
)
) : (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#888' }}>
<Space vertical align="center">
<FileTextOutlined style={{ fontSize: 48 }} />
<Text type="secondary">Kein Anhang ausgewählt</Text>
</Space>
</div>
)}
</div>
</Card>
</div>
{wizardOpen && email && (
<MailImportWizard
visible={wizardOpen}
onClose={() => setWizardOpen(false)}
email={email}
attachments={attachments}
/>
)}
</div>
);
}