Initial commit with Email Import Wizard and Task Processor updates
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
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 } 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]) => (
|
||||
<Tag
|
||||
color="blue"
|
||||
key={id}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const paperlessUrl = getEnv('VITE_PAPERLESS_URL');
|
||||
if (paperlessUrl) {
|
||||
window.open(`${paperlessUrl}/documents/${id}`, '_blank');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{id}
|
||||
</Tag>
|
||||
))}
|
||||
</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 direction="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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user