INSIGHT-MVP/packages/frontend/src/crm/contacts/CalendarTab.tsx
Thomas Reitz 1ecd7dad82 fix(ms365): OAuth-Connect via API-Call statt direktem Browser-Link
Problem: <a href="/api/v1/auth/integrations/microsoft-365"> sendet keinen
JWT-Authorization-Header (JWT liegt im Memory, nicht als Cookie).

Lösung:
- Backend: initM365Integration gibt JSON {url} zurück statt server-redirect
- Frontend: integrationsApi.connectM365() ruft Endpoint via Axios ab, dann
  window.location.href zur OAuth-URL
- ProfilePage + EmailsTab + CalendarTab + TasksTab: <a href> → <button onClick>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 22:57:20 +01:00

118 lines
3.9 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 { useContactCalendar, useIntegrations } from '../hooks';
import { integrationsApi } from '../api';
import type { M365CalendarEvent } from '../types';
interface Props {
contactId: string;
}
function formatEventDate(dt: string): string {
return new Date(dt).toLocaleString('de-DE', {
weekday: 'short',
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
export function CalendarTab({ contactId }: Props) {
const { data: integrationsData } = useIntegrations();
const isConnected = integrationsData?.data?.some(
(i) => i.provider === 'MICROSOFT_365' && i.connected,
) ?? false;
const { data, isLoading, error } = useContactCalendar(contactId);
const events: M365CalendarEvent[] = data?.data ?? [];
if (!isConnected) {
return (
<div style={{ padding: '1.5rem 0', textAlign: 'center' }}>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.9375rem', marginBottom: '1rem' }}>
Verbinden Sie Microsoft 365, um Kalendertermine zu diesem Kontakt zu sehen.
</p>
<button
type="button"
onClick={() => integrationsApi.connectM365()}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.4375rem 1rem',
background: 'var(--color-primary)',
color: 'white',
border: 'none',
borderRadius: 'var(--radius-sm)',
fontSize: '0.875rem',
fontWeight: 600,
cursor: 'pointer',
}}
>
Microsoft 365 verbinden
</button>
</div>
);
}
if (isLoading) {
return <p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem', padding: '1rem 0' }}>Laden</p>;
}
if (error) {
return <p style={{ color: 'var(--color-error)', fontSize: '0.875rem', padding: '1rem 0' }}>Kalendertermine konnten nicht geladen werden.</p>;
}
if (events.length === 0) {
return <p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem', padding: '1rem 0' }}>Keine Kalendertermine in den nächsten 90 Tagen.</p>;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{events.map((event) => (
<a
key={event.id}
href={event.webLink}
target="_blank"
rel="noopener noreferrer"
style={{
display: 'block',
padding: '0.75rem 1rem',
background: 'var(--color-bg-card)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-sm)',
textDecoration: 'none',
borderLeft: '3px solid var(--color-primary)',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '0.5rem' }}>
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-text)', flex: 1 }}>
{event.subject}
</span>
{event.isOnlineMeeting && (
<span style={{
fontSize: '0.6875rem',
fontWeight: 600,
padding: '0.125rem 0.375rem',
background: '#dbeafe',
color: '#1e40af',
borderRadius: '999px',
flexShrink: 0,
}}>
Online
</span>
)}
</div>
<div style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)', marginTop: '0.25rem' }}>
{formatEventDate(event.start.dateTime)} {new Date(event.end.dateTime).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</div>
{event.location?.displayName && (
<div style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)', marginTop: '0.125rem' }}>
📍 {event.location.displayName}
</div>
)}
</a>
))}
</div>
);
}