From 2af54246c8502074a69f35b6eafa17f11f73aef4 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Fri, 13 Mar 2026 15:51:10 +0100 Subject: [PATCH] feat(frontend): E-Mail-Popup, Aktivitaeten-Zeitstrahl + Profil in Tab-Leiste - EmailsTab: Outlook-aehnlicher Detail-Popup beim Klick auf E-Mail (Von/An/Datum/Anhang-Meta, Body-Vorschau, In Kontakt speichern als EMAIL-Aktivitaet) - Neues EmailsTab.module.css fuer kompakte Liste und Modal - ContactDetailPage: Aktivitaeten-Filterleiste (Typ + Zeitraum Von/Bis) - ContactDetailPage: Zeitstrahl mit vertikaler Verbindungslinie, farbigen Typ-Badges (Note/Call/Email/Meeting/Task/FollowUp) - DashboardPage: Profil-Bereich (Theme-Schalter, Avatar, Name, Logout) in Tab-Leiste integriert und leicht farblich abgesetzt Co-Authored-By: Claude Sonnet 4.6 --- .../crm/contacts/ContactDetailPage.module.css | 143 +++++++- .../src/crm/contacts/ContactDetailPage.tsx | 160 ++++++++- .../src/crm/contacts/EmailsTab.module.css | 332 ++++++++++++++++++ .../frontend/src/crm/contacts/EmailsTab.tsx | 326 +++++++++++++---- .../src/shell/DashboardPage.module.css | 97 +++++ packages/frontend/src/shell/DashboardPage.tsx | 82 ++++- 6 files changed, 1050 insertions(+), 90 deletions(-) create mode 100644 packages/frontend/src/crm/contacts/EmailsTab.module.css diff --git a/packages/frontend/src/crm/contacts/ContactDetailPage.module.css b/packages/frontend/src/crm/contacts/ContactDetailPage.module.css index 91f6a72..380d0cb 100644 --- a/packages/frontend/src/crm/contacts/ContactDetailPage.module.css +++ b/packages/frontend/src/crm/contacts/ContactDetailPage.module.css @@ -191,22 +191,99 @@ font-weight: 500; } -/* ---- Timeline ---- */ +/* ---- Aktivitäten-Filter ---- */ +.activityFilterBar { + display: flex; + align-items: center; + gap: 0.625rem; + flex-wrap: wrap; + padding: 0.625rem 0.875rem; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + margin-bottom: 1rem; +} + +.activityFilterSelect { + padding: 0.25rem 0.5rem; + font-size: 0.8125rem; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text); + cursor: pointer; + height: 30px; +} + +.activityFilterDate { + padding: 0.25rem 0.5rem; + font-size: 0.8125rem; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text); + height: 30px; + width: 130px; +} + +.activityFilterLabel { + font-size: 0.75rem; + color: var(--color-text-muted); + white-space: nowrap; +} + +.activityFilterReset { + padding: 0.25rem 0.625rem; + font-size: 0.75rem; + background: transparent; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-muted); + cursor: pointer; + transition: all 0.12s; + white-space: nowrap; +} + +.activityFilterReset:hover { + color: var(--color-text); + border-color: var(--color-text-muted); +} + +.activityFilterCount { + margin-left: auto; + font-size: 0.75rem; + color: var(--color-text-muted); + white-space: nowrap; +} + +/* ---- Timeline (Zeitstrahl) ---- */ .timeline { + position: relative; display: flex; flex-direction: column; - gap: 0; +} + +/* Verbindungslinie links */ +.timeline::before { + content: ''; + position: absolute; + left: 15px; /* Mitte des 32px Icons */ + top: 24px; + bottom: 16px; + width: 2px; + background: var(--color-border); + pointer-events: none; } .timelineItem { + position: relative; display: flex; - gap: 0.75rem; - padding: 0.75rem 0; - border-bottom: 1px solid var(--color-border); + gap: 0.875rem; + padding: 0.625rem 0; } .timelineItem:last-child { - border-bottom: none; + padding-bottom: 0; } .timelineIcon { @@ -214,35 +291,81 @@ width: 32px; height: 32px; border-radius: 50%; - background: var(--color-bg); - border: 1px solid var(--color-border); + background: var(--color-bg-card); + border: 2px solid var(--color-border); display: flex; align-items: center; justify-content: center; + position: relative; + z-index: 1; + transition: border-color 0.15s; } +/* Farbige Icon-Kreise pro Typ */ +.timelineIconNote { border-color: #94a3b8; color: #64748b; } +.timelineIconCall { border-color: #22c55e; color: #16a34a; } +.timelineIconEmail { border-color: #3b82f6; color: #2563eb; } +.timelineIconMeeting { border-color: #a855f7; color: #9333ea; } +.timelineIconTask { border-color: #f59e0b; color: #d97706; } +.timelineIconFollowup { border-color: #ef4444; color: #dc2626; } + .timelineContent { flex: 1; min-width: 0; + padding-top: 0.1875rem; } .timelineSubject { font-size: 0.875rem; font-weight: 500; color: var(--color-text); + line-height: 1.35; } .timelineMeta { + display: flex; + align-items: center; + gap: 0.375rem; + margin-top: 0.1875rem; +} + +.timelineTypeBadge { + display: inline-flex; + align-items: center; + padding: 0.0625rem 0.4375rem; + border-radius: 9999px; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.4px; + white-space: nowrap; +} + +.timelineBadgeNote { background: #f1f5f9; color: #64748b; } +.timelineBadgeCall { background: #dcfce7; color: #16a34a; } +.timelineBadgeEmail { background: #dbeafe; color: #2563eb; } +.timelineBadgeMeeting { background: #f3e8ff; color: #9333ea; } +.timelineBadgeTask { background: #fef9c3; color: #d97706; } +.timelineBadgeFollowup { background: #fee2e2; color: #dc2626; } + +.timelineDate { font-size: 0.75rem; color: var(--color-text-muted); - margin-top: 0.125rem; } .timelineDesc { font-size: 0.8125rem; color: var(--color-text-secondary); - margin-top: 0.375rem; + margin-top: 0.3125rem; white-space: pre-wrap; + line-height: 1.45; +} + +.timelineEmpty { + text-align: center; + padding: 1.5rem 0; + color: var(--color-text-muted); + font-size: 0.875rem; } /* ---- Notes ---- */ diff --git a/packages/frontend/src/crm/contacts/ContactDetailPage.tsx b/packages/frontend/src/crm/contacts/ContactDetailPage.tsx index fff2df5..de6bb56 100644 --- a/packages/frontend/src/crm/contacts/ContactDetailPage.tsx +++ b/packages/frontend/src/crm/contacts/ContactDetailPage.tsx @@ -119,6 +119,11 @@ export function ContactDetailPage() { const [isDeleteOpen, setDeleteOpen] = useState(false); const [m365Tab, setM365Tab] = useState('emails'); + // Aktivitäten-Filter + const [actTypeFilter, setActTypeFilter] = useState('ALL'); + const [actDateFrom, setActDateFrom] = useState(''); + const [actDateTo, setActDateTo] = useState(''); + if (isLoading) return

Laden…

; if (error || !data) return ( @@ -128,9 +133,31 @@ export function ContactDetailPage() { ); const contact = data.data; - const activities: Activity[] = contact.activities ?? []; + const allActivities: Activity[] = contact.activities ?? []; const deals = dealsData?.data ?? []; + // Gefilterte Aktivitäten + const activities = allActivities.filter((act) => { + if (actTypeFilter !== 'ALL' && act.type !== actTypeFilter) return false; + if (actDateFrom) { + const actDate = new Date(act.createdAt).toISOString().slice(0, 10); + if (actDate < actDateFrom) return false; + } + if (actDateTo) { + const actDate = new Date(act.createdAt).toISOString().slice(0, 10); + if (actDate > actDateTo) return false; + } + return true; + }); + + const isFilterActive = actTypeFilter !== 'ALL' || actDateFrom !== '' || actDateTo !== ''; + + function resetFilters() { + setActTypeFilter('ALL'); + setActDateFrom(''); + setActDateTo(''); + } + return (
{/* Back link */} @@ -495,6 +522,7 @@ export function ContactDetailPage() { {/* ── Aktivitäten (full width below) ── */}
+ {/* Karten-Header */}

Aktivitäten + {allActivities.length > 0 && ( + + ({allActivities.length}) + + )}

+ {/* Filter-Leiste */} + {allActivities.length > 0 && ( +
+ Typ: + + + Von: + setActDateFrom(e.target.value)} + /> + + Bis: + setActDateTo(e.target.value)} + /> + + {isFilterActive && ( + + )} + + + {activities.length} von {allActivities.length} + +
+ )} + + {/* Zeitstrahl */} {activities.length === 0 ? ( -

- Keine Aktivitäten vorhanden +

+ {isFilterActive + ? 'Keine Aktivitäten für den gewählten Filter' + : 'Keine Aktivitäten vorhanden'}

) : (
- {activities.map((act) => ( -
-
- {activityIcon(act.type)} -
-
-
{act.subject}
-
- {ACTIVITY_TYPE_LABELS[act.type]} ·{' '} - {formatDate(act.createdAt)} + {activities.map((act) => { + const iconClass = { + NOTE: styles.timelineIconNote, + CALL: styles.timelineIconCall, + EMAIL: styles.timelineIconEmail, + MEETING: styles.timelineIconMeeting, + TASK: styles.timelineIconTask, + FOLLOWUP: styles.timelineIconFollowup, + }[act.type] ?? ''; + + const badgeClass = { + NOTE: styles.timelineBadgeNote, + CALL: styles.timelineBadgeCall, + EMAIL: styles.timelineBadgeEmail, + MEETING: styles.timelineBadgeMeeting, + TASK: styles.timelineBadgeTask, + FOLLOWUP: styles.timelineBadgeFollowup, + }[act.type] ?? ''; + + return ( +
+
+ {activityIcon(act.type)}
- {act.description && ( -
- {act.description} +
+
{act.subject}
+
+ + {ACTIVITY_TYPE_LABELS[act.type]} + + + {formatDate(act.createdAt)} +
- )} + {act.description && ( +
+ {act.description} +
+ )} +
-
- ))} + ); + })}
)}
diff --git a/packages/frontend/src/crm/contacts/EmailsTab.module.css b/packages/frontend/src/crm/contacts/EmailsTab.module.css new file mode 100644 index 0000000..833e508 --- /dev/null +++ b/packages/frontend/src/crm/contacts/EmailsTab.module.css @@ -0,0 +1,332 @@ +/* ============================================================ + EmailsTab – E-Mail-Liste + Detail-Modal + ============================================================ */ + +/* ---- E-Mail-Liste ---- */ +.emailList { + display: flex; + flex-direction: column; + gap: 0.3125rem; +} + +.emailItem { + display: flex; + align-items: flex-start; + gap: 0.625rem; + padding: 0.5rem 0.75rem; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-left-width: 3px; + border-left-color: transparent; + border-radius: var(--radius-sm); + cursor: pointer; + transition: background 0.12s, border-color 0.12s; + text-align: left; + width: 100%; + min-width: 0; +} + +.emailItem:hover { + background: var(--color-bg-subtle, var(--color-bg)); +} + +.emailItemUnread { + border-left-color: var(--color-primary); +} + +.emailMeta { + flex: 1; + min-width: 0; + overflow: hidden; +} + +.emailRow1 { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.125rem; +} + +.emailSubject { + font-size: 0.8125rem; + font-weight: 400; + color: var(--color-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} + +.emailSubjectUnread { + font-weight: 600; +} + +.emailTime { + font-size: 0.75rem; + color: var(--color-text-muted); + flex-shrink: 0; + white-space: nowrap; +} + +.emailFrom { + font-size: 0.75rem; + color: var(--color-text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.emailPreview { + font-size: 0.75rem; + color: var(--color-text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: 0.125rem; + opacity: 0.8; +} + +/* ---- Status-Dots ---- */ +.unreadDot { + flex-shrink: 0; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-primary); + margin-top: 0.4rem; +} + +.readSpacer { + flex-shrink: 0; + width: 7px; +} + +/* ---- Anhang-Badge ---- */ +.attachBadge { + flex-shrink: 0; + font-size: 0.875rem; + margin-top: 0.125rem; + opacity: 0.7; +} + +/* ============================================================ + Detail-Modal + ============================================================ */ + +.backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 200; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.modal { + background: var(--color-bg-card); + border-radius: var(--radius-md); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25); + border: 1px solid var(--color-border); + width: 100%; + max-width: 660px; + max-height: 82vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Modal-Header */ +.modalHeader { + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--color-border); + display: flex; + align-items: flex-start; + gap: 0.75rem; + flex-shrink: 0; +} + +.modalHeaderLeft { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.modalSubject { + font-size: 1rem; + font-weight: 600; + color: var(--color-text); + word-break: break-word; + margin: 0; + line-height: 1.35; +} + +.modalUnreadBadge { + display: inline-block; + padding: 0.125rem 0.5rem; + background: var(--color-primary); + color: white; + border-radius: 9999px; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + align-self: flex-start; +} + +.modalClose { + flex-shrink: 0; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--color-text-muted); + font-size: 1.375rem; + cursor: pointer; + border-radius: var(--radius-sm); + transition: color 0.12s, background 0.12s; + line-height: 1; +} + +.modalClose:hover { + color: var(--color-text); + background: var(--color-bg); +} + +/* Modal-Body */ +.modalBody { + flex: 1; + overflow-y: auto; + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Meta-Tabelle (Von / An / Datum / Anhang) */ +.metaTable { + display: grid; + grid-template-columns: 52px 1fr; + gap: 0.3125rem 0.625rem; + font-size: 0.8125rem; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: 0.75rem; +} + +.metaLabel { + color: var(--color-text-muted); + font-weight: 500; + padding-top: 0.0625rem; +} + +.metaValue { + color: var(--color-text); + word-break: break-word; +} + +/* Body-Vorschau */ +.bodyPreview { + font-size: 0.875rem; + color: var(--color-text-secondary); + white-space: pre-wrap; + line-height: 1.65; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: 1rem; +} + +.bodyHint { + font-size: 0.75rem; + color: var(--color-text-muted); + text-align: center; + margin: 0; +} + +/* Speichern-Bereich */ +.saveSection { + border-top: 1px solid var(--color-border); + padding-top: 0.875rem; + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.saveBtn { + padding: 0.375rem 0.875rem; + background: var(--color-primary); + color: white; + border: none; + border-radius: var(--radius-sm); + font-size: 0.8125rem; + font-weight: 600; + cursor: pointer; + transition: opacity 0.12s; + white-space: nowrap; +} + +.saveBtn:disabled { + opacity: 0.65; + cursor: wait; +} + +.saveBtnSuccess { + background: #16a34a; +} + +.saveBtnError { + background: var(--color-error, #dc2626); +} + +.saveMsg { + font-size: 0.75rem; + color: var(--color-text-muted); +} + +/* Modal-Footer */ +.modalFooter { + padding: 0.75rem 1.25rem; + border-top: 1px solid var(--color-border); + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + flex-shrink: 0; +} + +.modalFooterDate { + font-size: 0.75rem; + color: var(--color-text-muted); +} + +.outlookBtn { + padding: 0.375rem 0.875rem; + background: transparent; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.8125rem; + color: var(--color-text-secondary); + cursor: pointer; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 0.375rem; + transition: all 0.12s; +} + +.outlookBtn:hover { + background: var(--color-bg); + color: var(--color-text); + text-decoration: none; +} diff --git a/packages/frontend/src/crm/contacts/EmailsTab.tsx b/packages/frontend/src/crm/contacts/EmailsTab.tsx index dd30893..e8bd67f 100644 --- a/packages/frontend/src/crm/contacts/EmailsTab.tsx +++ b/packages/frontend/src/crm/contacts/EmailsTab.tsx @@ -1,6 +1,8 @@ -import { useContactEmails, useIntegrations } from '../hooks'; +import { useState } from 'react'; +import { useContactEmails, useIntegrations, useCreateActivity } from '../hooks'; import { integrationsApi } from '../api'; -import type { M365Email } from '../types'; +import type { M365Email, M365EmailAddress } from '../types'; +import styles from './EmailsTab.module.css'; interface Props { contactId: string; @@ -16,19 +18,194 @@ function formatEmailDate(iso: string): string { }); } +function formatEmailTime(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: 'numeric' }); +} + +function recipientsText( + recipients: Array<{ emailAddress: M365EmailAddress }>, +): string { + return recipients + .map((r) => r.emailAddress.name ?? r.emailAddress.address ?? '—') + .join(', '); +} + +type SaveState = 'idle' | 'saving' | 'saved' | 'error'; + +/* ── Detail-Popup (Outlook-ähnlich) ───────────────────────────────────────── */ + +function EmailDetailModal({ + email, + contactId, + onClose, +}: { + email: M365Email; + contactId: string; + onClose: () => void; +}) { + const createActivity = useCreateActivity(); + const [saveState, setSaveState] = useState('idle'); + + const fromName = + email.from?.emailAddress?.name ?? email.from?.emailAddress?.address ?? '—'; + const toText = email.toRecipients?.length + ? recipientsText(email.toRecipients) + : '—'; + + async function handleSave() { + if (saveState === 'saved') return; + setSaveState('saving'); + try { + await createActivity.mutateAsync({ + contactId, + type: 'EMAIL', + subject: email.subject ?? '(kein Betreff)', + description: email.bodyPreview ?? undefined, + scheduledAt: email.receivedDateTime, + completedAt: email.receivedDateTime, + }); + setSaveState('saved'); + } catch { + setSaveState('error'); + } + } + + return ( +
+
e.stopPropagation()}> + {/* Header */} +
+
+

{email.subject ?? '(kein Betreff)'}

+ {!email.isRead && ( + Ungelesen + )} +
+ +
+ + {/* Body */} +
+ {/* Meta: Von / An / Datum / Anhang */} +
+ Von + {fromName} + + An + {toText} + + Datum + {formatEmailDate(email.receivedDateTime)} + + {email.hasAttachments && ( + <> + Anhang + 📎 Anhänge vorhanden + + )} +
+ + {/* Body-Vorschau */} + {email.bodyPreview ? ( + <> +
{email.bodyPreview}
+

+ Vorschau — vollständige E-Mail in Outlook öffnen +

+ + ) : ( +

Kein Vorschautext verfügbar

+ )} + + {/* In Kontakt speichern */} +
+ + {saveState === 'saved' && ( + + E-Mail als Aktivität gespeichert + + )} +
+
+ + {/* Footer */} +
+ + {formatEmailDate(email.receivedDateTime)} + + + + + + + + In Outlook öffnen + +
+
+
+ ); +} + +/* ── Hauptkomponente ──────────────────────────────────────────────────────── */ + export function EmailsTab({ contactId }: Props) { const { data: integrationsData } = useIntegrations(); - const isConnected = integrationsData?.data?.some( - (i) => i.provider === 'MICROSOFT_365' && i.connected, - ) ?? false; + const isConnected = + (integrationsData?.data?.some( + (i) => i.provider === 'MICROSOFT_365' && i.connected, + ) ?? false); const { data, isLoading, error } = useContactEmails(contactId); const emails: M365Email[] = data?.data ?? []; + const [selectedEmail, setSelectedEmail] = useState(null); if (!isConnected) { return (
-

+

Verbinden Sie Microsoft 365, um E-Mails zu diesem Kontakt zu sehen.

+ {email.hasAttachments && ( + + 📎 + + )} + + ))} +
+ + {selectedEmail !== null && ( + setSelectedEmail(null)} + /> + )} + ); } diff --git a/packages/frontend/src/shell/DashboardPage.module.css b/packages/frontend/src/shell/DashboardPage.module.css index 68916b2..5a353ec 100644 --- a/packages/frontend/src/shell/DashboardPage.module.css +++ b/packages/frontend/src/shell/DashboardPage.module.css @@ -22,6 +22,7 @@ .tabBar { display: flex; + align-items: stretch; gap: 0; border-bottom: 2px solid var(--color-border); margin-bottom: 1.75rem; @@ -55,6 +56,102 @@ min-height: 200px; } +/* ── Profil-Bereich in der Tab-Leiste (rechts) ── */ + +.tabBarProfile { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0 0.75rem; + border-left: 1px solid var(--color-border); + background: var(--color-bg-card); + border-radius: var(--radius-sm) var(--radius-sm) 0 0; + /* leicht farblich abgesetzt */ + box-shadow: inset 0 -2px 0 var(--color-border); +} + +.tabBarThemeGroup { + display: flex; + gap: 1px; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: 2px; +} + +.tabBarThemeBtn { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + border-radius: 3px; + color: var(--color-text-muted); + font-size: 0.75rem; + cursor: pointer; + transition: all 0.15s; +} + +.tabBarThemeBtn:hover { + color: var(--color-text); + background: var(--color-bg-card); +} + +.tabBarThemeBtnActive { + background: var(--color-primary) !important; + color: white !important; +} + +.tabBarUser { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.1875rem 0.4375rem 0.1875rem 0.1875rem; + border-radius: var(--radius-sm); + border: 1px solid transparent; + cursor: pointer; + transition: all 0.15s; + text-decoration: none; +} + +.tabBarUser:hover { + background: var(--color-bg); + border-color: var(--color-border); +} + +.tabBarUserName { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-text); + white-space: nowrap; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; +} + +.tabBarLogout { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-muted); + cursor: pointer; + transition: all 0.15s; + flex-shrink: 0; +} + +.tabBarLogout:hover { + background: var(--color-bg); + color: var(--color-text); +} + /* ── Home-Layout (Inhalt links + Agenda rechts) ── */ .homeLayout { diff --git a/packages/frontend/src/shell/DashboardPage.tsx b/packages/frontend/src/shell/DashboardPage.tsx index 282bcd7..0a48efc 100644 --- a/packages/frontend/src/shell/DashboardPage.tsx +++ b/packages/frontend/src/shell/DashboardPage.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useAuth } from '../auth/AuthContext'; +import { UserAvatar } from '../components/UserAvatar'; +import { useTheme } from '../theme/ThemeContext'; import { WeatherWidget } from '../components/WeatherWidget'; import { AnalogClock } from '../components/AnalogClock'; import { DashboardEmailTab } from './DashboardEmailTab'; @@ -688,11 +690,24 @@ function HomeTab({ // ── Main ────────────────────────────────────────────────────────────────────── +const THEME_OPTIONS_DASH = [ + { value: 'light' as const, icon: '☀' }, + { value: 'dark' as const, icon: '☾' }, + { value: 'system' as const, icon: '⚙' }, +]; + export function DashboardPage() { - const { user } = useAuth(); + const { user, logout } = useAuth(); const location = useLocation(); + const navigate = useNavigate(); + const { mode, setMode } = useTheme(); const [activeTab, setActiveTab] = useState('home'); + const handleLogout = async () => { + await logout(); + navigate('/login'); + }; + // Immer auf Home-Tab springen wenn Dashboard-NavLink geklickt wird useEffect(() => { setActiveTab('home'); @@ -712,6 +727,69 @@ export function DashboardPage() { {tab.label} ))} + + {/* Profil-Bereich rechts in der Tab-Leiste */} +
+ {/* Theme-Schalter */} +
+ {THEME_OPTIONS_DASH.map((opt) => ( + + ))} +
+ + {/* Benutzer (→ Profil) */} +
navigate('/profile')} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') navigate('/profile'); + }} + title="Profil bearbeiten" + > + + + {user?.firstName} {user?.lastName} + +
+ + {/* Abmelden */} + +
{/* Tab-Inhalt */}