import { useState } from 'react';
import { useIntegrations, useOffice365CalendarRange } from '../crm/hooks';
import type { M365CalendarEvent } 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);
}
function formatTime(iso: string): string {
return new Date(iso).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
});
}
function getEventsForDay(events: M365CalendarEvent[], day: Date): M365CalendarEvent[] {
return events
.filter((e) => isSameDay(new Date(e.start.dateTime), day))
.sort(
(a, b) =>
new Date(a.start.dateTime).getTime() - new Date(b.start.dateTime).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];
}
// ── Day Agenda (Tages-Detailansicht, auch für Home) ───────────────────────────
export function DayAgenda({
day,
events,
}: {
day: Date;
events: M365CalendarEvent[];
}) {
const dayEvents = getEventsForDay(events, day);
return (
);
}
// ── 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 (
{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' })}
);
})}
);
}
// ── 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.subject}
))}
{dayEvents.length > 2 && (
+{dayEvents.length - 2}
)}
);
})}
);
}
// ── Wochenansicht (Mo–Fr) ─────────────────────────────────────────────────────
function WeekView({
currentDate,
selectedDay,
events,
onDayClick,
}: {
currentDate: Date;
selectedDay: Date;
events: M365CalendarEvent[];
onDayClick: (d: Date) => 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();
window.open(e.webLink, '_blank', 'noopener,noreferrer');
}}
>
{formatTime(e.start.dateTime)}
{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());
// 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') && (
)}
)}
);
}