mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
feat: Kompakter Messe-Ticker im Home-Sidebar mit Detail-Popup
- EventCountdownTiles aus homeMain entfernt - Neuer CompactMesseTicker im Sidebar: kleine klickbare Zeilen mit farbiger linker Linie (blau=bevorstehend, grün=läuft) - Klick öffnet MesseDetailModal: Name, Status-Chip, Countdown, Fortschrittsbalken (bei laufenden Messen), Datum, Ort/Stand, Beschreibung, Website-Link - HomeDayAgendaWidget → HomeSidebar: zeigt Ticker + Tages-Agenda in einer Spalte; Sidebar auf 300px verbreitert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3b15c8ab9b
commit
653464c89b
2 changed files with 409 additions and 16 deletions
|
|
@ -72,8 +72,228 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.homeSidebar {
|
.homeSidebar {
|
||||||
width: 280px;
|
width: 300px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Kompakter Messe-Ticker ── */
|
||||||
|
|
||||||
|
.compactMesse {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compactMesseTitle {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compactMesseList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
padding: 0.3125rem 0.5rem;
|
||||||
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeRow:hover {
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeRowName {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeRowCountdown {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Messe-Detail-Modal ── */
|
||||||
|
|
||||||
|
.messeModalBackdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 500;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModal {
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-top: 4px solid #3b82f6;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 440px;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalTitle {
|
||||||
|
font-size: 1.0625rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text);
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeStatusChip {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeChip_upcoming {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeChip_ongoing {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeChip_ended {
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalClose {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.375rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalClose:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalCountdown {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeProgressBar {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeProgressFill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalMeta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalMetaRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalMetaRow svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalDesc {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.55;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalLink {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messeModalLink:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* dark mode Messe-Modal */
|
||||||
|
:global([data-theme='dark']) .messeChip_upcoming {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global([data-theme='dark']) .messeChip_ongoing {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #86efac;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Platzhalter für bestehenden Home-Inhalt ── */
|
/* ── Platzhalter für bestehenden Home-Inhalt ── */
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,185 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAuth } from '../auth/AuthContext';
|
import { useAuth } from '../auth/AuthContext';
|
||||||
import { WeatherWidget } from '../components/WeatherWidget';
|
import { WeatherWidget } from '../components/WeatherWidget';
|
||||||
import { EventCountdownTiles } from '../components/EventCountdownTiles';
|
|
||||||
import { DashboardEmailTab } from './DashboardEmailTab';
|
import { DashboardEmailTab } from './DashboardEmailTab';
|
||||||
import { DashboardCalendarTab, DayAgenda } from './DashboardCalendarTab';
|
import { DashboardCalendarTab, DayAgenda } from './DashboardCalendarTab';
|
||||||
import { DashboardTasksTab } from './DashboardTasksTab';
|
import { DashboardTasksTab } from './DashboardTasksTab';
|
||||||
import { useIntegrations, useOffice365CalendarRange } from '../crm/hooks';
|
import { useIntegrations, useOffice365CalendarRange, useActiveTradeEvents } from '../crm/hooks';
|
||||||
import type { M365CalendarEvent } from '../crm/types';
|
import { useEventCountdown } from '../hooks/useEventCountdown';
|
||||||
|
import type { M365CalendarEvent, TradeEvent } from '../crm/types';
|
||||||
import styles from './DashboardPage.module.css';
|
import styles from './DashboardPage.module.css';
|
||||||
|
|
||||||
|
// ── Messe-Ticker: Hilfsfunktionen ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
function isStillRelevant(event: TradeEvent): boolean {
|
||||||
|
const endDay = new Date(event.endDate);
|
||||||
|
endDay.setHours(23, 59, 59, 999);
|
||||||
|
const cutoff = new Date(endDay.getTime() + 24 * 60 * 60 * 1000);
|
||||||
|
return new Date() < cutoff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateRange(start: string, end: string): string {
|
||||||
|
const opts: Intl.DateTimeFormatOptions = { day: '2-digit', month: '2-digit', year: 'numeric' };
|
||||||
|
return `${new Date(start).toLocaleDateString('de-DE', opts)} – ${new Date(end).toLocaleDateString('de-DE', opts)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Kompakte Messe-Zeile (klickbar) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
function CompactMesseRow({
|
||||||
|
event,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
event: TradeEvent;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
const countdown = useEventCountdown(event);
|
||||||
|
const color =
|
||||||
|
countdown.status === 'upcoming'
|
||||||
|
? '#3b82f6'
|
||||||
|
: countdown.status === 'ongoing'
|
||||||
|
? '#22c55e'
|
||||||
|
: '#9ca3af';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.messeRow}
|
||||||
|
style={{ borderLeftColor: color }}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<span className={styles.messeRowName}>{event.name}</span>
|
||||||
|
<span className={styles.messeRowCountdown}>{countdown.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Messe-Detail-Popup ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function MesseDetailModal({
|
||||||
|
event,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
event: TradeEvent;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const countdown = useEventCountdown(event);
|
||||||
|
const color =
|
||||||
|
countdown.status === 'upcoming'
|
||||||
|
? '#3b82f6'
|
||||||
|
: countdown.status === 'ongoing'
|
||||||
|
? '#22c55e'
|
||||||
|
: '#9ca3af';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.messeModalBackdrop} onClick={onClose}>
|
||||||
|
<div
|
||||||
|
className={styles.messeModal}
|
||||||
|
style={{ borderTopColor: color }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.messeModalHeader}>
|
||||||
|
<div>
|
||||||
|
<h3 className={styles.messeModalTitle}>{event.name}</h3>
|
||||||
|
<span
|
||||||
|
className={`${styles.messeStatusChip} ${styles[`messeChip_${countdown.status}`]}`}
|
||||||
|
>
|
||||||
|
{countdown.status === 'upcoming'
|
||||||
|
? 'Bevorstehend'
|
||||||
|
: countdown.status === 'ongoing'
|
||||||
|
? 'Läuft'
|
||||||
|
: 'Beendet'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.messeModalClose}
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Schließen"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Countdown */}
|
||||||
|
<div className={styles.messeModalCountdown}>{countdown.label}</div>
|
||||||
|
{countdown.status === 'ongoing' && (
|
||||||
|
<div className={styles.messeProgressBar}>
|
||||||
|
<div
|
||||||
|
className={styles.messeProgressFill}
|
||||||
|
style={{ width: `${countdown.progressPercent}%`, background: color }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Daten */}
|
||||||
|
<div className={styles.messeModalMeta}>
|
||||||
|
<div className={styles.messeModalMetaRow}>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
||||||
|
<rect x="2" y="3" width="12" height="11" rx="1" /><path d="M5 1v4M11 1v4M2 7h12" />
|
||||||
|
</svg>
|
||||||
|
<span>{formatDateRange(event.startDate, event.endDate)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{event.location && (
|
||||||
|
<div className={styles.messeModalMetaRow}>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M8 1C5.2 1 3 3.2 3 6c0 4 5 9 5 9s5-5 5-9c0-2.8-2.2-5-5-5z" /><circle cx="8" cy="6" r="1.5" />
|
||||||
|
</svg>
|
||||||
|
<span>{event.location}{event.boothInfo ? ` · Stand: ${event.boothInfo}` : ''}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!event.location && event.boothInfo && (
|
||||||
|
<div className={styles.messeModalMetaRow}>
|
||||||
|
<span>Stand: {event.boothInfo}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{event.description && (
|
||||||
|
<p className={styles.messeModalDesc}>{event.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{event.websiteUrl && (
|
||||||
|
<a
|
||||||
|
href={event.websiteUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={styles.messeModalLink}
|
||||||
|
>
|
||||||
|
Website öffnen ↗
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Kompakter Messe-Ticker für Sidebar ───────────────────────────────────────
|
||||||
|
|
||||||
|
function CompactMesseTicker() {
|
||||||
|
const { data } = useActiveTradeEvents();
|
||||||
|
const [selected, setSelected] = useState<TradeEvent | null>(null);
|
||||||
|
const events = (data?.data ?? []).filter(isStillRelevant);
|
||||||
|
|
||||||
|
if (events.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.compactMesse}>
|
||||||
|
<h4 className={styles.compactMesseTitle}>Messen</h4>
|
||||||
|
<div className={styles.compactMesseList}>
|
||||||
|
{events.map((ev) => (
|
||||||
|
<CompactMesseRow key={ev.id} event={ev} onClick={() => setSelected(ev)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{selected && (
|
||||||
|
<MesseDetailModal event={selected} onClose={() => setSelected(null)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type DashboardTab = 'home' | 'emails' | 'calendar' | 'tasks' | 'contacts';
|
type DashboardTab = 'home' | 'emails' | 'calendar' | 'tasks' | 'contacts';
|
||||||
|
|
||||||
const TABS: { id: DashboardTab; label: string }[] = [
|
const TABS: { id: DashboardTab; label: string }[] = [
|
||||||
|
|
@ -19,9 +190,9 @@ const TABS: { id: DashboardTab; label: string }[] = [
|
||||||
{ id: 'contacts', label: 'Kontakte' },
|
{ id: 'contacts', label: 'Kontakte' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Tages-Agenda Widget für Home-Tab ─────────────────────────────────────────
|
// ── Sidebar: Messe-Ticker + Tages-Agenda ─────────────────────────────────────
|
||||||
|
|
||||||
function HomeDayAgendaWidget() {
|
function HomeSidebar() {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const todayISO = today.toISOString().slice(0, 10);
|
const todayISO = today.toISOString().slice(0, 10);
|
||||||
const tomorrowISO = new Date(today.getTime() + 86_400_000).toISOString().slice(0, 10);
|
const tomorrowISO = new Date(today.getTime() + 86_400_000).toISOString().slice(0, 10);
|
||||||
|
|
@ -35,16 +206,19 @@ function HomeDayAgendaWidget() {
|
||||||
const { data: eventsData, isLoading } = useOffice365CalendarRange(todayISO, tomorrowISO);
|
const { data: eventsData, isLoading } = useOffice365CalendarRange(todayISO, tomorrowISO);
|
||||||
const events: M365CalendarEvent[] = eventsData?.data ?? [];
|
const events: M365CalendarEvent[] = eventsData?.data ?? [];
|
||||||
|
|
||||||
if (!isConnected) return null;
|
|
||||||
if (isLoading) return (
|
|
||||||
<div className={styles.homeSidebar}>
|
|
||||||
<p style={{ fontSize: '0.875rem', color: 'var(--color-text-muted)' }}>Kalender wird geladen…</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.homeSidebar}>
|
<div className={styles.homeSidebar}>
|
||||||
<DayAgenda day={today} events={events} />
|
{/* Kompakter Messe-Ticker immer oben */}
|
||||||
|
<CompactMesseTicker />
|
||||||
|
|
||||||
|
{/* Tages-Agenda nur wenn O365 verbunden */}
|
||||||
|
{isConnected && (
|
||||||
|
isLoading
|
||||||
|
? <p style={{ fontSize: '0.875rem', color: 'var(--color-text-muted)', marginTop: '0.5rem' }}>
|
||||||
|
Kalender wird geladen…
|
||||||
|
</p>
|
||||||
|
: <DayAgenda day={today} events={events} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +241,6 @@ function HomeTab({ firstName, lastName, city, role }: {
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.homeLayout}>
|
<div className={styles.homeLayout}>
|
||||||
<div className={styles.homeMain}>
|
<div className={styles.homeMain}>
|
||||||
<EventCountdownTiles />
|
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
<p style={{ color: 'var(--color-text-secondary)' }}>
|
<p style={{ color: 'var(--color-text-secondary)' }}>
|
||||||
INSIGHT Platform - Sprint 1 Alpha
|
INSIGHT Platform - Sprint 1 Alpha
|
||||||
|
|
@ -77,7 +250,7 @@ function HomeTab({ firstName, lastName, city, role }: {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HomeDayAgendaWidget />
|
<HomeSidebar />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue