feat: redesign daily digest email with card layout and timezone fix
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s

- Replace table layout with modern card-based design per dashboard area
- Add icon, color accent, badge and "Öffnen" link per card
- Show summary bar with total open items count
- Fix cron timezone to Europe/Berlin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 11:00:20 +02:00
parent 15e06bd60f
commit 2747b0046a
2 changed files with 126 additions and 57 deletions
+1 -1
View File
@@ -66,4 +66,4 @@ AGRARMONITOR_UPLOAD_CHECK_CRON=0 * * * * * # Upload-Check-Intervall (Standard:
# Basis-URL der App für klickbare Links in Digest-E-Mails (z.B. https://paperless.example.com)
# Leer lassen: E-Mails werden ohne Links versendet
APP_URL=
DAILY_DIGEST_CRON= # Standard: 0 7 * * * (täglich 07:00 Uhr)
DAILY_DIGEST_CRON= # Standard: 0 7 * * * (täglich 07:00 Uhr Europe/Berlin)
@@ -35,7 +35,7 @@ export class DailyDigestService {
this.logger.log(`Manueller Digest gesendet an ${email} (userId: ${userId})`);
}
@Cron(process.env.DAILY_DIGEST_CRON || '0 7 * * *')
@Cron(process.env.DAILY_DIGEST_CRON || '0 7 * * *', { timeZone: 'Europe/Berlin' })
async sendDailyDigests() {
this.logger.log('Starte täglichen E-Mail-Digest...');
@@ -66,69 +66,138 @@ export class DailyDigestService {
}
}
function countColor(n: number): string {
if (n === 0) return '#16a34a';
if (n <= 5) return '#d97706';
return '#dc2626';
}
function buildDigestHtml(counts: DashboardCounts, today: string, appUrl: string, agrarmonitorBaseUrl: string): string {
const rows: { label: string; count: number; url: string }[] = [
{ label: 'Eingangsbox (Scanner)', count: counts.inbox, url: appUrl ? `${appUrl}/inbox` : '' },
{ label: 'Posteingang', count: counts.posteingang, url: appUrl ? `${appUrl}/posteingang` : '' },
{ label: 'Manuell bearbeiten', count: counts.manuell, url: appUrl ? `${appUrl}/manuell` : '' },
{ label: 'Mailpostfach', count: counts.mailpostfach, url: appUrl ? `${appUrl}/mailpostfach` : '' },
{ label: 'In Agrarmonitor', count: counts.agrarmonitor, url: agrarmonitorBaseUrl ? `${agrarmonitorBaseUrl}/dateien/eingang#dateien` : '' },
const tiles = [
{
key: 'inbox' as const,
title: 'Eingangsbox',
description: 'Neu eingegangene Scans, Uploads und E-Mail-Anhänge prüfen.',
icon: '📥',
accent: '#1677ff',
accentSoft: '#e6f0ff',
url: appUrl ? `${appUrl}/inbox` : '',
count: counts.inbox,
},
{
key: 'posteingang' as const,
title: 'Posteingang',
description: 'Verarbeitete Dokumente sichten und an Paperless übergeben.',
icon: '📄',
accent: '#13c2c2',
accentSoft: '#e6fffb',
url: appUrl ? `${appUrl}/posteingang` : '',
count: counts.posteingang,
},
{
key: 'manuell' as const,
title: 'Manuell bearbeiten',
description: 'Dokumente mit fehlender Erkennung manuell ergänzen.',
icon: '✏️',
accent: '#fa8c16',
accentSoft: '#fff7e6',
url: appUrl ? `${appUrl}/manuell` : '',
count: counts.manuell,
},
{
key: 'mailpostfach' as const,
title: 'Mailpostfach',
description: 'Eingehende E-Mails mit Anhängen durchsuchen und zuordnen.',
icon: '📬',
accent: '#722ed1',
accentSoft: '#f9f0ff',
url: appUrl ? `${appUrl}/mailpostfach` : '',
count: counts.mailpostfach,
},
{
key: 'agrarmonitor' as const,
title: 'In Agrarmonitor',
description: 'Dokumente im Agrarmonitor-Eingang anzeigen und verwalten.',
icon: '🌱',
accent: '#52c41a',
accentSoft: '#f6ffed',
url: agrarmonitorBaseUrl ? `${agrarmonitorBaseUrl}/dateien/eingang#dateien` : '',
count: counts.agrarmonitor,
},
];
const tableRows = rows
.map(r => {
const labelCell = r.url
? `<a href="${r.url}" style="color:#1d4ed8;text-decoration:none;">${r.label}</a>`
: r.label;
const totalOpen = tiles.reduce((sum, t) => sum + t.count, 0);
const summaryText = totalOpen > 0
? `Sie haben <strong>${totalOpen} offene Vorgänge</strong> in Ihren Bereichen.`
: 'Alle Bereiche sind auf dem aktuellen Stand. ✓';
const cards = tiles.map(t => {
const badge = t.count > 0
? `<td align="right" valign="top"><span style="display:inline-block;background:${t.accent};color:#ffffff;font-family:sans-serif;font-size:12px;font-weight:700;padding:2px 9px;border-radius:10px;">${t.count}</span></td>`
: '<td></td>';
const footerCount = t.count > 0
? `<span style="font-family:sans-serif;font-size:13px;color:${t.accent};font-weight:600;">${t.count} offen</span>`
: `<span style="font-family:sans-serif;font-size:13px;color:#9ca3af;">Keine offenen Vorgänge</span>`;
const openLink = t.url
? `<a href="${t.url}" style="font-family:sans-serif;font-size:13px;color:${t.accent};text-decoration:none;font-weight:500;">Öffnen </a>`
: '';
return `
<tr><td style="padding:0 0 16px 0;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#ffffff;border:1px solid #e5e7eb;border-top:3px solid ${t.accent};border-radius:8px;">
<tr><td style="padding:20px 20px 16px;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="padding:10px 16px;border-bottom:1px solid #e5e7eb;font-family:sans-serif;font-size:14px;color:#374151;">${labelCell}</td>
<td style="padding:10px 16px;border-bottom:1px solid #e5e7eb;text-align:center;font-family:sans-serif;font-size:16px;font-weight:bold;color:${countColor(r.count)};">${r.url ? `<a href="${r.url}" style="color:${countColor(r.count)};text-decoration:none;">${r.count}</a>` : r.count}</td>
</tr>`;
})
.join('');
<td valign="middle">
<span style="display:inline-block;width:40px;height:40px;line-height:40px;text-align:center;background:${t.accentSoft};border-radius:10px;font-size:20px;">${t.icon}</span>
</td>
${badge}
</tr>
</table>
<p style="margin:14px 0 4px;font-family:sans-serif;font-size:15px;font-weight:700;color:#111827;">${t.title}</p>
<p style="margin:0;font-family:sans-serif;font-size:13px;color:#6b7280;line-height:1.5;">${t.description}</p>
</td></tr>
<tr><td style="padding:12px 20px;border-top:1px solid #f3f4f6;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td>${footerCount}</td>
<td align="right">${openLink}</td>
</tr>
</table>
</td></tr>
</table>
</td></tr>`;
}).join('');
return `<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="margin:0;padding:0;background:#f9fafb;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f9fafb;padding:32px 0;">
<body style="margin:0;padding:0;background:#f3f4f6;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6;padding:32px 0;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.1);">
<tr>
<td style="background:#1d4ed8;padding:24px 32px;">
<h1 style="margin:0;font-family:sans-serif;font-size:20px;color:#ffffff;font-weight:600;">Paperless Manager</h1>
<p style="margin:4px 0 0;font-family:sans-serif;font-size:14px;color:#bfdbfe;">Tagesübersicht ${today}</p>
</td>
</tr>
<tr>
<td style="padding:24px 32px 8px;">
<p style="margin:0 0 16px;font-family:sans-serif;font-size:14px;color:#6b7280;">Hier ist Ihre aktuelle Übersicht der offenen Vorgänge:</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:6px;overflow:hidden;">
<thead>
<tr style="background:#f3f4f6;">
<th style="padding:10px 16px;text-align:left;font-family:sans-serif;font-size:12px;color:#6b7280;text-transform:uppercase;letter-spacing:0.05em;">Bereich</th>
<th style="padding:10px 16px;text-align:center;font-family:sans-serif;font-size:12px;color:#6b7280;text-transform:uppercase;letter-spacing:0.05em;">Offen</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
<table width="560" cellpadding="0" cellspacing="0">
<!-- Header -->
<tr><td style="background:#1677ff;padding:28px 32px;border-radius:8px 8px 0 0;">
<p style="margin:0;font-family:sans-serif;font-size:12px;color:#bfdbfe;text-transform:uppercase;letter-spacing:0.08em;">Paperless Manager</p>
<h1 style="margin:6px 0 0;font-family:sans-serif;font-size:22px;font-weight:700;color:#ffffff;">Tagesübersicht</h1>
<p style="margin:4px 0 0;font-family:sans-serif;font-size:14px;color:#bfdbfe;">${today}</p>
</td></tr>
<!-- Summary bar -->
<tr><td style="background:#eff6ff;padding:14px 32px;border-bottom:1px solid #dbeafe;">
<p style="margin:0;font-family:sans-serif;font-size:14px;color:#1e40af;">${summaryText}</p>
</td></tr>
<!-- Cards -->
<tr><td style="padding:24px 32px 8px;">
<table width="100%" cellpadding="0" cellspacing="0">
${cards}
</table>
</td>
</tr>
<tr>
<td style="padding:16px 32px 32px;">
<p style="margin:0;font-family:sans-serif;font-size:12px;color:#9ca3af;">
</td></tr>
<!-- Footer -->
<tr><td style="padding:0 32px 32px;">
<p style="margin:0;font-family:sans-serif;font-size:12px;color:#9ca3af;line-height:1.6;">
Diese E-Mail wird täglich automatisch von Paperless Manager versendet.<br>
Sie können den Digest in den Benutzereinstellungen deaktivieren.
</p>
</td>
</tr>
</td></tr>
</table>
</td></tr>
</table>