E-Mail-Client: Spam-Ordner als eigener Tab
Neuer Tab "Spam" (zwischen Gesendet und Papierkorb), zeigt den Junk-/ Spam-Ordner des gewählten Postfachs – damit fälschlich als Spam einsortierte Mails auffindbar sind. Backend: - EmailFolder-Enum um SPAM erweitert (Migration, Wert angehängt → kein Rewrite bestehender Zeilen). - imapService.findJunkFolderPath: ermittelt den Junk-Ordner per Special-Use-Flag \Junk + üblicher Namensliste (Junk/Spam/…). - syncAllFoldersForAccount synct den Junk-Ordner zusätzlich als dbFolder=SPAM (syncEmailsForAccount bekommt dbFolder-Option). - getCachedEmails + getFolderCountsForAccount um SPAM erweitert. - Papierkorb-Move/Restore für Spam-Mails nutzt den echten Junk-Pfad als Quell-/Zielordner. Frontend: Tab + Badge (ungelesen/gesamt), gleicher List-/Detail-Pfad wie INBOX; Zuordnen-zu-Vertrag auch aus Spam möglich. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
-- EmailFolder-Enum um SPAM erweitern (Spam-/Junk-Ordner des Postfachs).
|
||||||
|
-- Neuer Wert wird ans Ende gehängt → bestehende INBOX/SENT-Zeilen bleiben
|
||||||
|
-- unverändert, kein Rewrite.
|
||||||
|
|
||||||
|
ALTER TABLE `CachedEmail`
|
||||||
|
MODIFY COLUMN `folder` ENUM('INBOX', 'SENT', 'SPAM') NOT NULL DEFAULT 'INBOX';
|
||||||
@@ -448,6 +448,7 @@ model StressfreiEmail {
|
|||||||
enum EmailFolder {
|
enum EmailFolder {
|
||||||
INBOX
|
INBOX
|
||||||
SENT
|
SENT
|
||||||
|
SPAM
|
||||||
}
|
}
|
||||||
|
|
||||||
model CachedEmail {
|
model CachedEmail {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { CachedEmail, Prisma, EmailFolder } from '@prisma/client';
|
import { CachedEmail, Prisma, EmailFolder } from '@prisma/client';
|
||||||
import prisma from '../lib/prisma.js';
|
import prisma from '../lib/prisma.js';
|
||||||
import { decrypt } from '../utils/encryption.js';
|
import { decrypt } from '../utils/encryption.js';
|
||||||
import { fetchEmails, ImapCredentials, FetchedEmail, moveToTrash, restoreFromTrash, permanentDelete, humanizeImapError } from './imapService.js';
|
import { fetchEmails, ImapCredentials, FetchedEmail, moveToTrash, restoreFromTrash, permanentDelete, humanizeImapError, findJunkFolderPath } from './imapService.js';
|
||||||
import { getImapSmtpSettings } from './emailProvider/emailProviderService.js';
|
import { getImapSmtpSettings } from './emailProvider/emailProviderService.js';
|
||||||
|
|
||||||
// ==================== TYPES ====================
|
// ==================== TYPES ====================
|
||||||
@@ -68,7 +68,7 @@ export interface EmailListOptions {
|
|||||||
// E-Mails für eine StressfreiEmail synchronisieren
|
// E-Mails für eine StressfreiEmail synchronisieren
|
||||||
export async function syncEmailsForAccount(
|
export async function syncEmailsForAccount(
|
||||||
stressfreiEmailId: number,
|
stressfreiEmailId: number,
|
||||||
options?: { folder?: string; fullSync?: boolean }
|
options?: { folder?: string; fullSync?: boolean; dbFolder?: EmailFolder }
|
||||||
): Promise<SyncResult> {
|
): Promise<SyncResult> {
|
||||||
const folder = options?.folder || 'INBOX';
|
const folder = options?.folder || 'INBOX';
|
||||||
const fullSync = options?.fullSync || false;
|
const fullSync = options?.fullSync || false;
|
||||||
@@ -111,10 +111,12 @@ export async function syncEmailsForAccount(
|
|||||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Folder-Mapping: IMAP-Ordner zu DB-Ordner
|
// Folder-Mapping: IMAP-Ordner zu DB-Ordner. Explizites dbFolder (z.B.
|
||||||
const dbFolder = folder.toUpperCase() === 'SENT' || folder.toLowerCase() === 'sent'
|
// SPAM für den Junk-Ordner, dessen IMAP-Name variiert) hat Vorrang.
|
||||||
? EmailFolder.SENT
|
const dbFolder = options?.dbFolder
|
||||||
: EmailFolder.INBOX;
|
?? (folder.toUpperCase() === 'SENT' || folder.toLowerCase() === 'sent'
|
||||||
|
? EmailFolder.SENT
|
||||||
|
: EmailFolder.INBOX);
|
||||||
|
|
||||||
// Für inkrementellen Sync: Höchste UID des Ordners ermitteln
|
// Für inkrementellen Sync: Höchste UID des Ordners ermitteln
|
||||||
let sinceUid: number | undefined;
|
let sinceUid: number | undefined;
|
||||||
@@ -168,7 +170,33 @@ export async function syncEmailsForAccount(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alle Ordner synchronisieren (INBOX + SENT)
|
/**
|
||||||
|
* Baut die IMAP-Credentials für ein Postfach (für Operationen, die eine
|
||||||
|
* eigene Verbindung brauchen – z.B. Junk-Ordner-Pfad ermitteln). Gibt null
|
||||||
|
* zurück, wenn kein Postfach/Passwort/Provider-Settings vorhanden sind.
|
||||||
|
*/
|
||||||
|
async function getImapCredentialsForAccount(stressfreiEmailId: number): Promise<ImapCredentials | null> {
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id: stressfreiEmailId } });
|
||||||
|
if (!stressfreiEmail?.hasMailbox || !stressfreiEmail.emailPasswordEncrypted) return null;
|
||||||
|
const settings = await getImapSmtpSettings();
|
||||||
|
if (!settings) return null;
|
||||||
|
let password: string;
|
||||||
|
try {
|
||||||
|
password = decrypt(stressfreiEmail.emailPasswordEncrypted);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
host: settings.imapServer,
|
||||||
|
port: settings.imapPort,
|
||||||
|
user: stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
encryption: settings.imapEncryption,
|
||||||
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Ordner synchronisieren (INBOX + SENT + SPAM/Junk)
|
||||||
export async function syncAllFoldersForAccount(
|
export async function syncAllFoldersForAccount(
|
||||||
stressfreiEmailId: number,
|
stressfreiEmailId: number,
|
||||||
options?: { fullSync?: boolean }
|
options?: { fullSync?: boolean }
|
||||||
@@ -181,6 +209,23 @@ export async function syncAllFoldersForAccount(
|
|||||||
// SENT synchronisieren (Plesk verwendet "Sent" als Ordnername)
|
// SENT synchronisieren (Plesk verwendet "Sent" als Ordnername)
|
||||||
const sentResult = await syncEmailsForAccount(stressfreiEmailId, { folder: 'Sent', fullSync });
|
const sentResult = await syncEmailsForAccount(stressfreiEmailId, { folder: 'Sent', fullSync });
|
||||||
|
|
||||||
|
// SPAM/Junk synchronisieren – Ordnername variiert je Server, daher erst
|
||||||
|
// per IMAP ermitteln. Existiert kein Junk-Ordner, wird Spam übersprungen
|
||||||
|
// (kein Fehler). Wird explizit als dbFolder=SPAM abgelegt.
|
||||||
|
let spamNew = 0;
|
||||||
|
const credentials = await getImapCredentialsForAccount(stressfreiEmailId);
|
||||||
|
if (credentials) {
|
||||||
|
const junkPath = await findJunkFolderPath(credentials);
|
||||||
|
if (junkPath) {
|
||||||
|
const spamResult = await syncEmailsForAccount(stressfreiEmailId, {
|
||||||
|
folder: junkPath,
|
||||||
|
dbFolder: EmailFolder.SPAM,
|
||||||
|
fullSync,
|
||||||
|
});
|
||||||
|
if (spamResult.success) spamNew = spamResult.newEmails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ergebnisse kombinieren
|
// Ergebnisse kombinieren
|
||||||
if (!inboxResult.success && !sentResult.success) {
|
if (!inboxResult.success && !sentResult.success) {
|
||||||
return {
|
return {
|
||||||
@@ -193,7 +238,7 @@ export async function syncAllFoldersForAccount(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
newEmails: inboxResult.newEmails + sentResult.newEmails,
|
newEmails: inboxResult.newEmails + sentResult.newEmails + spamNew,
|
||||||
totalEmails: inboxResult.totalEmails,
|
totalEmails: inboxResult.totalEmails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -280,9 +325,11 @@ export async function getCachedEmails(
|
|||||||
where.contractId = options.contractId;
|
where.contractId = options.contractId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Folder-Filter (INBOX oder SENT)
|
// Folder-Filter (INBOX, SENT oder SPAM)
|
||||||
if (options.folder === 'SENT') {
|
if (options.folder === 'SENT') {
|
||||||
where.folder = EmailFolder.SENT;
|
where.folder = EmailFolder.SENT;
|
||||||
|
} else if (options.folder === 'SPAM') {
|
||||||
|
where.folder = EmailFolder.SPAM;
|
||||||
} else if (options.folder === 'INBOX' || !options.folder) {
|
} else if (options.folder === 'INBOX' || !options.folder) {
|
||||||
// Standard: Nur Posteingang
|
// Standard: Nur Posteingang
|
||||||
where.folder = EmailFolder.INBOX;
|
where.folder = EmailFolder.INBOX;
|
||||||
@@ -573,10 +620,12 @@ export async function getFolderCountsForAccount(stressfreiEmailId: number): Prom
|
|||||||
inboxUnread: number;
|
inboxUnread: number;
|
||||||
sent: number;
|
sent: number;
|
||||||
sentUnread: number;
|
sentUnread: number;
|
||||||
|
spam: number;
|
||||||
|
spamUnread: number;
|
||||||
trash: number;
|
trash: number;
|
||||||
trashUnread: number;
|
trashUnread: number;
|
||||||
}> {
|
}> {
|
||||||
const [inbox, inboxUnread, sent, sentUnread, trash, trashUnread] = await Promise.all([
|
const [inbox, inboxUnread, sent, sentUnread, spam, spamUnread, trash, trashUnread] = await Promise.all([
|
||||||
// INBOX total
|
// INBOX total
|
||||||
prisma.cachedEmail.count({
|
prisma.cachedEmail.count({
|
||||||
where: { stressfreiEmailId, folder: EmailFolder.INBOX, isDeleted: false },
|
where: { stressfreiEmailId, folder: EmailFolder.INBOX, isDeleted: false },
|
||||||
@@ -593,6 +642,14 @@ export async function getFolderCountsForAccount(stressfreiEmailId: number): Prom
|
|||||||
prisma.cachedEmail.count({
|
prisma.cachedEmail.count({
|
||||||
where: { stressfreiEmailId, folder: EmailFolder.SENT, isDeleted: false, isRead: false },
|
where: { stressfreiEmailId, folder: EmailFolder.SENT, isDeleted: false, isRead: false },
|
||||||
}),
|
}),
|
||||||
|
// SPAM total
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { stressfreiEmailId, folder: EmailFolder.SPAM, isDeleted: false },
|
||||||
|
}),
|
||||||
|
// SPAM unread
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { stressfreiEmailId, folder: EmailFolder.SPAM, isDeleted: false, isRead: false },
|
||||||
|
}),
|
||||||
// TRASH total (isDeleted = true)
|
// TRASH total (isDeleted = true)
|
||||||
prisma.cachedEmail.count({
|
prisma.cachedEmail.count({
|
||||||
where: { stressfreiEmailId, isDeleted: true },
|
where: { stressfreiEmailId, isDeleted: true },
|
||||||
@@ -603,7 +660,7 @@ export async function getFolderCountsForAccount(stressfreiEmailId: number): Prom
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { inbox, inboxUnread, sent, sentUnread, trash, trashUnread };
|
return { inbox, inboxUnread, sent, sentUnread, spam, spamUnread, trash, trashUnread };
|
||||||
}
|
}
|
||||||
|
|
||||||
// E-Mail-Anzahl pro Ordner für einen Vertrag (zugeordnete E-Mails).
|
// E-Mail-Anzahl pro Ordner für einen Vertrag (zugeordnete E-Mails).
|
||||||
@@ -759,8 +816,17 @@ export async function moveEmailToTrash(id: number): Promise<TrashOperationResult
|
|||||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ordner bestimmen (INBOX oder Sent)
|
// IMAP-Quellordner bestimmen. Bei SPAM den tatsächlichen Junk-Pfad
|
||||||
const sourceFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
// ermitteln (Name variiert je Server) – sonst würde moveToTrash im
|
||||||
|
// falschen Ordner nach der UID suchen.
|
||||||
|
let sourceFolder: string;
|
||||||
|
if (email.folder === 'SENT') {
|
||||||
|
sourceFolder = 'Sent';
|
||||||
|
} else if (email.folder === 'SPAM') {
|
||||||
|
sourceFolder = (await findJunkFolderPath(credentials)) || 'Junk';
|
||||||
|
} else {
|
||||||
|
sourceFolder = 'INBOX';
|
||||||
|
}
|
||||||
|
|
||||||
// Auf IMAP-Server in Trash verschieben
|
// Auf IMAP-Server in Trash verschieben
|
||||||
const imapResult = await moveToTrash(credentials, email.uid, sourceFolder);
|
const imapResult = await moveToTrash(credentials, email.uid, sourceFolder);
|
||||||
@@ -826,8 +892,16 @@ export async function restoreEmailFromTrash(id: number): Promise<TrashOperationR
|
|||||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ziel-Ordner bestimmen (basierend auf originalem Ordner)
|
// Ziel-Ordner bestimmen (basierend auf originalem Ordner). SPAM zurück
|
||||||
const targetFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
// in den tatsächlichen Junk-Ordner (Name variiert je Server).
|
||||||
|
let targetFolder: string;
|
||||||
|
if (email.folder === 'SENT') {
|
||||||
|
targetFolder = 'Sent';
|
||||||
|
} else if (email.folder === 'SPAM') {
|
||||||
|
targetFolder = (await findJunkFolderPath(credentials)) || 'Junk';
|
||||||
|
} else {
|
||||||
|
targetFolder = 'INBOX';
|
||||||
|
}
|
||||||
|
|
||||||
// Auf IMAP-Server aus Trash wiederherstellen
|
// Auf IMAP-Server aus Trash wiederherstellen
|
||||||
const imapResult = await restoreFromTrash(credentials, email.uid, targetFolder);
|
const imapResult = await restoreFromTrash(credentials, email.uid, targetFolder);
|
||||||
|
|||||||
@@ -784,6 +784,9 @@ export async function fetchAttachmentList(
|
|||||||
// Typische Namen für Trash-Ordner (verschiedene Server/Sprachen)
|
// Typische Namen für Trash-Ordner (verschiedene Server/Sprachen)
|
||||||
const TRASH_FOLDER_NAMES = ['Trash', 'INBOX.Trash', 'Deleted', 'Deleted Items', 'Deleted Messages', 'Papierkorb'];
|
const TRASH_FOLDER_NAMES = ['Trash', 'INBOX.Trash', 'Deleted', 'Deleted Items', 'Deleted Messages', 'Papierkorb'];
|
||||||
|
|
||||||
|
// Übliche Namen für den Spam-/Junk-Ordner (Plesk/Dovecot: "Junk").
|
||||||
|
const JUNK_FOLDER_NAMES = ['Junk', 'INBOX.Junk', 'Spam', 'INBOX.Spam', 'Junk E-mail', 'Junk Email', 'Bulk Mail'];
|
||||||
|
|
||||||
// Helper: Trash-Ordner finden
|
// Helper: Trash-Ordner finden
|
||||||
async function findTrashFolder(client: ImapFlow): Promise<string | null> {
|
async function findTrashFolder(client: ImapFlow): Promise<string | null> {
|
||||||
const mailboxes = await client.list();
|
const mailboxes = await client.list();
|
||||||
@@ -803,6 +806,48 @@ async function findTrashFolder(client: ImapFlow): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den tatsächlichen Pfad des Spam-/Junk-Ordners eines Postfachs.
|
||||||
|
* Bevorzugt das IMAP-Special-Use-Flag \Junk, fällt sonst auf übliche
|
||||||
|
* Ordnernamen zurück. Öffnet eine eigene Verbindung; gibt null zurück,
|
||||||
|
* wenn kein Junk-Ordner existiert (dann wird Spam einfach nicht synct).
|
||||||
|
*/
|
||||||
|
export async function findJunkFolderPath(credentials: ImapCredentials): Promise<string | null> {
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: { user: credentials.user, pass: credentials.password },
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
const mailboxes = await client.list();
|
||||||
|
// Zuerst per Special-Use-Flag \Junk
|
||||||
|
const byFlag = mailboxes.find(m => m.specialUse === '\\Junk');
|
||||||
|
if (byFlag) return byFlag.path;
|
||||||
|
// Sonst per Namensliste
|
||||||
|
for (const name of JUNK_FOLDER_NAMES) {
|
||||||
|
const found = mailboxes.find(m =>
|
||||||
|
m.path.toLowerCase() === name.toLowerCase() ||
|
||||||
|
m.name.toLowerCase() === name.toLowerCase()
|
||||||
|
);
|
||||||
|
if (found) return found.path;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
try { await client.logout(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface MoveToTrashResult {
|
export interface MoveToTrashResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
newUid?: number; // UID im Trash-Ordner
|
newUid?: number; // UID im Trash-Ordner
|
||||||
|
|||||||
@@ -97,6 +97,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **📧 E-Mail-Client: Spam-Ordner als eigener Tab**
|
||||||
|
- Neuer Tab „Spam" (zwischen Gesendet und Papierkorb) im E-Mail-Client,
|
||||||
|
zeigt den Junk-/Spam-Ordner des gewählten Postfachs – damit
|
||||||
|
fälschlich als Spam eingestufte Mails auffindbar sind.
|
||||||
|
- Backend: `EmailFolder`-Enum um `SPAM` erweitert (Migration
|
||||||
|
`20260727110000_email_folder_spam`, Enum-Wert angehängt). IMAP-Sync
|
||||||
|
ermittelt den Junk-Ordnerpfad per Special-Use-Flag `\Junk` +
|
||||||
|
Namensliste (`findJunkFolderPath`), `syncAllFoldersForAccount` synct
|
||||||
|
ihn zusätzlich als `dbFolder=SPAM`. `getCachedEmails` +
|
||||||
|
`getFolderCountsForAccount` um SPAM erweitert.
|
||||||
|
- Papierkorb-Verschieben/Wiederherstellen für Spam-Mails nutzt den
|
||||||
|
tatsächlichen Junk-Pfad als Quell-/Zielordner.
|
||||||
|
- Frontend: Tab + Badge (ungelesen/gesamt), nutzt denselben
|
||||||
|
List-/Detail-Pfad wie INBOX; Zuordnen-zu-Vertrag auch aus Spam
|
||||||
|
möglich (Rettung legitimer Mails).
|
||||||
|
|
||||||
- [x] **📱 Mobilfunk: Feld „Mobilfunknetz" unter Anbieter & Tarif**
|
- [x] **📱 Mobilfunk: Feld „Mobilfunknetz" unter Anbieter & Tarif**
|
||||||
- Neues Dropdown „Mobilfunknetz" in der Anbieter-&-Tarif-Karte, nur
|
- Neues Dropdown „Mobilfunknetz" in der Anbieter-&-Tarif-Karte, nur
|
||||||
sichtbar wenn Vertragstyp = Mobilfunk. Optionen: Bitte auswählen
|
sichtbar wenn Vertragstyp = Mobilfunk. Optionen: Bitte auswählen
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { RefreshCw, Plus, Mail, Inbox, Send, Trash2, Search, SlidersHorizontal, X } from 'lucide-react';
|
import { RefreshCw, Plus, Mail, Inbox, Send, Trash2, Search, SlidersHorizontal, X, ShieldAlert } from 'lucide-react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { cachedEmailApi, stressfreiEmailApi, CachedEmail, EmailFilterParams } from '../../services/api';
|
import { cachedEmailApi, stressfreiEmailApi, CachedEmail, EmailFilterParams } from '../../services/api';
|
||||||
@@ -13,7 +13,7 @@ import ComposeEmailModal from './ComposeEmailModal';
|
|||||||
import AssignToContractModal from './AssignToContractModal';
|
import AssignToContractModal from './AssignToContractModal';
|
||||||
import TrashEmailList from './TrashEmailList';
|
import TrashEmailList from './TrashEmailList';
|
||||||
|
|
||||||
type EmailFolder = 'INBOX' | 'SENT' | 'TRASH';
|
type EmailFolder = 'INBOX' | 'SENT' | 'SPAM' | 'TRASH';
|
||||||
|
|
||||||
interface EmailClientTabProps {
|
interface EmailClientTabProps {
|
||||||
customerId: number;
|
customerId: number;
|
||||||
@@ -117,7 +117,7 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
|
|||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
cachedEmailApi.getForCustomer(customerId, {
|
cachedEmailApi.getForCustomer(customerId, {
|
||||||
accountId: selectedAccountId || undefined,
|
accountId: selectedAccountId || undefined,
|
||||||
folder: selectedFolder as 'INBOX' | 'SENT',
|
folder: selectedFolder as 'INBOX' | 'SENT' | 'SPAM',
|
||||||
...filterParams,
|
...filterParams,
|
||||||
}),
|
}),
|
||||||
enabled: !!selectedAccountId && selectedFolder !== 'TRASH',
|
enabled: !!selectedAccountId && selectedFolder !== 'TRASH',
|
||||||
@@ -152,6 +152,8 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
|
|||||||
inboxUnread: 0,
|
inboxUnread: 0,
|
||||||
sent: 0,
|
sent: 0,
|
||||||
sentUnread: 0,
|
sentUnread: 0,
|
||||||
|
spam: 0,
|
||||||
|
spamUnread: 0,
|
||||||
trash: 0,
|
trash: 0,
|
||||||
trashUnread: 0,
|
trashUnread: 0,
|
||||||
};
|
};
|
||||||
@@ -396,6 +398,31 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleFolderChange('SPAM')}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||||
|
selectedFolder === 'SPAM'
|
||||||
|
? 'bg-white text-amber-600 shadow-sm font-medium'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ShieldAlert className="w-4 h-4" />
|
||||||
|
Spam
|
||||||
|
{folderCounts.spam > 0 && (
|
||||||
|
<span
|
||||||
|
className={`ml-1 px-1.5 py-0.5 text-xs rounded-full cursor-help ${
|
||||||
|
folderCounts.spamUnread > 0
|
||||||
|
? 'bg-amber-100 text-amber-600 font-medium'
|
||||||
|
: 'bg-gray-100 text-gray-500'
|
||||||
|
}`}
|
||||||
|
title={`${folderCounts.spamUnread} ungelesen / ${folderCounts.spam} gesamt`}
|
||||||
|
>
|
||||||
|
{folderCounts.spamUnread > 0
|
||||||
|
? `${folderCounts.spamUnread}/${folderCounts.spam}`
|
||||||
|
: folderCounts.spam}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
{canAccessTrash && (
|
{canAccessTrash && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFolderChange('TRASH')}
|
onClick={() => handleFolderChange('TRASH')}
|
||||||
@@ -626,7 +653,7 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
|
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
|
||||||
}}
|
}}
|
||||||
isLoading={emailsLoading}
|
isLoading={emailsLoading}
|
||||||
folder={selectedFolder as 'INBOX' | 'SENT'}
|
folder={selectedFolder as 'INBOX' | 'SENT' | 'SPAM'}
|
||||||
accountId={selectedAccountId}
|
accountId={selectedAccountId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface EmailListProps {
|
|||||||
onSelectEmail: (email: CachedEmail) => void;
|
onSelectEmail: (email: CachedEmail) => void;
|
||||||
onEmailDeleted?: (emailId: number) => void;
|
onEmailDeleted?: (emailId: number) => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
folder?: 'INBOX' | 'SENT';
|
folder?: 'INBOX' | 'SENT' | 'SPAM';
|
||||||
accountId?: number | null; // Für Folder-Count-Aktualisierung bei Lesen/Ungelesen
|
accountId?: number | null; // Für Folder-Count-Aktualisierung bei Lesen/Ungelesen
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,8 +250,8 @@ export default function EmailList({
|
|||||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
||||||
{email.contract.contractNumber}
|
{email.contract.contractNumber}
|
||||||
</span>
|
</span>
|
||||||
{/* X-Button nur für INBOX oder manuell zugeordnete gesendete E-Mails */}
|
{/* X-Button nur für empfangene (INBOX/SPAM) oder manuell zugeordnete gesendete E-Mails */}
|
||||||
{(folder === 'INBOX' || (folder === 'SENT' && !email.isAutoAssigned)) && (
|
{(folder === 'INBOX' || folder === 'SPAM' || (folder === 'SENT' && !email.isAutoAssigned)) && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => handleUnassign(e, email.id)}
|
onClick={(e) => handleUnassign(e, email.id)}
|
||||||
className="p-0.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded"
|
className="p-0.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded"
|
||||||
|
|||||||
@@ -585,6 +585,8 @@ export const stressfreiEmailApi = {
|
|||||||
inboxUnread: number;
|
inboxUnread: number;
|
||||||
sent: number;
|
sent: number;
|
||||||
sentUnread: number;
|
sentUnread: number;
|
||||||
|
spam: number;
|
||||||
|
spamUnread: number;
|
||||||
trash: number;
|
trash: number;
|
||||||
trashUnread: number;
|
trashUnread: number;
|
||||||
}>>(`/stressfrei-emails/${id}/folder-counts`);
|
}>>(`/stressfrei-emails/${id}/folder-counts`);
|
||||||
@@ -595,7 +597,7 @@ export const stressfreiEmailApi = {
|
|||||||
// Cached Email API (E-Mail-Client)
|
// Cached Email API (E-Mail-Client)
|
||||||
export interface EmailFilterParams {
|
export interface EmailFilterParams {
|
||||||
accountId?: number;
|
accountId?: number;
|
||||||
folder?: 'INBOX' | 'SENT';
|
folder?: 'INBOX' | 'SENT' | 'SPAM';
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
// Suche / Filter (alle AND-verknüpft)
|
// Suche / Filter (alle AND-verknüpft)
|
||||||
|
|||||||
Reference in New Issue
Block a user