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)
|
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)
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-2
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user