feat(dashboard): E-Mail Lesefenster + Kalender Umbau

E-Mail Tab:
- Klick auf E-Mail öffnet Detail-Modal (Lesefenster wie Outlook)
- Modal zeigt: Betreff, Absender, Datum, Anhang-Info, Body-Vorschau
- CRM-Bereich: gefundener Kontakt mit "Im CRM öffnen" + "Als Aktivität"
  speichern; kein Kontakt → "Kontakt anlegen" navigiert zu /crm/contacts
- "In Outlook öffnen" Link im Footer des Modals

Kalender Tab:
- WeekView: nur Arbeitstage Mo–Fr (5-Spalten-Grid statt 7)
- Neue Ansicht "Agenda": 14-Tage-Listenansicht (eigener Toggle-Button)
- Tages-Agenda nur bei Monat- und Wochenansicht sichtbar (nicht Agenda-View)
- Home-Tab: Tages-Agenda des heutigen Tages als Widget rechts
  (nur sichtbar wenn M365 verbunden)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-13 11:36:30 +01:00
parent f2ed8d0a93
commit fbf0b33a1f
6 changed files with 759 additions and 82 deletions

View file

@ -277,11 +277,11 @@
padding-left: 4px;
}
/* ── Wochenansicht ── */
/* ── Wochenansicht (MoFr) ── */
.weekGrid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-columns: repeat(5, 1fr); /* nur Arbeitstage */
}
.weekCol {
@ -383,6 +383,130 @@
text-overflow: ellipsis;
}
/* ── Agenda-Listenansicht (eigene View, 14 Tage) ── */
.agendaFullView {
display: flex;
flex-direction: column;
}
.agendaFullDay {
display: flex;
align-items: flex-start;
gap: 0;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
transition: background 0.1s;
}
.agendaFullDay:last-child {
border-bottom: none;
}
.agendaFullDay:hover {
background: rgba(59, 130, 246, 0.03);
}
.agendaFullDaySelected {
background: rgba(59, 130, 246, 0.06) !important;
}
.agendaFullDayHdr {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 72px;
flex-shrink: 0;
padding: 0.875rem 0.5rem;
gap: 0.125rem;
border-right: 1px solid var(--color-border);
}
.agendaFullDayHdrToday .agendaFullNum {
background: var(--color-primary);
color: #fff;
border-radius: 50%;
}
.agendaFullWeekday {
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.agendaFullNum {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
.agendaFullNumToday {
background: var(--color-primary);
color: #fff;
border-radius: 50%;
}
.agendaFullMonth {
font-size: 0.6875rem;
color: var(--color-text-muted);
}
.agendaFullEvents {
flex: 1;
padding: 0.625rem 1rem;
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.agendaFullEmpty {
font-size: 0.8125rem;
color: var(--color-text-muted);
padding: 0.25rem 0;
}
.agendaFullEvent {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.375rem 0.625rem;
border-left: 3px solid transparent;
border-radius: 2px;
text-decoration: none;
color: inherit;
background: var(--color-bg);
transition: background 0.12s;
}
.agendaFullEvent:hover {
background: rgba(59, 130, 246, 0.07);
}
.agendaFullEventTime {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-muted);
flex-shrink: 0;
min-width: 42px;
}
.agendaFullEventSubj {
font-size: 0.875rem;
color: var(--color-text);
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Tages-Agenda (rechte Spalte) ── */
.agenda {

View file

@ -3,7 +3,7 @@ import { useIntegrations, useOffice365CalendarRange } from '../crm/hooks';
import type { M365CalendarEvent } from '../crm/types';
import styles from './DashboardCalendarTab.module.css';
type ViewMode = 'month' | 'week';
export type CalendarViewMode = 'month' | 'week' | 'agenda';
// ── Date Helpers ───────────────────────────────────────────────────────────────
@ -36,10 +36,10 @@ function getMonthCells(date: Date): Date[] {
return Array.from({ length: 42 }, (_, i) => addDays(gridStart, i));
}
/** 7 Tage der Woche (MoSo) */
function getWeekDays(date: Date): Date[] {
/** 5 Arbeitstage der Woche (MoFr) */
function getWorkWeekDays(date: Date): Date[] {
const monday = startOfWeekMonday(date);
return Array.from({ length: 7 }, (_, i) => addDays(monday, i));
return Array.from({ length: 5 }, (_, i) => addDays(monday, i));
}
function toISODate(date: Date): string {
@ -70,9 +70,9 @@ function eventColor(id: string): string {
return EVENT_COLORS[h];
}
// ── Day Agenda (rechte Spalte) ─────────────────────────────────────────────────
// ── Day Agenda (Tages-Detailansicht, auch für Home) ───────────────────────────
function DayAgenda({
export function DayAgenda({
day,
events,
}: {
@ -137,6 +137,78 @@ function DayAgenda({
);
}
// ── Agenda-Listenansicht (eigene Ansicht, zeigt mehrere Tage) ─────────────────
function AgendaView({
currentDate,
events,
selectedDay,
onDayClick,
}: {
currentDate: Date;
events: M365CalendarEvent[];
selectedDay: Date;
onDayClick: (d: Date) => void;
}) {
const today = new Date();
// 14 Tage ab currentDate
const days = Array.from({ length: 14 }, (_, i) => addDays(currentDate, i));
return (
<div className={styles.agendaFullView}>
{days.map((day) => {
const dayEvents = getEventsForDay(events, day);
const isToday = isSameDay(day, today);
const isSelected = isSameDay(day, selectedDay);
return (
<div
key={day.toISOString()}
className={`${styles.agendaFullDay} ${isSelected ? styles.agendaFullDaySelected : ''}`}
onClick={() => onDayClick(day)}
>
<div className={`${styles.agendaFullDayHdr} ${isToday ? styles.agendaFullDayHdrToday : ''}`}>
<span className={styles.agendaFullWeekday}>
{day.toLocaleDateString('de-DE', { weekday: 'short' })}
</span>
<span className={`${styles.agendaFullNum} ${isToday ? styles.agendaFullNumToday : ''}`}>
{day.getDate()}
</span>
<span className={styles.agendaFullMonth}>
{day.toLocaleDateString('de-DE', { month: 'short' })}
</span>
</div>
<div className={styles.agendaFullEvents}>
{dayEvents.length === 0 ? (
<span className={styles.agendaFullEmpty}>Kein Termin</span>
) : (
dayEvents.map((e) => (
<a
key={e.id}
href={e.webLink}
target="_blank"
rel="noopener noreferrer"
className={styles.agendaFullEvent}
style={{ borderLeftColor: eventColor(e.id) }}
onClick={(ev) => ev.stopPropagation()}
>
<span className={styles.agendaFullEventTime}>
{formatTime(e.start.dateTime)}
</span>
<span className={styles.agendaFullEventSubj}>{e.subject}</span>
{e.isOnlineMeeting && (
<span className={styles.onlineBadge}>Online</span>
)}
</a>
))
)}
</div>
</div>
);
})}
</div>
);
}
// ── Monatsansicht ─────────────────────────────────────────────────────────────
const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
@ -211,7 +283,7 @@ function MonthView({
);
}
// ── Wochenansicht ─────────────────────────────────────────────────────────────
// ── Wochenansicht (MoFr) ─────────────────────────────────────────────────────
function WeekView({
currentDate,
@ -225,7 +297,7 @@ function WeekView({
onDayClick: (d: Date) => void;
}) {
const today = new Date();
const weekDays = getWeekDays(currentDate);
const weekDays = getWorkWeekDays(currentDate); // nur MoFr
return (
<div className={styles.weekGrid}>
@ -287,7 +359,7 @@ export function DashboardCalendarTab() {
(i) => i.provider === 'MICROSOFT_365' && i.connected,
) ?? false;
const [viewMode, setViewMode] = useState<ViewMode>('month');
const [viewMode, setViewMode] = useState<CalendarViewMode>('month');
const [currentDate, setCurrentDate] = useState(() => new Date());
const [selectedDay, setSelectedDay] = useState(() => new Date());
@ -297,12 +369,16 @@ export function DashboardCalendarTab() {
? startOfWeekMonday(
new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
)
: startOfWeekMonday(currentDate);
: viewMode === 'week'
? startOfWeekMonday(currentDate)
: currentDate; // agenda: ab currentDate
const rangeEnd =
viewMode === 'month'
? addDays(rangeStart, 42) // 6 Wochen
: addDays(startOfWeekMonday(currentDate), 7);
? addDays(rangeStart, 42)
: viewMode === 'week'
? addDays(startOfWeekMonday(currentDate), 5) // MoFr
: addDays(currentDate, 14); // 14 Tage Agenda
const { data: eventsData, isLoading, error } = useOffice365CalendarRange(
toISODate(rangeStart),
@ -314,8 +390,10 @@ export function DashboardCalendarTab() {
const d = new Date(currentDate);
if (viewMode === 'month') {
d.setMonth(d.getMonth() + delta);
} else {
} else if (viewMode === 'week') {
d.setDate(d.getDate() + delta * 7);
} else {
d.setDate(d.getDate() + delta * 14);
}
setCurrentDate(d);
};
@ -327,17 +405,25 @@ export function DashboardCalendarTab() {
};
// Anzeigebezeichnung
const rangeLabel =
viewMode === 'month'
? currentDate.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })
: (() => {
const days = getWeekDays(currentDate);
const rangeLabel = (() => {
if (viewMode === 'month') {
return currentDate.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
}
if (viewMode === 'week') {
const days = getWorkWeekDays(currentDate);
const f = days[0];
const l = days[6];
const l = days[4];
const sm = f.getMonth() === l.getMonth();
return sm
? `${f.getDate()}. ${l.getDate()}. ${l.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })}`
: `${f.getDate()}. ${f.toLocaleDateString('de-DE', { month: 'short' })} ${l.getDate()}. ${l.toLocaleDateString('de-DE', { month: 'short', year: 'numeric' })}`;
}
// agenda
const end = addDays(currentDate, 13);
const sm = currentDate.getMonth() === end.getMonth();
return sm
? `${currentDate.getDate()}. ${end.getDate()}. ${end.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })}`
: `${currentDate.getDate()}. ${currentDate.toLocaleDateString('de-DE', { month: 'short' })} ${end.getDate()}. ${end.toLocaleDateString('de-DE', { month: 'short', year: 'numeric' })}`;
})();
if (intLoading) {
@ -403,6 +489,13 @@ export function DashboardCalendarTab() {
>
Woche
</button>
<button
type="button"
className={`${styles.viewBtn} ${viewMode === 'agenda' ? styles.viewBtnActive : ''}`}
onClick={() => setViewMode('agenda')}
>
Agenda
</button>
</div>
</div>
@ -416,18 +509,19 @@ export function DashboardCalendarTab() {
</p>
)}
{/* Hauptbereich: Kalender + Agenda */}
{/* Hauptbereich */}
{!isLoading && (
<div className={styles.content}>
<div className={styles.calendarPanel}>
{viewMode === 'month' ? (
{viewMode === 'month' && (
<MonthView
currentDate={currentDate}
selectedDay={selectedDay}
events={events}
onDayClick={setSelectedDay}
/>
) : (
)}
{viewMode === 'week' && (
<WeekView
currentDate={currentDate}
selectedDay={selectedDay}
@ -435,9 +529,20 @@ export function DashboardCalendarTab() {
onDayClick={setSelectedDay}
/>
)}
{viewMode === 'agenda' && (
<AgendaView
currentDate={currentDate}
events={events}
selectedDay={selectedDay}
onDayClick={setSelectedDay}
/>
)}
</div>
{/* Tages-Agenda nur bei Monat- und Wochenansicht */}
{(viewMode === 'month' || viewMode === 'week') && (
<DayAgenda day={selectedDay} events={events} />
)}
</div>
)}
</div>

View file

@ -212,6 +212,7 @@
border-radius: var(--radius-sm);
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
cursor: pointer;
}
.emailCard:hover {
@ -223,12 +224,10 @@
border-left: 3px solid var(--color-primary);
}
.emailLink {
.emailCardInner {
display: block;
padding: 0.75rem 1rem;
text-decoration: none;
color: inherit;
padding-right: 6rem; /* Platz für den "Aktivität"-Button */
}
.emailHeader {
@ -307,36 +306,261 @@
word-break: break-word;
}
/* ── Aktivität-Button (absolut positioniert innerhalb der Karte) ── */
/* ── Detail-Modal (E-Mail Lesefenster) ── */
.saveActivityBtn {
position: absolute;
top: 0.75rem;
right: 0.75rem;
padding: 0.3rem 0.625rem;
.detailModal {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius);
width: 100%;
max-width: 680px;
max-height: 88vh;
display: flex;
flex-direction: column;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);
overflow: hidden;
}
.detailHeader {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.25rem 1.5rem 1rem;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.detailSubject {
font-size: 1.0625rem;
font-weight: 600;
color: var(--color-text);
margin: 0;
line-height: 1.35;
}
.closeBtn {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
background: none;
border: none;
color: var(--color-text-muted);
font-size: 1rem;
cursor: pointer;
border-radius: 50%;
transition: background 0.12s, color 0.12s;
margin-top: -0.125rem;
}
.closeBtn:hover {
background: var(--color-bg);
color: var(--color-text);
}
.detailMeta {
display: flex;
flex-direction: column;
gap: 0.3rem;
padding: 0.875rem 1.5rem;
background: var(--color-bg);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.detailMetaRow {
display: flex;
align-items: baseline;
gap: 0.75rem;
}
.detailMetaLabel {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-muted);
min-width: 48px;
flex-shrink: 0;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.detailMetaValue {
font-size: 0.875rem;
color: var(--color-text);
}
.detailSenderEmail {
color: var(--color-text-muted);
font-size: 0.8125rem;
}
.detailBody {
flex: 1;
overflow-y: auto;
padding: 1.25rem 1.5rem;
font-size: 0.9rem;
color: var(--color-text);
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
min-height: 100px;
max-height: 280px;
background: var(--color-bg-card);
}
.detailBodyEmpty {
color: var(--color-text-muted);
font-style: italic;
}
/* ── CRM-Bereich im Detail-Modal ── */
.detailCrm {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.875rem 1.5rem;
background: var(--color-bg);
color: var(--color-primary);
border-top: 1px solid var(--color-border);
flex-shrink: 0;
flex-wrap: wrap;
}
.detailCrmTitle {
font-size: 0.75rem;
font-weight: 700;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
flex-shrink: 0;
}
.detailCrmLoading {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.detailCrmFound {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.detailCrmInfo {
display: flex;
align-items: center;
gap: 0.5rem;
}
.detailCrmAvatar {
font-size: 1.125rem;
}
.detailCrmName {
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text);
}
.detailCrmCompany {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.detailCrmActions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.detailCrmOpenBtn {
padding: 0.375rem 0.875rem;
background: none;
border: 1px solid var(--color-primary);
border-radius: var(--radius-sm);
color: var(--color-primary);
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
opacity: 0;
pointer-events: none;
}
.emailCard:hover .saveActivityBtn {
opacity: 1;
pointer-events: auto;
}
.saveActivityBtn:hover {
.detailCrmOpenBtn:hover {
background: var(--color-primary);
color: #fff;
}
.detailCrmMissing {
flex: 1;
display: flex;
align-items: center;
gap: 0.875rem;
flex-wrap: wrap;
}
.detailCrmMissingText {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.detailCrmCreateBtn {
padding: 0.375rem 0.875rem;
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.detailCrmCreateBtn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
/* ── Detail-Footer ── */
.detailFooter {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
padding: 0.875rem 1.5rem;
border-top: 1px solid var(--color-border);
background: var(--color-bg-card);
flex-shrink: 0;
}
.outlookBtn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.4375rem 1rem;
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: all 0.15s;
}
.outlookBtn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
/* ── Aktivität-Modal ── */
.modalOverlay {

View file

@ -1,4 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
useIntegrations,
@ -67,27 +68,28 @@ function formatEmailDate(iso: string): string {
});
}
// ── EmailCard ─────────────────────────────────────────────────────────────────
// ── EmailCard (Klick öffnet Detail-Modal) ────────────────────────────────────
interface EmailCardProps {
email: M365Email;
onSaveActivity: (email: M365Email, contact: CrmContactLookup) => void;
onClick: () => void;
}
function EmailCard({ email, onSaveActivity }: EmailCardProps) {
function EmailCard({ email, onClick }: 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 (
<div className={`${styles.emailCard} ${!email.isRead ? styles.emailCardUnread : ''}`}>
<a
href={email.webLink}
target="_blank"
rel="noopener noreferrer"
className={styles.emailLink}
<div
role="button"
tabIndex={0}
className={`${styles.emailCard} ${!email.isRead ? styles.emailCardUnread : ''}`}
onClick={onClick}
onKeyDown={(e) => e.key === 'Enter' && onClick()}
>
<div className={styles.emailCardInner}>
<div className={styles.emailHeader}>
<span className={styles.emailSubject}>
{!email.isRead && <span className={styles.unreadDot} aria-hidden="true" />}
@ -114,18 +116,169 @@ function EmailCard({ email, onSaveActivity }: EmailCardProps) {
{email.bodyPreview && (
<p className={styles.emailPreview}>{email.bodyPreview}</p>
)}
</a>
{contact && (
</div>
</div>
);
}
// ── EmailDetailModal ──────────────────────────────────────────────────────────
interface EmailDetailModalProps {
email: M365Email;
onClose: () => void;
onSaveActivity: (email: M365Email, contact: CrmContactLookup) => void;
}
function EmailDetailModal({ email, onClose, onSaveActivity }: EmailDetailModalProps) {
const navigate = useNavigate();
const senderEmail = email.from?.emailAddress?.address ?? null;
const senderName = email.from?.emailAddress?.name ?? '';
const { data: contactData, isLoading: contactLoading } = useContactByEmail(senderEmail);
const contact = contactData?.data ?? null;
const contactName =
contact
? [contact.firstName, contact.lastName].filter(Boolean).join(' ') ||
contact.email ||
'—'
: null;
const handleOpenContact = () => {
onClose();
navigate(`/crm/contacts/${contact!.id}`);
};
const handleCreateContact = () => {
onClose();
navigate('/crm/contacts');
};
return (
<div className={styles.modalOverlay} onClick={onClose}>
<div className={styles.detailModal} onClick={(e) => e.stopPropagation()}>
{/* ── Kopfzeile ── */}
<div className={styles.detailHeader}>
<h2 className={styles.detailSubject}>
{email.subject || '(Kein Betreff)'}
</h2>
<button
type="button"
className={styles.saveActivityBtn}
title={`Als Aktivität für ${[contact.firstName, contact.lastName].filter(Boolean).join(' ')} speichern`}
onClick={() => onSaveActivity(email, contact)}
className={styles.closeBtn}
onClick={onClose}
aria-label="Schließen"
>
+ Aktivität
</button>
</div>
{/* ── Meta ── */}
<div className={styles.detailMeta}>
<div className={styles.detailMetaRow}>
<span className={styles.detailMetaLabel}>Von</span>
<span className={styles.detailMetaValue}>
{senderName || senderEmail || '—'}
{senderName && senderEmail && (
<span className={styles.detailSenderEmail}> &lt;{senderEmail}&gt;</span>
)}
</span>
</div>
<div className={styles.detailMetaRow}>
<span className={styles.detailMetaLabel}>Datum</span>
<span className={styles.detailMetaValue}>
{new Date(email.receivedDateTime).toLocaleString('de-DE', {
weekday: 'short',
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
{email.hasAttachments && (
<div className={styles.detailMetaRow}>
<span className={styles.detailMetaLabel}>Anhang</span>
<span className={styles.detailMetaValue}>📎 Vorhanden</span>
</div>
)}
</div>
{/* ── Body-Vorschau ── */}
<div className={styles.detailBody}>
{email.bodyPreview || (
<em className={styles.detailBodyEmpty}>Kein Vorschautext verfügbar.</em>
)}
</div>
{/* ── CRM-Bereich ── */}
<div className={styles.detailCrm}>
<span className={styles.detailCrmTitle}>CRM</span>
{contactLoading ? (
<span className={styles.detailCrmLoading}>Wird geprüft</span>
) : contact ? (
<div className={styles.detailCrmFound}>
<div className={styles.detailCrmInfo}>
<span className={styles.detailCrmAvatar}>👤</span>
<div>
<span className={styles.detailCrmName}>{contactName}</span>
{contact.companyName && (
<span className={styles.detailCrmCompany}> · {contact.companyName}</span>
)}
</div>
</div>
<div className={styles.detailCrmActions}>
<button
type="button"
className={styles.detailCrmOpenBtn}
onClick={handleOpenContact}
>
Im CRM öffnen
</button>
<button
type="button"
className={styles.saveBtn}
onClick={() => { onClose(); onSaveActivity(email, contact); }}
>
Als Aktivität speichern
</button>
</div>
</div>
) : (
<div className={styles.detailCrmMissing}>
<span className={styles.detailCrmMissingText}>
{senderEmail
? `Kein CRM-Kontakt für ${senderEmail}`
: 'Kein Absender bekannt'}
</span>
{senderEmail && (
<button
type="button"
className={styles.detailCrmCreateBtn}
onClick={handleCreateContact}
>
+ Kontakt anlegen
</button>
)}
</div>
)}
</div>
{/* ── Footer ── */}
<div className={styles.detailFooter}>
<a
href={email.webLink}
target="_blank"
rel="noopener noreferrer"
className={styles.outlookBtn}
>
In Outlook öffnen
</a>
</div>
</div>
</div>
);
}
@ -242,6 +395,7 @@ export function DashboardEmailTab() {
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
const [days, setDays] = useState<number>(7);
const [detailEmail, setDetailEmail] = useState<M365Email | null>(null);
const [activityTarget, setActivityTarget] = useState<ActivityTarget | null>(null);
const [lastSaved, setLastSaved] = useState<string | null>(null);
@ -252,7 +406,6 @@ export function DashboardEmailTab() {
// Standardmäßig Posteingang — Graph API akzeptiert Well-Known-Namen direkt als ID,
// sodass E-Mails sofort geladen werden, bevor die Ordnerliste verfügbar ist.
// Sobald Ordner geladen sind, wird die echte ID aus der Liste verwendet.
const inboxFolder = sortedFolders.find(isInboxFolder);
const activeFolderId = selectedFolderId ?? inboxFolder?.id ?? 'inbox';
@ -370,15 +523,28 @@ export function DashboardEmailTab() {
<EmailCard
key={email.id}
email={email}
onSaveActivity={(e, contact) => {
onClick={() => {
setLastSaved(null);
setActivityTarget({ email: e, contact });
setDetailEmail(email);
}}
/>
))}
</section>
</div>
{/* Detail-Modal */}
{detailEmail && (
<EmailDetailModal
email={detailEmail}
onClose={() => setDetailEmail(null)}
onSaveActivity={(e, contact) => {
setDetailEmail(null);
setLastSaved(null);
setActivityTarget({ email: e, contact });
}}
/>
)}
{/* Aktivität-Modal */}
{activityTarget && (
<ActivityModal

View file

@ -55,6 +55,27 @@
min-height: 200px;
}
/* ── Home-Layout (Inhalt links + Agenda rechts) ── */
.homeLayout {
display: flex;
gap: 1.5rem;
align-items: flex-start;
}
.homeMain {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.homeSidebar {
width: 280px;
flex-shrink: 0;
}
/* ── Platzhalter für bestehenden Home-Inhalt ── */
.placeholder {

View file

@ -3,7 +3,9 @@ import { useAuth } from '../auth/AuthContext';
import { WeatherWidget } from '../components/WeatherWidget';
import { EventCountdownTiles } from '../components/EventCountdownTiles';
import { DashboardEmailTab } from './DashboardEmailTab';
import { DashboardCalendarTab } from './DashboardCalendarTab';
import { DashboardCalendarTab, DayAgenda } from './DashboardCalendarTab';
import { useIntegrations, useOffice365CalendarRange } from '../crm/hooks';
import type { M365CalendarEvent } from '../crm/types';
import styles from './DashboardPage.module.css';
type DashboardTab = 'home' | 'emails' | 'calendar' | 'tasks' | 'contacts';
@ -16,6 +18,36 @@ const TABS: { id: DashboardTab; label: string }[] = [
{ id: 'contacts', label: 'Kontakte' },
];
// ── Tages-Agenda Widget für Home-Tab ─────────────────────────────────────────
function HomeDayAgendaWidget() {
const today = new Date();
const todayISO = today.toISOString().slice(0, 10);
const tomorrowISO = new Date(today.getTime() + 86_400_000).toISOString().slice(0, 10);
const { data: integrationsData } = useIntegrations();
const isConnected =
integrationsData?.data?.some(
(i) => i.provider === 'MICROSOFT_365' && i.connected,
) ?? false;
const { data: eventsData, isLoading } = useOffice365CalendarRange(todayISO, tomorrowISO);
const events: M365CalendarEvent[] = eventsData?.data ?? [];
if (!isConnected) return null;
if (isLoading) return (
<div className={styles.homeSidebar}>
<p style={{ fontSize: '0.875rem', color: 'var(--color-text-muted)' }}>Kalender wird geladen</p>
</div>
);
return (
<div className={styles.homeSidebar}>
<DayAgenda day={today} events={events} />
</div>
);
}
// ── Tab-Inhalte ───────────────────────────────────────────────────────────────
function HomeTab({ firstName, lastName, city, role }: {
@ -32,6 +64,8 @@ function HomeTab({ firstName, lastName, city, role }: {
</h1>
<WeatherWidget city={city ?? undefined} />
</div>
<div className={styles.homeLayout}>
<div className={styles.homeMain}>
<EventCountdownTiles />
<div className={styles.placeholder}>
<p style={{ color: 'var(--color-text-secondary)' }}>
@ -41,6 +75,9 @@ function HomeTab({ firstName, lastName, city, role }: {
Rolle: {role}
</p>
</div>
</div>
<HomeDayAgendaWidget />
</div>
</>
);
}