INSIGHT-MVP/packages/frontend/src/shell/DashboardPage.tsx
Thomas Reitz 2af54246c8 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 <noreply@anthropic.com>
2026-03-13 15:51:10 +01:00

812 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
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';
import { DashboardCalendarTab, DayAgenda } from './DashboardCalendarTab';
import { DashboardTasksTab } from './DashboardTasksTab';
import { DashboardContactsTab } from './DashboardContactsTab';
import {
useIntegrations,
useOffice365CalendarRange,
useActiveTradeEvents,
useOffice365Emails,
useOffice365TasksFlat,
useCrmOpenTasks,
useCompleteO365Task,
useCompleteCrmTask,
} from '../crm/hooks';
import { useEventCountdown } from '../hooks/useEventCountdown';
import { useWeather } from '../hooks/useWeather';
import type { M365CalendarEvent, TradeEvent, M365Email } from '../crm/types';
import type { M365TaskFlat, CrmOpenTask } from '../crm/types';
import styles from './DashboardPage.module.css';
// ── Spruch des Tages ──────────────────────────────────────────────────────────
const QUOTES: { text: string; author: string }[] = [
{ text: 'Der Weg ist das Ziel.', author: 'Konfuzius' },
{ text: 'Erfolg hat drei Buchstaben: Tun.', author: 'J. W. von Goethe' },
{ text: 'Wer aufhört, besser zu werden, hat aufgehört, gut zu sein.', author: 'Philip Rosenthal' },
{ text: 'Die Fantasie ist wichtiger als das Wissen.', author: 'Albert Einstein' },
{ text: 'Wer kämpft, kann verlieren. Wer nicht kämpft, hat schon verloren.', author: 'Bertolt Brecht' },
{ text: 'Sei du selbst die Veränderung, die du dir wünschst für diese Welt.', author: 'Mahatma Gandhi' },
{ text: 'Wenn der Wind der Veränderung weht, bauen die einen Mauern und die anderen Windmühlen.', author: 'Chinesisches Sprichwort' },
{ text: 'Das Geheimnis des Erfolgs ist, den Standpunkt des anderen zu verstehen.', author: 'Henry Ford' },
{ text: 'Tu erst das Notwendige, dann das Mögliche, und plötzlich schaffst du das Unmögliche.', author: 'Franz von Assisi' },
{ text: 'Man sieht nur mit dem Herzen gut. Das Wesentliche ist für die Augen unsichtbar.', author: 'Antoine de Saint-Exupéry' },
{ text: 'Nicht die Stärksten überleben, sondern die Anpassungsfähigsten.', author: 'Charles Darwin' },
{ text: 'Ein langer Marsch beginnt mit dem ersten Schritt.', author: 'Laotse' },
{ text: 'Wer wagt, gewinnt.', author: 'Volksweisheit' },
{ text: 'Jeder Experte war einmal ein Anfänger.', author: 'Unbekannt' },
{ text: 'In der Mitte liegt die Kraft.', author: 'Sprichwort' },
{ text: 'Nicht wer wenig hat, sondern wer viel begehrt, ist arm.', author: 'Seneca' },
{ text: 'Kreativität ist Intelligenz, die Spaß hat.', author: 'Albert Einstein' },
{ text: 'Stärke zeigt sich nicht darin, niemals zu fallen, sondern darin, nach jedem Fall aufzustehen.', author: 'Nelson Mandela' },
{ text: 'Der beste Zeitpunkt, einen Baum zu pflanzen, war vor zwanzig Jahren. Der zweitbeste ist jetzt.', author: 'Chinesisches Sprichwort' },
{ text: 'Übung macht den Meister.', author: 'Volksweisheit' },
{ text: 'Es ist nicht genug zu wissen — man muss auch anwenden.', author: 'J. W. von Goethe' },
{ text: 'Optimismus ist die Grundlage des Mutes.', author: 'Nick Butler' },
{ text: 'Wo ein Wille ist, ist auch ein Weg.', author: 'Volksweisheit' },
{ text: 'Die größte Entdeckungsreise liegt nicht darin, neue Länder zu suchen, sondern darin, die Dinge mit neuen Augen zu sehen.', author: 'Marcel Proust' },
{ text: 'Der Klügere gibt nach — eine traurige Wahrheit, sie begründet die Weltherrschaft der Dummheit.', author: 'Marie von Ebner-Eschenbach' },
{ text: 'Heute ist der erste Tag vom Rest deines Lebens.', author: 'Sprichwort' },
{ text: 'Ein gutes Gewissen ist ein sanftes Ruhekissen.', author: 'Volksweisheit' },
{ text: 'Kein Mensch hat das Recht, das zu unterlassen, was er kann.', author: 'Albert Schweitzer' },
{ text: 'Wissen ist Macht.', author: 'Francis Bacon' },
{ text: 'Lächle und die Welt lächelt zurück.', author: 'Volksweisheit' },
{ text: 'Der Mut macht erst den Mann.', author: 'Friedrich Schiller' },
{ text: 'Ein Lächeln kostet nichts, bringt aber viel.', author: 'Unbekannt' },
{ text: 'Aus Fehlern lernt man — außer man macht sie nicht.', author: 'Sprichwort' },
{ text: 'Denke nicht daran, was du tun könntest, sondern was du tust.', author: 'Unbekannt' },
{ text: 'Die Zeit heilt alle Wunden.', author: 'Volksweisheit' },
// Weitere 30 Sprüche
{ text: 'Das Leben ist zu kurz, um schlechten Wein zu trinken.', author: 'Johann Wolfgang von Goethe' },
{ text: 'Zwei Dinge sind unendlich: das Universum und die menschliche Dummheit. Beim Universum bin ich mir nicht ganz sicher.', author: 'Albert Einstein' },
{ text: 'Wer mit Ungeheuern kämpft, mag zusehen, dass er nicht dabei zum Ungeheuer wird.', author: 'Friedrich Nietzsche' },
{ text: 'Die Kunst des Lebens liegt darin, den richtigen Augenblick zu erkennen.', author: 'Unbekannt' },
{ text: 'Tue jeden Tag etwas, das dich ein kleines Stück voranbringt.', author: 'Unbekannt' },
{ text: 'Es ist leichter, einen Fehler zu gestehen als ihn zu rechtfertigen.', author: 'Unbekannt' },
{ text: 'Wer andere kennt, ist klug. Wer sich selbst kennt, ist weise.', author: 'Laotse' },
{ text: 'Wer immer tut, was er schon kann, bleibt immer das, was er schon ist.', author: 'Henry Ford' },
{ text: 'Eine Reise von tausend Meilen beginnt mit einem einzigen Schritt.', author: 'Laotse' },
{ text: 'Niemand kann dir das Gefühl geben, minderwertig zu sein, ohne deine Zustimmung.', author: 'Eleanor Roosevelt' },
{ text: 'In jeder Schwierigkeit steckt eine Möglichkeit.', author: 'Albert Einstein' },
{ text: 'Charakter zeigt sich nicht in Ausnahmesituationen des Lebens, sondern in seiner Alltäglichkeit.', author: 'Aristoteles' },
{ text: 'Rede nicht darüber, was du tun wirst. Tu es.', author: 'Unbekannt' },
{ text: 'Die größte Schwäche liegt darin aufzugeben. Der sicherste Weg zum Erfolg ist, es noch einmal zu versuchen.', author: 'Thomas Edison' },
{ text: 'Gib jedem Tag die Chance, der schönste deines Lebens zu werden.', author: 'Mark Twain' },
{ text: 'Wenn der Schüler bereit ist, erscheint der Lehrer.', author: 'Buddhistisches Sprichwort' },
{ text: 'Der einzige Weg, gute Arbeit zu leisten, ist zu lieben, was man tut.', author: 'Steve Jobs' },
{ text: 'Träume nicht dein Leben, sondern lebe deinen Traum.', author: 'Unbekannt' },
{ text: 'Das Leben ist das, was passiert, während du eifrig dabei bist, andere Pläne zu machen.', author: 'John Lennon' },
{ text: 'Manchmal muss man einen Schritt zurückgehen, um zwei nach vorne machen zu können.', author: 'Unbekannt' },
{ text: 'Das Herz sieht schärfer als das Auge.', author: 'Russisches Sprichwort' },
{ text: 'Was du nicht willst, das man dir tu, das füg auch keinem andern zu.', author: 'Goldene Regel' },
{ text: 'Der Erfolg hat viele Väter, aber die Niederlage ist eine Waise.', author: 'John F. Kennedy' },
{ text: 'Qualität ist keine Handlung, sie ist eine Gewohnheit.', author: 'Aristoteles' },
{ text: 'Das Ziel des Lebens ist Selbstentfaltung.', author: 'Oscar Wilde' },
{ text: 'Die Natur eilt nie und bringt doch alles zustande.', author: 'Laotse' },
{ text: 'Wir sind, was wir wiederholt tun. Herausragend sein ist keine Handlung, sondern eine Gewohnheit.', author: 'Aristoteles' },
{ text: 'Mut ist nicht die Abwesenheit von Angst, sondern die Überzeugung, dass es etwas Wichtigeres gibt als Angst.', author: 'Nelson Mandela' },
{ text: 'Vergiss nicht: Du bist der Architekt deines eigenen Schicksals.', author: 'Alfred A. Montapert' },
{ text: 'Jeder, der aufgehört hat zu lernen, ist alt geworden — ob mit zwanzig oder mit achtzig.', author: 'Henry Ford' },
];
function getDayOfYear(date: Date): number {
const start = new Date(date.getFullYear(), 0, 0);
return Math.floor((date.getTime() - start.getTime()) / 86_400_000);
}
function QuoteOfTheDay() {
const q = QUOTES[getDayOfYear(new Date()) % QUOTES.length];
return (
<div className={styles.quoteOfDay}>
<span className={styles.quoteText}>{q.text}"</span>
<span className={styles.quoteAuthor}>— {q.author}</span>
</div>
);
}
// ── Messe-Ticker: Hilfsfunktionen ─────────────────────────────────────────────
function isStillRelevant(event: TradeEvent): boolean {
const endDay = new Date(event.endDate);
endDay.setHours(23, 59, 59, 999);
const cutoff = new Date(endDay.getTime() + 24 * 60 * 60 * 1000);
return new Date() < cutoff;
}
function formatDateRange(start: string, end: string): string {
const opts: Intl.DateTimeFormatOptions = { day: '2-digit', month: '2-digit', year: 'numeric' };
return `${new Date(start).toLocaleDateString('de-DE', opts)} ${new Date(end).toLocaleDateString('de-DE', opts)}`;
}
// ── Kompakte Messe-Zeile (klickbar) ──────────────────────────────────────────
function CompactMesseRow({
event,
onClick,
}: {
event: TradeEvent;
onClick: () => void;
}) {
const countdown = useEventCountdown(event);
const color =
countdown.status === 'upcoming'
? '#3b82f6'
: countdown.status === 'ongoing'
? '#22c55e'
: '#9ca3af';
return (
<button
type="button"
className={styles.messeRow}
style={{ borderLeftColor: color }}
onClick={onClick}
>
<span className={styles.messeRowName}>{event.name}</span>
<span className={styles.messeRowCountdown}>{countdown.label}</span>
</button>
);
}
// ── Messe-Detail-Popup ────────────────────────────────────────────────────────
function MesseDetailModal({
event,
onClose,
}: {
event: TradeEvent;
onClose: () => void;
}) {
const countdown = useEventCountdown(event);
const color =
countdown.status === 'upcoming'
? '#3b82f6'
: countdown.status === 'ongoing'
? '#22c55e'
: '#9ca3af';
return (
<div className={styles.messeModalBackdrop} onClick={onClose}>
<div
className={styles.messeModal}
style={{ borderTopColor: color }}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className={styles.messeModalHeader}>
<div>
<h3 className={styles.messeModalTitle}>{event.name}</h3>
<span
className={`${styles.messeStatusChip} ${styles[`messeChip_${countdown.status}`]}`}
>
{countdown.status === 'upcoming'
? 'Bevorstehend'
: countdown.status === 'ongoing'
? 'Läuft'
: 'Beendet'}
</span>
</div>
<button
type="button"
className={styles.messeModalClose}
onClick={onClose}
aria-label="Schließen"
>
×
</button>
</div>
{/* Countdown */}
<div className={styles.messeModalCountdown}>{countdown.label}</div>
{countdown.status === 'ongoing' && (
<div className={styles.messeProgressBar}>
<div
className={styles.messeProgressFill}
style={{ width: `${countdown.progressPercent}%`, background: color }}
/>
</div>
)}
{/* Daten */}
<div className={styles.messeModalMeta}>
<div className={styles.messeModalMetaRow}>
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<rect x="2" y="3" width="12" height="11" rx="1" /><path d="M5 1v4M11 1v4M2 7h12" />
</svg>
<span>{formatDateRange(event.startDate, event.endDate)}</span>
</div>
{event.location && (
<div className={styles.messeModalMetaRow}>
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 1C5.2 1 3 3.2 3 6c0 4 5 9 5 9s5-5 5-9c0-2.8-2.2-5-5-5z" /><circle cx="8" cy="6" r="1.5" />
</svg>
<span>{event.location}{event.boothInfo ? ` · Stand: ${event.boothInfo}` : ''}</span>
</div>
)}
{!event.location && event.boothInfo && (
<div className={styles.messeModalMetaRow}>
<span>Stand: {event.boothInfo}</span>
</div>
)}
</div>
{event.description && (
<p className={styles.messeModalDesc}>{event.description}</p>
)}
{event.websiteUrl && (
<a
href={event.websiteUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.messeModalLink}
>
Website öffnen ↗
</a>
)}
</div>
</div>
);
}
// ── Kompakter Messe-Ticker für Sidebar ───────────────────────────────────────
function CompactMesseTicker() {
const { data } = useActiveTradeEvents();
const [selected, setSelected] = useState<TradeEvent | null>(null);
const events = (data?.data ?? []).filter(isStillRelevant);
if (events.length === 0) return null;
return (
<div className={styles.compactMesse}>
<h4 className={styles.compactMesseTitle}>Messen</h4>
<div className={styles.compactMesseList}>
{events.map((ev) => (
<CompactMesseRow key={ev.id} event={ev} onClick={() => setSelected(ev)} />
))}
</div>
{selected && (
<MesseDetailModal event={selected} onClose={() => setSelected(null)} />
)}
</div>
);
}
// ── Linke Spalte: Uhr + Wetter + 3-Tage-Prognose ─────────────────────────────
function HomeLeftColumn({ city }: { city?: string | null }) {
const { data: weatherData } = useWeather(city ?? undefined);
return (
<div className={styles.homeLeft}>
{/* Analoge Uhr */}
<AnalogClock />
{/* Aktuelles Wetter */}
<div className={styles.homeWeatherBox}>
<WeatherWidget city={city ?? undefined} />
</div>
{/* 3-Tage-Prognose */}
{weatherData?.forecast && weatherData.forecast.length > 0 && (
<div className={styles.forecastStrip}>
<h4 className={styles.forecastTitle}>3-Tage-Prognose</h4>
<div className={styles.forecastDays}>
{weatherData.forecast.map((day) => {
const d = new Date(day.date);
const dayLabel = d.toLocaleDateString('de-DE', {
weekday: 'short',
day: '2-digit',
month: '2-digit',
});
return (
<div key={day.date} className={styles.forecastDay}>
<span className={styles.forecastDayLabel}>{dayLabel}</span>
<span className={styles.forecastDayIcon}>{day.icon}</span>
<span className={styles.forecastDayTemp}>
{day.tempMax}°{' '}
<span className={styles.forecastDayTempMin}>{day.tempMin}°</span>
</span>
</div>
);
})}
</div>
</div>
)}
</div>
);
}
// ── Mittlere Spalte: Kompakte Aufgaben ────────────────────────────────────────
const CRM_MARKER_RE = /\[INSIGHT_CRM:([^\]]+)\]/;
function extractCrmId(body: string | null | undefined): string | null {
if (!body) return null;
const m = CRM_MARKER_RE.exec(body);
return m ? m[1] : null;
}
function formatDue(isoOrDateTime: string | null): string | null {
if (!isoOrDateTime) return null;
try {
const d = new Date(isoOrDateTime);
const today = new Date();
const todayStr = today.toISOString().slice(0, 10);
const dStr = d.toISOString().slice(0, 10);
const tomorrow = new Date(today.getTime() + 86_400_000).toISOString().slice(0, 10);
if (dStr === todayStr) return 'Heute';
if (dStr === tomorrow) return 'Morgen';
if (dStr < todayStr) {
const days = Math.round((today.getTime() - d.getTime()) / 86_400_000);
return `Überfällig (${days}d)`;
}
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
} catch {
return null;
}
}
function isDue(iso: string | null): boolean {
if (!iso) return false;
try { return new Date(iso) < new Date(); } catch { return false; }
}
function HomeTasksWidget({ onSeeAll }: { onSeeAll?: () => void }) {
const { data: integrationsData } = useIntegrations();
const isO365Connected =
integrationsData?.data?.some(
(i) => i.provider === 'MICROSOFT_365' && i.connected,
) ?? false;
const { data: o365Data, isLoading: o365Loading } = useOffice365TasksFlat();
const { data: crmData, isLoading: crmLoading } = useCrmOpenTasks();
const completeO365 = useCompleteO365Task();
const completeCrm = useCompleteCrmTask();
const [pendingKey, setPendingKey] = useState<string | null>(null);
const o365Tasks: M365TaskFlat[] = o365Data?.data ?? [];
const crmTasks: CrmOpenTask[] = crmData?.data ?? [];
// CRM-IDs die bereits in O365 sind
const syncedCrmIds = new Set<string>();
for (const t of o365Tasks) {
const id = extractCrmId(t.bodyContent);
if (id) syncedCrmIds.add(id);
}
// Unified list (gleiche Logik wie DashboardTasksTab)
type TaskSource = 'o365' | 'crm' | 'synced';
interface HomeTask {
key: string;
source: TaskSource;
title: string;
dueDate: string | null;
o365ListId?: string;
o365TaskId?: string;
crmActivityId?: string;
}
const unified: HomeTask[] = [];
for (const t of o365Tasks) {
const crmId = extractCrmId(t.bodyContent);
unified.push({
key: `o365-${t.id}`,
source: crmId ? 'synced' : 'o365',
title: t.title,
dueDate: t.dueDateTime?.dateTime ?? null,
o365ListId: t.listId,
o365TaskId: t.id,
crmActivityId: crmId ?? undefined,
});
}
for (const c of crmTasks) {
if (syncedCrmIds.has(c.id)) continue;
unified.push({
key: `crm-${c.id}`,
source: 'crm',
title: c.subject,
dueDate: c.scheduledAt,
crmActivityId: c.id,
});
}
unified.sort((a, b) => {
const aO = a.dueDate && isDue(a.dueDate);
const bO = b.dueDate && isDue(b.dueDate);
if (aO && !bO) return -1;
if (!aO && bO) return 1;
if (a.dueDate && b.dueDate) return a.dueDate.localeCompare(b.dueDate);
if (a.dueDate) return -1;
if (b.dueDate) return 1;
return 0;
});
const visible = unified.slice(0, 8);
const isLoading = (isO365Connected && o365Loading) || crmLoading;
function handleComplete(task: HomeTask) {
setPendingKey(task.key);
const ps: Promise<unknown>[] = [];
if (task.o365TaskId && task.o365ListId) {
ps.push(completeO365.mutateAsync({ listId: task.o365ListId, taskId: task.o365TaskId }));
}
if (task.crmActivityId) {
ps.push(completeCrm.mutateAsync(task.crmActivityId));
}
Promise.allSettled(ps).finally(() => setPendingKey(null));
}
return (
<div className={styles.homeWidgetCard}>
<div className={styles.homeWidgetHeader}>
<h4 className={styles.homeWidgetTitle}>Aufgaben</h4>
{onSeeAll && (
<button type="button" className={styles.homeWidgetAll} onClick={onSeeAll}>
Alle →
</button>
)}
</div>
{isLoading && (
<p className={styles.homeEmptyHint}>Lädt…</p>
)}
{!isLoading && visible.length === 0 && (
<p className={styles.homeEmptyHint}>✅ Keine offenen Aufgaben</p>
)}
{!isLoading && visible.length > 0 && (
<div className={styles.homeTaskList}>
{visible.map((task) => {
const overdue = isDue(task.dueDate);
const dueFmt = formatDue(task.dueDate);
return (
<div key={task.key} className={styles.homeTaskRow}>
{/* Source-Badge */}
{(task.source === 'o365' || task.source === 'synced') && (
<span className={`${styles.homeTaskBadge} ${styles.homeTaskBadgeO365}`}>O</span>
)}
{task.source === 'crm' && (
<span className={`${styles.homeTaskBadge} ${styles.homeTaskBadgeCrm}`}>C</span>
)}
<div className={styles.homeTaskMain}>
<span className={styles.homeTaskTitle}>{task.title}</span>
{dueFmt && (
<span
className={`${styles.homeTaskDue} ${overdue ? styles.homeTaskDueOverdue : ''}`}
>
{dueFmt}
</span>
)}
</div>
<button
type="button"
className={styles.homeTaskComplete}
onClick={() => handleComplete(task)}
disabled={pendingKey === task.key}
title="Als erledigt markieren"
>
{pendingKey === task.key ? '…' : '✓'}
</button>
</div>
);
})}
</div>
)}
{unified.length > 8 && (
<div className={styles.homeWidgetFooter}>
{unified.length - 8} weitere Aufgaben
</div>
)}
</div>
);
}
// ── Mittlere Spalte: Kompakte E-Mails (letzte 3 Tage) ────────────────────────
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' });
}
function HomeEmailsWidget({ onSeeAll }: { onSeeAll?: () => void }) {
const { data: integrationsData } = useIntegrations();
const isConnected =
integrationsData?.data?.some(
(i) => i.provider === 'MICROSOFT_365' && i.connected,
) ?? false;
const { data: emailData, isLoading } = useOffice365Emails();
if (!isConnected) return null;
// Letzte 3 Tage filtern
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 3);
const emails: M365Email[] = (emailData?.data ?? []).filter(
(e) => new Date(e.receivedDateTime) >= cutoff,
);
return (
<div className={styles.homeWidgetCard}>
<div className={styles.homeWidgetHeader}>
<h4 className={styles.homeWidgetTitle}>E-Mails (3 Tage)</h4>
{onSeeAll && (
<button type="button" className={styles.homeWidgetAll} onClick={onSeeAll}>
Alle →
</button>
)}
</div>
{isLoading && <p className={styles.homeEmptyHint}>Lädt…</p>}
{!isLoading && emails.length === 0 && (
<p className={styles.homeEmptyHint}>Keine E-Mails in den letzten 3 Tagen</p>
)}
{!isLoading && emails.length > 0 && (
<div className={styles.homeEmailList}>
{emails.slice(0, 8).map((email) => {
const sender =
email.from?.emailAddress?.name ??
email.from?.emailAddress?.address ??
'Unbekannt';
const subject = email.subject ?? '(kein Betreff)';
const timeStr = formatEmailTime(email.receivedDateTime);
return (
<a
key={email.id}
href={email.webLink}
target="_blank"
rel="noopener noreferrer"
className={styles.homeEmailRow}
>
{!email.isRead ? (
<span className={styles.homeEmailUnread} title="Ungelesen" />
) : (
<span className={styles.homeEmailReadDot} />
)}
<div className={styles.homeEmailContent}>
<div className={styles.homeEmailSender}>{sender}</div>
<div className={styles.homeEmailSubject}>{subject}</div>
</div>
<span className={styles.homeEmailTime}>{timeStr}</span>
</a>
);
})}
</div>
)}
{emails.length > 8 && (
<div className={styles.homeWidgetFooter}>{emails.length - 8} weitere E-Mails</div>
)}
</div>
);
}
// ── Sidebar: Messe-Ticker + Tages-Agenda ─────────────────────────────────────
function HomeSidebar() {
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 ?? [];
return (
<div className={styles.homeSidebar}>
<CompactMesseTicker />
{isConnected && (
isLoading
? <p style={{ fontSize: '0.875rem', color: 'var(--color-text-muted)', marginTop: '0.5rem' }}>
Kalender wird geladen…
</p>
: <DayAgenda day={today} events={events} fullWidth />
)}
</div>
);
}
// ── Tab-Definitionen ──────────────────────────────────────────────────────────
type DashboardTab = 'home' | 'emails' | 'calendar' | 'tasks' | 'contacts';
const TABS: { id: DashboardTab; label: string }[] = [
{ id: 'home', label: 'Home' },
{ id: 'emails', label: 'E-Mail' },
{ id: 'calendar', label: 'Kalender' },
{ id: 'tasks', label: 'Aufgaben' },
{ id: 'contacts', label: 'Kontakte' },
];
// ── HomeTab ───────────────────────────────────────────────────────────────────
function HomeTab({
firstName,
lastName,
city,
onSwitchTab,
}: {
firstName?: string;
lastName?: string;
city?: string | null;
onSwitchTab: (tab: DashboardTab) => void;
}) {
return (
<>
{/* Header: Name links, Spruch rechts */}
<div className={styles.header}>
<h1 className={styles.title}>
Willkommen, {firstName} {lastName}
</h1>
<QuoteOfTheDay />
</div>
{/* 3-Spalten-Layout */}
<div className={styles.homeLayout}>
{/* Links: Uhr + Wetter + Prognose */}
<HomeLeftColumn city={city} />
{/* Mitte: Aufgaben + E-Mails */}
<div className={styles.homeMain}>
<HomeTasksWidget onSeeAll={() => onSwitchTab('tasks')} />
<HomeEmailsWidget onSeeAll={() => onSwitchTab('emails')} />
</div>
{/* Rechts: Messe-Ticker + Tagesagenda */}
<HomeSidebar />
</div>
</>
);
}
// ── 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, logout } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const { mode, setMode } = useTheme();
const [activeTab, setActiveTab] = useState<DashboardTab>('home');
const handleLogout = async () => {
await logout();
navigate('/login');
};
// Immer auf Home-Tab springen wenn Dashboard-NavLink geklickt wird
useEffect(() => {
setActiveTab('home');
}, [location.key]);
return (
<div>
{/* Tab-Leiste */}
<div className={styles.tabBar}>
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
className={`${styles.tab} ${activeTab === tab.id ? styles.activeTab : ''}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
{/* Profil-Bereich rechts in der Tab-Leiste */}
<div className={styles.tabBarProfile}>
{/* Theme-Schalter */}
<div className={styles.tabBarThemeGroup}>
{THEME_OPTIONS_DASH.map((opt) => (
<button
key={opt.value}
type="button"
className={`${styles.tabBarThemeBtn}${mode === opt.value ? ` ${styles.tabBarThemeBtnActive}` : ''}`}
onClick={() => setMode(opt.value)}
title={opt.value === 'light' ? 'Hell' : opt.value === 'dark' ? 'Dunkel' : 'System'}
>
{opt.icon}
</button>
))}
</div>
{/* Benutzer (→ Profil) */}
<div
className={styles.tabBarUser}
onClick={() => navigate('/profile')}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') navigate('/profile');
}}
title="Profil bearbeiten"
>
<UserAvatar
firstName={user?.firstName ?? ''}
lastName={user?.lastName ?? ''}
avatar={user?.avatar}
size={24}
/>
<span className={styles.tabBarUserName}>
{user?.firstName} {user?.lastName}
</span>
</div>
{/* Abmelden */}
<button
type="button"
className={styles.tabBarLogout}
onClick={handleLogout}
title="Abmelden"
>
<svg
width="13"
height="13"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10 3h3a1 1 0 011 1v8a1 1 0 01-1 1h-3" />
<path d="M7 11l3-3-3-3" />
<path d="M10 8H3" />
</svg>
</button>
</div>
</div>
{/* Tab-Inhalt */}
<div className={styles.tabContent}>
{activeTab === 'home' && (
<HomeTab
firstName={user?.firstName}
lastName={user?.lastName}
city={user?.city}
onSwitchTab={setActiveTab}
/>
)}
{activeTab === 'emails' && <DashboardEmailTab />}
{activeTab === 'calendar' && <DashboardCalendarTab />}
{activeTab === 'tasks' && <DashboardTasksTab />}
{activeTab === 'contacts' && <DashboardContactsTab />}
</div>
</div>
);
}