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:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -25,8 +25,14 @@ export class ImapFolderService {
|
||||
@Cron('0 3 * * *', { timeZone: 'Europe/Berlin' })
|
||||
async cleanupImportedEmails(): Promise<void> {
|
||||
if (!this.configService.get<string>('IMAP_HOST')) return;
|
||||
const importedFolder = this.configService.get<string>('IMAP_IMPORTED_FOLDER', 'importiert');
|
||||
const trashFolder = this.configService.get<string>('IMAP_TRASH_FOLDER', 'Trash');
|
||||
const importedFolder = this.configService.get<string>(
|
||||
'IMAP_IMPORTED_FOLDER',
|
||||
'importiert',
|
||||
);
|
||||
const trashFolder = this.configService.get<string>(
|
||||
'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<number> {
|
||||
if (!this.configService.get<string>('IMAP_HOST') || messageIds.length === 0) return 0;
|
||||
if (!this.configService.get<string>('IMAP_HOST') || messageIds.length === 0)
|
||||
return 0;
|
||||
|
||||
const importedFolder = this.configService.get<string>('IMAP_IMPORTED_FOLDER', 'importiert');
|
||||
const importedFolder = this.configService.get<string>(
|
||||
'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<void> {
|
||||
if (!this.configService.get<string>('IMAP_HOST')) return;
|
||||
|
||||
const importedFolder = this.configService.get<string>('IMAP_IMPORTED_FOLDER', 'importiert');
|
||||
const importedFolder = this.configService.get<string>(
|
||||
'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}`);
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ?? [];
|
||||
|
||||
Reference in New Issue
Block a user