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) 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) 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: else:
if not guest_name: if not guest_name:
return Response({'error': 'Bitte gib einen Namen an.'}, status=status.HTTP_400_BAD_REQUEST) 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(): if not is_manager and shift.required_skills.exists():
required_names = ", ".join([s.name for s in shift.required_skills.all()]) required_names = ", ".join([s.name for s in shift.required_skills.all()])
return Response({ 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) }, status=status.HTTP_400_BAD_REQUEST)
if not request.session.session_key: if not request.session.session_key:
request.session.create() request.session.create()
session_key = request.session.session_key session_key = request.session.session_key
request.session['guest_name'] = guest_name
if ShiftSignup.objects.filter(shift=shift, guest_session_key=session_key).exists(): import uuid
return Response({'error': 'Du bist mit deiner Gast-Sitzung bereits für diese Schicht eingetragen.'}, status=status.HTTP_400_BAD_REQUEST) guest_session_key = f"user_{user.id}_guest_{uuid.uuid4().hex[:6]}" if user else session_key
signup = ShiftSignup.objects.create( signup = ShiftSignup.objects.create(
shift=shift, shift=shift,
guest_name=guest_name, guest_name=guest_name,
guest_session_key=session_key guest_session_key=guest_session_key
) )
return Response({ return Response({
'message': f'Gast "{guest_name}" erfolgreich eingetragen!', 'message': f'Gast "{guest_name}" wurde erfolgreich eingetragen!',
'signup_id': signup.id 'signup_id': signup.id
}, status=status.HTTP_201_CREATED) }, status=status.HTTP_201_CREATED)
+2 -2
View File
@@ -11,8 +11,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <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> <title>Schichtplaner — Veranstaltungsschichtpläne</title>
<script type="module" crossorigin src="/assets/index-CaFJNwPw.js"></script> <script type="module" crossorigin src="/assets/index-BZVfc_jb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BpS4KT-L.css"> <link rel="stylesheet" crossorigin href="/assets/index-DQgIbK11.css">
</head> </head>
<body class="bg-paper text-main font-sans antialiased min-h-screen"> <body class="bg-paper text-main font-sans antialiased min-h-screen">
<div id="root"></div> <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) => { const handleGuestSubmit = async (guestData) => {
if (!guestTargetShift) return; if (!guestTargetShift) return;
const res = await apiFetch(`/shifts/${guestTargetShift.id}/signup/`, { const res = await apiFetch(`/shifts/${guestTargetShift.id}/signup/`, {
@@ -600,6 +605,7 @@ export default function App() {
event={selectedEventData} event={selectedEventData}
user={user} user={user}
onSignupClick={handleShiftSignupClick} onSignupClick={handleShiftSignupClick}
onGuestSignupClick={handleGuestSignupClick}
onCancelClick={handleCancelSignup} onCancelClick={handleCancelSignup}
onEditEvent={(evt) => { onEditEvent={(evt) => {
setEventToEdit(evt); setEventToEdit(evt);
@@ -647,6 +653,7 @@ export default function App() {
{showGuestModal && ( {showGuestModal && (
<GuestSignupModal <GuestSignupModal
shift={guestTargetShift} shift={guestTargetShift}
user={user}
onClose={() => setShowGuestModal(false)} onClose={() => setShowGuestModal(false)}
onSubmit={handleGuestSubmit} onSubmit={handleGuestSubmit}
/> />
+37 -28
View File
@@ -1,31 +1,33 @@
import React, { useState, useEffect } from 'react'; 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 [guestName, setGuestName] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
useEffect(() => { useEffect(() => {
// Load saved guest name from localStorage // Only prefill saved guest name for unauthenticated visitors
if (!user) {
const savedName = localStorage.getItem('guest_display_name') || ''; const savedName = localStorage.getItem('guest_display_name') || '';
if (savedName) { if (savedName) setGuestName(savedName);
setGuestName(savedName);
} }
}, []); }, [user]);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
if (!guestName.trim()) { if (!guestName.trim()) {
setError('Bitte gib deinen Namen ein.'); setError('Bitte gib den Namen der Person ein.');
return; return;
} }
// Save in localStorage for guest convenience // Save in localStorage only for unauthenticated guest visitors
if (!user) {
localStorage.setItem('guest_display_name', guestName.trim()); localStorage.setItem('guest_display_name', guestName.trim());
window.dispatchEvent(new Event('guest_name_updated')); window.dispatchEvent(new Event('guest_name_updated'));
}
setSubmitting(true); setSubmitting(true);
try { try {
@@ -39,47 +41,54 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
}; };
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm"> <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="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="bg-surface w-full max-w-md rounded-sm border border-grid p-6 shadow-2xl relative font-sans text-main">
<button <button
onClick={onClose} 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" /> <X className="w-5 h-5" />
</button> </button>
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4 pb-3 border-b border-grid font-mono">
<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"> <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">
<UserCheck className="w-5 h-5" /> <UserPlus className="w-5 h-5" />
</div> </div>
<div> <div>
<h3 className="font-bold text-lg text-white">Als Gast eintragen</h3> <h3 className="font-serif font-bold text-lg uppercase tracking-tight text-main">
<p className="text-xs text-slate-400">Schicht: {shift?.title} ({shift?.start_time} - {shift?.end_time})</p> {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>
</div> </div>
{error && ( {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" /> <AlertCircle className="w-4 h-4 shrink-0" />
<span>{error}</span> <span>{error}</span>
</div> </div>
)} )}
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4 font-mono text-xs">
<div> <div>
<label className="block text-xs font-semibold text-slate-300 mb-1"> <label className="block text-xs font-bold text-main mb-1 uppercase">
Dein Name / Anzeigename {user ? 'Name des Gastes' : 'Dein Name / Anzeigename'}
</label> </label>
<input <input
type="text" type="text"
required required
value={guestName} value={guestName}
onChange={(e) => setGuestName(e.target.value)} onChange={(e) => setGuestName(e.target.value)}
placeholder="z. B. Alex Muster" placeholder={user ? "z. B. Max Mustermann" : "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" 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"> <p className="text-[11px] text-muted font-sans 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! {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> </p>
</div> </div>
@@ -87,16 +96,16 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
<button <button
type="button" type="button"
onClick={onClose} 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>
<button <button
type="submit" type="submit"
disabled={submitting} 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> </button>
</div> </div>
</form> </form>
+15 -2
View File
@@ -7,6 +7,7 @@ export default function ShiftMatrixTable({
event, event,
user, user,
onSignupClick, onSignupClick,
onGuestSignupClick,
onCancelClick, onCancelClick,
onRemoveUserFromShift, onRemoveUserFromShift,
onGenerateClaimLink, onGenerateClaimLink,
@@ -263,10 +264,11 @@ export default function ShiftMatrixTable({
</td> </td>
<td className="py-3.5 px-4 text-right font-mono"> <td className="py-3.5 px-4 text-right font-mono">
<div className="flex items-center justify-end gap-1.5 flex-wrap">
{isUserSignedUp ? ( {isUserSignedUp ? (
<button <button
onClick={() => onCancelClick(shift)} 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" 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 <UserX className="w-3.5 h-3.5" /> AUSTRAGEN
</button> </button>
@@ -276,11 +278,22 @@ export default function ShiftMatrixTable({
<button <button
onClick={() => onSignupClick(shift)} onClick={() => onSignupClick(shift)}
style={{ backgroundColor: 'var(--brand-primary)' }} 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" 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 <CheckCircle2 className="w-3.5 h-3.5" /> EINTRAGEN
</button> </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> </td>
</tr> </tr>
); );