275 lines
11 KiB
TypeScript
275 lines
11 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
|
|
import { type Event } from '@/features/event/api/eventService';
|
|
import { ArrowLeft, LayoutGrid } from 'lucide-react';
|
|
import { useHeader } from '@/contexts/HeaderContext';
|
|
import EventFormInline from './EventFormInline';
|
|
|
|
const EVENT_WIZARD_STATE_KEY = 'kgb.eventWizard.state';
|
|
|
|
interface EventWizardProps {
|
|
onComplete: () => void;
|
|
onBackToOverview: () => void;
|
|
events: Event[];
|
|
loading: boolean;
|
|
error: string | null;
|
|
onReload: () => void;
|
|
}
|
|
|
|
const EventWizard: React.FC<EventWizardProps> = ({ onComplete, onBackToOverview, events, loading, error, onReload }) => {
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [activeEventId, setActiveEventId] = useState<string | number | null>(null);
|
|
const { setHeader, resetHeader } = useHeader();
|
|
|
|
useEffect(() => {
|
|
try {
|
|
const raw = sessionStorage.getItem(EVENT_WIZARD_STATE_KEY);
|
|
if (!raw) return;
|
|
const parsed = JSON.parse(raw) as { showForm?: boolean; activeEventId?: string | number | null };
|
|
if (typeof parsed.showForm === 'boolean') setShowForm(parsed.showForm);
|
|
if (parsed.activeEventId !== undefined) setActiveEventId(parsed.activeEventId);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
// only on mount
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
try {
|
|
sessionStorage.setItem(
|
|
EVENT_WIZARD_STATE_KEY,
|
|
JSON.stringify({ showForm, activeEventId: activeEventId ?? null })
|
|
);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}, [activeEventId, showForm]);
|
|
|
|
const activeEvent = useMemo(() => {
|
|
if (!activeEventId) return null;
|
|
return events.find((e) => String(e.id) === String(activeEventId)) || null;
|
|
}, [activeEventId, events]);
|
|
|
|
const canGoTimeline = showForm;
|
|
const handleBackToTimeline = () => {
|
|
setShowForm(false);
|
|
setActiveEventId(null);
|
|
};
|
|
|
|
useEffect(() => {
|
|
onReload();
|
|
}, [onReload]);
|
|
|
|
useEffect(() => {
|
|
const eventName = (activeEvent as any)?.title || (activeEvent as any)?.name || null;
|
|
|
|
const title = showForm
|
|
? activeEventId
|
|
? (
|
|
<div className="flex flex-col items-center leading-tight">
|
|
<div>Event bearbeiten</div>
|
|
{eventName ? <div className="text-xs text-muted-foreground font-normal">{eventName}</div> : null}
|
|
</div>
|
|
)
|
|
: 'Event erstellen'
|
|
: 'Veranstaltungen';
|
|
|
|
setHeader({
|
|
title,
|
|
left: (
|
|
<>
|
|
<Button type="button" variant="ghost" size="sm" onClick={onBackToOverview} className="h-10 px-2">
|
|
<span className="flex flex-col items-center leading-tight">
|
|
<LayoutGrid className="h-4 w-4" />
|
|
<span className="text-[10px] mt-0.5">Dashboard</span>
|
|
</span>
|
|
</Button>
|
|
{showForm && (
|
|
<Button type="button" variant="ghost" size="sm" onClick={handleBackToTimeline} className="h-10 px-2">
|
|
<span className="flex flex-col items-center leading-tight">
|
|
<ArrowLeft className="h-4 w-4" />
|
|
<span className="text-[10px] mt-0.5">Timeline</span>
|
|
</span>
|
|
</Button>
|
|
)}
|
|
</>
|
|
),
|
|
mobileMenu: (
|
|
<>
|
|
<DropdownMenuItem onClick={onBackToOverview} className="gap-2">
|
|
<LayoutGrid className="h-4 w-4" />
|
|
<span>Zurück zum Dashboard</span>
|
|
</DropdownMenuItem>
|
|
{showForm ? <DropdownMenuSeparator /> : null}
|
|
{showForm && (
|
|
<DropdownMenuItem onClick={handleBackToTimeline} className="gap-2">
|
|
<ArrowLeft className="h-4 w-4" />
|
|
<span>Zurück zur Timeline</span>
|
|
</DropdownMenuItem>
|
|
)}
|
|
</>
|
|
),
|
|
});
|
|
|
|
return () => {
|
|
resetHeader();
|
|
};
|
|
}, [activeEvent, activeEventId, onBackToOverview, resetHeader, setHeader, showForm]);
|
|
|
|
const sortedEvents = useMemo(() => {
|
|
if (!events.length) return [] as Event[];
|
|
return [...events].sort((a, b) => {
|
|
const aDate = a?.eventDate || a?.event_date || a?.begin_date;
|
|
const bDate = b?.eventDate || b?.event_date || b?.begin_date;
|
|
const aTime = aDate ? new Date(aDate).getTime() : 0;
|
|
const bTime = bDate ? new Date(bDate).getTime() : 0;
|
|
return aTime - bTime;
|
|
});
|
|
}, [events]);
|
|
|
|
const activeRef = useRef<HTMLDivElement | null>(null);
|
|
const todayIndex = useMemo(() => {
|
|
if (!sortedEvents.length) return -1;
|
|
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
// Finde das erste Event mit Datum heute oder in der Zukunft
|
|
const upcomingIndex = sortedEvents.findIndex((event) => {
|
|
const baseDate = event?.eventDate || event?.event_date || event?.begin_date;
|
|
if (!baseDate) return false;
|
|
const date = new Date(baseDate);
|
|
if (Number.isNaN(date.getTime())) return false;
|
|
date.setHours(0, 0, 0, 0);
|
|
return date.getTime() >= today.getTime();
|
|
});
|
|
|
|
// Wenn nichts in der Zukunft liegt, scrolle zum letzten Event (jüngstes vergangenes)
|
|
if (upcomingIndex === -1) {
|
|
return sortedEvents.length - 1;
|
|
}
|
|
|
|
return upcomingIndex;
|
|
}, [sortedEvents]);
|
|
|
|
useEffect(() => {
|
|
if (todayIndex === -1) return;
|
|
if (!activeRef.current) return;
|
|
activeRef.current.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
|
}, [todayIndex, sortedEvents]);
|
|
|
|
const formatDate = (dateString?: string) => {
|
|
if (!dateString) return '';
|
|
const date = new Date(dateString);
|
|
if (Number.isNaN(date.getTime())) return dateString;
|
|
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
};
|
|
|
|
return (
|
|
<Card className="w-full mx-auto md:max-w-5xl">
|
|
<CardContent>
|
|
{showForm ? (
|
|
<div className="space-y-4">
|
|
<EventFormInline
|
|
mode={activeEventId ? 'edit' : 'create'}
|
|
eventId={activeEventId}
|
|
onSuccess={() => {
|
|
setShowForm(false);
|
|
setActiveEventId(null);
|
|
onReload();
|
|
}}
|
|
onCancel={() => {
|
|
setShowForm(false);
|
|
setActiveEventId(null);
|
|
}}
|
|
/>
|
|
</div>
|
|
) : loading ? (
|
|
<div className="text-sm text-muted-foreground py-10">Events werden geladen...</div>
|
|
) : error ? (
|
|
<div className="rounded-md border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive">{error}</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
<div className="pb-2">
|
|
<div className="mb-2 text-xs text-muted-foreground">
|
|
{sortedEvents.length} Veranstaltung{sortedEvents.length === 1 ? '' : 'en'} geladen
|
|
</div>
|
|
<div className="relative sm:min-h-[200px]">
|
|
<div className="hidden sm:block absolute left-0 right-0 top-1/2 h-px -translate-y-1/2 bg-muted-foreground/30" />
|
|
<div className="relative -mx-4 sm:mx-0 px-4 sm:px-0 overflow-x-auto py-4 sm:py-8">
|
|
<div className="flex flex-row gap-4 sm:gap-6">
|
|
{sortedEvents.map((event, index) => {
|
|
const isToday = index === todayIndex && todayIndex !== -1;
|
|
const title = (event as any).title || event.name || 'Unbenanntes Event';
|
|
const singleDate = (event as any).eventDate || (event as any).event_date || event.begin_date;
|
|
const status = ((event as any).status || (event as any).overall_status) as 'PENDING' | 'ON_AIR' | undefined;
|
|
|
|
// Status nur für heutige oder zukünftige Events anzeigen
|
|
let statusToShow: 'PENDING' | 'ON_AIR' | undefined = status;
|
|
if (singleDate) {
|
|
const d = new Date(singleDate);
|
|
if (!Number.isNaN(d.getTime())) {
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
d.setHours(0, 0, 0, 0);
|
|
if (d.getTime() < today.getTime()) {
|
|
statusToShow = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
const statusLabel = statusToShow === 'ON_AIR' ? 'On Air' : 'Pending';
|
|
const statusClass = statusToShow === 'ON_AIR' ? 'bg-emerald-100 text-emerald-700' : 'bg-amber-100 text-amber-700';
|
|
return (
|
|
<div
|
|
key={event.id || `${event.name}-${index}`}
|
|
ref={isToday ? activeRef : undefined}
|
|
className="flex w-[220px] sm:w-auto flex-shrink-0 sm:flex-shrink min-w-0 sm:min-w-[240px] flex-col items-stretch sm:items-center text-left sm:text-center cursor-pointer"
|
|
onClick={() => {
|
|
const eid = event.id ?? `${event.name || 'event'}-${index}`;
|
|
setActiveEventId(eid as any);
|
|
setShowForm(true);
|
|
}}
|
|
>
|
|
<div className={`hidden sm:block h-3 w-3 rounded-full border-2 ${isToday ? 'border-primary bg-primary' : 'border-muted-foreground/40 bg-background'}`} />
|
|
<div className={`mt-2 sm:mt-4 w-full rounded-lg border p-4 text-left transition-shadow ${isToday ? 'border-primary/60 shadow-lg' : 'border-border'}`}>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="text-sm font-semibold leading-tight line-clamp-2">{title}</div>
|
|
{statusToShow && (
|
|
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${statusClass}`}>
|
|
{statusLabel}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="mt-2 text-xs text-muted-foreground">{formatDate(singleDate)}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-center">
|
|
<Button
|
|
onClick={() => {
|
|
setActiveEventId(null);
|
|
setShowForm(true);
|
|
}}
|
|
>
|
|
Neu erstellen
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default EventWizard;
|