feat: redesign daily digest email with card layout and timezone fix
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s
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:
+1
-1
@@ -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)
|
# 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
|
# Leer lassen: E-Mails werden ohne Links versendet
|
||||||
APP_URL=
|
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})`);
|
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() {
|
async sendDailyDigests() {
|
||||||
this.logger.log('Starte täglichen E-Mail-Digest...');
|
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 {
|
function buildDigestHtml(counts: DashboardCounts, today: string, appUrl: string, agrarmonitorBaseUrl: string): string {
|
||||||
const rows: { label: string; count: number; url: string }[] = [
|
const tiles = [
|
||||||
{ label: 'Eingangsbox (Scanner)', count: counts.inbox, url: appUrl ? `${appUrl}/inbox` : '' },
|
{
|
||||||
{ label: 'Posteingang', count: counts.posteingang, url: appUrl ? `${appUrl}/posteingang` : '' },
|
key: 'inbox' as const,
|
||||||
{ label: 'Manuell bearbeiten', count: counts.manuell, url: appUrl ? `${appUrl}/manuell` : '' },
|
title: 'Eingangsbox',
|
||||||
{ label: 'Mailpostfach', count: counts.mailpostfach, url: appUrl ? `${appUrl}/mailpostfach` : '' },
|
description: 'Neu eingegangene Scans, Uploads und E-Mail-Anhänge prüfen.',
|
||||||
{ label: 'In Agrarmonitor', count: counts.agrarmonitor, url: agrarmonitorBaseUrl ? `${agrarmonitorBaseUrl}/dateien/eingang#dateien` : '' },
|
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
|
const totalOpen = tiles.reduce((sum, t) => sum + t.count, 0);
|
||||||
.map(r => {
|
const summaryText = totalOpen > 0
|
||||||
const labelCell = r.url
|
? `Sie haben <strong>${totalOpen} offene Vorgänge</strong> in Ihren Bereichen.`
|
||||||
? `<a href="${r.url}" style="color:#1d4ed8;text-decoration:none;">${r.label}</a>`
|
: 'Alle Bereiche sind auf dem aktuellen Stand. ✓';
|
||||||
: r.label;
|
|
||||||
return `
|
const cards = tiles.map(t => {
|
||||||
<tr>
|
const badge = t.count > 0
|
||||||
<td style="padding:10px 16px;border-bottom:1px solid #e5e7eb;font-family:sans-serif;font-size:14px;color:#374151;">${labelCell}</td>
|
? `<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 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>
|
: '<td></td>';
|
||||||
</tr>`;
|
const footerCount = t.count > 0
|
||||||
})
|
? `<span style="font-family:sans-serif;font-size:13px;color:${t.accent};font-weight:600;">${t.count} offen</span>`
|
||||||
.join('');
|
: `<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 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>
|
return `<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
||||||
<body style="margin:0;padding:0;background:#f9fafb;">
|
<body style="margin:0;padding:0;background:#f3f4f6;">
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f9fafb;padding:32px 0;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6;padding:32px 0;">
|
||||||
<tr><td align="center">
|
<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);">
|
<table width="560" cellpadding="0" cellspacing="0">
|
||||||
<tr>
|
|
||||||
<td style="background:#1d4ed8;padding:24px 32px;">
|
<!-- Header -->
|
||||||
<h1 style="margin:0;font-family:sans-serif;font-size:20px;color:#ffffff;font-weight:600;">Paperless Manager</h1>
|
<tr><td style="background:#1677ff;padding:28px 32px;border-radius:8px 8px 0 0;">
|
||||||
<p style="margin:4px 0 0;font-family:sans-serif;font-size:14px;color:#bfdbfe;">Tagesübersicht – ${today}</p>
|
<p style="margin:0;font-family:sans-serif;font-size:12px;color:#bfdbfe;text-transform:uppercase;letter-spacing:0.08em;">Paperless Manager</p>
|
||||||
</td>
|
<h1 style="margin:6px 0 0;font-family:sans-serif;font-size:22px;font-weight:700;color:#ffffff;">Tagesübersicht</h1>
|
||||||
</tr>
|
<p style="margin:4px 0 0;font-family:sans-serif;font-size:14px;color:#bfdbfe;">${today}</p>
|
||||||
<tr>
|
</td></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>
|
<!-- Summary bar -->
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:6px;overflow:hidden;">
|
<tr><td style="background:#eff6ff;padding:14px 32px;border-bottom:1px solid #dbeafe;">
|
||||||
<thead>
|
<p style="margin:0;font-family:sans-serif;font-size:14px;color:#1e40af;">${summaryText}</p>
|
||||||
<tr style="background:#f3f4f6;">
|
</td></tr>
|
||||||
<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>
|
<!-- Cards -->
|
||||||
</tr>
|
<tr><td style="padding:24px 32px 8px;">
|
||||||
</thead>
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
<tbody>${tableRows}</tbody>
|
${cards}
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td></tr>
|
||||||
</tr>
|
|
||||||
<tr>
|
<!-- Footer -->
|
||||||
<td style="padding:16px 32px 32px;">
|
<tr><td style="padding:0 32px 32px;">
|
||||||
<p style="margin:0;font-family:sans-serif;font-size:12px;color:#9ca3af;">
|
<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>
|
Diese E-Mail wird täglich automatisch von Paperless Manager versendet.<br>
|
||||||
Sie können den Digest in den Benutzereinstellungen deaktivieren.
|
Sie können den Digest in den Benutzereinstellungen deaktivieren.
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td></tr>
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user