feat: add daily digest email notification module
Build and Push Multi-Platform Images / build-and-push (push) Successful in 50s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 50s
- New DailyDigestModule with scheduled summary email for open dashboard items - Extract StatsService from StatsController for reuse in digest - Add DailyDigestEnabled, UserEmail, UserPreferredUsername to UserSettings entity - Sync email/username from OIDC token on each get/update call - Add dailyDigestEnabled to UserSettingsDto and update API - Notifications tab in UserSettingsPage with enable toggle and "Jetzt senden" button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import { UserSettingsModule } from './user-settings/user-settings.module';
|
||||
import { LabelPrintAgentModule } from './label-print-agent/label-print-agent.module';
|
||||
import { AgrarmonitorModule } from './agrarmonitor/agrarmonitor.module';
|
||||
import { FreigabeModule } from './freigabe/freigabe.module';
|
||||
import { DailyDigestModule } from './daily-digest/daily-digest.module';
|
||||
import * as path from 'path';
|
||||
|
||||
@Module({
|
||||
@@ -51,6 +52,7 @@ import * as path from 'path';
|
||||
LabelPrintAgentModule,
|
||||
AgrarmonitorModule,
|
||||
FreigabeModule,
|
||||
DailyDigestModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Controller, Post, Request, HttpCode } from '@nestjs/common';
|
||||
import { DailyDigestService } from './daily-digest.service';
|
||||
|
||||
@Controller('api/daily-digest')
|
||||
export class DailyDigestController {
|
||||
constructor(private readonly dailyDigestService: DailyDigestService) {}
|
||||
|
||||
@Post('send-now')
|
||||
@HttpCode(200)
|
||||
async sendNow(@Request() req: any) {
|
||||
const { userId, email, preferredUsername } = req.user;
|
||||
if (!email) {
|
||||
return { ok: false, error: 'Keine E-Mail-Adresse im Benutzerprofil gefunden.' };
|
||||
}
|
||||
try {
|
||||
await this.dailyDigestService.sendDigestForUser(userId, email, preferredUsername);
|
||||
return { ok: true };
|
||||
} catch (err: any) {
|
||||
return { ok: false, error: err.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DailyDigestService } from './daily-digest.service';
|
||||
import { DailyDigestController } from './daily-digest.controller';
|
||||
import { StatsModule } from '../stats/stats.module';
|
||||
import { UserSettingsModule } from '../user-settings/user-settings.module';
|
||||
import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
||||
|
||||
@Module({
|
||||
imports: [StatsModule, UserSettingsModule, PostprocessingModule],
|
||||
providers: [DailyDigestService],
|
||||
controllers: [DailyDigestController],
|
||||
})
|
||||
export class DailyDigestModule {}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { StatsService, DashboardCounts } from '../stats/stats.service';
|
||||
import { UserSettingsService } from '../user-settings/user-settings.service';
|
||||
import { MailService } from '../postprocessing/mail.service';
|
||||
|
||||
@Injectable()
|
||||
export class DailyDigestService {
|
||||
private readonly logger = new Logger(DailyDigestService.name);
|
||||
|
||||
constructor(
|
||||
private readonly statsService: StatsService,
|
||||
private readonly userSettingsService: UserSettingsService,
|
||||
private readonly mailService: MailService,
|
||||
) {}
|
||||
|
||||
async sendDigestForUser(userId: string, email: string, preferredUsername?: string) {
|
||||
const today = new Date().toLocaleDateString('de-DE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const counts = await this.statsService.getDashboardCounts(preferredUsername);
|
||||
const html = buildDigestHtml(counts, today);
|
||||
const plainText = buildDigestPlainText(counts, today);
|
||||
await this.mailService.sendMail({
|
||||
to: email,
|
||||
subject: `Paperless Manager – Tagesübersicht ${today}`,
|
||||
body: plainText,
|
||||
html,
|
||||
});
|
||||
this.logger.log(`Manueller Digest gesendet an ${email} (userId: ${userId})`);
|
||||
}
|
||||
|
||||
@Cron(process.env.DAILY_DIGEST_CRON || '0 7 * * *')
|
||||
async sendDailyDigests() {
|
||||
this.logger.log('Starte täglichen E-Mail-Digest...');
|
||||
|
||||
const subscribers = await this.userSettingsService.findAllDigestSubscribers();
|
||||
if (subscribers.length === 0) {
|
||||
this.logger.log('Keine Abonnenten für den täglichen Digest.');
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date().toLocaleDateString('de-DE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
|
||||
for (const sub of subscribers) {
|
||||
try {
|
||||
const counts = await this.statsService.getDashboardCounts(sub.UserPreferredUsername ?? undefined);
|
||||
const html = buildDigestHtml(counts, today);
|
||||
const plainText = buildDigestPlainText(counts, today);
|
||||
await this.mailService.sendMail({
|
||||
to: sub.UserEmail!,
|
||||
subject: `Paperless Manager – Tagesübersicht ${today}`,
|
||||
body: plainText,
|
||||
html,
|
||||
});
|
||||
this.logger.log(`Digest gesendet an ${sub.UserEmail}`);
|
||||
} catch (err) {
|
||||
this.logger.error(`Fehler beim Senden des Digests an ${sub.UserEmail}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function countColor(n: number): string {
|
||||
if (n === 0) return '#16a34a';
|
||||
if (n <= 5) return '#d97706';
|
||||
return '#dc2626';
|
||||
}
|
||||
|
||||
function buildDigestHtml(counts: DashboardCounts, today: string): string {
|
||||
const rows: { label: string; count: number }[] = [
|
||||
{ label: 'Eingangsbox (Scanner)', count: counts.inbox },
|
||||
{ label: 'Posteingang', count: counts.posteingang },
|
||||
{ label: 'Manuell bearbeiten', count: counts.manuell },
|
||||
{ label: 'Mailpostfach', count: counts.mailpostfach },
|
||||
{ label: 'In Agrarmonitor', count: counts.agrarmonitor },
|
||||
];
|
||||
|
||||
const tableRows = rows
|
||||
.map(
|
||||
r => `
|
||||
<tr>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e5e7eb;font-family:sans-serif;font-size:14px;color:#374151;">${r.label}</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.count}</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;">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:16px 32px 32px;">
|
||||
<p style="margin:0;font-family:sans-serif;font-size:12px;color:#9ca3af;">
|
||||
Diese E-Mail wird täglich automatisch von Paperless Manager versendet.<br>
|
||||
Sie können den Digest in den Benutzereinstellungen deaktivieren.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function buildDigestPlainText(counts: DashboardCounts, today: string): string {
|
||||
return `Paperless Manager – Tagesübersicht ${today}
|
||||
|
||||
Offene Vorgänge:
|
||||
Eingangsbox (Scanner): ${counts.inbox}
|
||||
Posteingang: ${counts.posteingang}
|
||||
Manuell bearbeiten: ${counts.manuell}
|
||||
Mailpostfach: ${counts.mailpostfach}
|
||||
In Agrarmonitor: ${counts.agrarmonitor}
|
||||
|
||||
Diese E-Mail wird täglich automatisch von Paperless Manager versendet.
|
||||
`;
|
||||
}
|
||||
@@ -34,4 +34,13 @@ export class UserSettings {
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
EmailRecipientHistory!: string[] | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
DailyDigestEnabled!: boolean;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
UserEmail!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
UserPreferredUsername!: string | null;
|
||||
}
|
||||
|
||||
@@ -1,90 +1,12 @@
|
||||
import { Controller, Get, Request, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Email } from '../database/entities/email.entity';
|
||||
import { InboxService } from '../inbox/inbox.service';
|
||||
import { PaperlessService } from '../paperless/paperless.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Controller, Get, Request } from '@nestjs/common';
|
||||
import { StatsService } from './stats.service';
|
||||
|
||||
@Controller('api/stats')
|
||||
export class StatsController {
|
||||
private readonly logger = new Logger(StatsController.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Email) private readonly emailRepo: Repository<Email>,
|
||||
private readonly inboxService: InboxService,
|
||||
private readonly paperlessService: PaperlessService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
constructor(private readonly statsService: StatsService) {}
|
||||
|
||||
@Get('counts')
|
||||
async getCounts(@Request() req: any) {
|
||||
let inboxCount = 0;
|
||||
let posteingangCount = 0;
|
||||
let manuellCount = 0;
|
||||
let mailpostfachCount = 0;
|
||||
let agrarmonitorCount = 0;
|
||||
|
||||
// 1. Eingangsbox (Dateien aus /mnt/scans)
|
||||
if (req.user) {
|
||||
try {
|
||||
const files = await this.inboxService.listFiles(req.user.preferredUsername ?? null);
|
||||
inboxCount = files.length;
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der Inbox-Stats: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Posteingang (Paperless tag 1)
|
||||
try {
|
||||
const response1 = await this.paperlessService.getDocuments({
|
||||
page: 1,
|
||||
page_size: 1,
|
||||
tags__id__all: 1,
|
||||
});
|
||||
posteingangCount = response1.count || 0;
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der Posteingang-Stats: ' + err.message);
|
||||
}
|
||||
|
||||
// 3. Manuell bearbeiten (Paperless tag errorTag)
|
||||
try {
|
||||
const errorTag = this.configService.get<number>('MANUELL_BEARBEITEN_TAG', 6);
|
||||
const response2 = await this.paperlessService.getDocuments({
|
||||
page: 1,
|
||||
page_size: 1,
|
||||
tags__id__all: errorTag,
|
||||
});
|
||||
manuellCount = response2.count || 0;
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der Manuell-Stats: ' + err.message);
|
||||
}
|
||||
|
||||
// 4. Mailpostfach (Emails with status 0)
|
||||
try {
|
||||
mailpostfachCount = await this.emailRepo.count({ where: { Status: 0 } });
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der E-Mail-Stats: ' + err.message);
|
||||
}
|
||||
|
||||
// 5. Agrarmonitor (Paperless tag 3)
|
||||
try {
|
||||
const agrarmonitorResponse = await this.paperlessService.getDocuments({
|
||||
page: 1,
|
||||
page_size: 1,
|
||||
tags__id__all: 3,
|
||||
});
|
||||
agrarmonitorCount = agrarmonitorResponse.count || 0;
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der Agrarmonitor-Stats: ' + err.message);
|
||||
}
|
||||
|
||||
return {
|
||||
inbox: inboxCount,
|
||||
posteingang: posteingangCount,
|
||||
manuell: manuellCount,
|
||||
mailpostfach: mailpostfachCount,
|
||||
agrarmonitor: agrarmonitorCount,
|
||||
};
|
||||
return this.statsService.getDashboardCounts(req.user?.preferredUsername);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { StatsController } from './stats.controller';
|
||||
import { StatsService } from './stats.service';
|
||||
import { Email } from '../database/entities/email.entity';
|
||||
import { InboxModule } from '../inbox/inbox.module';
|
||||
import { PaperlessModule } from '../paperless/paperless.module';
|
||||
@@ -12,5 +13,7 @@ import { PaperlessModule } from '../paperless/paperless.module';
|
||||
PaperlessModule,
|
||||
],
|
||||
controllers: [StatsController],
|
||||
providers: [StatsService],
|
||||
exports: [StatsService],
|
||||
})
|
||||
export class StatsModule {}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Email } from '../database/entities/email.entity';
|
||||
import { InboxService } from '../inbox/inbox.service';
|
||||
import { PaperlessService } from '../paperless/paperless.service';
|
||||
|
||||
export interface DashboardCounts {
|
||||
inbox: number;
|
||||
posteingang: number;
|
||||
manuell: number;
|
||||
mailpostfach: number;
|
||||
agrarmonitor: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class StatsService {
|
||||
private readonly logger = new Logger(StatsService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Email) private readonly emailRepo: Repository<Email>,
|
||||
private readonly inboxService: InboxService,
|
||||
private readonly paperlessService: PaperlessService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async getDashboardCounts(preferredUsername?: string): Promise<DashboardCounts> {
|
||||
let inboxCount = 0;
|
||||
let posteingangCount = 0;
|
||||
let manuellCount = 0;
|
||||
let mailpostfachCount = 0;
|
||||
let agrarmonitorCount = 0;
|
||||
|
||||
try {
|
||||
const files = await this.inboxService.listFiles(preferredUsername ?? null);
|
||||
inboxCount = files.length;
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der Inbox-Stats: ' + err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.paperlessService.getDocuments({ page: 1, page_size: 1, tags__id__all: 1 });
|
||||
posteingangCount = response.count || 0;
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der Posteingang-Stats: ' + err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const errorTag = this.configService.get<number>('MANUELL_BEARBEITEN_TAG', 6);
|
||||
const response = await this.paperlessService.getDocuments({ page: 1, page_size: 1, tags__id__all: errorTag });
|
||||
manuellCount = response.count || 0;
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der Manuell-Stats: ' + err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
mailpostfachCount = await this.emailRepo.count({ where: { Status: 0 } });
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der E-Mail-Stats: ' + err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.paperlessService.getDocuments({ page: 1, page_size: 1, tags__id__all: 3 });
|
||||
agrarmonitorCount = response.count || 0;
|
||||
} catch (err) {
|
||||
this.logger.error('Fehler beim Abrufen der Agrarmonitor-Stats: ' + err.message);
|
||||
}
|
||||
|
||||
return { inbox: inboxCount, posteingang: posteingangCount, manuell: manuellCount, mailpostfach: mailpostfachCount, agrarmonitor: agrarmonitorCount };
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ export class UserSettingsController {
|
||||
|
||||
@Get()
|
||||
async getSettings(@Request() req: any) {
|
||||
return this.userSettingsService.getSettings(req.user.userId);
|
||||
return this.userSettingsService.getSettings(req.user.userId, req.user.email, req.user.preferredUsername);
|
||||
}
|
||||
|
||||
@Put()
|
||||
async updateSettings(@Request() req: any, @Body() body: any) {
|
||||
return this.userSettingsService.updateSettings(req.user.userId, body);
|
||||
return this.userSettingsService.updateSettings(req.user.userId, body, req.user.email, req.user.preferredUsername);
|
||||
}
|
||||
|
||||
@Get('senders')
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface UserSettingsDto {
|
||||
mailSignatureHtml: string | null;
|
||||
defaultLabelTemplateId: number | null;
|
||||
emailRecipientHistory: string[] | null;
|
||||
dailyDigestEnabled: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -64,8 +65,16 @@ export class UserSettingsService {
|
||||
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
|
||||
}
|
||||
|
||||
async getSettings(userId: string): Promise<UserSettingsDto> {
|
||||
const entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||
async getSettings(userId: string, email?: string, preferredUsername?: string): Promise<UserSettingsDto> {
|
||||
let entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||
if (email || preferredUsername) {
|
||||
if (!entity) {
|
||||
entity = this.repo.create({ UserId: userId });
|
||||
}
|
||||
if (email) entity.UserEmail = email;
|
||||
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
|
||||
await this.repo.save(entity);
|
||||
}
|
||||
return this.toDto(entity);
|
||||
}
|
||||
|
||||
@@ -82,7 +91,10 @@ export class UserSettingsService {
|
||||
mailSignatureHtml?: string | null;
|
||||
defaultLabelTemplateId?: number | null;
|
||||
emailRecipientHistory?: string[] | null;
|
||||
dailyDigestEnabled?: boolean;
|
||||
},
|
||||
email?: string,
|
||||
preferredUsername?: string,
|
||||
): Promise<UserSettingsDto> {
|
||||
let entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||
if (!entity) {
|
||||
@@ -101,6 +113,9 @@ export class UserSettingsService {
|
||||
if (data.mailSignatureHtml !== undefined) entity.MailSignatureHtml = data.mailSignatureHtml;
|
||||
if (data.defaultLabelTemplateId !== undefined) entity.DefaultLabelTemplateId = data.defaultLabelTemplateId;
|
||||
if (data.emailRecipientHistory !== undefined) entity.EmailRecipientHistory = data.emailRecipientHistory;
|
||||
if (data.dailyDigestEnabled !== undefined) entity.DailyDigestEnabled = data.dailyDigestEnabled;
|
||||
if (email) entity.UserEmail = email;
|
||||
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
|
||||
|
||||
await this.repo.save(entity);
|
||||
return this.toDto(entity);
|
||||
@@ -144,6 +159,12 @@ export class UserSettingsService {
|
||||
};
|
||||
}
|
||||
|
||||
async findAllDigestSubscribers(): Promise<UserSettings[]> {
|
||||
return this.repo.find({
|
||||
where: { DailyDigestEnabled: true },
|
||||
}).then(rows => rows.filter(r => !!r.UserEmail));
|
||||
}
|
||||
|
||||
async getAvailableSenders(userId: string): Promise<{ id: string; label: string }[]> {
|
||||
const defaultEmail = this.configService.get<string>('SMTP_FROM', 'paperless@localhost');
|
||||
const defaultName = this.configService.get<string>('SMTP_FROM_NAME', '');
|
||||
@@ -172,6 +193,7 @@ export class UserSettingsService {
|
||||
mailSignatureHtml: entity?.MailSignatureHtml ?? null,
|
||||
defaultLabelTemplateId: entity?.DefaultLabelTemplateId ?? null,
|
||||
emailRecipientHistory: entity?.EmailRecipientHistory ?? null,
|
||||
dailyDigestEnabled: entity?.DailyDigestEnabled ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface UserSettingsData {
|
||||
mailSignatureHtml: string | null;
|
||||
defaultLabelTemplateId: number | null;
|
||||
emailRecipientHistory: string[] | null;
|
||||
dailyDigestEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface SenderOption {
|
||||
@@ -31,4 +32,7 @@ export const userSettingsApi = {
|
||||
|
||||
getSenders: () =>
|
||||
api.get<SenderOption[]>('/api/user-settings/senders').then((r) => r.data),
|
||||
|
||||
sendDigestNow: () =>
|
||||
api.post<{ ok: boolean; error?: string }>('/api/daily-digest/send-now').then((r) => r.data),
|
||||
};
|
||||
|
||||
@@ -197,6 +197,71 @@ function MailSettingsTab() {
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationsTab() {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
userSettingsApi.get()
|
||||
.then((data) => setEnabled(data.dailyDigestEnabled ?? false))
|
||||
.catch(() => message.error('Einstellungen konnten nicht geladen werden'))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await userSettingsApi.update({ dailyDigestEnabled: enabled });
|
||||
message.success('Einstellungen gespeichert');
|
||||
} catch {
|
||||
message.error('Speichern fehlgeschlagen');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendNow = async () => {
|
||||
setSending(true);
|
||||
try {
|
||||
const result = await userSettingsApi.sendDigestNow();
|
||||
if (result.ok) {
|
||||
message.success('Tagesübersicht wurde gesendet');
|
||||
} else {
|
||||
message.error(result.error ?? 'Senden fehlgeschlagen');
|
||||
}
|
||||
} catch {
|
||||
message.error('Senden fehlgeschlagen');
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
return (
|
||||
<Form layout="vertical" style={{ maxWidth: 600 }}>
|
||||
<Form.Item
|
||||
label="Tägliche E-Mail-Zusammenfassung"
|
||||
extra="Sie erhalten jeden Morgen eine E-Mail mit der Übersicht aller offenen Vorgänge aus dem Dashboard."
|
||||
>
|
||||
<Switch checked={enabled} onChange={setEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" loading={saving} onClick={handleSave}>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button loading={sending} onClick={handleSendNow}>
|
||||
Jetzt senden
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserSettingsPage() {
|
||||
return (
|
||||
<div>
|
||||
@@ -214,6 +279,11 @@ export default function UserSettingsPage() {
|
||||
label: 'Etikettendruck',
|
||||
children: <LabelSettingsTab />,
|
||||
},
|
||||
{
|
||||
key: 'notifications',
|
||||
label: 'Benachrichtigungen',
|
||||
children: <NotificationsTab />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user