mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
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>
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
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>
|
||
);
|
||
}
|