diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index 6cc9df2..0e6190e 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -1,10 +1,12 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { type Event } from '@/features/event/api/eventService'; import { type DashboardModule, type DashboardModuleContext } from '@/components/dashboard/dashboardModuleTypes'; import { eventDashboardModule } from '@/features/event/dashboardModule'; import { quickPostDashboardModule } from '@/features/photopost/dashboardModule'; +const DASHBOARD_ACTIVE_MODULE_KEY = 'kgb.dashboard.activeModuleId'; + interface DashboardCardProps { title: string; description: string; @@ -49,7 +51,25 @@ interface DashboardProps { } const Dashboard: React.FC = ({ onActionClick, hasActiveEvent, currentEvent }) => { - const [activeModuleId, setActiveModuleId] = useState(null); + const [activeModuleId, setActiveModuleId] = useState(() => { + try { + return sessionStorage.getItem(DASHBOARD_ACTIVE_MODULE_KEY); + } catch { + return null; + } + }); + + useEffect(() => { + try { + if (activeModuleId) { + sessionStorage.setItem(DASHBOARD_ACTIVE_MODULE_KEY, activeModuleId); + } else { + sessionStorage.removeItem(DASHBOARD_ACTIVE_MODULE_KEY); + } + } catch { + // ignore + } + }, [activeModuleId]); const ctx: DashboardModuleContext = { currentEvent, diff --git a/src/features/event/components/EventFormInline.tsx b/src/features/event/components/EventFormInline.tsx index 63f3cb6..c471c16 100644 --- a/src/features/event/components/EventFormInline.tsx +++ b/src/features/event/components/EventFormInline.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; @@ -36,6 +36,7 @@ const EventFormInline: React.FC = ({ onSuccess, onCancel, const [form, setForm] = useState(initialForm); const [mediaPreview, setMediaPreview] = useState(null); const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); const fileInputRef = React.useRef(null); const [promotionStartDate, setPromotionStartDate] = useState(''); const [sectionDates, setSectionDates] = useState(() => Array(INITIAL_ARTISTS + 1).fill('')); @@ -65,6 +66,34 @@ const EventFormInline: React.FC = ({ onSuccess, onCancel, const [activeTab, setActiveTab] = useState<'stammdaten' | 'promotion' | 'artists' | 'schedule'>('stammdaten'); const navExpanded = !isMobile; + const initialSnapshotRef = useRef(null); + const [isDirty, setIsDirty] = useState(false); + + const currentSnapshot = useMemo(() => { + return JSON.stringify({ + form, + promotionStartDate, + promotionOffsetDays, + sectionDates, + artistSections, + }); + }, [artistSections, form, promotionOffsetDays, promotionStartDate, sectionDates]); + + useEffect(() => { + if (initialSnapshotRef.current === null) return; + setIsDirty(initialSnapshotRef.current !== currentSnapshot); + }, [currentSnapshot]); + + useEffect(() => { + if (!isDirty) return; + const handler = (e: BeforeUnloadEvent) => { + e.preventDefault(); + e.returnValue = ''; + }; + window.addEventListener('beforeunload', handler); + return () => window.removeEventListener('beforeunload', handler); + }, [isDirty]); + useEffect(() => { return () => { setForm(initialForm); @@ -72,6 +101,22 @@ const EventFormInline: React.FC = ({ onSuccess, onCancel, }; }, []); + useEffect(() => { + // establish baseline snapshot for create mode immediately + if (mode !== 'create') return; + if (initialSnapshotRef.current !== null) return; + initialSnapshotRef.current = currentSnapshot; + }, [currentSnapshot, mode]); + + useEffect(() => { + // establish baseline snapshot for edit mode after initial load finished + if (mode !== 'edit') return; + if (loadingInitial) return; + if (initialSnapshotRef.current !== null) return; + initialSnapshotRef.current = currentSnapshot; + setIsDirty(false); + }, [currentSnapshot, loadingInitial, mode]); + useEffect(() => { if (mode !== 'edit' || !eventId) return; @@ -118,10 +163,13 @@ const EventFormInline: React.FC = ({ onSuccess, onCancel, dates[0] = section1?.promoDate || start; artistSlots.forEach((slot, idx) => { - if (idx >= artists.length) return; + artists[idx] = { + name: slot.artistName || '', + email: (slot as any).artistEmail || '', + link: (slot as any).artistLink || '', + status: (slot as any).artistStatus || 'Wartet auf Künstler', + }; dates[idx + 1] = slot.promoDate || ''; - artists[idx].name = slot.artistName || ''; - artists[idx].email = slot.artistInstagram || ''; const collabUrl = (slot as any).collabUrl || (slot as any).collab_url; const collabToken = (slot as any).collabToken || (slot as any).collab_token; @@ -130,44 +178,19 @@ const EventFormInline: React.FC = ({ onSuccess, onCancel, } else if (collabToken) { artists[idx].link = `/collab/${collabToken}`; } - - const rawStatus = (slot as any).collab_status || (slot as any).collabStatus; - const status = typeof rawStatus === 'string' ? rawStatus.toUpperCase() : ''; - - // Fallbacks für alte Daten, falls collab_status noch nicht gesetzt ist - const hasArtistData = !!(slot as any).artist_description || !!(slot as any).artistDescription; - const hasLink = !!collabUrl || !!collabToken; - - if (status === 'APPROVED') { - artists[idx].status = 'Fertig'; - } else if (status === 'UPDATED') { - artists[idx].status = 'Fertig'; - } else if (status === 'SUBMITTED') { - artists[idx].status = 'Fertig'; - } else if (status === 'INVITED') { - artists[idx].status = 'Wartet auf Künstler'; - } else if (status === 'PENDING') { - artists[idx].status = 'Künstler einladen'; - } else if (hasArtistData) { - // Legacy: keine Status-Info, aber Beschreibung vorhanden - artists[idx].status = 'Fertig'; - } else if (hasLink) { - artists[idx].status = 'Link generiert'; - } else { - artists[idx].status = 'Wartet auf Künstler'; - } }); setSectionDates(dates); setArtistSections(artists); - } catch (e) { - if (!cancelled) { - setError('Event konnte nicht geladen werden'); - } + + // reset baseline so it can be re-established after all state updates + initialSnapshotRef.current = null; + setIsDirty(false); + } catch (err) { + console.error('Error loading event details', err); + setError('Event konnte nicht geladen werden'); } finally { - if (!cancelled) { - setLoadingInitial(false); - } + if (!cancelled) setLoadingInitial(false); } }; @@ -323,6 +346,7 @@ const EventFormInline: React.FC = ({ onSuccess, onCancel, } setError(null); + setSaving(true); const payload = { title: form.name, @@ -356,10 +380,16 @@ const EventFormInline: React.FC = ({ onSuccess, onCancel, } else { await createEvent(payload); } - onSuccess?.(); + onSuccess(); + + // mark clean after successful save + initialSnapshotRef.current = currentSnapshot; + setIsDirty(false); } catch (err) { - console.error('Error while saving event', err); - setError('Event konnte nicht gespeichert werden'); + console.error('Error saving event:', err); + setError('Speichern fehlgeschlagen'); + } finally { + setSaving(false); } }; diff --git a/src/features/event/components/EventWizard.tsx b/src/features/event/components/EventWizard.tsx index d6904c8..aaf2519 100644 --- a/src/features/event/components/EventWizard.tsx +++ b/src/features/event/components/EventWizard.tsx @@ -7,6 +7,8 @@ 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; @@ -21,6 +23,31 @@ const EventWizard: React.FC = ({ onComplete, onBackToOverview, const [activeEventId, setActiveEventId] = useState(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;