diff --git a/packages/frontend/src/shell/DashboardCalendarTab.module.css b/packages/frontend/src/shell/DashboardCalendarTab.module.css index 75e3f97..b73cfb9 100644 --- a/packages/frontend/src/shell/DashboardCalendarTab.module.css +++ b/packages/frontend/src/shell/DashboardCalendarTab.module.css @@ -277,11 +277,11 @@ padding-left: 4px; } -/* ── Wochenansicht ── */ +/* ── Wochenansicht (Mo–Fr) ── */ .weekGrid { display: grid; - grid-template-columns: repeat(7, 1fr); + grid-template-columns: repeat(5, 1fr); /* nur Arbeitstage */ } .weekCol { @@ -383,6 +383,130 @@ text-overflow: ellipsis; } +/* ── Agenda-Listenansicht (eigene View, 14 Tage) ── */ + +.agendaFullView { + display: flex; + flex-direction: column; +} + +.agendaFullDay { + display: flex; + align-items: flex-start; + gap: 0; + border-bottom: 1px solid var(--color-border); + cursor: pointer; + transition: background 0.1s; +} + +.agendaFullDay:last-child { + border-bottom: none; +} + +.agendaFullDay:hover { + background: rgba(59, 130, 246, 0.03); +} + +.agendaFullDaySelected { + background: rgba(59, 130, 246, 0.06) !important; +} + +.agendaFullDayHdr { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 72px; + flex-shrink: 0; + padding: 0.875rem 0.5rem; + gap: 0.125rem; + border-right: 1px solid var(--color-border); +} + +.agendaFullDayHdrToday .agendaFullNum { + background: var(--color-primary); + color: #fff; + border-radius: 50%; +} + +.agendaFullWeekday { + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted); +} + +.agendaFullNum { + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text); + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; +} + +.agendaFullNumToday { + background: var(--color-primary); + color: #fff; + border-radius: 50%; +} + +.agendaFullMonth { + font-size: 0.6875rem; + color: var(--color-text-muted); +} + +.agendaFullEvents { + flex: 1; + padding: 0.625rem 1rem; + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.agendaFullEmpty { + font-size: 0.8125rem; + color: var(--color-text-muted); + padding: 0.25rem 0; +} + +.agendaFullEvent { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.375rem 0.625rem; + border-left: 3px solid transparent; + border-radius: 2px; + text-decoration: none; + color: inherit; + background: var(--color-bg); + transition: background 0.12s; +} + +.agendaFullEvent:hover { + background: rgba(59, 130, 246, 0.07); +} + +.agendaFullEventTime { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text-muted); + flex-shrink: 0; + min-width: 42px; +} + +.agendaFullEventSubj { + font-size: 0.875rem; + color: var(--color-text); + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + /* ── Tages-Agenda (rechte Spalte) ── */ .agenda { diff --git a/packages/frontend/src/shell/DashboardCalendarTab.tsx b/packages/frontend/src/shell/DashboardCalendarTab.tsx index 14c243b..749021c 100644 --- a/packages/frontend/src/shell/DashboardCalendarTab.tsx +++ b/packages/frontend/src/shell/DashboardCalendarTab.tsx @@ -3,7 +3,7 @@ import { useIntegrations, useOffice365CalendarRange } from '../crm/hooks'; import type { M365CalendarEvent } from '../crm/types'; import styles from './DashboardCalendarTab.module.css'; -type ViewMode = 'month' | 'week'; +export type CalendarViewMode = 'month' | 'week' | 'agenda'; // ── Date Helpers ─────────────────────────────────────────────────────────────── @@ -36,10 +36,10 @@ function getMonthCells(date: Date): Date[] { return Array.from({ length: 42 }, (_, i) => addDays(gridStart, i)); } -/** 7 Tage der Woche (Mo–So) */ -function getWeekDays(date: Date): Date[] { +/** 5 Arbeitstage der Woche (Mo–Fr) */ +function getWorkWeekDays(date: Date): Date[] { const monday = startOfWeekMonday(date); - return Array.from({ length: 7 }, (_, i) => addDays(monday, i)); + return Array.from({ length: 5 }, (_, i) => addDays(monday, i)); } function toISODate(date: Date): string { @@ -70,9 +70,9 @@ function eventColor(id: string): string { return EVENT_COLORS[h]; } -// ── Day Agenda (rechte Spalte) ───────────────────────────────────────────────── +// ── Day Agenda (Tages-Detailansicht, auch für Home) ─────────────────────────── -function DayAgenda({ +export function DayAgenda({ day, events, }: { @@ -137,6 +137,78 @@ function DayAgenda({ ); } +// ── 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' })} + +
+
+ {dayEvents.length === 0 ? ( + Kein Termin + ) : ( + dayEvents.map((e) => ( + ev.stopPropagation()} + > + + {formatTime(e.start.dateTime)} + + {e.subject} + {e.isOnlineMeeting && ( + Online + )} + + )) + )} +
+
+ ); + })} +
+ ); +} + // ── Monatsansicht ───────────────────────────────────────────────────────────── const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; @@ -211,7 +283,7 @@ function MonthView({ ); } -// ── Wochenansicht ───────────────────────────────────────────────────────────── +// ── Wochenansicht (Mo–Fr) ───────────────────────────────────────────────────── function WeekView({ currentDate, @@ -225,7 +297,7 @@ function WeekView({ onDayClick: (d: Date) => void; }) { const today = new Date(); - const weekDays = getWeekDays(currentDate); + const weekDays = getWorkWeekDays(currentDate); // nur Mo–Fr return (
@@ -287,7 +359,7 @@ export function DashboardCalendarTab() { (i) => i.provider === 'MICROSOFT_365' && i.connected, ) ?? false; - const [viewMode, setViewMode] = useState('month'); + const [viewMode, setViewMode] = useState('month'); const [currentDate, setCurrentDate] = useState(() => new Date()); const [selectedDay, setSelectedDay] = useState(() => new Date()); @@ -297,12 +369,16 @@ export function DashboardCalendarTab() { ? startOfWeekMonday( new Date(currentDate.getFullYear(), currentDate.getMonth(), 1), ) - : startOfWeekMonday(currentDate); + : viewMode === 'week' + ? startOfWeekMonday(currentDate) + : currentDate; // agenda: ab currentDate const rangeEnd = viewMode === 'month' - ? addDays(rangeStart, 42) // 6 Wochen - : addDays(startOfWeekMonday(currentDate), 7); + ? 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), @@ -314,8 +390,10 @@ export function DashboardCalendarTab() { const d = new Date(currentDate); if (viewMode === 'month') { d.setMonth(d.getMonth() + delta); - } else { + } else if (viewMode === 'week') { d.setDate(d.getDate() + delta * 7); + } else { + d.setDate(d.getDate() + delta * 14); } setCurrentDate(d); }; @@ -327,18 +405,26 @@ export function DashboardCalendarTab() { }; // Anzeigebezeichnung - const rangeLabel = - viewMode === 'month' - ? currentDate.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }) - : (() => { - const days = getWeekDays(currentDate); - const f = days[0]; - const l = days[6]; - 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' })}`; - })(); + 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…

; @@ -403,6 +489,13 @@ export function DashboardCalendarTab() { > Woche +
@@ -416,18 +509,19 @@ export function DashboardCalendarTab() {

)} - {/* Hauptbereich: Kalender + Agenda */} + {/* Hauptbereich */} {!isLoading && (
- {viewMode === 'month' ? ( + {viewMode === 'month' && ( - ) : ( + )} + {viewMode === 'week' && ( )} + {viewMode === 'agenda' && ( + + )}
- + {/* Tages-Agenda nur bei Monat- und Wochenansicht */} + {(viewMode === 'month' || viewMode === 'week') && ( + + )}
)} diff --git a/packages/frontend/src/shell/DashboardEmailTab.module.css b/packages/frontend/src/shell/DashboardEmailTab.module.css index 81e94d4..06806b1 100644 --- a/packages/frontend/src/shell/DashboardEmailTab.module.css +++ b/packages/frontend/src/shell/DashboardEmailTab.module.css @@ -212,6 +212,7 @@ border-radius: var(--radius-sm); transition: border-color 0.15s, box-shadow 0.15s; overflow: hidden; + cursor: pointer; } .emailCard:hover { @@ -223,12 +224,10 @@ border-left: 3px solid var(--color-primary); } -.emailLink { +.emailCardInner { display: block; padding: 0.75rem 1rem; - text-decoration: none; color: inherit; - padding-right: 6rem; /* Platz für den "Aktivität"-Button */ } .emailHeader { @@ -307,36 +306,261 @@ word-break: break-word; } -/* ── Aktivität-Button (absolut positioniert innerhalb der Karte) ── */ +/* ── Detail-Modal (E-Mail Lesefenster) ── */ -.saveActivityBtn { - position: absolute; - top: 0.75rem; - right: 0.75rem; - padding: 0.3rem 0.625rem; +.detailModal { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius); + width: 100%; + max-width: 680px; + max-height: 88vh; + display: flex; + flex-direction: column; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28); + overflow: hidden; +} + +.detailHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + padding: 1.25rem 1.5rem 1rem; + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} + +.detailSubject { + font-size: 1.0625rem; + font-weight: 600; + color: var(--color-text); + margin: 0; + line-height: 1.35; +} + +.closeBtn { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + background: none; + border: none; + color: var(--color-text-muted); + font-size: 1rem; + cursor: pointer; + border-radius: 50%; + transition: background 0.12s, color 0.12s; + margin-top: -0.125rem; +} + +.closeBtn:hover { + background: var(--color-bg); + color: var(--color-text); +} + +.detailMeta { + display: flex; + flex-direction: column; + gap: 0.3rem; + padding: 0.875rem 1.5rem; + background: var(--color-bg); + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} + +.detailMetaRow { + display: flex; + align-items: baseline; + gap: 0.75rem; +} + +.detailMetaLabel { font-size: 0.75rem; font-weight: 600; + color: var(--color-text-muted); + min-width: 48px; + flex-shrink: 0; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.detailMetaValue { + font-size: 0.875rem; + color: var(--color-text); +} + +.detailSenderEmail { + color: var(--color-text-muted); + font-size: 0.8125rem; +} + +.detailBody { + flex: 1; + overflow-y: auto; + padding: 1.25rem 1.5rem; + font-size: 0.9rem; + color: var(--color-text); + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; + min-height: 100px; + max-height: 280px; + background: var(--color-bg-card); +} + +.detailBodyEmpty { + color: var(--color-text-muted); + font-style: italic; +} + +/* ── CRM-Bereich im Detail-Modal ── */ + +.detailCrm { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.875rem 1.5rem; background: var(--color-bg); - color: var(--color-primary); + border-top: 1px solid var(--color-border); + flex-shrink: 0; + flex-wrap: wrap; +} + +.detailCrmTitle { + font-size: 0.75rem; + font-weight: 700; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + flex-shrink: 0; +} + +.detailCrmLoading { + font-size: 0.875rem; + color: var(--color-text-muted); +} + +.detailCrmFound { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.detailCrmInfo { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.detailCrmAvatar { + font-size: 1.125rem; +} + +.detailCrmName { + font-size: 0.9rem; + font-weight: 600; + color: var(--color-text); +} + +.detailCrmCompany { + font-size: 0.875rem; + color: var(--color-text-muted); +} + +.detailCrmActions { + display: flex; + gap: 0.5rem; + flex-shrink: 0; +} + +.detailCrmOpenBtn { + padding: 0.375rem 0.875rem; + background: none; border: 1px solid var(--color-primary); border-radius: var(--radius-sm); + color: var(--color-primary); + font-size: 0.8125rem; + font-weight: 600; cursor: pointer; transition: all 0.15s; white-space: nowrap; - opacity: 0; - pointer-events: none; } -.emailCard:hover .saveActivityBtn { - opacity: 1; - pointer-events: auto; -} - -.saveActivityBtn:hover { +.detailCrmOpenBtn:hover { background: var(--color-primary); color: #fff; } +.detailCrmMissing { + flex: 1; + display: flex; + align-items: center; + gap: 0.875rem; + flex-wrap: wrap; +} + +.detailCrmMissingText { + font-size: 0.875rem; + color: var(--color-text-muted); +} + +.detailCrmCreateBtn { + padding: 0.375rem 0.875rem; + background: none; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-muted); + font-size: 0.8125rem; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.detailCrmCreateBtn:hover { + border-color: var(--color-primary); + color: var(--color-primary); +} + +/* ── Detail-Footer ── */ + +.detailFooter { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.75rem; + padding: 0.875rem 1.5rem; + border-top: 1px solid var(--color-border); + background: var(--color-bg-card); + flex-shrink: 0; +} + +.outlookBtn { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.4375rem 1rem; + background: none; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-muted); + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + transition: all 0.15s; +} + +.outlookBtn:hover { + border-color: var(--color-primary); + color: var(--color-primary); +} + /* ── Aktivität-Modal ── */ .modalOverlay { diff --git a/packages/frontend/src/shell/DashboardEmailTab.tsx b/packages/frontend/src/shell/DashboardEmailTab.tsx index dc4a550..3805585 100644 --- a/packages/frontend/src/shell/DashboardEmailTab.tsx +++ b/packages/frontend/src/shell/DashboardEmailTab.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useIntegrations, @@ -67,27 +68,28 @@ function formatEmailDate(iso: string): string { }); } -// ── EmailCard ───────────────────────────────────────────────────────────────── +// ── EmailCard (Klick öffnet Detail-Modal) ──────────────────────────────────── interface EmailCardProps { email: M365Email; - onSaveActivity: (email: M365Email, contact: CrmContactLookup) => void; + onClick: () => void; } -function EmailCard({ email, onSaveActivity }: EmailCardProps) { +function EmailCard({ email, onClick }: EmailCardProps) { const senderEmail = email.from?.emailAddress?.address ?? null; const { data: contactData } = useContactByEmail(senderEmail); const contact = contactData?.data ?? null; const displayName = email.from?.emailAddress?.name || senderEmail || '—'; return ( -
- +
e.key === 'Enter' && onClick()} + > +
{!email.isRead && - {contact && ( - - )} +
+
+ ); +} + +// ── EmailDetailModal ────────────────────────────────────────────────────────── + +interface EmailDetailModalProps { + email: M365Email; + onClose: () => void; + onSaveActivity: (email: M365Email, contact: CrmContactLookup) => void; +} + +function EmailDetailModal({ email, onClose, onSaveActivity }: EmailDetailModalProps) { + const navigate = useNavigate(); + const senderEmail = email.from?.emailAddress?.address ?? null; + const senderName = email.from?.emailAddress?.name ?? ''; + + const { data: contactData, isLoading: contactLoading } = useContactByEmail(senderEmail); + const contact = contactData?.data ?? null; + + const contactName = + contact + ? [contact.firstName, contact.lastName].filter(Boolean).join(' ') || + contact.email || + '—' + : null; + + const handleOpenContact = () => { + onClose(); + navigate(`/crm/contacts/${contact!.id}`); + }; + + const handleCreateContact = () => { + onClose(); + navigate('/crm/contacts'); + }; + + return ( +
+
e.stopPropagation()}> + + {/* ── Kopfzeile ── */} +
+

+ {email.subject || '(Kein Betreff)'} +

+ +
+ + {/* ── Meta ── */} +
+
+ Von + + {senderName || senderEmail || '—'} + {senderName && senderEmail && ( + <{senderEmail}> + )} + +
+
+ Datum + + {new Date(email.receivedDateTime).toLocaleString('de-DE', { + weekday: 'short', + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} + +
+ {email.hasAttachments && ( +
+ Anhang + 📎 Vorhanden +
+ )} +
+ + {/* ── Body-Vorschau ── */} +
+ {email.bodyPreview || ( + Kein Vorschautext verfügbar. + )} +
+ + {/* ── CRM-Bereich ── */} +
+ CRM + + {contactLoading ? ( + Wird geprüft… + ) : contact ? ( +
+
+ 👤 +
+ {contactName} + {contact.companyName && ( + · {contact.companyName} + )} +
+
+
+ + +
+
+ ) : ( +
+ + {senderEmail + ? `Kein CRM-Kontakt für ${senderEmail}` + : 'Kein Absender bekannt'} + + {senderEmail && ( + + )} +
+ )} +
+ + {/* ── Footer ── */} +
+ + ↗ In Outlook öffnen + +
+
); } @@ -242,6 +395,7 @@ export function DashboardEmailTab() { const [selectedFolderId, setSelectedFolderId] = useState(null); const [days, setDays] = useState(7); + const [detailEmail, setDetailEmail] = useState(null); const [activityTarget, setActivityTarget] = useState(null); const [lastSaved, setLastSaved] = useState(null); @@ -252,7 +406,6 @@ export function DashboardEmailTab() { // Standardmäßig Posteingang — Graph API akzeptiert Well-Known-Namen direkt als ID, // sodass E-Mails sofort geladen werden, bevor die Ordnerliste verfügbar ist. - // Sobald Ordner geladen sind, wird die echte ID aus der Liste verwendet. const inboxFolder = sortedFolders.find(isInboxFolder); const activeFolderId = selectedFolderId ?? inboxFolder?.id ?? 'inbox'; @@ -370,15 +523,28 @@ export function DashboardEmailTab() { { + onClick={() => { setLastSaved(null); - setActivityTarget({ email: e, contact }); + setDetailEmail(email); }} /> ))}
+ {/* Detail-Modal */} + {detailEmail && ( + setDetailEmail(null)} + onSaveActivity={(e, contact) => { + setDetailEmail(null); + setLastSaved(null); + setActivityTarget({ email: e, contact }); + }} + /> + )} + {/* Aktivität-Modal */} {activityTarget && ( i.provider === 'MICROSOFT_365' && i.connected, + ) ?? false; + + const { data: eventsData, isLoading } = useOffice365CalendarRange(todayISO, tomorrowISO); + const events: M365CalendarEvent[] = eventsData?.data ?? []; + + if (!isConnected) return null; + if (isLoading) return ( +
+

Kalender wird geladen…

+
+ ); + + return ( +
+ +
+ ); +} + // ── Tab-Inhalte ─────────────────────────────────────────────────────────────── function HomeTab({ firstName, lastName, city, role }: { @@ -32,14 +64,19 @@ function HomeTab({ firstName, lastName, city, role }: {
- -
-

- INSIGHT Platform - Sprint 1 Alpha -

-

- Rolle: {role} -

+
+
+ +
+

+ INSIGHT Platform - Sprint 1 Alpha +

+

+ Rolle: {role} +

+
+
+
);