671 lines
23 KiB
React
671 lines
23 KiB
React
/* 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 (
|
|
<div className="min-h-screen bg-canvas text-main flex flex-col font-sans selection:bg-surface-hover transition-colors duration-200">
|
|
{/* Toast Notification */}
|
|
{toastMessage && (
|
|
<div className="fixed bottom-6 right-6 z-50 bg-emerald-600 text-white px-4 py-2.5 rounded-sm shadow-2xl font-mono text-xs flex items-center gap-2 border border-emerald-400 animate-in fade-in slide-in-from-bottom-3 duration-200">
|
|
<CheckCircle2 className="w-4 h-4" />
|
|
<span>{toastMessage}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation Masthead */}
|
|
<Navbar
|
|
branding={branding}
|
|
user={user}
|
|
activeTab={activeTab}
|
|
onChangeTab={setActiveTab}
|
|
themeMode={themeMode}
|
|
onChangeThemeMode={setThemeMode}
|
|
onLogout={handleLogout}
|
|
onOpenAuth={() => setShowAuthModal(true)}
|
|
onOpenCreateEvent={() => { setEventToEdit(null); setActiveTab('event-editor'); }}
|
|
onOpenTemplates={() => setActiveTab('templates')}
|
|
onOpenProfile={() => setShowProfileModal(true)}
|
|
onOpenAdmin={() => setActiveTab('admin')}
|
|
pwaInstallPrompt={Boolean(deferredPrompt)}
|
|
onInstallPwa={handleInstallPwa}
|
|
/>
|
|
|
|
{/* Main Content Area */}
|
|
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
|
|
{loading ? (
|
|
<div className="hallmark-panel p-16 text-center text-xs text-muted font-mono border border-grid">
|
|
Lade Daten...
|
|
</div>
|
|
) : activeTab === 'home' ? (
|
|
<HomePage
|
|
events={events}
|
|
user={user}
|
|
onSelectEvent={handleSelectEvent}
|
|
onOpenCreateEvent={() => { setEventToEdit(null); setActiveTab('event-editor'); }}
|
|
onToggleEventActive={async (evt) => {
|
|
try {
|
|
await apiFetch(`/events/${evt.id}/`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ is_active: evt.is_active === false ? true : false })
|
|
});
|
|
refreshEvents();
|
|
} catch (err) {
|
|
alert(err.message || 'Aktion fehlgeschlagen.');
|
|
}
|
|
}}
|
|
onShareEvent={handleShareEvent}
|
|
onEditEvent={(evt) => {
|
|
setEventToEdit(evt);
|
|
setActiveTab('event-editor');
|
|
}}
|
|
onDeleteEvent={handleDeleteEvent}
|
|
branding={branding}
|
|
/>
|
|
) : activeTab === 'calendar' ? (
|
|
<CalendarPage
|
|
events={events}
|
|
onSelectEvent={handleSelectEvent}
|
|
branding={branding}
|
|
/>
|
|
) : activeTab === 'admin' && user && (user.is_admin_user || user.is_staff || user.is_superuser) ? (
|
|
<AdminPage
|
|
branding={branding}
|
|
onRefreshBranding={async () => {
|
|
const b = await apiFetch('/branding/');
|
|
setBranding(b);
|
|
}}
|
|
onRefreshEvents={refreshEvents}
|
|
skills={skills}
|
|
onRefreshSkills={refreshSkills}
|
|
/>
|
|
) : activeTab === 'event-editor' ? (
|
|
<EventEditorPage
|
|
key={eventToEdit?.id || 'new-event'}
|
|
eventToEdit={eventToEdit}
|
|
skills={skills}
|
|
user={user}
|
|
onBack={() => setActiveTab('schedule')}
|
|
onSubmit={handleSaveEventSubmit}
|
|
onDeleteEvent={handleDeleteEvent}
|
|
/>
|
|
) : activeTab === 'templates' ? (
|
|
<TemplatesPage
|
|
onBack={() => setActiveTab('schedule')}
|
|
onInstantiateTemplate={handleInstantiateTemplate}
|
|
user={user}
|
|
skills={skills}
|
|
/>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{/* Event Selector Sub-Bar in Schedule View */}
|
|
<div className="flex items-center justify-between gap-4 overflow-x-auto pb-2 no-scrollbar font-mono">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs font-bold text-muted uppercase tracking-wider whitespace-nowrap">
|
|
Ausgewähltes Event:
|
|
</span>
|
|
{events.map((evt) => {
|
|
const isActive = selectedEventId === evt.id;
|
|
return (
|
|
<button
|
|
key={evt.id}
|
|
onClick={() => handleSelectEvent(evt.id)}
|
|
style={{
|
|
backgroundColor: isActive ? (branding?.primary_color || 'var(--brand-primary)') : undefined,
|
|
borderColor: isActive ? (branding?.primary_color || 'var(--brand-primary)') : undefined
|
|
}}
|
|
className={`px-3.5 py-1.5 rounded-sm text-xs font-bold transition whitespace-nowrap border ${
|
|
isActive
|
|
? 'text-white shadow-sm'
|
|
: 'bg-subtle text-muted border-grid hover:text-main'
|
|
}`}
|
|
>
|
|
{evt.title}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{user && (
|
|
<button
|
|
onClick={() => { setEventToEdit(null); setActiveTab('event-editor'); }}
|
|
className="px-3.5 py-1.5 rounded-sm text-xs font-bold bg-subtle hover:bg-surface-hover text-main border border-grid transition flex items-center gap-1.5 shrink-0"
|
|
>
|
|
<Plus className="w-4 h-4" /> Neues Event
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<ShiftMatrixTable
|
|
event={selectedEventData}
|
|
user={user}
|
|
onSignupClick={handleShiftSignupClick}
|
|
onCancelClick={handleCancelSignup}
|
|
onEditEvent={(evt) => {
|
|
setEventToEdit(evt);
|
|
setActiveTab('event-editor');
|
|
}}
|
|
onDeleteEvent={handleDeleteEvent}
|
|
onRemoveUserFromShift={async (shift, signupId) => {
|
|
try {
|
|
const res = await apiFetch(`/shifts/${shift.id}/signup/${signupId}/`, {
|
|
method: 'DELETE'
|
|
});
|
|
showNotification(res.message || 'Eintragung entfernt.');
|
|
refreshEvents();
|
|
if (selectedEventId) {
|
|
handleSelectEvent(selectedEventId);
|
|
}
|
|
} catch (err) {
|
|
alert(err.message || 'Entfernen der Person fehlgeschlagen.');
|
|
}
|
|
}}
|
|
onGenerateClaimLink={handleGenerateClaimLink}
|
|
onExportPdf={handleExportPdf}
|
|
onPrintView={handlePrintView}
|
|
onShareLink={() => handleShareEvent(selectedEventId)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className="border-t border-grid py-6 text-center text-xs text-muted font-mono no-print">
|
|
<p>{branding?.app_name || "Veranstaltungsschichtplaner"} • PWA Enabled • PostgreSQL & Docker Ready</p>
|
|
</footer>
|
|
|
|
{/* Modals */}
|
|
{showAuthModal && (
|
|
<AuthModal
|
|
onClose={() => { setShowAuthModal(false); setClaimToken(null); setPrefilledGuestName(null); }}
|
|
onSuccess={handleAuthSuccess}
|
|
claimToken={claimToken}
|
|
prefilledGuestName={prefilledGuestName}
|
|
/>
|
|
)}
|
|
|
|
{showGuestModal && (
|
|
<GuestSignupModal
|
|
shift={guestTargetShift}
|
|
onClose={() => setShowGuestModal(false)}
|
|
onSubmit={handleGuestSubmit}
|
|
/>
|
|
)}
|
|
|
|
{showProfileModal && (
|
|
<ProfileModal
|
|
user={user}
|
|
skills={skills}
|
|
onClose={() => setShowProfileModal(false)}
|
|
onUserUpdated={(updatedUser) => {
|
|
setUser(updatedUser);
|
|
setShowProfileModal(false);
|
|
refreshEvents();
|
|
}}
|
|
onRefreshSkills={refreshSkills}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|