/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */ /* Hallmark · component: App · genre: editorial · theme: Atelier (Dark & Light) */ import React, { useState, useEffect } from 'react'; import Navbar from './components/Navbar'; import ShiftMatrixTable from './components/ShiftMatrixTable'; import GuestSignupModal from './components/GuestSignupModal'; import SkillsModal from './components/SkillsModal'; import AuthModal from './components/AuthModal'; import HomePage from './pages/HomePage'; import CalendarPage from './pages/CalendarPage'; import AdminPage from './pages/AdminPage'; import EventEditorPage from './pages/EventEditorPage'; import TemplatesPage from './pages/TemplatesPage'; import { apiFetch, getAuthToken, setAuthToken } from './api/client'; import { Plus, CheckCircle2 } from 'lucide-react'; export default function App() { const [user, setUser] = useState(null); const [branding, setBranding] = useState(null); const [skills, setSkills] = useState([]); const [events, setEvents] = useState([]); const [selectedEventId, setSelectedEventId] = useState(null); const [selectedEventData, setSelectedEventData] = useState(null); const [loading, setLoading] = useState(true); // Active Navigation Tab: 'home' | 'calendar' | 'schedule' | 'admin' | 'event-editor' | 'templates' const [activeTab, setActiveTab] = useState('home'); // Modals state (Authentication & Guest Signup) const [showAuthModal, setShowAuthModal] = useState(false); const [showGuestModal, setShowGuestModal] = useState(false); const [guestTargetShift, setGuestTargetShift] = useState(null); const [showSkillsModal, setShowSkillsModal] = useState(false); const [showProfileModal, setShowProfileModal] = useState(false); // Theme Mode: 'dark' | 'light' | 'auto' const [themeMode, setThemeMode] = useState( localStorage.getItem('theme_mode') || 'auto' ); // Toast / notification state const [toastMessage, setToastMessage] = useState(null); // PWA install prompt const [deferredPrompt, setDeferredPrompt] = useState(null); useEffect(() => { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); setDeferredPrompt(e); }); initApp(); }, []); useEffect(() => { localStorage.setItem('theme_mode', themeMode); const applyTheme = () => { let activeTheme = themeMode; if (themeMode === 'auto') { activeTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } document.documentElement.setAttribute('data-theme', activeTheme); }; applyTheme(); if (themeMode === 'auto') { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleChange = () => applyTheme(); mediaQuery.addEventListener('change', handleChange); return () => mediaQuery.removeEventListener('change', handleChange); } }, [themeMode]); useEffect(() => { if (branding?.primary_color) { document.documentElement.style.setProperty('--brand-primary', branding.primary_color); document.documentElement.style.setProperty( '--brand-secondary', branding.secondary_color || branding.primary_color ); } }, [branding]); const [claimToken, setClaimToken] = useState(null); const [prefilledGuestName, setPrefilledGuestName] = useState(null); const showNotification = (msg) => { setToastMessage(msg); setTimeout(() => setToastMessage(null), 5000); }; const initApp = async () => { setLoading(true); try { // 1. Fetch Branding try { const brandData = await apiFetch('/branding/'); setBranding(brandData); } catch (e) {} // 2. Fetch User profile if token exists if (getAuthToken()) { try { const userData = await apiFetch('/users/me/'); setUser(userData); if (!userData.is_admin_user && !userData.is_staff && !userData.is_superuser && activeTab === 'admin') { setActiveTab('home'); } } catch (e) { setAuthToken(null); setUser(null); setActiveTab('home'); } } // 3. Check URL parameters for email verification and claim link const urlParams = new URLSearchParams(window.location.search); const verifyToken = urlParams.get('verify_email'); if (verifyToken) { try { const data = await apiFetch('/users/verify-email/', { method: 'POST', body: JSON.stringify({ token: verifyToken }) }); setAuthToken(data.token); setUser(data.user); showNotification(data.message || '✅ E-Mail-Adresse erfolgreich bestätigt!'); window.history.replaceState({}, document.title, window.location.pathname); } catch (err) { showNotification(`⚠️ E-Mail Bestätigung: ${err.message}`); } } const cToken = urlParams.get('claim_token'); const gName = urlParams.get('guest_name'); if (cToken && gName) { setClaimToken(cToken); setPrefilledGuestName(gName); if (!getAuthToken()) { setShowAuthModal(true); } } // 4. Fetch Skills & Events await refreshSkills(); await refreshEvents(); } catch (err) { console.error(err); } finally { setLoading(false); } }; const handleGenerateClaimLink = async (signup) => { try { const data = await apiFetch(`/users/signups/${signup.id}/generate-claim-link/`, { method: 'POST' }); const claimUrl = `${window.location.origin}/?claim_token=${data.token}&guest_name=${encodeURIComponent(data.guest_name)}`; if (navigator.clipboard) { await navigator.clipboard.writeText(claimUrl); showNotification(`✅ Einladungs-Link für ${data.guest_name} in Zwischenablage kopiert!`); } else { prompt(`Einladungs-Link für ${data.guest_name} kopieren:`, claimUrl); } } catch (err) { alert(err.message || 'Fehler beim Erstellen des Links.'); } }; const refreshSkills = async () => { try { const data = await apiFetch('/skills/'); setSkills(data.results || data); } catch (e) {} }; const updateUrlParam = (id) => { if (!id) return; try { const url = new URL(window.location.href); url.searchParams.set('event', id); window.history.replaceState(null, '', url.pathname + url.search); } catch (e) {} }; const refreshEvents = async () => { try { const data = await apiFetch('/events/'); const list = data.results || data; setEvents(list); // Check URL query parameters for deep linking (?event=12) const urlParams = new URLSearchParams(window.location.search); const urlEventParam = urlParams.get('event') || urlParams.get('event_id'); const targetIdFromUrl = urlEventParam ? parseInt(urlEventParam, 10) : null; if (targetIdFromUrl && list.some(e => e.id === targetIdFromUrl)) { setSelectedEventId(targetIdFromUrl); fetchEventMatrix(targetIdFromUrl); setActiveTab('schedule'); updateUrlParam(targetIdFromUrl); } else if (list.length > 0 && !selectedEventId) { setSelectedEventId(list[0].id); fetchEventMatrix(list[0].id); updateUrlParam(list[0].id); } else if (selectedEventId) { fetchEventMatrix(selectedEventId); updateUrlParam(selectedEventId); } } catch (e) {} }; const fetchEventMatrix = async (eventId) => { try { const matrixData = await apiFetch(`/events/${eventId}/matrix/`); setSelectedEventData(matrixData); } catch (e) {} }; const handleSelectEvent = (id) => { setSelectedEventId(id); fetchEventMatrix(id); setActiveTab('schedule'); updateUrlParam(id); }; const fallbackCopyText = (text) => { const el = document.createElement('textarea'); el.value = text; document.body.appendChild(el); el.select(); document.execCommand('copy'); document.body.removeChild(el); showNotification('Direktlink zum Schichtplan in Zwischenablage kopiert! 🔗'); }; const handleShareEvent = (targetEventOrId) => { const targetId = typeof targetEventOrId === 'object' ? targetEventOrId.id : (targetEventOrId || selectedEventId); if (!targetId) return; const shareUrl = `${window.location.origin}${window.location.pathname}?event=${targetId}`; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(shareUrl).then(() => { showNotification('Direktlink zum Schichtplan in Zwischenablage kopiert! 🔗'); }).catch(() => { fallbackCopyText(shareUrl); }); } else { fallbackCopyText(shareUrl); } }; const handleLogout = () => { setAuthToken(null); setUser(null); setActiveTab('home'); showNotification('Erfolgreich abgemeldet.'); if (selectedEventId) fetchEventMatrix(selectedEventId); }; const handleAuthSuccess = (userData, message) => { setUser(userData); if (userData && !userData.is_admin_user && !userData.is_staff && !userData.is_superuser && activeTab === 'admin') { setActiveTab('home'); } showNotification(message || 'Erfolgreich angemeldet!'); if (selectedEventId) fetchEventMatrix(selectedEventId); }; // Shift Signup Handler const handleShiftSignupClick = async (shift) => { if (!user) { setGuestTargetShift(shift); setShowGuestModal(true); } else { try { const res = await apiFetch(`/shifts/${shift.id}/signup/`, { method: 'POST' }); showNotification(res.message || 'Erfolgreich für Schicht eingetragen!'); if (selectedEventId) fetchEventMatrix(selectedEventId); } catch (err) { showNotification(`Fehler: ${err.message}`); } } }; const handleGuestSubmit = async (guestData) => { if (!guestTargetShift) return; const res = await apiFetch(`/shifts/${guestTargetShift.id}/signup/`, { method: 'POST', body: JSON.stringify(guestData) }); showNotification(res.message || 'Als Gast eingetragen!'); if (selectedEventId) fetchEventMatrix(selectedEventId); }; const handleCancelSignup = async (shift) => { try { const res = await apiFetch(`/shifts/${shift.id}/signup/`, { method: 'DELETE' }); showNotification(res.message || 'Eintragung storniert.'); if (selectedEventId) fetchEventMatrix(selectedEventId); } catch (err) { showNotification(`Fehler: ${err.message}`); } }; const handleDeleteEvent = async (eventObj) => { if (!eventObj || !eventObj.id) return; if (!window.confirm(`Möchtest du die Veranstaltung "${eventObj.title}" und alle zugehörigen Schichten wirklich unwiderruflich löschen?`)) { return; } try { await apiFetch(`/events/${eventObj.id}/`, { method: 'DELETE' }); showNotification(`Veranstaltung "${eventObj.title}" wurde erfolgreich gelöscht.`); if (selectedEventId === eventObj.id) { setSelectedEventId(null); } if (activeTab === 'event-editor') { setActiveTab('home'); } refreshEvents(); } catch (err) { showNotification(`Fehler beim Löschen: ${err.message}`); } }; const [eventToEdit, setEventToEdit] = useState(null); const handleSaveEventSubmit = async (eventPayload) => { let targetEventId = eventPayload.id; if (targetEventId) { // Edit existing event await apiFetch(`/events/${targetEventId}/`, { method: 'PATCH', body: JSON.stringify({ title: eventPayload.title, description: eventPayload.description, location: eventPayload.location, start_date: eventPayload.start_date, end_date: eventPayload.end_date, co_manager_ids: eventPayload.co_manager_ids || [] }) }); } else { // Create new event const created = await apiFetch('/events/', { method: 'POST', body: JSON.stringify({ title: eventPayload.title, description: eventPayload.description, location: eventPayload.location, start_date: eventPayload.start_date, end_date: eventPayload.end_date, co_manager_ids: eventPayload.co_manager_ids || [] }) }); targetEventId = created.id; } for (let taData of eventPayload.task_areas) { if (!taData.name) continue; let taId = taData.id; if (taId) { await apiFetch(`/task-areas/${taId}/`, { method: 'PATCH', body: JSON.stringify({ name: taData.name, description: taData.description || '' }) }); } else { const createdTa = await apiFetch('/task-areas/', { method: 'POST', body: JSON.stringify({ event: targetEventId, name: taData.name, description: taData.description || '' }) }); taId = createdTa.id; } for (let sData of taData.shifts) { const shiftDate = sData.date || eventPayload.start_date; if (sData.id) { await apiFetch(`/shifts/${sData.id}/`, { method: 'PATCH', body: JSON.stringify({ title: sData.title, date: shiftDate, start_time: sData.start_time, end_time: sData.end_time, max_participants: sData.max_participants, required_skill_ids: sData.required_skill_ids || [] }) }); } else { await apiFetch('/shifts/', { method: 'POST', body: JSON.stringify({ task_area: taId, title: sData.title, date: shiftDate, start_time: sData.start_time, end_time: sData.end_time, max_participants: sData.max_participants, required_skill_ids: sData.required_skill_ids || [] }) }); } } } showNotification(eventPayload.id ? 'Veranstaltung & Schichten aktualisiert!' : 'Veranstaltung erfolgreich erstellt!'); await refreshEvents(); setEventToEdit(null); handleSelectEvent(targetEventId); }; const handleInstantiateTemplate = async (templateId, payload) => { const res = await apiFetch(`/templates/${templateId}/instantiate/`, { method: 'POST', body: JSON.stringify(payload) }); showNotification('Veranstaltung aus Vorlage erstellt!'); await refreshEvents(); if (res.id) handleSelectEvent(res.id); }; const handleExportPdf = () => { if (!selectedEventId) return; window.open(`/api/events/${selectedEventId}/export_pdf/`, '_blank'); }; const handlePrintView = () => { window.print(); }; const handleInstallPwa = () => { if (deferredPrompt) { deferredPrompt.prompt(); deferredPrompt.userChoice.then((choiceResult) => { if (choiceResult.outcome === 'accepted') { showNotification('PWA Installation gestartet!'); } setDeferredPrompt(null); }); } }; return (