import { useState } from 'react'; import { useIntegrations, useOffice365CalendarRange, useContacts, useCreateActivity } from '../crm/hooks'; import type { M365CalendarEvent, Contact } from '../crm/types'; import styles from './DashboardCalendarTab.module.css'; export type CalendarViewMode = 'month' | 'week' | 'agenda'; // ── Date Helpers ─────────────────────────────────────────────────────────────── function startOfWeekMonday(date: Date): Date { const d = new Date(date); const dow = (d.getDay() + 6) % 7; // Mon=0 … Sun=6 d.setDate(d.getDate() - dow); d.setHours(0, 0, 0, 0); return d; } function addDays(date: Date, n: number): Date { const d = new Date(date); d.setDate(d.getDate() + n); return d; } function isSameDay(a: Date, b: Date): boolean { return ( a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() ); } /** 6×7 Tageszellen für die Monatsansicht */ function getMonthCells(date: Date): Date[] { const firstDay = new Date(date.getFullYear(), date.getMonth(), 1); const gridStart = startOfWeekMonday(firstDay); return Array.from({ length: 42 }, (_, i) => addDays(gridStart, i)); } /** 5 Arbeitstage der Woche (Mo–Fr) */ function getWorkWeekDays(date: Date): Date[] { const monday = startOfWeekMonday(date); return Array.from({ length: 5 }, (_, i) => addDays(monday, i)); } function toISODate(date: Date): string { return date.toISOString().slice(0, 10); } /** * Timezone-aware Date: MS Graph gibt UTC-Zeiten ohne 'Z'-Suffix zurück. * Ohne diesen Fix interpretiert JavaScript die Zeiten als Ortszeit → 1h Versatz. */ function toDate(dateTime: string, timeZone: string): Date { if (timeZone === 'UTC' && !dateTime.endsWith('Z') && !dateTime.includes('+')) { return new Date(dateTime + 'Z'); } return new Date(dateTime); } function formatTime(dateTime: string, timeZone: string): string { return toDate(dateTime, timeZone).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', }); } function getEventsForDay(events: M365CalendarEvent[], day: Date): M365CalendarEvent[] { return events .filter((e) => isSameDay(toDate(e.start.dateTime, e.start.timeZone), day)) .sort( (a, b) => toDate(a.start.dateTime, a.start.timeZone).getTime() - toDate(b.start.dateTime, b.start.timeZone).getTime(), ); } // Deterministische Farbe anhand Event-ID const EVENT_COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981', '#ef4444']; function eventColor(id: string): string { let h = 0; for (const ch of id) h = (h * 31 + ch.charCodeAt(0)) % EVENT_COLORS.length; return EVENT_COLORS[h]; } // ── Calendar Event Detail Modal ──────────────────────────────────────────────── function CalendarEventModal({ event, onClose, }: { event: M365CalendarEvent; onClose: () => void; }) { const createActivity = useCreateActivity(); const [contactSearch, setContactSearch] = useState(''); const [selectedContact, setSelectedContact] = useState(null); const [actStatus, setActStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); const contactsQuery = useContacts({ page: 1, pageSize: 8, search: contactSearch || undefined, }); const contacts: Contact[] = contactsQuery.data?.data ?? []; const startDate = toDate(event.start.dateTime, event.start.timeZone); const endDate = toDate(event.end.dateTime, event.end.timeZone); const dateLabel = startDate.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', }); const timeLabel = `${formatTime(event.start.dateTime, event.start.timeZone)} – ${formatTime(event.end.dateTime, event.end.timeZone)} Uhr`; const contactDisplayName = (c: Contact) => [c.firstName, c.lastName].filter(Boolean).join(' ') || c.email || c.id; const handleSaveActivity = async () => { if (!selectedContact || actStatus === 'loading') return; if (actStatus === 'error') { setActStatus('idle'); return; } setActStatus('loading'); try { const now = new Date(); const isPast = endDate < now; await createActivity.mutateAsync({ contactId: selectedContact.id, type: 'MEETING', subject: event.subject, description: event.attendees?.length ? `Teilnehmer: ${event.attendees.map((a) => a.emailAddress.name || a.emailAddress.address).join(', ')}` : undefined, scheduledAt: startDate.toISOString(), completedAt: isPast ? endDate.toISOString() : undefined, }); setActStatus('success'); } catch { setActStatus('error'); } }; return (
{ if (e.target === e.currentTarget) onClose(); }} >
{/* Schließen */} {/* Titel */}

