feat: Allow registered users to sign up guests via new + GAST button

This commit is contained in:
Richard
2026-08-01 12:37:56 +02:00
parent b0e727e4a5
commit d9280c3078
6 changed files with 86 additions and 58 deletions
+6 -7
View File
@@ -341,7 +341,7 @@ class ShiftSignupView(views.APIView):
signup = ShiftSignup.objects.create(shift=shift, user=user)
return Response({'message': 'Erfolgreich für Schicht eingetragen.', 'signup_id': signup.id}, status=status.HTTP_201_CREATED)
# Guest flow for visitors
# Guest flow for visitors or registered users adding a guest
else:
if not guest_name:
return Response({'error': 'Bitte gib einen Namen an.'}, status=status.HTTP_400_BAD_REQUEST)
@@ -350,24 +350,23 @@ class ShiftSignupView(views.APIView):
if not is_manager and shift.required_skills.exists():
required_names = ", ".join([s.name for s in shift.required_skills.all()])
return Response({
'error': f'Gäste können keine Fähigkeiten haben. Diese Schicht erfordert: {required_names}. Bitte registriere ein Konto mit den passenden Fähigkeiten.'
'error': f'Gäste können keine Fähigkeiten haben. Diese Schicht erfordert: {required_names}. Ein Gast kann nur durch den Veranstaltungs-Manager/Admin eingetragen werden.'
}, status=status.HTTP_400_BAD_REQUEST)
if not request.session.session_key:
request.session.create()
session_key = request.session.session_key
request.session['guest_name'] = guest_name
if ShiftSignup.objects.filter(shift=shift, guest_session_key=session_key).exists():
return Response({'error': 'Du bist mit deiner Gast-Sitzung bereits für diese Schicht eingetragen.'}, status=status.HTTP_400_BAD_REQUEST)
import uuid
guest_session_key = f"user_{user.id}_guest_{uuid.uuid4().hex[:6]}" if user else session_key
signup = ShiftSignup.objects.create(
shift=shift,
guest_name=guest_name,
guest_session_key=session_key
guest_session_key=guest_session_key
)
return Response({
'message': f'Gast "{guest_name}" erfolgreich eingetragen!',
'message': f'Gast "{guest_name}" wurde erfolgreich eingetragen!',
'signup_id': signup.id
}, status=status.HTTP_201_CREATED)
+2 -2
View File
@@ -11,8 +11,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
<script type="module" crossorigin src="/assets/index-CaFJNwPw.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BpS4KT-L.css">
<script type="module" crossorigin src="/assets/index-BZVfc_jb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DQgIbK11.css">
</head>
<body class="bg-paper text-main font-sans antialiased min-h-screen">
<div id="root"></div>
+7
View File
@@ -292,6 +292,11 @@ export default function App() {
}
};
const handleGuestSignupClick = (shift) => {
setGuestTargetShift(shift);
setShowGuestModal(true);
};
const handleGuestSubmit = async (guestData) => {
if (!guestTargetShift) return;
const res = await apiFetch(`/shifts/${guestTargetShift.id}/signup/`, {
@@ -600,6 +605,7 @@ export default function App() {
event={selectedEventData}
user={user}
onSignupClick={handleShiftSignupClick}
onGuestSignupClick={handleGuestSignupClick}
onCancelClick={handleCancelSignup}
onEditEvent={(evt) => {
setEventToEdit(evt);
@@ -647,6 +653,7 @@ export default function App() {
{showGuestModal && (
<GuestSignupModal
shift={guestTargetShift}
user={user}
onClose={() => setShowGuestModal(false)}
onSubmit={handleGuestSubmit}
/>
+40 -31
View File
@@ -1,31 +1,33 @@
import React, { useState, useEffect } from 'react';
import { X, UserCheck, AlertCircle } from 'lucide-react';
import { X, UserPlus, AlertCircle } from 'lucide-react';
export default function GuestSignupModal({ shift, onClose, onSubmit }) {
export default function GuestSignupModal({ shift, user, onClose, onSubmit }) {
const [guestName, setGuestName] = useState('');
const [error, setError] = useState('');
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
// Load saved guest name from localStorage
const savedName = localStorage.getItem('guest_display_name') || '';
if (savedName) {
setGuestName(savedName);
// Only prefill saved guest name for unauthenticated visitors
if (!user) {
const savedName = localStorage.getItem('guest_display_name') || '';
if (savedName) setGuestName(savedName);
}
}, []);
}, [user]);
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
if (!guestName.trim()) {
setError('Bitte gib deinen Namen ein.');
setError('Bitte gib den Namen der Person ein.');
return;
}
// Save in localStorage for guest convenience
localStorage.setItem('guest_display_name', guestName.trim());
window.dispatchEvent(new Event('guest_name_updated'));
// Save in localStorage only for unauthenticated guest visitors
if (!user) {
localStorage.setItem('guest_display_name', guestName.trim());
window.dispatchEvent(new Event('guest_name_updated'));
}
setSubmitting(true);
try {
@@ -39,47 +41,54 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm">
<div className="glass-panel w-full max-w-md rounded-2xl border border-slate-800 p-6 shadow-2xl relative animate-in fade-in zoom-in-95 duration-200">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-surface w-full max-w-md rounded-sm border border-grid p-6 shadow-2xl relative font-sans text-main">
<button
onClick={onClose}
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-white rounded-lg transition"
className="absolute top-4 right-4 p-2 text-muted hover:text-main rounded-sm transition"
title="Schließen"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-blue-500/10 text-blue-400 border border-blue-500/20 flex items-center justify-center">
<UserCheck className="w-5 h-5" />
<div className="flex items-center gap-3 mb-4 pb-3 border-b border-grid font-mono">
<div className="w-10 h-10 rounded-sm bg-amber-500/10 text-amber-500 border border-amber-500/20 flex items-center justify-center font-bold">
<UserPlus className="w-5 h-5" />
</div>
<div>
<h3 className="font-bold text-lg text-white">Als Gast eintragen</h3>
<p className="text-xs text-slate-400">Schicht: {shift?.title} ({shift?.start_time} - {shift?.end_time})</p>
<h3 className="font-serif font-bold text-lg uppercase tracking-tight text-main">
{user ? 'Gast in Schicht eintragen' : 'Als Gast eintragen'}
</h3>
<p className="text-xs text-muted font-sans">
Schicht: <strong className="text-main">{shift?.title}</strong> ({shift?.start_time} - {shift?.end_time} Uhr)
</p>
</div>
</div>
{error && (
<div className="mb-4 p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-xs flex items-center gap-2">
<div className="mb-4 p-3 rounded-sm bg-red-500/10 border border-red-500/20 text-red-400 text-xs font-mono flex items-center gap-2">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{error}</span>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-4 font-mono text-xs">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Dein Name / Anzeigename
<label className="block text-xs font-bold text-main mb-1 uppercase">
{user ? 'Name des Gastes' : 'Dein Name / Anzeigename'}
</label>
<input
type="text"
required
value={guestName}
onChange={(e) => setGuestName(e.target.value)}
placeholder="z. B. Alex Muster"
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-white text-sm focus:outline-none focus:border-blue-500 transition"
placeholder={user ? "z. B. Max Mustermann" : "z. B. Alex Muster"}
className="w-full px-3.5 py-2.5 rounded-sm bg-subtle border border-grid text-main text-sm focus:outline-none focus:border-muted font-sans"
/>
<p className="text-[11px] text-slate-500 mt-1">
Dein Name bleibt in deiner aktuellen Sitzung gespeichert. Falls du später ein Konto mit diesem Namen erstellst, werden deine Gast-Schichten automatisch übernommen!
<p className="text-[11px] text-muted font-sans mt-1">
{user
? 'Du kannst hier Helfer*innen eintragen, die über kein eigenes Benutzerkonto verfügen.'
: 'Dein Name wird im Schichtplan hinterlegt. Du kannst später jederzeit ein Konto erstellen.'}
</p>
</div>
@@ -87,16 +96,16 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
<button
type="button"
onClick={onClose}
className="px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 hover:bg-slate-800 transition"
className="px-4 py-2 rounded-sm text-xs font-bold bg-subtle hover:bg-surface-hover text-main border border-grid transition"
>
Abbrechen
[ ABBRECHEN ]
</button>
<button
type="submit"
disabled={submitting}
className="px-5 py-2 rounded-xl text-xs font-bold bg-blue-600 hover:bg-blue-500 text-white transition shadow-lg shadow-blue-600/30 disabled:opacity-50"
className="px-5 py-2 rounded-sm text-xs font-bold bg-amber-600 hover:bg-amber-500 text-white transition shadow-sm disabled:opacity-50"
>
{submitting ? 'Eintragen...' : 'Jetzt eintragen'}
{submitting ? '[ EINTRAGEN... ]' : user ? '[ GAST EINTRAGEN ]' : '[ JETZT EINTRAGEN ]'}
</button>
</div>
</form>
+31 -18
View File
@@ -7,6 +7,7 @@ export default function ShiftMatrixTable({
event,
user,
onSignupClick,
onGuestSignupClick,
onCancelClick,
onRemoveUserFromShift,
onGenerateClaimLink,
@@ -263,24 +264,36 @@ export default function ShiftMatrixTable({
</td>
<td className="py-3.5 px-4 text-right font-mono">
{isUserSignedUp ? (
<button
onClick={() => onCancelClick(shift)}
className="px-3 py-1.5 rounded-sm text-xs font-semibold bg-red-500/10 hover:bg-red-500/20 text-red-500 border border-red-500/20 transition flex items-center gap-1 ml-auto"
>
<UserX className="w-3.5 h-3.5" /> AUSTRAGEN
</button>
) : isFull ? (
<span className="text-xs text-muted font-bold">[ VOLL ]</span>
) : (
<button
onClick={() => onSignupClick(shift)}
style={{ backgroundColor: 'var(--brand-primary)' }}
className="px-3.5 py-1.5 rounded-sm text-xs font-bold text-white transition hover:brightness-110 flex items-center gap-1 ml-auto shadow-sm"
>
<CheckCircle2 className="w-3.5 h-3.5" /> EINTRAGEN
</button>
)}
<div className="flex items-center justify-end gap-1.5 flex-wrap">
{isUserSignedUp ? (
<button
onClick={() => onCancelClick(shift)}
className="px-2.5 py-1.5 rounded-sm text-xs font-semibold bg-red-500/10 hover:bg-red-500/20 text-red-500 border border-red-500/20 transition flex items-center gap-1"
>
<UserX className="w-3.5 h-3.5" /> AUSTRAGEN
</button>
) : isFull ? (
<span className="text-xs text-muted font-bold">[ VOLL ]</span>
) : (
<button
onClick={() => onSignupClick(shift)}
style={{ backgroundColor: 'var(--brand-primary)' }}
className="px-3 py-1.5 rounded-sm text-xs font-bold text-white transition hover:brightness-110 flex items-center gap-1 shadow-sm"
>
<CheckCircle2 className="w-3.5 h-3.5" /> EINTRAGEN
</button>
)}
{user && !isFull && onGuestSignupClick && (
<button
onClick={() => onGuestSignupClick(shift)}
className="px-2 py-1.5 rounded-sm text-[11px] font-mono font-bold bg-amber-500/10 hover:bg-amber-500/20 text-amber-400 border border-amber-500/30 transition flex items-center gap-1"
title="Eine andere Person als Gast in diese Schicht eintragen"
>
<UserPlus className="w-3.5 h-3.5" /> + GAST
</button>
)}
</div>
</td>
</tr>
);