From 11bed63bada0b04a7e76b6e4e2d8a169980f4a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20P=C3=B6ttker?= Date: Mon, 18 May 2026 09:14:26 +0200 Subject: [PATCH] feat: add manual Paperless ID synchronization for email attachments and update default barcode margins to 7mm. --- .../src/email/email.controller.ts | 41 ++++++++++------ .../src/paperless/paperless.service.ts | 10 ++++ paperless-frontend/src/api/emails.ts | 4 +- .../src/components/BarcodePositioner.tsx | 4 +- .../src/components/MailImportWizard.tsx | 4 +- .../src/pages/MailpostfachPage.tsx | 49 ++++++++++++++----- 6 files changed, 78 insertions(+), 34 deletions(-) diff --git a/paperless-backend/src/email/email.controller.ts b/paperless-backend/src/email/email.controller.ts index d933d86..50f2fd8 100644 --- a/paperless-backend/src/email/email.controller.ts +++ b/paperless-backend/src/email/email.controller.ts @@ -89,19 +89,21 @@ export class EmailController { @Post('check-attachments') @RequirePermissions(Permission.MANAGE_ALL) - async checkAttachments() { - this.logger.log('Starte manuelle Prüfung der E-Mail-Anhänge in Paperless...'); - + async checkAttachments(@Body() body: { includeProcessed?: boolean } = {}) { + const { includeProcessed = false } = body; + this.logger.log(`Starte manuelle Prüfung der E-Mail-Anhänge in Paperless... (includeProcessed=${includeProcessed})`); + try { - // Hole alle neuen E-Mails (Status = 0) inkl. Anhängen + const whereCondition = includeProcessed ? [{ Status: 0 }, { Status: 1 }] : { Status: 0 }; const emails = await this.emailRepo.find({ - where: { Status: 0 }, + where: whereCondition, relations: ['Attachments'], }); - this.logger.log(`Gefunden: ${emails.length} E-Mails mit Status "Neu" (0). Beginne Prüfung...`); + this.logger.log(`Gefunden: ${emails.length} E-Mails. Beginne Prüfung...`); let updatedCount = 0; + let idsUpdated = 0; let skippedCount = 0; for (const [index, email] of emails.entries()) { @@ -113,15 +115,23 @@ export class EmailController { let hasMatch = false; for (const attachment of email.Attachments) { - // Prüfe nur PDFs und wenn eine Checksumme vorhanden ist + // Prüfe nur PDFs mit Checksumme if (attachment.ContentType === 'application/pdf' && attachment.Checksum) { this.logger.debug(`Prüfe Checksumme für E-Mail ${email.Id}, Anhang ${attachment.Id} (${attachment.FileName})`); try { - const exists = await this.paperlessService.checksumExists(attachment.Checksum); - if (exists) { - this.logger.log(`Treffer! Anhang ${attachment.Id} (E-Mail ${email.Id}) in Paperless gefunden.`); + const docId = await this.paperlessService.getDocumentIdByChecksum(attachment.Checksum); + if (docId !== null) { + this.logger.log(`Treffer! Anhang ${attachment.Id} (E-Mail ${email.Id}) → Paperless-Dokument ${docId}.`); hasMatch = true; - break; // Ein Treffer reicht für diese E-Mail + + // PaperlessDocumentId hinterlegen, falls noch nicht vorhanden + const existingIds: Record = attachment.PaperlessDocumentIds ?? {}; + if (!existingIds['full']) { + attachment.PaperlessDocumentIds = { ...existingIds, full: docId }; + await this.attachmentRepo.save(attachment); + idsUpdated++; + this.logger.log(`Anhang ${attachment.Id}: PaperlessDocumentIds aktualisiert (full=${docId}).`); + } } } catch (err: any) { this.logger.error(`Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${err.message}`, err.stack); @@ -129,22 +139,21 @@ export class EmailController { } } - // Wenn mindestens ein Anhang in Paperless existiert, markiere die Mail als verarbeitet (Status = 1) - if (hasMatch) { + // Neu gefundene Mails auf Status 1 setzen + if (hasMatch && email.Status === 0) { email.Status = 1; await this.emailRepo.save(email); updatedCount++; this.logger.log(`E-Mail ${email.Id} auf Status 1 (Verarbeitet) gesetzt.`); } - // Zwischenstand loggen if ((index + 1) % 10 === 0) { this.logger.log(`Zwischenstand: ${index + 1} von ${emails.length} E-Mails geprüft.`); } } - this.logger.log(`Prüfung abgeschlossen. ${updatedCount} aktualisiert, ${skippedCount} ohne (PDF-)Anhänge übersprungen.`); - return { updatedCount }; + this.logger.log(`Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`); + return { updatedCount, idsUpdated }; } catch (error: any) { this.logger.error(`Kritischer Fehler bei checkAttachments: ${error.message}`, error.stack); throw error; diff --git a/paperless-backend/src/paperless/paperless.service.ts b/paperless-backend/src/paperless/paperless.service.ts index bdaa10e..00efbad 100644 --- a/paperless-backend/src/paperless/paperless.service.ts +++ b/paperless-backend/src/paperless/paperless.service.ts @@ -229,6 +229,16 @@ export class PaperlessService { return response.data.count > 0; } + async getDocumentIdByChecksum(checksum: string): Promise { + const response = await this.client.get('/documents/', { + params: { checksum__iexact: checksum }, + }); + if (response.data.count > 0 && response.data.results?.length > 0) { + return response.data.results[0].id as number; + } + return null; + } + /** * Prüft, ob eine ASN bereits vergeben ist und wirft einen Fehler, falls ja. */ diff --git a/paperless-frontend/src/api/emails.ts b/paperless-frontend/src/api/emails.ts index 05715fe..b006378 100644 --- a/paperless-frontend/src/api/emails.ts +++ b/paperless-frontend/src/api/emails.ts @@ -43,8 +43,8 @@ export const emailsApi = { triggerFetch: () => api.post<{ message: string }>('/api/emails/fetch').then((r) => r.data), - checkAttachments: () => - api.post<{ updatedCount: number }>('/api/emails/check-attachments').then((r) => r.data), + checkAttachments: (includeProcessed = false) => + api.post<{ updatedCount: number; idsUpdated: number }>('/api/emails/check-attachments', { includeProcessed }).then((r) => r.data), updateStatus: (id: number, status: number) => api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data), diff --git a/paperless-frontend/src/components/BarcodePositioner.tsx b/paperless-frontend/src/components/BarcodePositioner.tsx index 702caf2..7202905 100644 --- a/paperless-frontend/src/components/BarcodePositioner.tsx +++ b/paperless-frontend/src/components/BarcodePositioner.tsx @@ -116,8 +116,8 @@ export default function BarcodePositioner({ py += containerRef.current.scrollTop; } - // Constraints: 6mm margin from edge - const margin = 6 * scale; + // Constraints: 7mm margin from edge + const margin = 7 * scale; px = Math.max(margin, Math.min(px, cr.width - barcodeW - margin)); const pageHeightPx = PAGE_HEIGHT_MM * scale; py = Math.max(margin, Math.min(py, pageHeightPx - barcodeH - margin)); diff --git a/paperless-frontend/src/components/MailImportWizard.tsx b/paperless-frontend/src/components/MailImportWizard.tsx index d44dac8..8864ef9 100644 --- a/paperless-frontend/src/components/MailImportWizard.tsx +++ b/paperless-frontend/src/components/MailImportWizard.tsx @@ -65,8 +65,8 @@ export default function MailImportWizard({ visible, onClose, email, attachments initialData.forEach(d => { initialDates[d.virtualId] = mailDate; initialBarcodes[d.virtualId] = { - x: 6, - y: 6, + x: 7, + y: 7, datum: mailDate.format('YYYY-MM-DD'), jahr: mailDate.format('YYYY'), isNeu: true, diff --git a/paperless-frontend/src/pages/MailpostfachPage.tsx b/paperless-frontend/src/pages/MailpostfachPage.tsx index d6ac553..0d129a4 100644 --- a/paperless-frontend/src/pages/MailpostfachPage.tsx +++ b/paperless-frontend/src/pages/MailpostfachPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Table, Card, Typography, Button, Space, Tag, message, Input, Select } from 'antd'; -import { ReloadOutlined, DownloadOutlined, CheckCircleOutlined, SearchOutlined } from '@ant-design/icons'; +import { Table, Card, Typography, Button, Space, Tag, message, Input, Select, Dropdown } from 'antd'; +import { ReloadOutlined, DownloadOutlined, CheckCircleOutlined, SearchOutlined, DownOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; import { emailsApi, type EmailItem } from '../api/emails'; @@ -141,26 +141,51 @@ export default function MailpostfachPage() { Aktualisieren {hasPermission(Permission.MANAGE_ALL) && ( - + Anhänge prüfen + )}