diff --git a/packages/crm-service/src/contacts/contacts.controller.ts b/packages/crm-service/src/contacts/contacts.controller.ts index d16f4aa..b882634 100644 --- a/packages/crm-service/src/contacts/contacts.controller.ts +++ b/packages/crm-service/src/contacts/contacts.controller.ts @@ -11,6 +11,7 @@ import { HttpCode, HttpStatus, UseGuards, + NotFoundException, } from '@nestjs/common'; import { ApiTags, @@ -71,6 +72,17 @@ export class ContactsController { ); } + @Get('lookup') + @ApiOperation({ summary: 'Kontakt anhand E-Mail-Adresse suchen' }) + async lookupByEmail( + @CurrentUser() user: JwtPayload, + @Query('email') email: string, + ) { + const contact = await this.contactsService.findByEmail(user.tenantId!, email); + if (!contact) throw new NotFoundException('Kein Kontakt mit dieser E-Mail-Adresse gefunden'); + return singleResponse(contact); + } + @Get(':id') @ApiOperation({ summary: 'Kontakt-Details abrufen' }) @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) diff --git a/packages/crm-service/src/contacts/contacts.service.ts b/packages/crm-service/src/contacts/contacts.service.ts index 7ed6f3c..dd5d1fa 100644 --- a/packages/crm-service/src/contacts/contacts.service.ts +++ b/packages/crm-service/src/contacts/contacts.service.ts @@ -173,6 +173,26 @@ export class ContactsService { return { data, total, page, pageSize }; } + /** Kontakt anhand einer E-Mail-Adresse suchen (erster Treffer) */ + async findByEmail(tenantId: string, email: string) { + return this.prisma.contact.findFirst({ + where: { + tenantId, + OR: [ + { email: { equals: email, mode: 'insensitive' } }, + { emails: { some: { email: { equals: email, mode: 'insensitive' } } } }, + ], + }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + companyName: true, + }, + }); + } + async findOne(tenantId: string, id: string) { const contact = await this.prisma.contact.findFirst({ where: { id, tenantId }, diff --git a/packages/crm-service/src/graph/graph.service.ts b/packages/crm-service/src/graph/graph.service.ts index 14efa62..7dbc585 100644 --- a/packages/crm-service/src/graph/graph.service.ts +++ b/packages/crm-service/src/graph/graph.service.ts @@ -57,6 +57,15 @@ export interface M365Contact { companyName: string | null; } +export interface M365MailFolder { + id: string; + displayName: string; + totalItemCount: number; + unreadItemCount: number; + childFolderCount: number; + wellKnownName: string | null; +} + const GRAPH_BASE = 'https://graph.microsoft.com/v1.0'; const CACHE_TTL = 300; // 5 Minuten @@ -338,4 +347,67 @@ export class GraphService { await this.redis.set(cacheKey, JSON.stringify(lists), CACHE_TTL); return lists; } + + // ── Mail-Ordner ─────────────────────────────────────────────────────── + + /** Alle Mail-Ordner des Benutzers (Inbox, Gesendet, Entwürfe, …) */ + async getMailFolders(userJwt: string, userId: string): Promise { + const cacheKey = `graph:mail-folders:${userId}`; + const cached = await this.redis.get(cacheKey); + if (cached) return JSON.parse(cached) as M365MailFolder[]; + + const accessToken = await this.getM365Token(userJwt); + + const data = await this.graphGet<{ value: M365MailFolder[] }>( + accessToken, + '/me/mailFolders', + { + $top: '50', + $select: 'id,displayName,totalItemCount,unreadItemCount,childFolderCount,wellKnownName', + }, + ); + + const folders = data.value ?? []; + await this.redis.set(cacheKey, JSON.stringify(folders), CACHE_TTL); + this.logger.debug(`Graph: ${folders.length} Mail-Ordner geladen`); + return folders; + } + + /** E-Mails in einem bestimmten Ordner (mit optionalem Tages-Filter) */ + async getMailsByFolder( + userJwt: string, + userId: string, + folderId: string, + days: number, + ): Promise { + const cacheKey = `graph:folder-mails:${userId}:${folderId}:${days}`; + const cached = await this.redis.get(cacheKey); + if (cached) return JSON.parse(cached) as M365Email[]; + + const accessToken = await this.getM365Token(userJwt); + + const params: Record = { + $top: '50', + $orderby: 'receivedDateTime desc', + $select: 'id,subject,bodyPreview,receivedDateTime,from,hasAttachments,isRead,webLink', + }; + + if (days > 0) { + const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); + params['$filter'] = `receivedDateTime ge ${since}`; + } + + const data = await this.graphGet<{ value: M365Email[] }>( + accessToken, + `/me/mailFolders/${folderId}/messages`, + params, + ); + + const emails = data.value ?? []; + await this.redis.set(cacheKey, JSON.stringify(emails), CACHE_TTL); + this.logger.debug( + `Graph: ${emails.length} E-Mails in Ordner ${folderId} (days=${days}) geladen`, + ); + return emails; + } } diff --git a/packages/crm-service/src/graph/office365.controller.ts b/packages/crm-service/src/graph/office365.controller.ts index ca47860..7d69737 100644 --- a/packages/crm-service/src/graph/office365.controller.ts +++ b/packages/crm-service/src/graph/office365.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Req, Logger } from '@nestjs/common'; +import { Controller, Get, Param, Query, Req, Logger } from '@nestjs/common'; import { Request } from 'express'; import { GraphService } from './graph.service'; @@ -14,10 +14,12 @@ interface JwtUser { * Zeigt alle M365-Daten des eingeloggten Users (ohne Kontakt-Filter). * * Routen: - * GET /crm/office365/emails — Alle E-Mails (Posteingang) - * GET /crm/office365/calendar — Alle Kalendertermine (nächste 30 Tage) - * GET /crm/office365/contacts — Alle Outlook-Kontakte - * GET /crm/office365/tasks — Alle Aufgaben (Microsoft To Do) + * GET /crm/office365/emails — Alle E-Mails (Posteingang) + * GET /crm/office365/calendar — Alle Kalendertermine (nächste 30 Tage) + * GET /crm/office365/contacts — Alle Outlook-Kontakte + * GET /crm/office365/tasks — Alle Aufgaben (Microsoft To Do) + * GET /crm/office365/folders — Alle Mail-Ordner + * GET /crm/office365/folders/:folderId/messages — E-Mails in einem Ordner (mit ?days=7) */ @Controller('office365') export class Office365Controller { @@ -57,4 +59,28 @@ export class Office365Controller { meta: { listCount: taskLists.length, taskCount: totalTasks }, }; } + + @Get('folders') + async getMailFolders(@Req() req: Request & { user: JwtUser }) { + const jwt = (req.headers.authorization ?? '').replace('Bearer ', ''); + const folders = await this.graphService.getMailFolders(jwt, req.user.sub); + return { success: true, data: folders, meta: { count: folders.length } }; + } + + @Get('folders/:folderId/messages') + async getMailsInFolder( + @Req() req: Request & { user: JwtUser }, + @Param('folderId') folderId: string, + @Query('days') days?: string, + ) { + const jwt = (req.headers.authorization ?? '').replace('Bearer ', ''); + const daysNum = days ? parseInt(days, 10) : 7; + const emails = await this.graphService.getMailsByFolder( + jwt, + req.user.sub, + folderId, + isNaN(daysNum) ? 7 : daysNum, + ); + return { success: true, data: emails, meta: { count: emails.length } }; + } } diff --git a/packages/frontend/src/crm/api.ts b/packages/frontend/src/crm/api.ts index ad426f5..1620325 100644 --- a/packages/frontend/src/crm/api.ts +++ b/packages/frontend/src/crm/api.ts @@ -5,6 +5,7 @@ import api from '../api/client'; import type { Contact, + CrmContactLookup, CreateContactPayload, UpdateContactPayload, ContactsQueryParams, @@ -74,6 +75,7 @@ import type { M365CalendarEvent, M365TaskList, M365Contact, + M365MailFolder, } from './types'; // --- Contacts --- @@ -103,6 +105,11 @@ export const contactsApi = { api .delete>(`/crm/contacts/${id}`) .then((r) => r.data), + + lookupByEmail: (email: string) => + api + .get>('/crm/contacts/lookup', { params: { email } }) + .then((r) => r.data), }; // --- Deals --- @@ -838,4 +845,19 @@ export const office365Api = { '/crm/office365/tasks', ) .then((r) => r.data), + + getMailFolders: () => + api + .get<{ success: boolean; data: M365MailFolder[]; meta: { count: number } }>( + '/crm/office365/folders', + ) + .then((r) => r.data), + + getMailsInFolder: (folderId: string, days: number) => + api + .get<{ success: boolean; data: M365Email[]; meta: { count: number } }>( + `/crm/office365/folders/${folderId}/messages`, + { params: { days } }, + ) + .then((r) => r.data), }; diff --git a/packages/frontend/src/crm/hooks.ts b/packages/frontend/src/crm/hooks.ts index fea1577..41e2f70 100644 --- a/packages/frontend/src/crm/hooks.ts +++ b/packages/frontend/src/crm/hooks.ts @@ -1394,3 +1394,44 @@ export function useOffice365Tasks() { staleTime: 5 * 60 * 1000, }); } + +export function useOffice365MailFolders() { + const { data: integrationsData } = useIntegrations(); + const isConnected = integrationsData?.data?.some( + (i) => i.provider === 'MICROSOFT_365' && i.connected, + ) ?? false; + + return useQuery({ + queryKey: ['office365', 'folders'], + queryFn: () => office365Api.getMailFolders(), + enabled: isConnected, + staleTime: 10 * 60 * 1000, + }); +} + +export function useOffice365MailsInFolder( + folderId: string | null, + days: number, +) { + const { data: integrationsData } = useIntegrations(); + const isConnected = integrationsData?.data?.some( + (i) => i.provider === 'MICROSOFT_365' && i.connected, + ) ?? false; + + return useQuery({ + queryKey: ['office365', 'folder-mails', folderId, days], + queryFn: () => office365Api.getMailsInFolder(folderId!, days), + enabled: isConnected && !!folderId, + staleTime: 2 * 60 * 1000, + }); +} + +export function useContactByEmail(email: string | null) { + return useQuery({ + queryKey: ['crm', 'contacts', 'lookup', email], + queryFn: () => contactsApi.lookupByEmail(email!), + enabled: !!email, + staleTime: 10 * 60 * 1000, + retry: false, + }); +} diff --git a/packages/frontend/src/crm/types.ts b/packages/frontend/src/crm/types.ts index 70f8f35..59b2503 100644 --- a/packages/frontend/src/crm/types.ts +++ b/packages/frontend/src/crm/types.ts @@ -1013,3 +1013,21 @@ export interface M365Contact { jobTitle: string | null; companyName: string | null; } + +export interface M365MailFolder { + id: string; + displayName: string; + totalItemCount: number; + unreadItemCount: number; + childFolderCount: number; + wellKnownName: string | null; +} + +/** Minimaler CRM-Kontakt für E-Mail-Lookup */ +export interface CrmContactLookup { + id: string; + firstName: string | null; + lastName: string | null; + email: string | null; + companyName: string | null; +} diff --git a/packages/frontend/src/shell/DashboardEmailTab.module.css b/packages/frontend/src/shell/DashboardEmailTab.module.css new file mode 100644 index 0000000..0a4167e --- /dev/null +++ b/packages/frontend/src/shell/DashboardEmailTab.module.css @@ -0,0 +1,484 @@ +/* ============================================================ + DashboardEmailTab — Outlook-Postfach im Dashboard + ============================================================ */ + +.root { + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* ── Status / Leer-Zustände ── */ + +.status { + color: var(--color-text-muted); + font-size: 0.9375rem; + padding: 1rem 0; +} + +.errorText { + color: #ef4444; + font-size: 0.9375rem; + padding: 1rem 0; +} + +/* ── Nicht verbunden ── */ + +.notConnected { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + gap: 0.5rem; +} + +.notConnectedIcon { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.notConnectedTitle { + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text); + margin: 0; +} + +.notConnectedSub { + font-size: 0.9375rem; + color: var(--color-text-muted); + margin: 0; +} + +.notConnectedLink { + color: var(--color-primary); + text-decoration: none; +} + +.notConnectedLink:hover { + text-decoration: underline; +} + +/* ── Filter-Leiste ── */ + +.filterBar { + display: flex; + align-items: center; + gap: 0.375rem; + margin-bottom: 0.25rem; +} + +.filterLabel { + font-size: 0.8125rem; + color: var(--color-text-muted); + margin-right: 0.25rem; +} + +.filterBtn { + padding: 0.3125rem 0.875rem; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-text-muted); + cursor: pointer; + transition: all 0.15s; +} + +.filterBtn:hover { + border-color: var(--color-primary); + color: var(--color-primary); +} + +.filterBtnActive { + background: var(--color-primary); + border-color: var(--color-primary); + color: #fff; +} + +.filterBtnActive:hover { + color: #fff; + opacity: 0.9; +} + +/* ── Erfolgs-Banner ── */ + +.successBanner { + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: var(--radius-sm); + padding: 0.625rem 1rem; + font-size: 0.875rem; + color: #16a34a; +} + +/* ── Hauptlayout ── */ + +.layout { + display: flex; + gap: 1rem; + align-items: flex-start; + min-height: 300px; +} + +/* ── Ordner-Baum (Sidebar) ── */ + +.folderTree { + width: 200px; + flex-shrink: 0; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: 0.5rem 0; +} + +.folderList { + list-style: none; + margin: 0; + padding: 0; +} + +.folderItem { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0.5rem 0.875rem; + background: none; + border: none; + text-align: left; + font-size: 0.875rem; + color: var(--color-text-muted); + cursor: pointer; + transition: background 0.12s, color 0.12s; + gap: 0.375rem; +} + +.folderItem:hover { + background: var(--color-bg); + color: var(--color-text); +} + +.folderItemActive { + color: var(--color-primary); + font-weight: 600; + background: rgba(59, 130, 246, 0.07); +} + +.folderItemActive:hover { + color: var(--color-primary); + background: rgba(59, 130, 246, 0.1); +} + +.folderName { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.folderBadge { + font-size: 0.6875rem; + font-weight: 700; + background: var(--color-primary); + color: #fff; + border-radius: 999px; + padding: 0.0625rem 0.375rem; + min-width: 1.125rem; + text-align: center; + flex-shrink: 0; +} + +/* ── E-Mail-Liste ── */ + +.emailList { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.375rem; + min-width: 0; +} + +/* ── E-Mail-Karte ── */ + +.emailCard { + position: relative; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + transition: border-color 0.15s, box-shadow 0.15s; + overflow: hidden; +} + +.emailCard:hover { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.08); +} + +.emailCardUnread { + border-left: 3px solid var(--color-primary); +} + +.emailLink { + display: block; + padding: 0.75rem 1rem; + text-decoration: none; + color: inherit; + padding-right: 6rem; /* Platz für den "Aktivität"-Button */ +} + +.emailHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 0.75rem; + margin-bottom: 0.2rem; +} + +.emailSubject { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.9375rem; + font-weight: 400; + color: var(--color-text); + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.emailCardUnread .emailSubject { + font-weight: 600; +} + +.unreadDot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-primary); + flex-shrink: 0; +} + +.emailDate { + font-size: 0.75rem; + color: var(--color-text-muted); + flex-shrink: 0; +} + +.emailMeta { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.2rem; +} + +.emailFrom { + font-size: 0.8125rem; + color: var(--color-text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.crmBadge { + font-size: 0.6875rem; + font-weight: 700; + background: rgba(34, 197, 94, 0.12); + color: #16a34a; + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: 999px; + padding: 0.0625rem 0.375rem; + flex-shrink: 0; + white-space: nowrap; +} + +.attachBadge { + font-size: 0.75rem; + flex-shrink: 0; +} + +.emailPreview { + font-size: 0.8125rem; + color: var(--color-text-muted); + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ── Aktivität-Button (absolut positioniert innerhalb der Karte) ── */ + +.saveActivityBtn { + position: absolute; + top: 50%; + right: 0.75rem; + transform: translateY(-50%); + padding: 0.3rem 0.625rem; + font-size: 0.75rem; + font-weight: 600; + background: var(--color-bg); + color: var(--color-primary); + border: 1px solid var(--color-primary); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; + opacity: 0; + pointer-events: none; +} + +.emailCard:hover .saveActivityBtn { + opacity: 1; + pointer-events: auto; +} + +.saveActivityBtn:hover { + background: var(--color-primary); + color: #fff; +} + +/* ── Aktivität-Modal ── */ + +.modalOverlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; +} + +.modal { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 1.75rem; + width: 100%; + max-width: 480px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25); +} + +.modalTitle { + font-size: 1.125rem; + font-weight: 600; + margin: 0 0 1.25rem; + color: var(--color-text); +} + +.modalInfo { + display: flex; + flex-direction: column; + gap: 0.5rem; + background: var(--color-bg); + border-radius: var(--radius-sm); + padding: 0.875rem; + margin-bottom: 1.25rem; +} + +.modalInfoRow { + display: flex; + gap: 0.75rem; + align-items: flex-start; +} + +.modalLabel { + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-text-muted); + min-width: 64px; + flex-shrink: 0; +} + +.modalValue { + font-size: 0.875rem; + color: var(--color-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.modalField { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.modalField .modalLabel { + min-width: unset; +} + +.modalTextarea { + width: 100%; + padding: 0.625rem 0.75rem; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text); + font-size: 0.875rem; + resize: vertical; + font-family: inherit; + box-sizing: border-box; +} + +.modalTextarea:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} + +.modalError { + font-size: 0.875rem; + color: #ef4444; + margin: 0 0 1rem; +} + +.modalActions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; +} + +.cancelBtn { + padding: 0.5rem 1.125rem; + background: none; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-muted); + font-size: 0.875rem; + cursor: pointer; + transition: all 0.15s; +} + +.cancelBtn:hover { + border-color: var(--color-text-muted); + color: var(--color-text); +} + +.saveBtn { + padding: 0.5rem 1.25rem; + background: var(--color-primary); + border: none; + border-radius: var(--radius-sm); + color: #fff; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: opacity 0.15s; +} + +.saveBtn:hover:not(:disabled) { + opacity: 0.9; +} + +.saveBtn:disabled { + opacity: 0.6; + cursor: not-allowed; +} diff --git a/packages/frontend/src/shell/DashboardEmailTab.tsx b/packages/frontend/src/shell/DashboardEmailTab.tsx new file mode 100644 index 0000000..6e1b5e1 --- /dev/null +++ b/packages/frontend/src/shell/DashboardEmailTab.tsx @@ -0,0 +1,369 @@ +import { useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + useIntegrations, + useOffice365MailFolders, + useOffice365MailsInFolder, + useContactByEmail, +} from '../crm/hooks'; +import { activitiesApi } from '../crm/api'; +import type { M365Email, M365MailFolder, CrmContactLookup } from '../crm/types'; +import styles from './DashboardEmailTab.module.css'; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +const DAYS_OPTIONS = [ + { label: '1 Tag', value: 1 }, + { label: '7 Tage', value: 7 }, + { label: '14 Tage', value: 14 }, + { label: 'Alle', value: 0 }, +] as const; + +const FOLDER_PRIORITY: Record = { + inbox: 0, + sentitems: 1, + drafts: 2, + archive: 3, + junkemail: 4, + deleteditems: 5, +}; + +function sortFolders(folders: M365MailFolder[]): M365MailFolder[] { + return [...folders].sort((a, b) => { + const pa = FOLDER_PRIORITY[a.wellKnownName ?? ''] ?? 99; + const pb = FOLDER_PRIORITY[b.wellKnownName ?? ''] ?? 99; + if (pa !== pb) return pa - pb; + return a.displayName.localeCompare(b.displayName, 'de'); + }); +} + +function formatEmailDate(iso: string): string { + const d = new Date(iso); + const now = new Date(); + if (d.toDateString() === now.toDateString()) { + return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); + } + return d.toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: '2-digit', + }); +} + +// ── EmailCard ───────────────────────────────────────────────────────────────── + +interface EmailCardProps { + email: M365Email; + onSaveActivity: (email: M365Email, contact: CrmContactLookup) => void; +} + +function EmailCard({ email, onSaveActivity }: EmailCardProps) { + const senderEmail = email.from?.emailAddress?.address ?? null; + const { data: contactData } = useContactByEmail(senderEmail); + const contact = contactData?.data ?? null; + const displayName = email.from?.emailAddress?.name || senderEmail || '—'; + + return ( + + ); +} + +// ── ActivityModal ───────────────────────────────────────────────────────────── + +interface ActivityModalProps { + email: M365Email; + contact: CrmContactLookup; + onClose: () => void; + onSaved: () => void; +} + +function ActivityModal({ email, contact, onClose, onSaved }: ActivityModalProps) { + const [comment, setComment] = useState(''); + const qc = useQueryClient(); + + const contactName = + [contact.firstName, contact.lastName].filter(Boolean).join(' ') || + contact.email || + '—'; + + const { mutate, isPending, isError } = useMutation({ + mutationFn: () => + activitiesApi.create({ + contactId: contact.id, + type: 'EMAIL', + subject: email.subject || '(Kein Betreff)', + description: comment.trim() || undefined, + completedAt: email.receivedDateTime, + }), + onSuccess: () => { + void qc.invalidateQueries({ queryKey: ['crm', 'activities'] }); + onSaved(); + }, + }); + + return ( +
+
e.stopPropagation()}> +

E-Mail als Aktivität speichern

+ +
+
+ Kontakt + {contactName} +
+
+ Betreff + + {email.subject || '(Kein Betreff)'} + +
+
+ Von + + {email.from?.emailAddress?.name || + email.from?.emailAddress?.address || + '—'} + +
+
+ +
+ +