feat: add manual Paperless ID synchronization for email attachments and update default barcode margins to 7mm.
Build and Push Multi-Platform Images / build-and-push (push) Successful in 36s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 36s
This commit is contained in:
@@ -89,19 +89,21 @@ export class EmailController {
|
|||||||
|
|
||||||
@Post('check-attachments')
|
@Post('check-attachments')
|
||||||
@RequirePermissions(Permission.MANAGE_ALL)
|
@RequirePermissions(Permission.MANAGE_ALL)
|
||||||
async checkAttachments() {
|
async checkAttachments(@Body() body: { includeProcessed?: boolean } = {}) {
|
||||||
this.logger.log('Starte manuelle Prüfung der E-Mail-Anhänge in Paperless...');
|
const { includeProcessed = false } = body;
|
||||||
|
this.logger.log(`Starte manuelle Prüfung der E-Mail-Anhänge in Paperless... (includeProcessed=${includeProcessed})`);
|
||||||
|
|
||||||
try {
|
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({
|
const emails = await this.emailRepo.find({
|
||||||
where: { Status: 0 },
|
where: whereCondition,
|
||||||
relations: ['Attachments'],
|
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 updatedCount = 0;
|
||||||
|
let idsUpdated = 0;
|
||||||
let skippedCount = 0;
|
let skippedCount = 0;
|
||||||
|
|
||||||
for (const [index, email] of emails.entries()) {
|
for (const [index, email] of emails.entries()) {
|
||||||
@@ -113,15 +115,23 @@ export class EmailController {
|
|||||||
let hasMatch = false;
|
let hasMatch = false;
|
||||||
|
|
||||||
for (const attachment of email.Attachments) {
|
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) {
|
if (attachment.ContentType === 'application/pdf' && attachment.Checksum) {
|
||||||
this.logger.debug(`Prüfe Checksumme für E-Mail ${email.Id}, Anhang ${attachment.Id} (${attachment.FileName})`);
|
this.logger.debug(`Prüfe Checksumme für E-Mail ${email.Id}, Anhang ${attachment.Id} (${attachment.FileName})`);
|
||||||
try {
|
try {
|
||||||
const exists = await this.paperlessService.checksumExists(attachment.Checksum);
|
const docId = await this.paperlessService.getDocumentIdByChecksum(attachment.Checksum);
|
||||||
if (exists) {
|
if (docId !== null) {
|
||||||
this.logger.log(`Treffer! Anhang ${attachment.Id} (E-Mail ${email.Id}) in Paperless gefunden.`);
|
this.logger.log(`Treffer! Anhang ${attachment.Id} (E-Mail ${email.Id}) → Paperless-Dokument ${docId}.`);
|
||||||
hasMatch = true;
|
hasMatch = true;
|
||||||
break; // Ein Treffer reicht für diese E-Mail
|
|
||||||
|
// PaperlessDocumentId hinterlegen, falls noch nicht vorhanden
|
||||||
|
const existingIds: Record<string, number> = 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) {
|
} catch (err: any) {
|
||||||
this.logger.error(`Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${err.message}`, err.stack);
|
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)
|
// Neu gefundene Mails auf Status 1 setzen
|
||||||
if (hasMatch) {
|
if (hasMatch && email.Status === 0) {
|
||||||
email.Status = 1;
|
email.Status = 1;
|
||||||
await this.emailRepo.save(email);
|
await this.emailRepo.save(email);
|
||||||
updatedCount++;
|
updatedCount++;
|
||||||
this.logger.log(`E-Mail ${email.Id} auf Status 1 (Verarbeitet) gesetzt.`);
|
this.logger.log(`E-Mail ${email.Id} auf Status 1 (Verarbeitet) gesetzt.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zwischenstand loggen
|
|
||||||
if ((index + 1) % 10 === 0) {
|
if ((index + 1) % 10 === 0) {
|
||||||
this.logger.log(`Zwischenstand: ${index + 1} von ${emails.length} E-Mails geprüft.`);
|
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.`);
|
this.logger.log(`Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`);
|
||||||
return { updatedCount };
|
return { updatedCount, idsUpdated };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Kritischer Fehler bei checkAttachments: ${error.message}`, error.stack);
|
this.logger.error(`Kritischer Fehler bei checkAttachments: ${error.message}`, error.stack);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -229,6 +229,16 @@ export class PaperlessService {
|
|||||||
return response.data.count > 0;
|
return response.data.count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDocumentIdByChecksum(checksum: string): Promise<number | null> {
|
||||||
|
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.
|
* Prüft, ob eine ASN bereits vergeben ist und wirft einen Fehler, falls ja.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export const emailsApi = {
|
|||||||
triggerFetch: () =>
|
triggerFetch: () =>
|
||||||
api.post<{ message: string }>('/api/emails/fetch').then((r) => r.data),
|
api.post<{ message: string }>('/api/emails/fetch').then((r) => r.data),
|
||||||
|
|
||||||
checkAttachments: () =>
|
checkAttachments: (includeProcessed = false) =>
|
||||||
api.post<{ updatedCount: number }>('/api/emails/check-attachments').then((r) => r.data),
|
api.post<{ updatedCount: number; idsUpdated: number }>('/api/emails/check-attachments', { includeProcessed }).then((r) => r.data),
|
||||||
|
|
||||||
updateStatus: (id: number, status: number) =>
|
updateStatus: (id: number, status: number) =>
|
||||||
api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data),
|
api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data),
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ export default function BarcodePositioner({
|
|||||||
py += containerRef.current.scrollTop;
|
py += containerRef.current.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constraints: 6mm margin from edge
|
// Constraints: 7mm margin from edge
|
||||||
const margin = 6 * scale;
|
const margin = 7 * scale;
|
||||||
px = Math.max(margin, Math.min(px, cr.width - barcodeW - margin));
|
px = Math.max(margin, Math.min(px, cr.width - barcodeW - margin));
|
||||||
const pageHeightPx = PAGE_HEIGHT_MM * scale;
|
const pageHeightPx = PAGE_HEIGHT_MM * scale;
|
||||||
py = Math.max(margin, Math.min(py, pageHeightPx - barcodeH - margin));
|
py = Math.max(margin, Math.min(py, pageHeightPx - barcodeH - margin));
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ export default function MailImportWizard({ visible, onClose, email, attachments
|
|||||||
initialData.forEach(d => {
|
initialData.forEach(d => {
|
||||||
initialDates[d.virtualId] = mailDate;
|
initialDates[d.virtualId] = mailDate;
|
||||||
initialBarcodes[d.virtualId] = {
|
initialBarcodes[d.virtualId] = {
|
||||||
x: 6,
|
x: 7,
|
||||||
y: 6,
|
y: 7,
|
||||||
datum: mailDate.format('YYYY-MM-DD'),
|
datum: mailDate.format('YYYY-MM-DD'),
|
||||||
jahr: mailDate.format('YYYY'),
|
jahr: mailDate.format('YYYY'),
|
||||||
isNeu: true,
|
isNeu: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Table, Card, Typography, Button, Space, Tag, message, Input, Select } from 'antd';
|
import { Table, Card, Typography, Button, Space, Tag, message, Input, Select, Dropdown } from 'antd';
|
||||||
import { ReloadOutlined, DownloadOutlined, CheckCircleOutlined, SearchOutlined } from '@ant-design/icons';
|
import { ReloadOutlined, DownloadOutlined, CheckCircleOutlined, SearchOutlined, DownOutlined } from '@ant-design/icons';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { emailsApi, type EmailItem } from '../api/emails';
|
import { emailsApi, type EmailItem } from '../api/emails';
|
||||||
@@ -141,26 +141,51 @@ export default function MailpostfachPage() {
|
|||||||
Aktualisieren
|
Aktualisieren
|
||||||
</Button>
|
</Button>
|
||||||
{hasPermission(Permission.MANAGE_ALL) && (
|
{hasPermission(Permission.MANAGE_ALL) && (
|
||||||
<Button
|
<Dropdown.Button
|
||||||
icon={<CheckCircleOutlined />}
|
icon={<DownOutlined />}
|
||||||
|
loading={checking}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setChecking(true);
|
setChecking(true);
|
||||||
try {
|
try {
|
||||||
const result = await emailsApi.checkAttachments();
|
const result = await emailsApi.checkAttachments(false);
|
||||||
message.success(`${result.updatedCount} E-Mail(s) wurden als verarbeitet markiert.`);
|
const parts = [];
|
||||||
if (result.updatedCount > 0) {
|
if (result.updatedCount > 0) parts.push(`${result.updatedCount} E-Mail(s) als verarbeitet markiert`);
|
||||||
await loadData();
|
if (result.idsUpdated > 0) parts.push(`${result.idsUpdated} Paperless-ID(s) ergänzt`);
|
||||||
}
|
message.success(parts.length > 0 ? parts.join(', ') + '.' : 'Keine Änderungen.');
|
||||||
|
if (result.updatedCount > 0 || result.idsUpdated > 0) await loadData();
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Prüfung fehlgeschlagen.');
|
message.error('Prüfung fehlgeschlagen.');
|
||||||
} finally {
|
} finally {
|
||||||
setChecking(false);
|
setChecking(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={checking}
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'includeProcessed',
|
||||||
|
icon: <CheckCircleOutlined />,
|
||||||
|
label: 'Bereits verarbeitete Anhänge prüfen',
|
||||||
|
async onClick() {
|
||||||
|
setChecking(true);
|
||||||
|
try {
|
||||||
|
const result = await emailsApi.checkAttachments(true);
|
||||||
|
const parts = [];
|
||||||
|
if (result.updatedCount > 0) parts.push(`${result.updatedCount} E-Mail(s) aktualisiert`);
|
||||||
|
if (result.idsUpdated > 0) parts.push(`${result.idsUpdated} Paperless-ID(s) ergänzt`);
|
||||||
|
message.success(parts.length > 0 ? parts.join(', ') + '.' : 'Keine Änderungen.');
|
||||||
|
if (result.updatedCount > 0 || result.idsUpdated > 0) await loadData();
|
||||||
|
} catch {
|
||||||
|
message.error('Prüfung fehlgeschlagen.');
|
||||||
|
} finally {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Anhänge prüfen
|
<CheckCircleOutlined /> Anhänge prüfen
|
||||||
</Button>
|
</Dropdown.Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user