feat: add save to paperless and send email functionality to inbox detail page with tiptap editor integration
Build and Push Multi-Platform Images / build-and-push (push) Successful in 46s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 46s
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button, Empty, Modal, Popconfirm, Space, Spin, Steps, Tag, Tooltip, Typography, message } from 'antd';
|
||||
import { Button, DatePicker, Dropdown, Empty, Form, Input, Modal, Popconfirm, Select, Space, Spin, Steps, Tag, Tooltip, Typography, message } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
FolderOpenOutlined,
|
||||
LeftOutlined,
|
||||
LoadingOutlined,
|
||||
MailOutlined,
|
||||
QrcodeOutlined,
|
||||
RedoOutlined,
|
||||
RightOutlined,
|
||||
SaveOutlined,
|
||||
ScissorOutlined,
|
||||
ThunderboltOutlined,
|
||||
UndoOutlined,
|
||||
@@ -17,8 +21,11 @@ import {
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Underline from '@tiptap/extension-underline';
|
||||
import { inboxApi, type InboxBarcode, type InboxFile, type PostprocessActionResult } from '../api/inbox';
|
||||
import { paperlessApi } from '../api/paperless';
|
||||
import { paperlessApi, type PaperlessDocType, type PaperlessCorrespondent, type PaperlessTag } from '../api/paperless';
|
||||
|
||||
const ZOOM_MIN = 0.5;
|
||||
const ZOOM_MAX = 3;
|
||||
@@ -521,6 +528,188 @@ function PostprocessWizardModal({
|
||||
);
|
||||
}
|
||||
|
||||
interface SaveToPaperlessDialogProps {
|
||||
open: boolean;
|
||||
fileId: string;
|
||||
defaultTitle: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function SaveToPaperlessDialog({ open, fileId, defaultTitle, onClose }: SaveToPaperlessDialogProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [docTypes, setDocTypes] = useState<PaperlessDocType[]>([]);
|
||||
const [correspondents, setCorrespondents] = useState<PaperlessCorrespondent[]>([]);
|
||||
const [tags, setTags] = useState<PaperlessTag[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ title: defaultTitle });
|
||||
Promise.all([
|
||||
paperlessApi.getDocumentTypes(),
|
||||
paperlessApi.getCorrespondents(),
|
||||
paperlessApi.getTags(),
|
||||
]).then(([dt, co, tg]) => {
|
||||
setDocTypes(dt);
|
||||
setCorrespondents(co);
|
||||
setTags(tg);
|
||||
}).catch(() => {});
|
||||
}, [open, defaultTitle, form]);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
await inboxApi.saveToPaperless(fileId, {
|
||||
title: values.title,
|
||||
date: values.date ? values.date.format('YYYY-MM-DD') : undefined,
|
||||
documentTypeId: values.documentTypeId,
|
||||
correspondentId: values.correspondentId,
|
||||
tagIds: values.tagIds,
|
||||
});
|
||||
message.success('Dokument wurde an Paperless übertragen');
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
if (err?.errorFields) return;
|
||||
message.error('Übertragung fehlgeschlagen');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title="Dokument in Paperless speichern"
|
||||
onCancel={onClose}
|
||||
onOk={handleOk}
|
||||
okText="Speichern"
|
||||
cancelText="Abbrechen"
|
||||
confirmLoading={submitting}
|
||||
destroyOnClose
|
||||
width={520}
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="title" label="Titel" rules={[{ required: true, message: 'Bitte Titel angeben' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="date" label="Datum">
|
||||
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
|
||||
</Form.Item>
|
||||
<Form.Item name="documentTypeId" label="Dokumenttyp">
|
||||
<Select allowClear placeholder="Kein Typ" options={docTypes.map((d) => ({ value: d.id, label: d.name }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="correspondentId" label="Korrespondent">
|
||||
<Select allowClear placeholder="Kein Korrespondent" options={correspondents.map((c) => ({ value: c.id, label: c.name }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="tagIds" label="Tags">
|
||||
<Select mode="multiple" allowClear placeholder="Keine Tags" options={tags.map((t) => ({ value: t.id, label: t.name }))} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function TiptapToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
||||
if (!editor) return null;
|
||||
const btnStyle = (active: boolean): React.CSSProperties => ({
|
||||
padding: '2px 8px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
background: active ? '#e6f4ff' : '#fff',
|
||||
fontWeight: active ? 600 : 400,
|
||||
});
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 4, padding: '4px 0', borderBottom: '1px solid #f0f0f0', marginBottom: 6, flexWrap: 'wrap' }}>
|
||||
<button style={btnStyle(editor.isActive('bold'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleBold().run(); }}>F</button>
|
||||
<button style={{ ...btnStyle(editor.isActive('italic')), fontStyle: 'italic' }} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleItalic().run(); }}>K</button>
|
||||
<button style={{ ...btnStyle(editor.isActive('underline')), textDecoration: 'underline' }} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleUnderline().run(); }}>U</button>
|
||||
<button style={btnStyle(editor.isActive('bulletList'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleBulletList().run(); }}>• Liste</button>
|
||||
<button style={btnStyle(editor.isActive('orderedList'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleOrderedList().run(); }}>1. Liste</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SendEmailDialogProps {
|
||||
open: boolean;
|
||||
fileId: string;
|
||||
defaultFilename: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function SendEmailDialog({ open, fileId, defaultFilename, onClose }: SendEmailDialogProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit, Underline],
|
||||
content: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ filename: defaultFilename });
|
||||
editor?.commands.clearContent();
|
||||
}
|
||||
}, [open, defaultFilename, form, editor]);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
await inboxApi.sendEmail(fileId, {
|
||||
to: values.to,
|
||||
subject: values.subject,
|
||||
body: editor?.getText() ?? '',
|
||||
html: editor?.getHTML(),
|
||||
filename: values.filename || undefined,
|
||||
});
|
||||
message.success('E-Mail wurde gesendet');
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
if (err?.errorFields) return;
|
||||
message.error('E-Mail konnte nicht gesendet werden');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title="Als E-Mail-Anhang versenden"
|
||||
onCancel={onClose}
|
||||
onOk={handleOk}
|
||||
okText="Senden"
|
||||
cancelText="Abbrechen"
|
||||
confirmLoading={submitting}
|
||||
destroyOnClose
|
||||
width={580}
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="to" label="Empfänger" rules={[{ required: true, message: 'Bitte Empfänger angeben' }, { type: 'email', message: 'Ungültige E-Mail-Adresse' }]}>
|
||||
<Input placeholder="empfaenger@beispiel.de" />
|
||||
</Form.Item>
|
||||
<Form.Item name="subject" label="Betreff" rules={[{ required: true, message: 'Bitte Betreff angeben' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Nachricht">
|
||||
<div style={{ border: '1px solid #d9d9d9', borderRadius: 6, padding: '6px 10px', minHeight: 160 }}>
|
||||
<TiptapToolbar editor={editor} />
|
||||
<EditorContent editor={editor} style={{ minHeight: 120, outline: 'none' }} />
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item name="filename" label="Dateiname des Anhangs (ohne .pdf)">
|
||||
<Input placeholder={defaultFilename} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InboxDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -531,6 +720,8 @@ export default function InboxDetailPage() {
|
||||
const [selectedPage, setSelectedPage] = useState(1);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
||||
const [emailDialogOpen, setEmailDialogOpen] = useState(false);
|
||||
const [scanMode, setScanMode] = useState(false);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [dragStart, setDragStart] = useState<{ clientX: number; clientY: number; relX: number; relY: number } | null>(null);
|
||||
@@ -893,14 +1084,24 @@ export default function InboxDetailPage() {
|
||||
</Title>
|
||||
<SourceTag source={file.source} />
|
||||
</Space>
|
||||
<Button
|
||||
<Dropdown.Button
|
||||
type="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={() => setWizardOpen(true)}
|
||||
icon={<DownOutlined />}
|
||||
disabled={documents.length === 0}
|
||||
onClick={() => setWizardOpen(true)}
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'save', label: 'Speichern', icon: <SaveOutlined /> },
|
||||
{ key: 'email', label: 'Als E-Mail-Anhang versenden', icon: <MailOutlined /> },
|
||||
] as MenuProps['items'],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'save') setSaveDialogOpen(true);
|
||||
if (key === 'email') setEmailDialogOpen(true);
|
||||
},
|
||||
}}
|
||||
>
|
||||
Weiterverarbeiten
|
||||
</Button>
|
||||
<ThunderboltOutlined /> Weiterverarbeiten
|
||||
</Dropdown.Button>
|
||||
</div>
|
||||
|
||||
{pendingEdits > 0 && (
|
||||
@@ -1322,6 +1523,18 @@ export default function InboxDetailPage() {
|
||||
onClose={() => setWizardOpen(false)}
|
||||
onDeleted={() => navigate('/inbox')}
|
||||
/>
|
||||
<SaveToPaperlessDialog
|
||||
open={saveDialogOpen}
|
||||
fileId={file.id}
|
||||
defaultTitle={file.name}
|
||||
onClose={() => setSaveDialogOpen(false)}
|
||||
/>
|
||||
<SendEmailDialog
|
||||
open={emailDialogOpen}
|
||||
fileId={file.id}
|
||||
defaultFilename={file.name.replace(/\.pdf$/i, '')}
|
||||
onClose={() => setEmailDialogOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user