feat: add mobile-responsive layout and card views across all pages
Build and Push Multi-Platform Images / build-and-push (push) Successful in 39s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 39s
- New useIsMobile hook and MobileCardList component - AppLayout: hamburger menu + Drawer navigation on mobile - All list pages (Inbox, Posteingang, Manuell, Mail, Freigabe, Zahlung, TaskLog) show card layout on mobile instead of tables - CSS: responsive modal height, horizontal table scroll, text-size-adjust - Backend: Agrarmonitor polling fixes, Zahlung service improvements, IMAP folder service extended, misc controller fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0765d14d3b
commit
7076eef57b
28 files changed
+1081
-462
No files matched your search
@@ -7,11 +7,14 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
import { freigabeApi, type FreigabeDocument, type FreigabeOption } from '../api/freigabe';
|
||||
import { paperlessApi, type PaperlessDocType, type PaperlessCorrespondent } from '../api/paperless';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import MobileCardList from '../components/MobileCardList';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Text } = Typography;
|
||||
const FREIGABE_FIELD_ID = 15;
|
||||
|
||||
export default function FreigabePage() {
|
||||
const isMobile = useIsMobile();
|
||||
const [data, setData] = useState<FreigabeDocument[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -119,7 +122,7 @@ export default function FreigabePage() {
|
||||
},
|
||||
{
|
||||
title: 'Erstellt',
|
||||
dataIndex: 'created_date',
|
||||
dataIndex: 'created',
|
||||
key: 'created',
|
||||
width: 110,
|
||||
render: (v: string) => v ? dayjs(v).format('DD.MM.YYYY') : '—',
|
||||
@@ -153,44 +156,92 @@ export default function FreigabePage() {
|
||||
},
|
||||
];
|
||||
|
||||
const paginationConfig = {
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['25', '50', '100'],
|
||||
onChange: (p: number, ps: number) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
showTotal: (t: number) => `${t} Belege`,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title level={4} style={{ marginTop: 0, marginBottom: 16 }}>Freigabe</Title>
|
||||
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Radio.Group
|
||||
{isMobile ? (
|
||||
<Select
|
||||
style={{ width: '100%', marginBottom: 16 }}
|
||||
value={nurNichtFreigegeben}
|
||||
onChange={(e) => {
|
||||
onChange={(v) => {
|
||||
setPage(1);
|
||||
setNurNichtFreigegeben(e.target.value);
|
||||
setNurNichtFreigegeben(v);
|
||||
}}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
>
|
||||
<Radio.Button value={true}>Nicht freigegeben</Radio.Button>
|
||||
<Radio.Button value={false}>Alle</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
options={[
|
||||
{ value: true, label: 'Nicht freigegeben' },
|
||||
{ value: false, label: 'Alle' },
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Radio.Group
|
||||
value={nurNichtFreigegeben}
|
||||
onChange={(e) => {
|
||||
setPage(1);
|
||||
setNurNichtFreigegeben(e.target.value);
|
||||
}}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
>
|
||||
<Radio.Button value={true}>Nicht freigegeben</Radio.Button>
|
||||
<Radio.Button value={false}>Alle</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<Table<FreigabeDocument>
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['25', '50', '100'],
|
||||
onChange: (p, ps) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
showTotal: (t) => `${t} Belege`,
|
||||
}}
|
||||
/>
|
||||
{isMobile ? (
|
||||
<MobileCardList<FreigabeDocument>
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationConfig}
|
||||
emptyText="Keine Belege vorhanden"
|
||||
renderCard={(doc) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<Text strong>{doc.title || '—'}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>Dokumenttyp: {getDocTypeName(doc.document_type)}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>Absender: {getCorrespondentName(doc.correspondent)}</Text>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Erstellt: {doc.created ? dayjs(doc.created).format('DD.MM.YYYY') : '—'}
|
||||
</Text>
|
||||
{getFreigabeValue(doc)}
|
||||
</div>
|
||||
<Button
|
||||
icon={<CheckCircleOutlined />}
|
||||
type="primary"
|
||||
block
|
||||
onClick={() => openModal(doc)}
|
||||
>
|
||||
Freigabe setzen
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Table<FreigabeDocument>
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={paginationConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title="Freigabe setzen"
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { inboxApi, type InboxBarcode, type InboxFile, type PostprocessActionResult } from '../api/inbox';
|
||||
import { paperlessApi } from '../api/paperless';
|
||||
import { userSettingsApi, type SenderOption } from '../api/userSettings';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
|
||||
const ZOOM_MIN = 0.5;
|
||||
const ZOOM_MAX = 3;
|
||||
@@ -113,6 +114,7 @@ function CompareModal({
|
||||
onCreateNewVersion,
|
||||
onSkip,
|
||||
}: CompareModalProps) {
|
||||
const compareIsMobile = useIsMobile();
|
||||
const [paperlessUrl, setPaperlessUrl] = useState<string | null>(null);
|
||||
const [inboxUrl, setInboxUrl] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -165,8 +167,8 @@ function CompareModal({
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12, height: '75vh' }}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', flexDirection: compareIsMobile ? 'column' : 'row', gap: 12, height: '75vh' }}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
||||
<Typography.Text strong style={{ marginBottom: 4 }}>
|
||||
Original (Paperless)
|
||||
</Typography.Text>
|
||||
@@ -180,7 +182,7 @@ function CompareModal({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
||||
<Typography.Text strong style={{ marginBottom: 4 }}>
|
||||
Aktueller Abschnitt (Inbox)
|
||||
</Typography.Text>
|
||||
@@ -858,6 +860,7 @@ function SendEmailDialog({ open, fileId, fileName, documents, thumbUrls, onClose
|
||||
export default function InboxDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
const [file, setFile] = useState<InboxFile | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [thumbUrls, setThumbUrls] = useState<Map<number, string>>(new Map());
|
||||
@@ -1211,13 +1214,13 @@ export default function InboxDetailPage() {
|
||||
})();
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 120px)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 12, gap: 12 }}>
|
||||
<Space>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: isMobile ? 'calc(100dvh - 180px)' : 'calc(100vh - 120px)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', rowGap: 8, marginBottom: 12, gap: 12 }}>
|
||||
<Space wrap>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/inbox')}>
|
||||
Zurück
|
||||
</Button>
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
<Title level={isMobile ? 5 : 3} style={{ margin: 0, maxWidth: isMobile ? 180 : undefined }} ellipsis={{ tooltip: file.name }}>
|
||||
{file.name}
|
||||
</Title>
|
||||
<SourceTag source={file.source} />
|
||||
@@ -1399,12 +1402,13 @@ export default function InboxDetailPage() {
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 140,
|
||||
width: isMobile ? 84 : 140,
|
||||
overflowY: 'auto',
|
||||
padding: 6,
|
||||
background: '#fafafa',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 8,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{sidebarPages.map((n, idx) => {
|
||||
@@ -1428,7 +1432,7 @@ export default function InboxDetailPage() {
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 170,
|
||||
height: isMobile ? 100 : 170,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -1439,7 +1443,7 @@ export default function InboxDetailPage() {
|
||||
<img
|
||||
src={url}
|
||||
alt={`Seite ${docPage}`}
|
||||
style={thumbImageStyle(rotationFor(n), 130)}
|
||||
style={thumbImageStyle(rotationFor(n), isMobile ? 72 : 130)}
|
||||
/>
|
||||
) : (
|
||||
<Spin size="small" />
|
||||
|
||||
@@ -37,6 +37,8 @@ import { inboxApi, type InboxBarcode, type InboxFile } from '../api/inbox';
|
||||
import { barcodeTemplatesApi, type BarcodeTemplate } from '../api/barcode-templates';
|
||||
import { labelPrintAgentApi } from '../api/labelPrintAgent';
|
||||
import { userSettingsApi } from '../api/userSettings';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import MobileCardList from '../components/MobileCardList';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -51,6 +53,18 @@ function formatDate(iso: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
function renderSourceTag(src: InboxFile['source']): ReactNode {
|
||||
return src === 'user' ? (
|
||||
<Tag icon={<UserOutlined />} color="purple">
|
||||
Persönlich
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<FolderOpenOutlined />} color="blue">
|
||||
Gemeinsam
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBarcodes(barcodes: InboxBarcode[]): ReactNode {
|
||||
if (!barcodes || barcodes.length === 0) {
|
||||
return <Typography.Text type="secondary">—</Typography.Text>;
|
||||
@@ -137,6 +151,7 @@ function buildInitialFieldValues(template: BarcodeTemplate | null): Record<strin
|
||||
|
||||
export default function InboxPage() {
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
const [files, setFiles] = useState<InboxFile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
@@ -261,6 +276,50 @@ export default function InboxPage() {
|
||||
search ? f.name.toLowerCase().includes(search.toLowerCase()) : true,
|
||||
);
|
||||
|
||||
// Karten-Ansicht sortiert wie die Tabelle (neueste zuerst)
|
||||
const sortedForMobile = [...filtered].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
|
||||
const renderActions = (record: InboxFile, direction: 'column' | 'row' = 'column') => (
|
||||
<div style={{ display: 'flex', flexDirection: direction, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
||||
<Tooltip title="Vorschau öffnen">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/inbox/${encodeURIComponent(record.id)}`)}
|
||||
>
|
||||
Weiterverarbeiten
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="Dokument löschen?"
|
||||
description="Datei und Datenbank-Eintrag werden dauerhaft entfernt."
|
||||
okText="Löschen"
|
||||
cancelText="Abbrechen"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
Löschen
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Tooltip
|
||||
title={
|
||||
record.source === 'all'
|
||||
? 'In meinen persönlichen Scan-Ordner verschieben'
|
||||
: 'In den gemeinsamen Ordner (Öffentlich) verschieben'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
icon={record.source === 'all' ? <UserOutlined /> : <TeamOutlined />}
|
||||
onClick={() => handleUpdateSource(record.id, record.source)}
|
||||
>
|
||||
{record.source === 'all' ? 'Zu Persönlich' : 'Zu Öffentlich'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
const columns: ColumnsType<InboxFile> = [
|
||||
{
|
||||
title: 'Dateiname',
|
||||
@@ -283,16 +342,7 @@ export default function InboxPage() {
|
||||
{ text: 'Persönlich', value: 'user' },
|
||||
],
|
||||
onFilter: (value, record) => record.source === value,
|
||||
render: (src: InboxFile['source']) =>
|
||||
src === 'user' ? (
|
||||
<Tag icon={<UserOutlined />} color="purple">
|
||||
Persönlich
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<FolderOpenOutlined />} color="blue">
|
||||
Gemeinsam
|
||||
</Tag>
|
||||
),
|
||||
render: (src: InboxFile['source']) => renderSourceTag(src),
|
||||
},
|
||||
{
|
||||
title: 'QR-Code / Vorlage',
|
||||
@@ -321,46 +371,7 @@ export default function InboxPage() {
|
||||
title: 'Aktionen',
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
render: (_, record) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Tooltip title="Vorschau öffnen">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/inbox/${encodeURIComponent(record.id)}`)}
|
||||
>
|
||||
Weiterverarbeiten
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="Dokument löschen?"
|
||||
description="Datei und Datenbank-Eintrag werden dauerhaft entfernt."
|
||||
okText="Löschen"
|
||||
cancelText="Abbrechen"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
Löschen
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Tooltip
|
||||
title={
|
||||
record.source === 'all'
|
||||
? 'In meinen persönlichen Scan-Ordner verschieben'
|
||||
: 'In den gemeinsamen Ordner (Öffentlich) verschieben'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
icon={record.source === 'all' ? <UserOutlined /> : <TeamOutlined />}
|
||||
onClick={() => handleUpdateSource(record.id, record.source)}
|
||||
>
|
||||
{record.source === 'all' ? 'Zu Persönlich' : 'Zu Öffentlich'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
render: (_, record) => renderActions(record),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -371,6 +382,8 @@ export default function InboxPage() {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
@@ -383,11 +396,11 @@ export default function InboxPage() {
|
||||
Scan-Ordner.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Space wrap>
|
||||
<Input
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="Suchen …"
|
||||
style={{ width: 260 }}
|
||||
style={{ width: isMobile ? '100%' : 260 }}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
allowClear
|
||||
@@ -478,20 +491,48 @@ export default function InboxPage() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Card>
|
||||
<Table<InboxFile>
|
||||
{isMobile ? (
|
||||
<MobileCardList<InboxFile>
|
||||
dataSource={sortedForMobile}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={filtered}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 25,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `${t} Dateien`,
|
||||
}}
|
||||
locale={{ emptyText: 'Keine Dateien vorhanden' }}
|
||||
emptyText="Keine Dateien vorhanden"
|
||||
renderCard={(record) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<DocumentPreviewPopover record={record}>
|
||||
<Typography.Text strong>{record.name}</Typography.Text>
|
||||
</DocumentPreviewPopover>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
{renderSourceTag(record.source)}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{record.pageCount > 0 ? `${record.pageCount} Seiten` : '—'} · {formatDate(record.createdAt)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{renderBarcodes(record.barcodes)}
|
||||
{renderActions(record, 'row')}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<Table<InboxFile>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={filtered}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 25,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `${t} Dateien`,
|
||||
}}
|
||||
locale={{ emptyText: 'Keine Dateien vorhanden' }}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,12 +10,14 @@ 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';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function MailDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
const [email, setEmail] = useState<EmailItem | null>(null);
|
||||
const [attachments, setAttachments] = useState<EmailAttachment[]>([]);
|
||||
const [selected, setSelected] = useState<EmailAttachment | null>(null);
|
||||
@@ -136,16 +138,16 @@ export default function MailDetailPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Space>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8, marginBottom: 16 }}>
|
||||
<Space wrap>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/mailpostfach')}>
|
||||
Zurück
|
||||
</Button>
|
||||
<Title level={3} style={{ margin: 0 }}>{email.Subject}</Title>
|
||||
<Title level={isMobile ? 5 : 3} style={{ margin: 0 }}>{email.Subject}</Title>
|
||||
{hasErechnung && <Tag color="green">eRechnung</Tag>}
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
|
||||
<Space wrap>
|
||||
<Popconfirm
|
||||
title="E-Mail ignorieren"
|
||||
description="Möchten Sie diese E-Mail wirklich als ignoriert markieren?"
|
||||
@@ -183,12 +185,26 @@ export default function MailDetailPage() {
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 3fr', gap: 16, height: 'calc(100vh - 140px)' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : '2fr 3fr',
|
||||
gap: 16,
|
||||
height: isMobile ? 'auto' : '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' } }}
|
||||
styles={{
|
||||
body: {
|
||||
overflow: 'auto',
|
||||
height: isMobile ? 'auto' : 'calc(100vh - 200px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div><Text type="secondary">Von:</Text> <Text>{email.SenderAddress}</Text></div>
|
||||
@@ -208,7 +224,17 @@ export default function MailDetailPage() {
|
||||
</Card>
|
||||
|
||||
{/* Rechte Seite: Anhänge + Vorschau */}
|
||||
<Card size="small" styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: 'calc(100vh - 200px)' } }}>
|
||||
<Card
|
||||
size="small"
|
||||
styles={{
|
||||
body: {
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: isMobile ? 'auto' : 'calc(100vh - 200px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: '0 0 auto', borderBottom: `1px solid ${token.colorBorder}`, maxHeight: 240, overflow: 'auto' }}>
|
||||
<Table<EmailAttachment>
|
||||
columns={columns}
|
||||
@@ -224,7 +250,7 @@ export default function MailDetailPage() {
|
||||
locale={{ emptyText: <Empty description="Keine Anhänge" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1, minHeight: 0, background: token.colorBgLayout }}>
|
||||
<div style={isMobile ? { height: '60vh', background: token.colorBgLayout } : { flex: 1, minHeight: 0, background: token.colorBgLayout }}>
|
||||
{previewLoading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<Spin />
|
||||
|
||||
@@ -7,11 +7,22 @@ import dayjs from 'dayjs';
|
||||
import { emailsApi, type EmailItem } from '../api/emails';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
import { Permission } from '../auth/permissions';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import MobileCardList from '../components/MobileCardList';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
function renderStatusTag(s: number) {
|
||||
if (s === 0) return <Tag color="blue">Neu</Tag>;
|
||||
if (s === 1) return <Tag color="green">Verarbeitet</Tag>;
|
||||
if (s === 2) return <Tag color="red">Fehler</Tag>;
|
||||
if (s === 3) return <Tag color="default">Ignoriert</Tag>;
|
||||
return <Tag>{s}</Tag>;
|
||||
}
|
||||
|
||||
export default function MailpostfachPage() {
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
const [emails, setEmails] = useState<EmailItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetching, setFetching] = useState(false);
|
||||
@@ -84,13 +95,7 @@ export default function MailpostfachPage() {
|
||||
dataIndex: 'Status',
|
||||
key: 'Status',
|
||||
width: 110,
|
||||
render: (s: number) => {
|
||||
if (s === 0) return <Tag color="blue">Neu</Tag>;
|
||||
if (s === 1) return <Tag color="green">Verarbeitet</Tag>;
|
||||
if (s === 2) return <Tag color="red">Fehler</Tag>;
|
||||
if (s === 3) return <Tag color="default">Ignoriert</Tag>;
|
||||
return <Tag>{s}</Tag>;
|
||||
},
|
||||
render: renderStatusTag,
|
||||
filters: [
|
||||
{ text: 'Neu', value: 0 },
|
||||
{ text: 'Verarbeitet', value: 1 },
|
||||
@@ -116,9 +121,9 @@ export default function MailpostfachPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12, marginBottom: 16 }}>
|
||||
<Title level={3} style={{ margin: 0 }}>Mailpostfach</Title>
|
||||
<Space>
|
||||
<Space wrap>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={async () => {
|
||||
@@ -198,13 +203,13 @@ export default function MailpostfachPage() {
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
style={{ width: isMobile ? '100%' : 300 }}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
style={{ width: 200 }}
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
options={[
|
||||
{ value: 'all', label: 'Alle Status' },
|
||||
{ value: 0, label: 'Neu' },
|
||||
@@ -214,18 +219,48 @@ export default function MailpostfachPage() {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Table<EmailItem>
|
||||
columns={columns}
|
||||
dataSource={filteredEmails}
|
||||
loading={loading}
|
||||
rowKey="Id"
|
||||
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} E-Mails` }}
|
||||
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => navigate(`/mailpostfach/${record.Id}`),
|
||||
style: { cursor: 'pointer' },
|
||||
})}
|
||||
/>
|
||||
{isMobile ? (
|
||||
<MobileCardList<EmailItem>
|
||||
dataSource={[...filteredEmails].sort(
|
||||
(a, b) => new Date(b.Date).getTime() - new Date(a.Date).getTime(),
|
||||
)}
|
||||
rowKey="Id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20, showTotal: (t) => `${t} E-Mails` }}
|
||||
onCardClick={(record) => navigate(`/mailpostfach/${record.Id}`)}
|
||||
renderCard={(record) => {
|
||||
const hasErechnung = record.Attachments?.some((a) => a.Erechnung);
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'baseline', flexWrap: 'wrap' }}>
|
||||
<Text strong>{record.Subject || '—'}</Text>
|
||||
{hasErechnung && <Tag color="green">eRechnung</Tag>}
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>{record.SenderAddress}</Text>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{record.Date ? dayjs(record.Date).format('DD.MM.YYYY HH:mm') : '-'}
|
||||
</Text>
|
||||
{renderStatusTag(record.Status)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Table<EmailItem>
|
||||
columns={columns}
|
||||
dataSource={filteredEmails}
|
||||
loading={loading}
|
||||
rowKey="Id"
|
||||
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} E-Mails` }}
|
||||
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => navigate(`/mailpostfach/${record.Id}`),
|
||||
style: { cursor: 'pointer' },
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Popover, Button, Space, message, Tooltip, Typography, Tag } from 'antd';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Text } = Typography;
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { posteingangApi } from '../api/posteingang';
|
||||
@@ -11,8 +11,11 @@ import type { PaperlessTag } from '../api/paperless';
|
||||
import DocumentEditModal from '../components/DocumentEditModal';
|
||||
import { getEnv } from '../utils/env';
|
||||
import { AuthImage } from '../utils/auth-resource';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import MobileCardList from '../components/MobileCardList';
|
||||
|
||||
export default function ManuellBearbeitenPage() {
|
||||
const isMobile = useIsMobile();
|
||||
const [data, setData] = useState<PosteingangDocument[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
@@ -77,6 +80,26 @@ export default function ManuellBearbeitenPage() {
|
||||
/>
|
||||
);
|
||||
|
||||
const getContentTags = (record: PosteingangDocument) =>
|
||||
(record.tags || [])
|
||||
.filter(id => !steuertagIds.includes(id))
|
||||
.map(id => allTags.find(t => t.id === id))
|
||||
.filter((t): t is PaperlessTag => !!t);
|
||||
|
||||
const renderContentTags = (record: PosteingangDocument) => {
|
||||
const contentTags = getContentTags(record);
|
||||
if (contentTags.length === 0) return null;
|
||||
return (
|
||||
<Space size={[4, 4]} wrap style={{ marginTop: 4 }}>
|
||||
{contentTags.map(t => (
|
||||
<Tag key={t.id} color={t.color} style={{ color: t.text_color, margin: 0 }}>
|
||||
{t.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Vorschau',
|
||||
@@ -97,26 +120,12 @@ export default function ManuellBearbeitenPage() {
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
width: '35%',
|
||||
render: (_: any, record: PosteingangDocument) => {
|
||||
const contentTags = (record.tags || [])
|
||||
.filter(id => !steuertagIds.includes(id))
|
||||
.map(id => allTags.find(t => t.id === id))
|
||||
.filter((t): t is PaperlessTag => !!t);
|
||||
return (
|
||||
<div>
|
||||
<div>{record.title}</div>
|
||||
{contentTags.length > 0 && (
|
||||
<Space size={[4, 4]} wrap style={{ marginTop: 4 }}>
|
||||
{contentTags.map(t => (
|
||||
<Tag key={t.id} color={t.color} style={{ color: t.text_color, margin: 0 }}>
|
||||
{t.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: (_: any, record: PosteingangDocument) => (
|
||||
<div>
|
||||
<div>{record.title}</div>
|
||||
{renderContentTags(record)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Eingangsdatum',
|
||||
@@ -153,14 +162,47 @@ export default function ManuellBearbeitenPage() {
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} Dokumente` }}
|
||||
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||
/>
|
||||
{isMobile ? (
|
||||
<MobileCardList<PosteingangDocument>
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20, showTotal: (t) => `${t} Dokumente` }}
|
||||
renderCard={(record) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<AuthImage
|
||||
src={`${getEnv('VITE_API_URL')}/api/paperless/inbox/preview/${record.id}`}
|
||||
width={72}
|
||||
style={{ border: '1px solid #d9d9d9', objectFit: 'contain', flexShrink: 0 }}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
||||
<Text strong>{record.title || '—'}</Text>
|
||||
{renderContentTags(record)}
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Eingangsdatum: {record.created ? dayjs(record.created).format('DD.MM.YYYY') : '-'}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Importiert am: {dayjs(record.added).format('DD.MM.YYYY HH:mm')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" block onClick={() => handleEdit(record)}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} Dokumente` }}
|
||||
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DocumentEditModal
|
||||
documentId={selectedDoc?.id || null}
|
||||
|
||||
@@ -3,14 +3,17 @@ import { Table, Popover, Button, Space, message, Tooltip, Typography } from 'ant
|
||||
import { AuthImage } from '../utils/auth-resource';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Text } = Typography;
|
||||
import dayjs from 'dayjs';
|
||||
import { posteingangApi } from '../api/posteingang';
|
||||
import type { PosteingangDocument } from '../api/posteingang';
|
||||
import DocumentEditModal from '../components/DocumentEditModal';
|
||||
import { getEnv } from '../utils/env';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import MobileCardList from '../components/MobileCardList';
|
||||
|
||||
export default function PosteingangPage() {
|
||||
const isMobile = useIsMobile();
|
||||
const [data, setData] = useState<PosteingangDocument[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
@@ -64,6 +67,11 @@ export default function PosteingangPage() {
|
||||
/>
|
||||
);
|
||||
|
||||
const getEingangsdatum = (record: PosteingangDocument) => {
|
||||
const cf = record.customFields?.find((f) => f.field === 9);
|
||||
return cf?.value ? dayjs(cf.value).format('DD.MM.YYYY') : '-';
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Vorschau',
|
||||
@@ -88,10 +96,7 @@ export default function PosteingangPage() {
|
||||
{
|
||||
title: 'Eingangsdatum',
|
||||
key: 'eingangsdatum',
|
||||
render: (_: any, record: PosteingangDocument) => {
|
||||
const cf = record.customFields?.find((f) => f.field === 9);
|
||||
return cf?.value ? dayjs(cf.value).format('DD.MM.YYYY') : '-';
|
||||
}
|
||||
render: (_: any, record: PosteingangDocument) => getEingangsdatum(record),
|
||||
},
|
||||
{
|
||||
title: 'Importiert am',
|
||||
@@ -121,14 +126,46 @@ export default function PosteingangPage() {
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} Dokumente` }}
|
||||
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||
/>
|
||||
{isMobile ? (
|
||||
<MobileCardList<PosteingangDocument>
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20, showTotal: (t) => `${t} Dokumente` }}
|
||||
renderCard={(record) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<AuthImage
|
||||
src={`${getEnv('VITE_API_URL')}/api/paperless/inbox/preview/${record.id}`}
|
||||
width={72}
|
||||
style={{ border: '1px solid #d9d9d9', objectFit: 'contain', flexShrink: 0 }}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
||||
<Text strong>{record.title || '—'}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Eingangsdatum: {getEingangsdatum(record)}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Importiert am: {dayjs(record.added).format('DD.MM.YYYY HH:mm')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" block onClick={() => handleEdit(record)}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} Dokumente` }}
|
||||
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DocumentEditModal
|
||||
documentId={selectedDoc?.id || null}
|
||||
|
||||
@@ -321,7 +321,7 @@ function UserClientsTab() {
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalOpen(true)} style={{ marginBottom: 16 }}>
|
||||
Zuordnung hinzufügen
|
||||
</Button>
|
||||
<Table dataSource={data} columns={columns} loading={loading} rowKey="Id" size="small" pagination={false} />
|
||||
<Table dataSource={data} columns={columns} loading={loading} rowKey="Id" size="small" pagination={false} scroll={{ x: 'max-content' }} />
|
||||
|
||||
<Divider />
|
||||
<Typography.Title level={5} style={{ marginBottom: 8 }}>Betriebe — Agrarmonitor-Zuordnung</Typography.Title>
|
||||
@@ -335,6 +335,7 @@ function UserClientsTab() {
|
||||
rowKey="Id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
|
||||
<Modal title="Neue Zuordnung" open={modalOpen} onOk={handleAdd} onCancel={() => setModalOpen(false)}>
|
||||
@@ -599,13 +600,14 @@ function DocTypesTab() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
rowKey="Id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
rowKey="Id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
<Modal
|
||||
title="Dokumenttyp bearbeiten"
|
||||
@@ -1039,7 +1041,7 @@ function PostprocessingTab() {
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openNew} style={{ marginBottom: 16 }}>
|
||||
Regel hinzufügen
|
||||
</Button>
|
||||
<Table dataSource={data} columns={columns} loading={loading} rowKey="Id" size="small" pagination={false} />
|
||||
<Table dataSource={data} columns={columns} loading={loading} rowKey="Id" size="small" pagination={false} scroll={{ x: 'max-content' }} />
|
||||
|
||||
<Modal
|
||||
title={isNew ? 'Neue Postprocessing-Regel' : 'Regel bearbeiten'}
|
||||
@@ -1181,7 +1183,7 @@ function ExportTargetsTab() {
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openNew} style={{ marginBottom: 16 }}>
|
||||
Export-Ziel hinzufügen
|
||||
</Button>
|
||||
<Table dataSource={data} columns={columns} loading={loading} rowKey="Id" size="small" pagination={false} />
|
||||
<Table dataSource={data} columns={columns} loading={loading} rowKey="Id" size="small" pagination={false} scroll={{ x: 'max-content' }} />
|
||||
|
||||
<Modal title={isNew ? 'Neues Export-Ziel' : 'Export-Ziel bearbeiten'} open={!!editing} onOk={handleSave} onCancel={() => setEditing(null)}>
|
||||
<Form form={form} layout="vertical">
|
||||
@@ -1247,6 +1249,7 @@ function PostprocessingLogsTab() {
|
||||
loading={loading}
|
||||
rowKey="Id"
|
||||
size="small"
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
current: page,
|
||||
total,
|
||||
@@ -1352,7 +1355,7 @@ function ApiKeysTab() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table dataSource={data} columns={columns} loading={loading} rowKey="id" size="small" pagination={false} />
|
||||
<Table dataSource={data} columns={columns} loading={loading} rowKey="id" size="small" pagination={false} scroll={{ x: 'max-content' }} />
|
||||
|
||||
<Modal
|
||||
title="Neuen API-Key erstellen"
|
||||
@@ -1572,13 +1575,14 @@ function CorrespondentsTab() {
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={{
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
@@ -1911,7 +1915,7 @@ function InboxActionsForTemplateEditor({ templateId }: { templateId: number }) {
|
||||
<h4 style={{ margin: 0 }}>Weiterverarbeitungs-Aktionen</h4>
|
||||
<Button type="dashed" size="small" icon={<PlusOutlined />} onClick={openNew}>Aktion hinzufügen</Button>
|
||||
</Space>
|
||||
<Table<InboxAction> rowKey="Id" columns={columns} dataSource={actions} loading={loading} pagination={false} size="small" />
|
||||
<Table<InboxAction> rowKey="Id" columns={columns} dataSource={actions} loading={loading} pagination={false} size="small" scroll={{ x: 'max-content' }} />
|
||||
|
||||
<Modal
|
||||
title={isNew ? 'Neue Aktion' : 'Aktion bearbeiten'}
|
||||
@@ -2168,6 +2172,7 @@ function BarcodeTemplatesTab() {
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Button, Space, Tag, Tooltip, Popconfirm, message, ConfigProvider } from 'antd';
|
||||
import { Table, Button, Space, Tag, Tooltip, Popconfirm, message, ConfigProvider, Typography } from 'antd';
|
||||
import { ReloadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { tasksApi } from '../api/tasks';
|
||||
import type { Task } from '../api/tasks';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import MobileCardList from '../components/MobileCardList';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function statusTag(fertig: number | null) {
|
||||
if (fertig === 1) return <Tag color="success">Fertig</Tag>;
|
||||
@@ -12,6 +16,7 @@ function statusTag(fertig: number | null) {
|
||||
}
|
||||
|
||||
export default function TaskLogPage() {
|
||||
const isMobile = useIsMobile();
|
||||
const [data, setData] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
@@ -155,14 +160,55 @@ export default function TaskLogPage() {
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="TaskId"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20 }}
|
||||
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||
/>
|
||||
{isMobile ? (
|
||||
<MobileCardList<Task>
|
||||
dataSource={data}
|
||||
rowKey="TaskId"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20 }}
|
||||
renderCard={(record) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
|
||||
<Text strong>{record.InterneBelegnummer || '—'}</Text>
|
||||
{statusTag(record.Fertig)}
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontFamily: 'monospace', fontSize: 12 }}>
|
||||
{record.TaskId.slice(0, 8)}…
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>Lieferant: {record.Lieferant || '-'}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Belegdatum: {record.Belegdatum ? dayjs(record.Belegdatum).format('DD.MM.YYYY') : '-'}
|
||||
{' · '}
|
||||
Eingang: {record.Eingangsdatum ? dayjs(record.Eingangsdatum).format('DD.MM.YYYY') : '-'}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Paperless-Dok.-ID: {record.PaperlessDocumentID ?? '-'}
|
||||
</Text>
|
||||
<Popconfirm
|
||||
title="Task löschen"
|
||||
description={`Task ${record.TaskId.slice(0, 8)}… dauerhaft entfernen?`}
|
||||
onConfirm={() => handleDeleteOne(record.TaskId)}
|
||||
okText="Löschen"
|
||||
cancelText="Abbrechen"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button danger size="small" icon={<DeleteOutlined />} block>
|
||||
Löschen
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="TaskId"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20 }}
|
||||
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
@@ -5,13 +5,22 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
import { zahlungApi, type ZahlungDocument, type ZahlungOption, type ZahlungFilter } from '../api/zahlung';
|
||||
import { paperlessApi, type PaperlessDocType, type PaperlessCorrespondent } from '../api/paperless';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import MobileCardList from '../components/MobileCardList';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Text } = Typography;
|
||||
const FREIGABE_FIELD_ID = 15;
|
||||
const ZAHLUNG_FIELD_ID = 16;
|
||||
const FREIGABE_WERT_FREIGEGEBEN = 'freigegeben';
|
||||
|
||||
const FILTER_OPTIONS: { value: ZahlungFilter; label: string }[] = [
|
||||
{ value: 'ausstehend', label: 'Freigegeben, noch nicht bezahlt' },
|
||||
{ value: 'freigegeben', label: 'Alle freigegebenen' },
|
||||
{ value: 'alle', label: 'Alle' },
|
||||
];
|
||||
|
||||
export default function ZahlungPage() {
|
||||
const isMobile = useIsMobile();
|
||||
const [data, setData] = useState<ZahlungDocument[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -132,7 +141,7 @@ export default function ZahlungPage() {
|
||||
},
|
||||
{
|
||||
title: 'Erstellt',
|
||||
dataIndex: 'created_date',
|
||||
dataIndex: 'created',
|
||||
key: 'created',
|
||||
width: 110,
|
||||
render: (v: string) => v ? dayjs(v).format('DD.MM.YYYY') : '—',
|
||||
@@ -178,45 +187,100 @@ export default function ZahlungPage() {
|
||||
},
|
||||
];
|
||||
|
||||
const paginationConfig = {
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['25', '50', '100'],
|
||||
onChange: (p: number, ps: number) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
showTotal: (t: number) => `${t} Belege`,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title level={4} style={{ marginTop: 0, marginBottom: 16 }}>Zahlung</Title>
|
||||
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Radio.Group
|
||||
{isMobile ? (
|
||||
<Select
|
||||
style={{ width: '100%', marginBottom: 16 }}
|
||||
value={filter}
|
||||
onChange={(e) => {
|
||||
onChange={(v) => {
|
||||
setPage(1);
|
||||
setFilter(e.target.value);
|
||||
setFilter(v);
|
||||
}}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
>
|
||||
<Radio.Button value="ausstehend">Freigegeben, noch nicht bezahlt</Radio.Button>
|
||||
<Radio.Button value="freigegeben">Alle freigegebenen</Radio.Button>
|
||||
<Radio.Button value="alle">Alle</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
options={FILTER_OPTIONS}
|
||||
/>
|
||||
) : (
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Radio.Group
|
||||
value={filter}
|
||||
onChange={(e) => {
|
||||
setPage(1);
|
||||
setFilter(e.target.value);
|
||||
}}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
>
|
||||
{FILTER_OPTIONS.map((o) => (
|
||||
<Radio.Button key={o.value} value={o.value}>{o.label}</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<Table<ZahlungDocument>
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['25', '50', '100'],
|
||||
onChange: (p, ps) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
showTotal: (t) => `${t} Belege`,
|
||||
}}
|
||||
/>
|
||||
{isMobile ? (
|
||||
<MobileCardList<ZahlungDocument>
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationConfig}
|
||||
emptyText="Keine Belege vorhanden"
|
||||
renderCard={(doc) => {
|
||||
const freigegeben = istFreigegeben(doc);
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<Text strong>{doc.title || '—'}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>Dokumenttyp: {getDocTypeName(doc.document_type)}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>Absender: {getCorrespondentName(doc.correspondent)}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Erstellt: {doc.created ? dayjs(doc.created).format('DD.MM.YYYY') : '—'}
|
||||
</Text>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{renderFreigabeTag(doc)}
|
||||
{renderZahlungTag(doc)}
|
||||
</div>
|
||||
<Button
|
||||
icon={<EuroOutlined />}
|
||||
type="primary"
|
||||
block
|
||||
disabled={!freigegeben}
|
||||
onClick={() => openModal(doc)}
|
||||
>
|
||||
Zahlung verbuchen
|
||||
</Button>
|
||||
{!freigegeben && (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Beleg muss zuerst freigegeben werden
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Table<ZahlungDocument>
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={paginationConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title="Zahlung verbuchen"
|
||||
|
||||
Reference in new issue
Block a user