diff --git a/paperless-backend/src/app.module.ts b/paperless-backend/src/app.module.ts
index 1af36eb..06faed6 100644
--- a/paperless-backend/src/app.module.ts
+++ b/paperless-backend/src/app.module.ts
@@ -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 {}
diff --git a/paperless-backend/src/daily-digest/daily-digest.controller.ts b/paperless-backend/src/daily-digest/daily-digest.controller.ts
new file mode 100644
index 0000000..684c8fe
--- /dev/null
+++ b/paperless-backend/src/daily-digest/daily-digest.controller.ts
@@ -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 };
+ }
+ }
+}
diff --git a/paperless-backend/src/daily-digest/daily-digest.module.ts b/paperless-backend/src/daily-digest/daily-digest.module.ts
new file mode 100644
index 0000000..01ee641
--- /dev/null
+++ b/paperless-backend/src/daily-digest/daily-digest.module.ts
@@ -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 {}
diff --git a/paperless-backend/src/daily-digest/daily-digest.service.ts b/paperless-backend/src/daily-digest/daily-digest.service.ts
new file mode 100644
index 0000000..a0d692c
--- /dev/null
+++ b/paperless-backend/src/daily-digest/daily-digest.service.ts
@@ -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 => `
+
+ | ${r.label} |
+ ${r.count} |
+
`,
+ )
+ .join('');
+
+ return `
+
+
+
+
+
+
+
+
+ Paperless Manager
+ Tagesübersicht – ${today}
+ |
+
+
+ |
+ Hier ist Ihre aktuelle Übersicht der offenen Vorgänge:
+
+
+
+ | Bereich |
+ Offen |
+
+
+ ${tableRows}
+
+ |
+
+
+ |
+
+ Diese E-Mail wird täglich automatisch von Paperless Manager versendet.
+ Sie können den Digest in den Benutzereinstellungen deaktivieren.
+
+ |
+
+
+ |
+
+
+`;
+}
+
+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.
+`;
+}
diff --git a/paperless-backend/src/database/entities/user-settings.entity.ts b/paperless-backend/src/database/entities/user-settings.entity.ts
index 2fec5b4..7e3c94f 100644
--- a/paperless-backend/src/database/entities/user-settings.entity.ts
+++ b/paperless-backend/src/database/entities/user-settings.entity.ts
@@ -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;
}
diff --git a/paperless-backend/src/stats/stats.controller.ts b/paperless-backend/src/stats/stats.controller.ts
index a8647ae..2074362 100644
--- a/paperless-backend/src/stats/stats.controller.ts
+++ b/paperless-backend/src/stats/stats.controller.ts
@@ -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,
- 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('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);
}
}
diff --git a/paperless-backend/src/stats/stats.module.ts b/paperless-backend/src/stats/stats.module.ts
index b4e4462..0fecb5e 100644
--- a/paperless-backend/src/stats/stats.module.ts
+++ b/paperless-backend/src/stats/stats.module.ts
@@ -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 {}
diff --git a/paperless-backend/src/stats/stats.service.ts b/paperless-backend/src/stats/stats.service.ts
new file mode 100644
index 0000000..9b792f8
--- /dev/null
+++ b/paperless-backend/src/stats/stats.service.ts
@@ -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,
+ private readonly inboxService: InboxService,
+ private readonly paperlessService: PaperlessService,
+ private readonly configService: ConfigService,
+ ) {}
+
+ async getDashboardCounts(preferredUsername?: string): Promise {
+ 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('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 };
+ }
+}
diff --git a/paperless-backend/src/user-settings/user-settings.controller.ts b/paperless-backend/src/user-settings/user-settings.controller.ts
index c2cb7ba..153cec5 100644
--- a/paperless-backend/src/user-settings/user-settings.controller.ts
+++ b/paperless-backend/src/user-settings/user-settings.controller.ts
@@ -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')
diff --git a/paperless-backend/src/user-settings/user-settings.service.ts b/paperless-backend/src/user-settings/user-settings.service.ts
index b11c08c..fb0b45e 100644
--- a/paperless-backend/src/user-settings/user-settings.service.ts
+++ b/paperless-backend/src/user-settings/user-settings.service.ts
@@ -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 {
- const entity = await this.repo.findOne({ where: { UserId: userId } });
+ async getSettings(userId: string, email?: string, preferredUsername?: string): Promise {
+ 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 {
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 {
+ 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('SMTP_FROM', 'paperless@localhost');
const defaultName = this.configService.get('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,
};
}
}
diff --git a/paperless-frontend/src/api/userSettings.ts b/paperless-frontend/src/api/userSettings.ts
index a0a919a..126c116 100644
--- a/paperless-frontend/src/api/userSettings.ts
+++ b/paperless-frontend/src/api/userSettings.ts
@@ -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('/api/user-settings/senders').then((r) => r.data),
+
+ sendDigestNow: () =>
+ api.post<{ ok: boolean; error?: string }>('/api/daily-digest/send-now').then((r) => r.data),
};
diff --git a/paperless-frontend/src/pages/UserSettingsPage.tsx b/paperless-frontend/src/pages/UserSettingsPage.tsx
index 5f253ae..8fc8ec8 100644
--- a/paperless-frontend/src/pages/UserSettingsPage.tsx
+++ b/paperless-frontend/src/pages/UserSettingsPage.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
export default function UserSettingsPage() {
return (
@@ -214,6 +279,11 @@ export default function UserSettingsPage() {
label: 'Etikettendruck',
children: ,
},
+ {
+ key: 'notifications',
+ label: 'Benachrichtigungen',
+ children: ,
+ },
]}
/>