261 lines
9.3 KiB
TypeScript
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>
|
|
);
|
|
}
|