{event.subject}

📅 {dateLabel} · 🕐 {timeLabel} {event.isOnlineMeeting && Online}
{event.location?.displayName && (
📍 {event.location.displayName}
)} {/* Detailbox */}
{event.organizer && (
Organisator {event.organizer.emailAddress.name || event.organizer.emailAddress.address}
)} {event.attendees && event.attendees.length > 0 && (
Teilnehmer
{event.attendees.map((a, i) => (
{a.emailAddress.name ? `${a.emailAddress.name} <${a.emailAddress.address}>` : a.emailAddress.address}
))}
)} {event.onlineMeetingUrl && (
Meeting Beitreten ↗
)} {!event.organizer && (!event.attendees || event.attendees.length === 0) && !event.onlineMeetingUrl && (

Keine weiteren Details verfügbar.

)}
{/* Zu Kontakt hinzufügen */} {actStatus !== 'success' ? (
Als Aktivität bei Kontakt speichern
{ setContactSearch(e.target.value); setSelectedContact(null); setActStatus('idle'); }} className={styles.contactSearchInput} />
{(contacts.length > 0 || contactsQuery.isLoading) && (
{contactsQuery.isLoading ? (
Suche…
) : ( contacts.map((c) => (
setSelectedContact(c)} > {contactDisplayName(c)} {c.company?.name && ( {c.company.name} )}
)) )}
)} {contacts.length === 0 && !contactsQuery.isLoading && contactSearch.length > 0 && (
Keine Kontakte gefunden
)} {selectedContact && ( )}
) : (
✓ Meeting-Aktivität wurde erfolgreich gespeichert
)} {/* Footer */}
Im Outlook öffnen ↗
); } // ── Day Agenda (Tages-Detailansicht, auch für Home) ─────────────────────────── export function DayAgenda({ day, events, fullWidth = false, onEventClick, }: { day: Date; events: M365CalendarEvent[]; fullWidth?: boolean; onEventClick?: (event: M365CalendarEvent) => void; }) { const dayEvents = getEventsForDay(events, day); return ( ); } // ── Agenda-Listenansicht (eigene Ansicht, zeigt mehrere Tage) ───────────────── function AgendaView({ currentDate, events, selectedDay, onDayClick, onEventClick, }: { currentDate: Date; events: M365CalendarEvent[]; selectedDay: Date; onDayClick: (d: Date) => void; onEventClick: (event: M365CalendarEvent) => void; }) { const today = new Date(); // 14 Tage ab currentDate const days = Array.from({ length: 14 }, (_, i) => addDays(currentDate, i)); return (
{days.map((day) => { const dayEvents = getEventsForDay(events, day); const isToday = isSameDay(day, today); const isSelected = isSameDay(day, selectedDay); return (
onDayClick(day)} >
{day.toLocaleDateString('de-DE', { weekday: 'short' })} {day.getDate()} {day.toLocaleDateString('de-DE', { month: 'short' })}
{dayEvents.length === 0 ? ( Kein Termin ) : ( dayEvents.map((e) => (
{ ev.stopPropagation(); onEventClick(e); }} onKeyDown={(ev) => { ev.stopPropagation(); if (ev.key === 'Enter' || ev.key === ' ') onEventClick(e); }} > {formatTime(e.start.dateTime, e.start.timeZone)} {e.subject} {e.isOnlineMeeting && ( Online )}
)) )}
); })}
); } // ── Monatsansicht ───────────────────────────────────────────────────────────── const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; function MonthView({ currentDate, selectedDay, events, onDayClick, }: { currentDate: Date; selectedDay: Date; events: M365CalendarEvent[]; onDayClick: (d: Date) => void; }) { const today = new Date(); const cells = getMonthCells(currentDate); const month = currentDate.getMonth(); return (
{/* Wochentag-Header */} {WEEKDAY_LABELS.map((d) => (
{d}
))} {/* Tageszellen */} {cells.map((day, i) => { const inMonth = day.getMonth() === month; const isToday = isSameDay(day, today); const isSelected = isSameDay(day, selectedDay); const dayEvents = getEventsForDay(events, day); return (
onDayClick(day)} > {day.getDate()}
{dayEvents.slice(0, 2).map((e) => (
{formatTime(e.start.dateTime, e.start.timeZone)} {e.subject}
))} {dayEvents.length > 2 && (
+{dayEvents.length - 2}
)}
); })}
); } // ── Wochenansicht (Mo–Fr) ───────────────────────────────────────────────────── function WeekView({ currentDate, selectedDay, events, onDayClick, onEventClick, }: { currentDate: Date; selectedDay: Date; events: M365CalendarEvent[]; onDayClick: (d: Date) => void; onEventClick: (event: M365CalendarEvent) => void; }) { const today = new Date(); const weekDays = getWorkWeekDays(currentDate); // nur Mo–Fr return (
{weekDays.map((day) => { const isToday = isSameDay(day, today); const isSelected = isSameDay(day, selectedDay); const dayEvents = getEventsForDay(events, day); return (
onDayClick(day)} >
{day.toLocaleDateString('de-DE', { weekday: 'short' })} {day.getDate()}
{dayEvents.map((e) => (
{ ev.stopPropagation(); onEventClick(e); }} > {formatTime(e.start.dateTime, e.start.timeZone)} {e.subject}
))}
); })}
); } // ── DashboardCalendarTab ────────────────────────────────────────────────────── export function DashboardCalendarTab() { const { data: integrationsData, isLoading: intLoading } = useIntegrations(); const isConnected = integrationsData?.data?.some( (i) => i.provider === 'MICROSOFT_365' && i.connected, ) ?? false; const [viewMode, setViewMode] = useState('month'); const [currentDate, setCurrentDate] = useState(() => new Date()); const [selectedDay, setSelectedDay] = useState(() => new Date()); const [selectedEvent, setSelectedEvent] = useState(null); // Datumsbereich berechnen const rangeStart = viewMode === 'month' ? startOfWeekMonday( new Date(currentDate.getFullYear(), currentDate.getMonth(), 1), ) : viewMode === 'week' ? startOfWeekMonday(currentDate) : currentDate; // agenda: ab currentDate const rangeEnd = viewMode === 'month' ? addDays(rangeStart, 42) : viewMode === 'week' ? addDays(startOfWeekMonday(currentDate), 5) // Mo–Fr : addDays(currentDate, 14); // 14 Tage Agenda const { data: eventsData, isLoading, error } = useOffice365CalendarRange( toISODate(rangeStart), toISODate(rangeEnd), ); const events: M365CalendarEvent[] = eventsData?.data ?? []; const navigate = (delta: number) => { const d = new Date(currentDate); if (viewMode === 'month') { d.setMonth(d.getMonth() + delta); } else if (viewMode === 'week') { d.setDate(d.getDate() + delta * 7); } else { d.setDate(d.getDate() + delta * 14); } setCurrentDate(d); }; const goToday = () => { const t = new Date(); setCurrentDate(t); setSelectedDay(t); }; // Anzeigebezeichnung 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[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) { return

Verbindung wird geprüft…

; } if (!isConnected) { return (
📅

Microsoft 365 nicht verbunden

Verbinden Sie Ihr Konto unter{' '} CRM → Office 365 .

); } return (
{/* Toolbar */}

{rangeLabel}

{/* Status */} {isLoading && (

Termine werden geladen…

)} {error && (

Kalendertermine konnten nicht geladen werden.

)} {/* Hauptbereich */} {!isLoading && (
{viewMode === 'month' && ( )} {viewMode === 'week' && ( )} {viewMode === 'agenda' && ( )}
{/* Tages-Agenda nur bei Monat- und Wochenansicht */} {(viewMode === 'month' || viewMode === 'week') && ( )}
)} {/* Event-Detail-Modal */} {selectedEvent && ( setSelectedEvent(null)} /> )}
); }