feat: filter digest tiles by user permissions and add import progress status
Build and Push Multi-Platform Images / build-and-push (push) Successful in 42s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 42s
- Store UserGroups from OIDC in UserSettings entity, sync on each request - Filter daily digest tiles based on user's permission groups - Add in-memory job status tracking to EmailImportService - Poll import job status in MailImportWizard and show progress in Spin tip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,12 +8,12 @@ export class DailyDigestController {
|
|||||||
@Post('send-now')
|
@Post('send-now')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
async sendNow(@Request() req: any) {
|
async sendNow(@Request() req: any) {
|
||||||
const { userId, email, preferredUsername } = req.user;
|
const { userId, email, preferredUsername, groups } = req.user;
|
||||||
if (!email) {
|
if (!email) {
|
||||||
return { ok: false, error: 'Keine E-Mail-Adresse im Benutzerprofil gefunden.' };
|
return { ok: false, error: 'Keine E-Mail-Adresse im Benutzerprofil gefunden.' };
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.dailyDigestService.sendDigestForUser(userId, email, preferredUsername);
|
await this.dailyDigestService.sendDigestForUser(userId, email, preferredUsername, groups);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return { ok: false, error: err.message };
|
return { ok: false, error: err.message };
|
||||||
|
|||||||
@@ -4,6 +4,30 @@ import { Cron } from '@nestjs/schedule';
|
|||||||
import { StatsService, DashboardCounts } from '../stats/stats.service';
|
import { StatsService, DashboardCounts } from '../stats/stats.service';
|
||||||
import { UserSettingsService } from '../user-settings/user-settings.service';
|
import { UserSettingsService } from '../user-settings/user-settings.service';
|
||||||
import { MailService } from '../postprocessing/mail.service';
|
import { MailService } from '../postprocessing/mail.service';
|
||||||
|
import { mapGroupsToPermissions, Permission } from '../auth/permissions.enum';
|
||||||
|
|
||||||
|
interface DigestTile {
|
||||||
|
key: keyof DashboardCounts;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
accent: string;
|
||||||
|
accentSoft: string;
|
||||||
|
permission: Permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIGEST_TILES: DigestTile[] = [
|
||||||
|
{ key: 'inbox', title: 'Eingangsbox', description: 'Neu eingegangene Scans, Uploads und E-Mail-Anhänge prüfen.', icon: '📥', accent: '#1677ff', accentSoft: '#e6f0ff', permission: Permission.VIEW_SCANNER },
|
||||||
|
{ key: 'posteingang', title: 'Posteingang', description: 'Verarbeitete Dokumente sichten und an Paperless übergeben.', icon: '📄', accent: '#13c2c2', accentSoft: '#e6fffb', permission: Permission.VIEW_INBOX },
|
||||||
|
{ key: 'manuell', title: 'Manuell bearbeiten', description: 'Dokumente mit fehlender Erkennung manuell ergänzen.', icon: '✏️', accent: '#fa8c16', accentSoft: '#fff7e6', permission: Permission.PROCESS_MANUALLY },
|
||||||
|
{ key: 'mailpostfach', title: 'Mailpostfach', description: 'Eingehende E-Mails mit Anhängen durchsuchen und zuordnen.', icon: '📬', accent: '#722ed1', accentSoft: '#f9f0ff', permission: Permission.VIEW_MAIL },
|
||||||
|
{ key: 'agrarmonitor', title: 'In Agrarmonitor', description: 'Dokumente im Agrarmonitor-Eingang anzeigen und verwalten.', icon: '🌱', accent: '#52c41a', accentSoft: '#f6ffed', permission: Permission.PROCESS_MANUALLY },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getVisibleTiles(groups: string[] | null | undefined): DigestTile[] {
|
||||||
|
const permissions = mapGroupsToPermissions(groups ?? []);
|
||||||
|
return DIGEST_TILES.filter(t => permissions.includes(t.permission));
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DailyDigestService {
|
export class DailyDigestService {
|
||||||
@@ -21,11 +45,16 @@ export class DailyDigestService {
|
|||||||
this.agrarmonitorBaseUrl = this.configService.get<string>('AGRARMONITOR_BASE_URL', '').replace(/\/+$/, '');
|
this.agrarmonitorBaseUrl = this.configService.get<string>('AGRARMONITOR_BASE_URL', '').replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendDigestForUser(userId: string, email: string, preferredUsername?: string) {
|
async sendDigestForUser(userId: string, email: string, preferredUsername?: string, groups?: string[]) {
|
||||||
|
const visibleTiles = getVisibleTiles(groups);
|
||||||
|
if (visibleTiles.length === 0) {
|
||||||
|
this.logger.warn(`Kein Digest für ${email}: keine sichtbaren Kacheln (Gruppen: ${JSON.stringify(groups)})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const today = new Date().toLocaleDateString('de-DE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
const today = new Date().toLocaleDateString('de-DE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
const counts = await this.statsService.getDashboardCounts(preferredUsername);
|
const counts = await this.statsService.getDashboardCounts(preferredUsername);
|
||||||
const html = buildDigestHtml(counts, today, this.appUrl, this.agrarmonitorBaseUrl);
|
const html = buildDigestHtml(counts, today, this.appUrl, this.agrarmonitorBaseUrl, visibleTiles);
|
||||||
const plainText = buildDigestPlainText(counts, today);
|
const plainText = buildDigestPlainText(counts, today, visibleTiles);
|
||||||
await this.mailService.sendMail({
|
await this.mailService.sendMail({
|
||||||
to: email,
|
to: email,
|
||||||
subject: `Paperless Manager – Tagesübersicht ${today}`,
|
subject: `Paperless Manager – Tagesübersicht ${today}`,
|
||||||
@@ -49,9 +78,14 @@ export class DailyDigestService {
|
|||||||
|
|
||||||
for (const sub of subscribers) {
|
for (const sub of subscribers) {
|
||||||
try {
|
try {
|
||||||
|
const visibleTiles = getVisibleTiles(sub.UserGroups);
|
||||||
|
if (visibleTiles.length === 0) {
|
||||||
|
this.logger.warn(`Überspringe Digest für ${sub.UserEmail}: keine sichtbaren Kacheln`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const counts = await this.statsService.getDashboardCounts(sub.UserPreferredUsername ?? undefined);
|
const counts = await this.statsService.getDashboardCounts(sub.UserPreferredUsername ?? undefined);
|
||||||
const html = buildDigestHtml(counts, today, this.appUrl, this.agrarmonitorBaseUrl);
|
const html = buildDigestHtml(counts, today, this.appUrl, this.agrarmonitorBaseUrl, visibleTiles);
|
||||||
const plainText = buildDigestPlainText(counts, today);
|
const plainText = buildDigestPlainText(counts, today, visibleTiles);
|
||||||
await this.mailService.sendMail({
|
await this.mailService.sendMail({
|
||||||
to: sub.UserEmail!,
|
to: sub.UserEmail!,
|
||||||
subject: `Paperless Manager – Tagesübersicht ${today}`,
|
subject: `Paperless Manager – Tagesübersicht ${today}`,
|
||||||
@@ -66,59 +100,17 @@ export class DailyDigestService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDigestHtml(counts: DashboardCounts, today: string, appUrl: string, agrarmonitorBaseUrl: string): string {
|
function tileUrl(tile: DigestTile, appUrl: string, agrarmonitorBaseUrl: string): string {
|
||||||
const tiles = [
|
if (tile.key === 'agrarmonitor') return agrarmonitorBaseUrl ? `${agrarmonitorBaseUrl}/dateien/eingang#dateien` : '';
|
||||||
{
|
return appUrl ? `${appUrl}/${tile.key === 'inbox' ? 'inbox' : tile.key}` : '';
|
||||||
key: 'inbox' as const,
|
}
|
||||||
title: 'Eingangsbox',
|
|
||||||
description: 'Neu eingegangene Scans, Uploads und E-Mail-Anhänge prüfen.',
|
function buildDigestHtml(counts: DashboardCounts, today: string, appUrl: string, agrarmonitorBaseUrl: string, visibleTiles: DigestTile[]): string {
|
||||||
icon: '📥',
|
const tiles = visibleTiles.map(t => ({
|
||||||
accent: '#1677ff',
|
...t,
|
||||||
accentSoft: '#e6f0ff',
|
url: tileUrl(t, appUrl, agrarmonitorBaseUrl),
|
||||||
url: appUrl ? `${appUrl}/inbox` : '',
|
count: counts[t.key],
|
||||||
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 totalOpen = tiles.reduce((sum, t) => sum + t.count, 0);
|
const totalOpen = tiles.reduce((sum, t) => sum + t.count, 0);
|
||||||
const summaryText = totalOpen > 0
|
const summaryText = totalOpen > 0
|
||||||
@@ -205,15 +197,12 @@ function buildDigestHtml(counts: DashboardCounts, today: string, appUrl: string,
|
|||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDigestPlainText(counts: DashboardCounts, today: string): string {
|
function buildDigestPlainText(counts: DashboardCounts, today: string, visibleTiles: DigestTile[]): string {
|
||||||
|
const lines = visibleTiles.map(t => ` ${t.title.padEnd(22)} ${counts[t.key]}`).join('\n');
|
||||||
return `Paperless Manager – Tagesübersicht ${today}
|
return `Paperless Manager – Tagesübersicht ${today}
|
||||||
|
|
||||||
Offene Vorgänge:
|
Offene Vorgänge:
|
||||||
Eingangsbox (Scanner): ${counts.inbox}
|
${lines}
|
||||||
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.
|
Diese E-Mail wird täglich automatisch von Paperless Manager versendet.
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export class UserSettings {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
DailyDigestEnabled!: boolean;
|
DailyDigestEnabled!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'json', nullable: true })
|
||||||
|
UserGroups!: string[] | null;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
UserEmail!: string | null;
|
UserEmail!: string | null;
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,13 @@ export class EmailImportController {
|
|||||||
res.sendFile(previewPath);
|
res.sendFile(previewPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Import Job Status ---
|
||||||
|
@Get('jobs/:jobId/status')
|
||||||
|
@RequirePermissions(Permission.VIEW_MAIL)
|
||||||
|
getJobStatus(@Param('jobId') jobId: string) {
|
||||||
|
return this.importService.getJobStatus(jobId) ?? { message: '', done: false };
|
||||||
|
}
|
||||||
|
|
||||||
// --- Final Import ---
|
// --- Final Import ---
|
||||||
@Post('execute')
|
@Post('execute')
|
||||||
@RequirePermissions(Permission.VIEW_MAIL)
|
@RequirePermissions(Permission.VIEW_MAIL)
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ import * as crypto from 'crypto';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailImportService {
|
export class EmailImportService {
|
||||||
private readonly logger = new Logger(EmailImportService.name);
|
private readonly logger = new Logger(EmailImportService.name);
|
||||||
|
private readonly importJobs = new Map<string, { message: string; done: boolean }>();
|
||||||
|
|
||||||
|
private setJobStatus(jobId: string | undefined, message: string, done = false): void {
|
||||||
|
if (!jobId) return;
|
||||||
|
this.importJobs.set(jobId, { message, done });
|
||||||
|
}
|
||||||
|
|
||||||
|
getJobStatus(jobId: string): { message: string; done: boolean } | null {
|
||||||
|
return this.importJobs.get(jobId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
@@ -328,12 +338,13 @@ export class EmailImportService {
|
|||||||
|
|
||||||
// --- Import Logic ---
|
// --- Import Logic ---
|
||||||
async executeImport(data: {
|
async executeImport(data: {
|
||||||
|
jobId?: string;
|
||||||
attachments: {
|
attachments: {
|
||||||
attachmentId: number;
|
attachmentId: number;
|
||||||
type: 'MAIN' | 'ATTACHMENT' | 'IGNORE';
|
type: 'MAIN' | 'ATTACHMENT' | 'IGNORE';
|
||||||
paperlessCorrespondentId?: number | null;
|
paperlessCorrespondentId?: number | null;
|
||||||
parentDocumentId?: number | null; // Used if type is ATTACHMENT (should map to a Custom Field theoretically, or just tags. For now, CF if configured, but we pass it)
|
parentDocumentId?: number | null;
|
||||||
splitRanges?: { start: number; end: number }[]; // 1-based pages, e.g. [{start: 1, end: 3}, {start: 4, end: 5}]
|
splitRanges?: { start: number; end: number }[];
|
||||||
barcode?: { x: number; y: number; nummer: string; datum: string; jahr: string };
|
barcode?: { x: number; y: number; nummer: string; datum: string; jahr: string };
|
||||||
belegnummer?: string;
|
belegnummer?: string;
|
||||||
}[];
|
}[];
|
||||||
@@ -341,6 +352,7 @@ export class EmailImportService {
|
|||||||
}): Promise<{ success: boolean; results: any[] }> {
|
}): Promise<{ success: boolean; results: any[] }> {
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'paperless-mail-import-'));
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'paperless-mail-import-'));
|
||||||
const results = [];
|
const results = [];
|
||||||
|
this.setJobStatus(data.jobId, 'Dokumente werden vorbereitet...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const att of data.attachments) {
|
for (const att of data.attachments) {
|
||||||
@@ -417,8 +429,9 @@ export class EmailImportService {
|
|||||||
};
|
};
|
||||||
if (att.paperlessCorrespondentId) options.correspondent = att.paperlessCorrespondentId;
|
if (att.paperlessCorrespondentId) options.correspondent = att.paperlessCorrespondentId;
|
||||||
|
|
||||||
|
this.setJobStatus(data.jobId, `Lade ${uploadItem.filename} hoch...`);
|
||||||
const paperlessTaskId = await this.paperlessService.uploadDocument(uploadItem.path, options);
|
const paperlessTaskId = await this.paperlessService.uploadDocument(uploadItem.path, options);
|
||||||
|
|
||||||
// Create background task for enrichment (same logic as Inbox)
|
// Create background task for enrichment (same logic as Inbox)
|
||||||
const backgroundTask = this.taskRepo.create({
|
const backgroundTask = this.taskRepo.create({
|
||||||
TaskId: paperlessTaskId,
|
TaskId: paperlessTaskId,
|
||||||
@@ -437,6 +450,7 @@ export class EmailImportService {
|
|||||||
// Still poll for Doc ID so we can return it to the frontend for immediate preview
|
// Still poll for Doc ID so we can return it to the frontend for immediate preview
|
||||||
let docId = null;
|
let docId = null;
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
|
this.setJobStatus(data.jobId, `Warte auf Paperless-Verarbeitung... (${i + 1}/30)`);
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
try {
|
try {
|
||||||
const taskStatus = await this.paperlessService.getTask(paperlessTaskId);
|
const taskStatus = await this.paperlessService.getTask(paperlessTaskId);
|
||||||
@@ -481,10 +495,12 @@ export class EmailImportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setJobStatus(data.jobId, 'Import abgeschlossen', true);
|
||||||
return { success: true, results };
|
return { success: true, results };
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up temp dir
|
// Clean up temp dir and job status
|
||||||
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
if (data.jobId) setTimeout(() => this.importJobs.delete(data.jobId!), 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export class UserSettingsController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async getSettings(@Request() req: any) {
|
async getSettings(@Request() req: any) {
|
||||||
return this.userSettingsService.getSettings(req.user.userId, req.user.email, req.user.preferredUsername);
|
return this.userSettingsService.getSettings(req.user.userId, req.user.email, req.user.preferredUsername, req.user.groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put()
|
@Put()
|
||||||
async updateSettings(@Request() req: any, @Body() body: any) {
|
async updateSettings(@Request() req: any, @Body() body: any) {
|
||||||
return this.userSettingsService.updateSettings(req.user.userId, body, req.user.email, req.user.preferredUsername);
|
return this.userSettingsService.updateSettings(req.user.userId, body, req.user.email, req.user.preferredUsername, req.user.groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('senders')
|
@Get('senders')
|
||||||
|
|||||||
@@ -65,14 +65,15 @@ export class UserSettingsService {
|
|||||||
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
|
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSettings(userId: string, email?: string, preferredUsername?: string): Promise<UserSettingsDto> {
|
async getSettings(userId: string, email?: string, preferredUsername?: string, groups?: string[]): Promise<UserSettingsDto> {
|
||||||
let entity = await this.repo.findOne({ where: { UserId: userId } });
|
let entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||||
if (email || preferredUsername) {
|
if (email || preferredUsername || groups) {
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
entity = this.repo.create({ UserId: userId });
|
entity = this.repo.create({ UserId: userId });
|
||||||
}
|
}
|
||||||
if (email) entity.UserEmail = email;
|
if (email) entity.UserEmail = email;
|
||||||
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
|
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
|
||||||
|
if (groups) entity.UserGroups = groups;
|
||||||
await this.repo.save(entity);
|
await this.repo.save(entity);
|
||||||
}
|
}
|
||||||
return this.toDto(entity);
|
return this.toDto(entity);
|
||||||
@@ -95,6 +96,7 @@ export class UserSettingsService {
|
|||||||
},
|
},
|
||||||
email?: string,
|
email?: string,
|
||||||
preferredUsername?: string,
|
preferredUsername?: string,
|
||||||
|
groups?: string[],
|
||||||
): Promise<UserSettingsDto> {
|
): Promise<UserSettingsDto> {
|
||||||
let entity = await this.repo.findOne({ where: { UserId: userId } });
|
let entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
@@ -116,6 +118,7 @@ export class UserSettingsService {
|
|||||||
if (data.dailyDigestEnabled !== undefined) entity.DailyDigestEnabled = data.dailyDigestEnabled;
|
if (data.dailyDigestEnabled !== undefined) entity.DailyDigestEnabled = data.dailyDigestEnabled;
|
||||||
if (email) entity.UserEmail = email;
|
if (email) entity.UserEmail = email;
|
||||||
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
|
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
|
||||||
|
if (groups) entity.UserGroups = groups;
|
||||||
|
|
||||||
await this.repo.save(entity);
|
await this.repo.save(entity);
|
||||||
return this.toDto(entity);
|
return this.toDto(entity);
|
||||||
|
|||||||
@@ -62,10 +62,19 @@ export const emailImportApi = {
|
|||||||
return res.data.isDuplicate;
|
return res.data.isDuplicate;
|
||||||
},
|
},
|
||||||
|
|
||||||
executeImport: async (emailDate: string, attachments: AttachmentImportData[]): Promise<{ success: boolean; results: any[] }> => {
|
executeImport: async (emailDate: string, attachments: AttachmentImportData[], jobId?: string): Promise<{ success: boolean; results: any[] }> => {
|
||||||
const res = await api.post('/api/email-import/execute', { emailDate, attachments });
|
const res = await api.post('/api/email-import/execute', { emailDate, attachments, jobId }, { timeout: 300_000 });
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getJobStatus: async (jobId: string): Promise<{ message: string; done: boolean } | null> => {
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/api/email-import/jobs/${jobId}/status`);
|
||||||
|
return res.data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
ensurePreviews: async (emailId: number): Promise<void> => {
|
ensurePreviews: async (emailId: number): Promise<void> => {
|
||||||
await api.post(`/api/email-import/emails/${emailId}/ensure-previews`);
|
await api.post(`/api/email-import/emails/${emailId}/ensure-previews`);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
|||||||
|
|
||||||
// Step 3 specific state
|
// Step 3 specific state
|
||||||
const [importSuccess, setImportSuccess] = useState(false);
|
const [importSuccess, setImportSuccess] = useState(false);
|
||||||
|
const [importStatus, setImportStatus] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && attachments.length > 0) {
|
if (visible && attachments.length > 0) {
|
||||||
@@ -315,6 +316,14 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
|||||||
|
|
||||||
const executeImport = async () => {
|
const executeImport = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setImportStatus('Import wird gestartet...');
|
||||||
|
const jobId = crypto.randomUUID ? crypto.randomUUID() : `job-${Date.now()}`;
|
||||||
|
const statusPoll = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const status = await emailImportApi.getJobStatus(jobId);
|
||||||
|
if (status?.message) setImportStatus(status.message);
|
||||||
|
} catch {}
|
||||||
|
}, 1500);
|
||||||
try {
|
try {
|
||||||
const finalData = [];
|
const finalData = [];
|
||||||
|
|
||||||
@@ -358,12 +367,14 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await emailImportApi.executeImport(email.Date, finalData);
|
await emailImportApi.executeImport(email.Date, finalData, jobId);
|
||||||
setImportSuccess(true);
|
setImportSuccess(true);
|
||||||
setCurrentStep(2);
|
setCurrentStep(2);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(`Fehler beim Import: ${e.message}`);
|
message.error(`Fehler beim Import: ${e.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
clearInterval(statusPoll);
|
||||||
|
setImportStatus('');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -747,7 +758,7 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
|||||||
style={{ marginBottom: 24 }}
|
style={{ marginBottom: 24 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading} tip={importStatus || undefined}>
|
||||||
<div style={{ minHeight: 300 }}>
|
<div style={{ minHeight: 300 }}>
|
||||||
{currentStep === 0 && renderStep1()}
|
{currentStep === 0 && renderStep1()}
|
||||||
{currentStep === 1 && renderStep2()}
|
{currentStep === 1 && renderStep2()}
|
||||||
|
|||||||
Reference in New Issue
Block a user