feat: Allow registered users to sign up guests via new + GAST button
This commit is contained in:
Binary file not shown.
@@ -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)
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user