diff --git a/CLAUDE.md b/CLAUDE.md index b3558cb..5b7b70d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,12 @@ Paperless Manager is a document automation platform that extends [Paperless-NGX] UI labels and comments are in **German**. +## Sprache + +Alle Antworten an den Nutzer, Erklärungen, Commit-Messages, Code-Kommentare und UI-Texte +sollen — wo möglich — auf **Deutsch** verfasst werden. Technische Bezeichner (Variablen-, +Funktions- und Dateinamen) bleiben unverändert in der bestehenden Konvention. + ## Structure ``` diff --git a/paperless-backend/src/agrarmonitor/agrarmonitor-polling.service.ts b/paperless-backend/src/agrarmonitor/agrarmonitor-polling.service.ts index 669c4ee..1702c11 100644 --- a/paperless-backend/src/agrarmonitor/agrarmonitor-polling.service.ts +++ b/paperless-backend/src/agrarmonitor/agrarmonitor-polling.service.ts @@ -85,18 +85,25 @@ export class AgrarmonitorPollingService implements OnModuleInit { importWartezeitMinuten: string; notizMarker: string; }> { - const [fertig, verbucht, hochgeladen, linkField, manuell, wartezeit, marker] = - await Promise.all([ - this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_fertig' }), - this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_verbucht' }), - this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_hochgeladen' }), - this.settingRepo.findOneBy({ Tag: 'agrarmonitor_link_field' }), - this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_manuell' }), - this.settingRepo.findOneBy({ - Tag: 'agrarmonitor_import_wartezeit_minuten', - }), - this.settingRepo.findOneBy({ Tag: 'agrarmonitor_notiz_marker' }), - ]); + const [ + fertig, + verbucht, + hochgeladen, + linkField, + manuell, + wartezeit, + marker, + ] = await Promise.all([ + this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_fertig' }), + this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_verbucht' }), + this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_hochgeladen' }), + this.settingRepo.findOneBy({ Tag: 'agrarmonitor_link_field' }), + this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_manuell' }), + this.settingRepo.findOneBy({ + Tag: 'agrarmonitor_import_wartezeit_minuten', + }), + this.settingRepo.findOneBy({ Tag: 'agrarmonitor_notiz_marker' }), + ]); return { tagFertig: fertig?.Wert ?? '4', tagVerbucht: verbucht?.Wert ?? '9', diff --git a/paperless-backend/src/email/email-import.service.ts b/paperless-backend/src/email/email-import.service.ts index cff63e0..38b2b47 100644 --- a/paperless-backend/src/email/email-import.service.ts +++ b/paperless-backend/src/email/email-import.service.ts @@ -648,11 +648,17 @@ export class EmailImportService { this.logger.log( `Email ${firstAtt.EmailMessageId} als verarbeitet markiert.`, ); - const emailEntity = await this.emailRepo.findOne({ where: { Id: firstAtt.EmailMessageId } }); + const emailEntity = await this.emailRepo.findOne({ + where: { Id: firstAtt.EmailMessageId }, + }); if (emailEntity) { - this.imapFolderService.moveToImportiert(emailEntity.MessageId).catch(err => - this.logger.error('IMAP-Verschieben fehlgeschlagen: ' + err.message), - ); + this.imapFolderService + .moveToImportiert(emailEntity.MessageId) + .catch((err) => + this.logger.error( + 'IMAP-Verschieben fehlgeschlagen: ' + err.message, + ), + ); } } } diff --git a/paperless-backend/src/email/email.controller.ts b/paperless-backend/src/email/email.controller.ts index 2b0b0d2..b08776e 100644 --- a/paperless-backend/src/email/email.controller.ts +++ b/paperless-backend/src/email/email.controller.ts @@ -204,9 +204,11 @@ export class EmailController { this.logger.log( `Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`, ); - this.imapFolderService.cleanupImportedEmails().catch(err => - this.logger.error('IMAP-Cleanup fehlgeschlagen: ' + err.message), - ); + this.imapFolderService + .cleanupImportedEmails() + .catch((err) => + this.logger.error('IMAP-Cleanup fehlgeschlagen: ' + err.message), + ); let movedToImportiert = 0; if (body.includeProcessed) { @@ -214,8 +216,13 @@ export class EmailController { where: [{ Status: 1 }, { Status: 3 }], select: ['MessageId'], }); - const messageIds = processedEmails.map((e) => e.MessageId).filter(Boolean); - movedToImportiert = await this.imapFolderService.moveProcessedInboxToImportiert(messageIds); + const messageIds = processedEmails + .map((e) => e.MessageId) + .filter(Boolean); + movedToImportiert = + await this.imapFolderService.moveProcessedInboxToImportiert( + messageIds, + ); } return { updatedCount, idsUpdated, movedToImportiert }; diff --git a/paperless-backend/src/email/imap-folder.service.ts b/paperless-backend/src/email/imap-folder.service.ts index 4fd4733..8f5496a 100644 --- a/paperless-backend/src/email/imap-folder.service.ts +++ b/paperless-backend/src/email/imap-folder.service.ts @@ -25,8 +25,14 @@ export class ImapFolderService { @Cron('0 3 * * *', { timeZone: 'Europe/Berlin' }) async cleanupImportedEmails(): Promise { if (!this.configService.get('IMAP_HOST')) return; - const importedFolder = this.configService.get('IMAP_IMPORTED_FOLDER', 'importiert'); - const trashFolder = this.configService.get('IMAP_TRASH_FOLDER', 'Trash'); + const importedFolder = this.configService.get( + 'IMAP_IMPORTED_FOLDER', + 'importiert', + ); + const trashFolder = this.configService.get( + 'IMAP_TRASH_FOLDER', + 'Trash', + ); const client = this.createClient(); try { await client.connect(); @@ -39,10 +45,14 @@ export class ImapFolderService { const oldUids = await client.search({ before: cutoff }, { uid: true }); if (Array.isArray(oldUids) && oldUids.length > 0) { await client.messageMove(oldUids, trashFolder, { uid: true }); - this.logger.log(`${oldUids.length} alte E-Mail(s) aus "${importedFolder}" in "${trashFolder}" verschoben.`); + this.logger.log( + `${oldUids.length} alte E-Mail(s) aus "${importedFolder}" in "${trashFolder}" verschoben.`, + ); } } catch (err: any) { - this.logger.warn(`Bereinigung "${importedFolder}" nicht möglich: ${err.message}`); + this.logger.warn( + `Bereinigung "${importedFolder}" nicht möglich: ${err.message}`, + ); } // Papierkorb leeren @@ -51,10 +61,14 @@ export class ImapFolderService { const trashUids = await client.search({ all: true }, { uid: true }); if (Array.isArray(trashUids) && trashUids.length > 0) { await client.messageDelete(trashUids, { uid: true }); - this.logger.log(`${trashUids.length} E-Mail(s) aus "${trashFolder}" gelöscht.`); + this.logger.log( + `${trashUids.length} E-Mail(s) aus "${trashFolder}" gelöscht.`, + ); } } catch (err: any) { - this.logger.warn(`Papierkorb "${trashFolder}" konnte nicht geleert werden: ${err.message}`); + this.logger.warn( + `Papierkorb "${trashFolder}" konnte nicht geleert werden: ${err.message}`, + ); } } catch (err: any) { this.logger.error(`IMAP-Cleanup fehlgeschlagen: ${err.message}`); @@ -64,9 +78,13 @@ export class ImapFolderService { } async moveProcessedInboxToImportiert(messageIds: string[]): Promise { - if (!this.configService.get('IMAP_HOST') || messageIds.length === 0) return 0; + if (!this.configService.get('IMAP_HOST') || messageIds.length === 0) + return 0; - const importedFolder = this.configService.get('IMAP_IMPORTED_FOLDER', 'importiert'); + const importedFolder = this.configService.get( + 'IMAP_IMPORTED_FOLDER', + 'importiert', + ); const client = this.createClient(); let movedCount = 0; @@ -74,7 +92,7 @@ export class ImapFolderService { await client.connect(); const mailboxes = await client.list(); - if (!mailboxes.some(m => m.path === importedFolder)) { + if (!mailboxes.some((m) => m.path === importedFolder)) { await client.mailboxCreate(importedFolder); this.logger.log(`IMAP-Ordner "${importedFolder}" erstellt.`); } @@ -86,7 +104,10 @@ export class ImapFolderService { const idSet = new Set(messageIds.map(normalize)); const uidsToMove: number[] = []; - for await (const msg of client.fetch('1:*', { uid: true, envelope: true })) { + for await (const msg of client.fetch('1:*', { + uid: true, + envelope: true, + })) { const msgId = msg.envelope?.messageId; if (msgId && idSet.has(normalize(msgId))) { uidsToMove.push(msg.uid); @@ -96,10 +117,14 @@ export class ImapFolderService { if (uidsToMove.length > 0) { await client.messageMove(uidsToMove, importedFolder, { uid: true }); movedCount = uidsToMove.length; - this.logger.log(`${movedCount} E-Mail(s) aus INBOX → "${importedFolder}" verschoben.`); + this.logger.log( + `${movedCount} E-Mail(s) aus INBOX → "${importedFolder}" verschoben.`, + ); } } catch (err: any) { - this.logger.error(`moveProcessedInboxToImportiert fehlgeschlagen: ${err.message}`); + this.logger.error( + `moveProcessedInboxToImportiert fehlgeschlagen: ${err.message}`, + ); } finally { await client.logout().catch(() => {}); } @@ -110,24 +135,34 @@ export class ImapFolderService { async moveToImportiert(messageId: string): Promise { if (!this.configService.get('IMAP_HOST')) return; - const importedFolder = this.configService.get('IMAP_IMPORTED_FOLDER', 'importiert'); + const importedFolder = this.configService.get( + 'IMAP_IMPORTED_FOLDER', + 'importiert', + ); const client = this.createClient(); try { await client.connect(); const mailboxes = await client.list(); - if (!mailboxes.some(m => m.path === importedFolder)) { + if (!mailboxes.some((m) => m.path === importedFolder)) { await client.mailboxCreate(importedFolder); this.logger.log(`IMAP-Ordner "${importedFolder}" erstellt.`); } await client.mailboxOpen('INBOX'); - const uids = await client.search({ header: { 'message-id': messageId } }, { uid: true }); + const uids = await client.search( + { header: { 'message-id': messageId } }, + { uid: true }, + ); if (Array.isArray(uids) && uids.length > 0) { await client.messageMove(uids, importedFolder, { uid: true }); - this.logger.log(`E-Mail ${messageId} → "${importedFolder}" verschoben.`); + this.logger.log( + `E-Mail ${messageId} → "${importedFolder}" verschoben.`, + ); } else { - this.logger.warn(`E-Mail ${messageId} nicht in INBOX gefunden (bereits verschoben?).`); + this.logger.warn( + `E-Mail ${messageId} nicht in INBOX gefunden (bereits verschoben?).`, + ); } } catch (err: any) { this.logger.error(`IMAP moveToImportiert fehlgeschlagen: ${err.message}`); diff --git a/paperless-backend/src/paperless/paperless-processor.service.ts b/paperless-backend/src/paperless/paperless-processor.service.ts index 7c7a034..0426529 100644 --- a/paperless-backend/src/paperless/paperless-processor.service.ts +++ b/paperless-backend/src/paperless/paperless-processor.service.ts @@ -181,7 +181,7 @@ export class PaperlessProcessorService { doc.correspondent !== null && doc.correspondent !== undefined; break; case 2: - isFilled = !!doc.created || !!doc.created_date; + isFilled = !!doc.created; break; case 3: isFilled = diff --git a/paperless-backend/src/paperless/paperless.controller.ts b/paperless-backend/src/paperless/paperless.controller.ts index 0b719cc..e9cec6f 100644 --- a/paperless-backend/src/paperless/paperless.controller.ts +++ b/paperless-backend/src/paperless/paperless.controller.ts @@ -155,7 +155,7 @@ export class PaperlessController { asn: doc.archive_serial_number, documentType: doc.document_type, correspondent: doc.correspondent, - created: doc.created_date, + created: doc.created, added: doc.added, tags: doc.tags, customFields: doc.custom_fields, @@ -179,7 +179,7 @@ export class PaperlessController { asn: doc.archive_serial_number, documentType: doc.document_type, correspondent: doc.correspondent, - created: doc.created_date, + created: doc.created, added: doc.added, tags: doc.tags, customFields: doc.custom_fields, @@ -338,7 +338,7 @@ export class PaperlessController { docDate.getHours() * 60 * 60 * 1000, ); } - oldDocument.created_date = docDate.toISOString().split('T')[0]; + oldDocument.created = docDate.toISOString(); } const cfDefinitions = await this.paperlessService.getCustomFields(); @@ -390,7 +390,7 @@ export class PaperlessController { for (const req of reqs) { let isFieldValid = false; if (req.Type === 1) isFieldValid = oldDocument.correspondent !== null; - if (req.Type === 2) isFieldValid = oldDocument.created_date !== null; + if (req.Type === 2) isFieldValid = oldDocument.created !== null; if (req.Type === 3) isFieldValid = oldDocument.archive_serial_number !== null; if (req.Type === 4) @@ -446,10 +446,8 @@ export class PaperlessController { ); } } - titleTemplate = titleTemplate.replace( - '{{DATE}}', - oldDocument.created_date, - ); + const createdDatePart = String(oldDocument.created ?? '').split('T')[0]; + titleTemplate = titleTemplate.replace('{{DATE}}', createdDatePart); oldDocument.title = titleTemplate; } } else { @@ -471,6 +469,9 @@ export class PaperlessController { } } + // Veraltetes Feld nicht mit-PATCHen (löst Paperless-Deprecation-Warnung aus) + delete oldDocument.created_date; + await this.paperlessService.updateDocument(documentId, oldDocument); return { success: true }; } diff --git a/paperless-backend/src/zahlung/zahlung.controller.ts b/paperless-backend/src/zahlung/zahlung.controller.ts index b8dad7e..c627acc 100644 --- a/paperless-backend/src/zahlung/zahlung.controller.ts +++ b/paperless-backend/src/zahlung/zahlung.controller.ts @@ -22,10 +22,7 @@ export class ZahlungController { } @Put('documents/:id/zahlung') - setZahlung( - @Param('id') id: string, - @Body('value') value: string | null, - ) { + setZahlung(@Param('id') id: string, @Body('value') value: string | null) { return this.zahlungService.setZahlung(parseInt(id, 10), value ?? null); } diff --git a/paperless-backend/src/zahlung/zahlung.service.ts b/paperless-backend/src/zahlung/zahlung.service.ts index 4d7ceda..aeb8fbc 100644 --- a/paperless-backend/src/zahlung/zahlung.service.ts +++ b/paperless-backend/src/zahlung/zahlung.service.ts @@ -20,7 +20,11 @@ export class ZahlungService { private readonly paperlessService: PaperlessService, ) {} - async getZahlungDocuments(page: number, pageSize: number, filter: ZahlungFilter) { + async getZahlungDocuments( + page: number, + pageSize: number, + filter: ZahlungFilter, + ) { const docTypes = await this.documentTypeRepo.find({ where: { FreigabeErforderlich: true as any }, }); @@ -44,7 +48,10 @@ export class ZahlungService { const result = await this.paperlessService.getDocuments(params); allDocs = result.results ?? []; } catch (err: any) { - this.logger.warn('Fehler beim Laden der Belege für Zahlung', err?.message); + this.logger.warn( + 'Fehler beim Laden der Belege für Zahlung', + err?.message, + ); return { count: 0, results: [] }; } @@ -73,9 +80,12 @@ export class ZahlungService { private getCfValue(doc: any, fieldId: number): string | null { const cf = (doc.custom_fields ?? []).find((f: any) => f.field === fieldId); - if (!cf || cf.value === null || cf.value === undefined || cf.value === '') return null; + if (!cf || cf.value === null || cf.value === undefined || cf.value === '') + return null; if (typeof cf.value === 'object') { - return String(cf.value?.id ?? cf.value?.value ?? cf.value?.label ?? '') || null; + return ( + String(cf.value?.id ?? cf.value?.value ?? cf.value?.label ?? '') || null + ); } return String(cf.value); } @@ -91,20 +101,24 @@ export class ZahlungService { } const customFields: any[] = [...(doc.custom_fields ?? [])]; - const existing = customFields.find((f: any) => f.field === ZAHLUNG_FIELD_ID); + const existing = customFields.find( + (f: any) => f.field === ZAHLUNG_FIELD_ID, + ); if (existing) { existing.value = value; } else if (value !== null && value !== '') { customFields.push({ field: ZAHLUNG_FIELD_ID, value }); } - await this.paperlessService.updateDocument(documentId, { custom_fields: customFields }); + await this.paperlessService.updateDocument(documentId, { + custom_fields: customFields, + }); return { success: true }; } async getZahlungOptions(): Promise<{ id: string; label: string }[]> { const fields = await this.paperlessService.getCustomFields(); - const field = (fields as any[]).find((f: any) => f.id === ZAHLUNG_FIELD_ID); + const field = fields.find((f: any) => f.id === ZAHLUNG_FIELD_ID); if (!field) return []; const rawOptions: any[] = field.extra_data?.select_options ?? []; diff --git a/paperless-frontend/src/api/freigabe.ts b/paperless-frontend/src/api/freigabe.ts index 1f4163e..607d0a0 100644 --- a/paperless-frontend/src/api/freigabe.ts +++ b/paperless-frontend/src/api/freigabe.ts @@ -4,7 +4,6 @@ export interface FreigabeDocument { id: number; title: string; created: string; - created_date: string; correspondent: number | null; document_type: number | null; archive_serial_number: number | null; diff --git a/paperless-frontend/src/api/zahlung.ts b/paperless-frontend/src/api/zahlung.ts index 3f33137..9079974 100644 --- a/paperless-frontend/src/api/zahlung.ts +++ b/paperless-frontend/src/api/zahlung.ts @@ -6,7 +6,6 @@ export interface ZahlungDocument { id: number; title: string; created: string; - created_date: string; correspondent: number | null; document_type: number | null; archive_serial_number: number | null; diff --git a/paperless-frontend/src/components/DocumentEditModal.tsx b/paperless-frontend/src/components/DocumentEditModal.tsx index 433c24d..dde6890 100644 --- a/paperless-frontend/src/components/DocumentEditModal.tsx +++ b/paperless-frontend/src/components/DocumentEditModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback } from 'react'; import { Modal, Form, Select, DatePicker, Input, Spin, message, Row, Col, Button, Space, Divider, Tag } from 'antd'; -import { PlusOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons'; +import { PlusOutlined, EyeOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import { posteingangApi } from '../api/posteingang'; import type { DocumentRequirement, PosteingangDocument, Kontonummer } from '../api/posteingang'; @@ -11,6 +11,7 @@ import type { PaperlessDocType, PaperlessCorrespondent, PaperlessTag } from '../ import { getEnv } from '../utils/env'; import { AuthIframe, openAuthUrl } from '../utils/auth-resource'; import DocumentSearchModal from './DocumentSearchModal'; +import { useIsMobile } from '../hooks/useIsMobile'; const { Option } = Select; @@ -26,6 +27,7 @@ interface Props { export default function DocumentEditModal({ documentId, document, open, onClose, onSave, isPosteingang = true, hasNextDocument = true }: Props) { const [form] = Form.useForm(); + const isMobile = useIsMobile(); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); @@ -349,8 +351,8 @@ export default function DocumentEditModal({ documentId, document, open, onClose, title={`Dokument bearbeiten (${document?.title || ''})`} open={open && !kontonummerMissing} onCancel={() => onClose(false)} - width={1400} - style={{ top: 20 }} + width={{ xs: '100vw', md: 900, xl: 1400 }} + style={isMobile ? {} : { top: 20 }} footer={ hasNextDocument ? [ , @@ -363,8 +365,12 @@ export default function DocumentEditModal({ documentId, document, open, onClose, } > - - + +
@@ -556,10 +562,20 @@ export default function DocumentEditModal({ documentId, document, open, onClose, })} - + + {isMobile && ( + + )} diff --git a/paperless-frontend/src/components/DocumentSearchModal.tsx b/paperless-frontend/src/components/DocumentSearchModal.tsx index 3a5573b..57d804e 100644 --- a/paperless-frontend/src/components/DocumentSearchModal.tsx +++ b/paperless-frontend/src/components/DocumentSearchModal.tsx @@ -57,7 +57,7 @@ export default function DocumentSearchModal({ open, onCancel, onSelect }: Props) open={open} onCancel={onCancel} footer={null} - width={800} + width={{ xs: '100vw', md: 800 }} style={{ top: 50 }} >
@@ -100,7 +100,7 @@ export default function DocumentSearchModal({ open, onCancel, onSelect }: Props) description={ ID: {doc.id} | ASN: {doc.archive_serial_number || 'Keine'} - Erstellt: {dayjs(doc.created_date).format('DD.MM.YYYY')} + Erstellt: {dayjs(doc.created).format('DD.MM.YYYY')} } /> diff --git a/paperless-frontend/src/components/MailImportWizard.tsx b/paperless-frontend/src/components/MailImportWizard.tsx index e9465d8..10b4644 100644 --- a/paperless-frontend/src/components/MailImportWizard.tsx +++ b/paperless-frontend/src/components/MailImportWizard.tsx @@ -538,8 +538,8 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a {toProcess.map(item => (
{item.fileName} - - + + {/* Eingangsdatum */}
Eingangsdatum: @@ -626,7 +626,7 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
- + {main.fileName} - - - + + + Eingangsdatum: {datum?.format('DD.MM.YYYY') ?? '—'} @@ -690,7 +690,7 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a )} {showPrint && ( - + @@ -735,7 +735,7 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a title="Paperless Import-Wizard" open={visible} onCancel={onClose} - width={1000} + width={{ xs: '100vw', md: 900, lg: 1000 }} footer={ importSuccess ? ( diff --git a/paperless-frontend/src/components/MobileCardList.tsx b/paperless-frontend/src/components/MobileCardList.tsx new file mode 100644 index 0000000..1b55de3 --- /dev/null +++ b/paperless-frontend/src/components/MobileCardList.tsx @@ -0,0 +1,64 @@ +import type { ReactNode } from 'react'; +import { List, Card } from 'antd'; +import type { TablePaginationConfig } from 'antd'; + +interface MobileCardListProps { + dataSource: T[]; + rowKey: keyof T | ((record: T) => React.Key); + loading?: boolean; + /** Gleiche Pagination-Config wie bei — wird durchgereicht (mobil kompakt). */ + pagination?: TablePaginationConfig | false; + /** Seitenspezifischer Karteninhalt für einen Datensatz. */ + renderCard: (record: T) => ReactNode; + onCardClick?: (record: T) => void; + emptyText?: string; +} + +/** + * Mobile Karten-Ansicht als Ersatz für breite Tabellen: + * eine Karte pro Datensatz, Pagination wie bei der Tabelle. + */ +export default function MobileCardList({ + dataSource, + rowKey, + loading, + pagination, + renderCard, + onCardClick, + emptyText = 'Keine Einträge vorhanden', +}: MobileCardListProps) { + const getKey = (record: T): React.Key => + typeof rowKey === 'function' ? rowKey(record) : (record[rowKey] as React.Key); + + return ( + ( + + onCardClick(record) : undefined} + > + {renderCard(record)} + + + )} + /> + ); +} diff --git a/paperless-frontend/src/hooks/useIsMobile.ts b/paperless-frontend/src/hooks/useIsMobile.ts new file mode 100644 index 0000000..e308c26 --- /dev/null +++ b/paperless-frontend/src/hooks/useIsMobile.ts @@ -0,0 +1,11 @@ +import { Grid } from 'antd'; + +/** + * Zentraler Breakpoint-Hook: mobil = Viewport unter `md` (768px). + * Beim allerersten Render liefert useBreakpoint() noch keine Werte — + * dann Desktop annehmen, um ein Layout-Flackern zu vermeiden. + */ +export function useIsMobile(): boolean { + const screens = Grid.useBreakpoint(); + return screens.md === undefined ? false : !screens.md; +} diff --git a/paperless-frontend/src/index.css b/paperless-frontend/src/index.css index 4ab9339..19d4fa7 100644 --- a/paperless-frontend/src/index.css +++ b/paperless-frontend/src/index.css @@ -139,3 +139,29 @@ body { .ant-picker-input > input { font-size: 14px !important; } + +/* ── Responsive / Mobile ─────────────────────────────────────── */ + +html { + -webkit-text-size-adjust: 100%; +} + +@media (max-width: 767px) { + .ant-modal { + top: 8px; + } + + /* Modal-Inhalte scrollbar statt abgeschnitten (v. a. Settings-Dialoge) */ + .ant-modal .ant-modal-body { + max-height: calc(100dvh - 160px); + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + + /* Sicherheitsnetz: Tabellen ohne Karten-Ansicht seitlich scrollbar */ + .ant-table-content, + .ant-table-body { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} diff --git a/paperless-frontend/src/layouts/AppLayout.tsx b/paperless-frontend/src/layouts/AppLayout.tsx index 7e1294b..f021793 100644 --- a/paperless-frontend/src/layouts/AppLayout.tsx +++ b/paperless-frontend/src/layouts/AppLayout.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { Outlet, useNavigate, useLocation } from 'react-router-dom'; -import { Layout, Menu, Avatar, Dropdown, theme, Typography, Tooltip, Badge } from 'antd'; +import { Layout, Menu, Avatar, Dropdown, theme, Typography, Tooltip, Badge, Drawer, Button } from 'antd'; import { InboxOutlined, FileTextOutlined, @@ -15,9 +15,11 @@ import { GlobalOutlined, CheckCircleOutlined, EuroOutlined, + MenuOutlined, } from '@ant-design/icons'; import { useAuth } from '../auth/AuthContext'; import { useTheme } from '../theme/ThemeContext'; +import { useIsMobile } from '../hooks/useIsMobile'; import { Permission } from '../auth/permissions'; import { statsApi, type StatsCounts } from '../api/stats'; @@ -47,17 +49,22 @@ const allMenuItems: MenuItemDef[] = [ export default function AppLayout() { const [collapsed, setCollapsed] = useState(false); + const [drawerOpen, setDrawerOpen] = useState(false); const navigate = useNavigate(); const location = useLocation(); const { user, logout, hasPermission, isAuthenticated } = useAuth(); const { token: themeToken } = theme.useToken(); const { isDark, toggleTheme } = useTheme(); - + const isMobile = useIsMobile(); + const [counts, setCounts] = useState(null); + // Im Drawer (mobil) ist die Navigation nie eingeklappt + const effectiveCollapsed = isMobile ? false : collapsed; + useEffect(() => { if (!isAuthenticated) return; - + const fetchCounts = async () => { try { const data = await statsApi.getCounts(); @@ -66,10 +73,10 @@ export default function AppLayout() { console.error('Fehler beim Abrufen der Zählerstände:', err); } }; - + fetchCounts(); const interval = setInterval(fetchCounts, 30000); // 30 Sekunden Polling - + return () => clearInterval(interval); }, [isAuthenticated, location.pathname]); // Update after navigation or auth change @@ -80,9 +87,9 @@ export default function AppLayout() { label: (
{item.label} - {item.countKey && counts && counts[item.countKey] > 0 && !collapsed && ( - 0 && !effectiveCollapsed && ( + void) => ( + <> + {/* Logo / Collapse-Toggle */} + + + {/* Navigation Menu */} + { + const item = allMenuItems.find((i) => i.key === key); + if (item?.externalUrl) { + window.open(item.externalUrl, '_blank', 'noopener,noreferrer'); + } else { + navigate(key); + } + onNavigate?.(); + }} + style={isDark ? { flex: 1 } : { background: 'transparent', flex: 1 }} + /> + + {/* Bottom Section: User + Theme Toggle */} +
+ {/* Theme Toggle */} + + + + + {/* User Menu */} + , + label: 'Benutzereinstellungen', + onClick: () => { + navigate('/user-settings'); + onNavigate?.(); + }, + }, + { + key: 'logout', + icon: , + label: 'Abmelden', + onClick: () => logout(), + }, + ], + }} + placement="topRight" + trigger={['click']} + > +
(e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.08)' : '#eef1f8')} + onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} + > + } /> + {!isCollapsed && ( + + {user?.profile?.name || 'Benutzer'} + + )} +
+
+
+ + ); return ( - - {/* Logo / Collapse-Toggle */} - + {isMobile ? ( + <> + {/* Mobile Top-Bar mit Hamburger */} +
+
- {/* Navigation Menu */} - setDrawerOpen(false)} + closable={false} + styles={{ + body: { + padding: 0, + position: 'relative', + background: isDark ? '#001529' : '#f0f2f7', + }, + }} + > + {renderSidebarContent(false, () => setDrawerOpen(false))} + + + ) : ( + { - const item = allMenuItems.find((i) => i.key === key); - if (item?.externalUrl) { - window.open(item.externalUrl, '_blank', 'noopener,noreferrer'); - } else { - navigate(key); - } - }} - style={isDark ? { flex: 1 } : { background: 'transparent', flex: 1 }} - /> - - {/* Bottom Section: User + Theme Toggle */} -
- {/* Theme Toggle */} - - - + {renderSidebarContent(collapsed)} + + )} - {/* User Menu */} - , - label: 'Benutzereinstellungen', - onClick: () => navigate('/user-settings'), - }, - { - key: 'logout', - icon: , - label: 'Abmelden', - onClick: () => logout(), - }, - ], - }} - placement="topRight" - trigger={['click']} - > -
(e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.08)' : '#eef1f8')} - onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} - > - } /> - {!collapsed && ( - - {user?.profile?.name || 'Benutzer'} - - )} -
-
-
-
- - - + + diff --git a/paperless-frontend/src/pages/FreigabePage.tsx b/paperless-frontend/src/pages/FreigabePage.tsx index d6c1b18..78275be 100644 --- a/paperless-frontend/src/pages/FreigabePage.tsx +++ b/paperless-frontend/src/pages/FreigabePage.tsx @@ -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([]); 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 ( <> Freigabe - - { + onChange={(v) => { setPage(1); - setNurNichtFreigegeben(e.target.value); + setNurNichtFreigegeben(v); }} - optionType="button" - buttonStyle="solid" - > - Nicht freigegeben - Alle - - + options={[ + { value: true, label: 'Nicht freigegeben' }, + { value: false, label: 'Alle' }, + ]} + /> + ) : ( + + { + setPage(1); + setNurNichtFreigegeben(e.target.value); + }} + optionType="button" + buttonStyle="solid" + > + Nicht freigegeben + Alle + + + )} - - 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 ? ( + + dataSource={data} + rowKey="id" + loading={loading} + pagination={paginationConfig} + emptyText="Keine Belege vorhanden" + renderCard={(doc) => ( +
+ {doc.title || '—'} + Dokumenttyp: {getDocTypeName(doc.document_type)} + Absender: {getCorrespondentName(doc.correspondent)} +
+ + Erstellt: {doc.created ? dayjs(doc.created).format('DD.MM.YYYY') : '—'} + + {getFreigabeValue(doc)} +
+ +
+ )} + /> + ) : ( + + dataSource={data} + columns={columns} + rowKey="id" + loading={loading} + size="small" + pagination={paginationConfig} + /> + )} (null); const [inboxUrl, setInboxUrl] = useState(null); const [loading, setLoading] = useState(false); @@ -165,8 +167,8 @@ function CompareModal({ , ]} > -
-
+
+
Original (Paperless) @@ -180,7 +182,7 @@ function CompareModal({ )}
-
+
Aktueller Abschnitt (Inbox) @@ -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(null); const [loading, setLoading] = useState(true); const [thumbUrls, setThumbUrls] = useState>(new Map()); @@ -1211,13 +1214,13 @@ export default function InboxDetailPage() { })(); return ( -
-
- +
+
+ - + <Title level={isMobile ? 5 : 3} style={{ margin: 0, maxWidth: isMobile ? 180 : undefined }} ellipsis={{ tooltip: file.name }}> {file.name} @@ -1399,12 +1402,13 @@ export default function InboxDetailPage() { >
{sidebarPages.map((n, idx) => { @@ -1428,7 +1432,7 @@ export default function InboxDetailPage() {
) : ( diff --git a/paperless-frontend/src/pages/InboxPage.tsx b/paperless-frontend/src/pages/InboxPage.tsx index 943c1ca..b8372a1 100644 --- a/paperless-frontend/src/pages/InboxPage.tsx +++ b/paperless-frontend/src/pages/InboxPage.tsx @@ -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' ? ( + } color="purple"> + Persönlich + + ) : ( + } color="blue"> + Gemeinsam + + ); +} + function renderBarcodes(barcodes: InboxBarcode[]): ReactNode { if (!barcodes || barcodes.length === 0) { return ; @@ -137,6 +151,7 @@ function buildInitialFieldValues(template: BarcodeTemplate | null): Record([]); 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') => ( +
+ + + + handleDelete(record.id)} + > + + + + + +
+ ); + const columns: ColumnsType = [ { 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' ? ( - } color="purple"> - Persönlich - - ) : ( - } color="blue"> - Gemeinsam - - ), + 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) => ( -
- - - - handleDelete(record.id)} - > - - - - - -
- ), + 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.
- + } 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() { - - + {isMobile ? ( + + 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) => ( +
+ + {record.name} + +
+ {renderSourceTag(record.source)} + + {record.pageCount > 0 ? `${record.pageCount} Seiten` : '—'} · {formatDate(record.createdAt)} + +
+ {renderBarcodes(record.barcodes)} + {renderActions(record, 'row')} +
+ )} /> -
+ ) : ( + + + rowKey="id" + columns={columns} + dataSource={filtered} + loading={loading} + pagination={{ + pageSize: 25, + showSizeChanger: true, + showTotal: (t) => `${t} Dateien`, + }} + locale={{ emptyText: 'Keine Dateien vorhanden' }} + /> + + )}
); } diff --git a/paperless-frontend/src/pages/MailDetailPage.tsx b/paperless-frontend/src/pages/MailDetailPage.tsx index d821e3b..e42ee07 100644 --- a/paperless-frontend/src/pages/MailDetailPage.tsx +++ b/paperless-frontend/src/pages/MailDetailPage.tsx @@ -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(null); const [attachments, setAttachments] = useState([]); const [selected, setSelected] = useState(null); @@ -136,16 +138,16 @@ export default function MailDetailPage() { return (
-
- +
+ - {email.Subject} + {email.Subject} {hasErechnung && eRechnung} - - + +
-
+
{/* Linke Seite: E-Mail-Inhalt */}
Von: {email.SenderAddress}
@@ -208,7 +224,17 @@ export default function MailDetailPage() { {/* Rechte Seite: Anhänge + Vorschau */} - +
columns={columns} @@ -224,7 +250,7 @@ export default function MailDetailPage() { locale={{ emptyText: }} />
-
+
{previewLoading ? (
diff --git a/paperless-frontend/src/pages/MailpostfachPage.tsx b/paperless-frontend/src/pages/MailpostfachPage.tsx index a865c79..07c0062 100644 --- a/paperless-frontend/src/pages/MailpostfachPage.tsx +++ b/paperless-frontend/src/pages/MailpostfachPage.tsx @@ -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 Neu; + if (s === 1) return Verarbeitet; + if (s === 2) return Fehler; + if (s === 3) return Ignoriert; + return {s}; +} export default function MailpostfachPage() { const navigate = useNavigate(); + const isMobile = useIsMobile(); const [emails, setEmails] = useState([]); 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 Neu; - if (s === 1) return Verarbeitet; - if (s === 2) return Fehler; - if (s === 3) return Ignoriert; - return {s}; - }, + render: renderStatusTag, filters: [ { text: 'Neu', value: 0 }, { text: 'Verarbeitet', value: 1 }, @@ -116,9 +121,9 @@ export default function MailpostfachPage() { return (
-
+
Mailpostfach - +
`${t} Dokumente` }} - locale={{ emptyText: 'Keine Einträge vorhanden' }} - /> + {isMobile ? ( + + dataSource={data} + rowKey="id" + loading={loading} + pagination={{ pageSize: 20, showTotal: (t) => `${t} Dokumente` }} + renderCard={(record) => ( +
+
+ +
+ {record.title || '—'} + {renderContentTags(record)} + + Eingangsdatum: {record.created ? dayjs(record.created).format('DD.MM.YYYY') : '-'} + + + Importiert am: {dayjs(record.added).format('DD.MM.YYYY HH:mm')} + +
+
+ +
+ )} + /> + ) : ( +
`${t} Dokumente` }} + locale={{ emptyText: 'Keine Einträge vorhanden' }} + /> + )} ([]); 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() { -
`${t} Dokumente` }} - locale={{ emptyText: 'Keine Einträge vorhanden' }} - /> + {isMobile ? ( + + dataSource={data} + rowKey="id" + loading={loading} + pagination={{ pageSize: 20, showTotal: (t) => `${t} Dokumente` }} + renderCard={(record) => ( +
+
+ +
+ {record.title || '—'} + + Eingangsdatum: {getEingangsdatum(record)} + + + Importiert am: {dayjs(record.added).format('DD.MM.YYYY HH:mm')} + +
+
+ +
+ )} + /> + ) : ( +
`${t} Dokumente` }} + locale={{ emptyText: 'Keine Einträge vorhanden' }} + /> + )} } onClick={() => setModalOpen(true)} style={{ marginBottom: 16 }}> Zuordnung hinzufügen -
+
Betriebe — Agrarmonitor-Zuordnung @@ -335,6 +335,7 @@ function UserClientsTab() { rowKey="Id" size="small" pagination={false} + scroll={{ x: 'max-content' }} /> setModalOpen(false)}> @@ -599,13 +600,14 @@ function DocTypesTab() { return ( <> -
} onClick={openNew} style={{ marginBottom: 16 }}> Regel hinzufügen -
+
} onClick={openNew} style={{ marginBottom: 16 }}> Export-Ziel hinzufügen -
+
setEditing(null)}>
@@ -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() { -
+
-
Weiterverarbeitungs-Aktionen - rowKey="Id" columns={columns} dataSource={actions} loading={loading} pagination={false} size="small" /> + rowKey="Id" columns={columns} dataSource={actions} loading={loading} pagination={false} size="small" scroll={{ x: 'max-content' }} /> Fertig; @@ -12,6 +16,7 @@ function statusTag(fertig: number | null) { } export default function TaskLogPage() { + const isMobile = useIsMobile(); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [deleting, setDeleting] = useState(false); @@ -155,14 +160,55 @@ export default function TaskLogPage() { -
+ {isMobile ? ( + + dataSource={data} + rowKey="TaskId" + loading={loading} + pagination={{ pageSize: 20 }} + renderCard={(record) => ( +
+
+ {record.InterneBelegnummer || '—'} + {statusTag(record.Fertig)} +
+ + {record.TaskId.slice(0, 8)}… + + Lieferant: {record.Lieferant || '-'} + + Belegdatum: {record.Belegdatum ? dayjs(record.Belegdatum).format('DD.MM.YYYY') : '-'} + {' · '} + Eingang: {record.Eingangsdatum ? dayjs(record.Eingangsdatum).format('DD.MM.YYYY') : '-'} + + + Paperless-Dok.-ID: {record.PaperlessDocumentID ?? '-'} + + handleDeleteOne(record.TaskId)} + okText="Löschen" + cancelText="Abbrechen" + okButtonProps={{ danger: true }} + > + + +
+ )} + /> + ) : ( +
+ )} ); diff --git a/paperless-frontend/src/pages/ZahlungPage.tsx b/paperless-frontend/src/pages/ZahlungPage.tsx index 1062ab7..077a526 100644 --- a/paperless-frontend/src/pages/ZahlungPage.tsx +++ b/paperless-frontend/src/pages/ZahlungPage.tsx @@ -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([]); 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 ( <> Zahlung - - { + onChange={(v) => { setPage(1); - setFilter(e.target.value); + setFilter(v); }} - optionType="button" - buttonStyle="solid" - > - Freigegeben, noch nicht bezahlt - Alle freigegebenen - Alle - - + options={FILTER_OPTIONS} + /> + ) : ( + + { + setPage(1); + setFilter(e.target.value); + }} + optionType="button" + buttonStyle="solid" + > + {FILTER_OPTIONS.map((o) => ( + {o.label} + ))} + + + )} - - 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 ? ( + + dataSource={data} + rowKey="id" + loading={loading} + pagination={paginationConfig} + emptyText="Keine Belege vorhanden" + renderCard={(doc) => { + const freigegeben = istFreigegeben(doc); + return ( +
+ {doc.title || '—'} + Dokumenttyp: {getDocTypeName(doc.document_type)} + Absender: {getCorrespondentName(doc.correspondent)} + + Erstellt: {doc.created ? dayjs(doc.created).format('DD.MM.YYYY') : '—'} + +
+ {renderFreigabeTag(doc)} + {renderZahlungTag(doc)} +
+ + {!freigegeben && ( + + Beleg muss zuerst freigegeben werden + + )} +
+ ); + }} + /> + ) : ( + + dataSource={data} + columns={columns} + rowKey="id" + loading={loading} + size="small" + pagination={paginationConfig} + /> + )}